@todo: Enable this in the future when Mixlib::Authentication supports signing the full request body instead of just the uploaded file parameter.
Headers
Create a new signing object from the given options. All options are required.
@see (#initialize)
@option options [String] :user @option options [String, OpenSSL::PKey::RSA] :key @option options [String, Symbol] verb @option options [String] :path @option options [String, IO] :body
# File lib/chef-api/authentication.rb, line 42 def from_options(options = {}) user = options.fetch(:user) key = options.fetch(:key) verb = options.fetch(:verb) path = options.fetch(:path) body = options.fetch(:body) new(user, key, verb, path, body) end
Create a new Authentication object for signing. Creating an instance will not run any validations or perform any operations (this is on purpose).
@param [String] user
the username/client/user of the user to sign the request. In Hosted Chef land, this is your "client". In Supermarket land, this is your "username".
@param [String, OpenSSL::PKey::RSA] key
the path to a private key on disk, the raw private key (as a String), or the raw private key (as an OpenSSL::PKey::RSA instance)
@param [Symbol, String] verb
the verb for the request (e.g. +:get+)
@param [String] path
the "path" part of the URI (e.g. +/path/to/resource+)
@param [String, IO] body
the body to sign for the request, as a raw string or an IO object to be read in chunks
# File lib/chef-api/authentication.rb, line 72 def initialize(user, key, verb, path, body) @user = user @key = key @verb = verb @path = path @body = body end
Parse the given private key. Users can specify the private key as:
- the path to the key on disk - the raw string key - an +OpenSSL::PKey::RSA object+
Any other implementations are not supported and will likely explode.
@todo
Handle errors when the file cannot be read due to insufficient permissions
@return [OpenSSL::PKey::RSA]
the RSA private key as an OpenSSL object
# File lib/chef-api/authentication.rb, line 148 def canonical_key return @canonical_key if @canonical_key log.info "Parsing private key..." if @key.nil? log.warn "No private key given!" raise 'No private key given!' end if @key.is_a?(OpenSSL::PKey::RSA) log.debug "Detected private key is an OpenSSL Ruby object" @canonical_key = @key elsif @key =~ /(.+)\.pem$/ || File.exists?(File.expand_path(@key)) log.debug "Detected private key is the path to a file" contents = File.read(File.expand_path(@key)) @canonical_key = OpenSSL::PKey::RSA.new(contents) else log.debug "Detected private key was the literal string key" @canonical_key = OpenSSL::PKey::RSA.new(@key) end @canonical_key end
The uppercase verb.
@example
:get #=> "GET"
@return [String]
# File lib/chef-api/authentication.rb, line 205 def canonical_method @canonical_method ||= @verb.to_s.upcase end
The canonical path, with duplicate and trailing slashes removed. This value is then hashed.
@example
"/zip//zap/foo" #=> "/zip/zap/foo"
@return [String]
# File lib/chef-api/authentication.rb, line 183 def canonical_path @canonical_path ||= hash(@path.squeeze('/').gsub(/(\/)+$/,'')).chomp end
The canonical request, from the path, body, user, and current timestamp.
@return [String]
# File lib/chef-api/authentication.rb, line 214 def canonical_request [ "Method:#{canonical_method}", "Hashed Path:#{canonical_path}", "X-Ops-Content-Hash:#{content_hash}", "X-Ops-Timestamp:#{canonical_timestamp}", "X-Ops-UserId:#{@user}", ].join("\n") end
The iso8601 timestamp for this request. This value must be cached so it is persisted throughout this entire request.
@return [String]
# File lib/chef-api/authentication.rb, line 193 def canonical_timestamp @canonical_timestamp ||= Time.now.utc.iso8601 end
The canonical body. This could be an IO object (such as
#body_stream
), an actual string (such as #body
),
or just the empty string if the request's body and stream was nil.
@return [String, IO]
# File lib/chef-api/authentication.rb, line 113 def content_hash return @content_hash if @content_hash if SIGN_FULL_BODY @content_hash = hash(@body || '').chomp else if @body.is_a?(Multipart::MultiIO) filepart = @body.ios.find { |io| io.is_a?(Multipart::MultiIO) } file = filepart.ios.find { |io| !io.is_a?(StringIO) } @content_hash = hash(file).chomp else @content_hash = hash(@body || '').chomp end end @content_hash end
The canonical request, encrypted by the given private key.
@return [String]
# File lib/chef-api/authentication.rb, line 229 def encrypted_request canonical_key.private_encrypt(canonical_request) end
The fully-qualified headers for this authentication object of the form:
{ 'X-Ops-Sign' => 'algorithm=sha1;version=1.1', 'X-Ops-Userid' => 'sethvargo', 'X-Ops-Timestamp' => '2014-07-07T02:17:15Z', 'X-Ops-Content-Hash' => '...', 'x-Ops-Authorization-1' => '...' 'x-Ops-Authorization-2' => '...' 'x-Ops-Authorization-3' => '...' # ... }
@return [Hash]
the signing headers
# File lib/chef-api/authentication.rb, line 97 def headers { X_OPS_SIGN => SIGNATURE, X_OPS_USERID => @user, X_OPS_TIMESTAMP => canonical_timestamp, X_OPS_CONTENT_HASH => content_hash, }.merge(signature_lines) end
The X-Ops-Authorization-N
headers. This method takes the
encrypted request, splits on a newline, and creates a signed header
authentication request. N begins at 1, not 0 because the original author of
Mixlib::Authentication did not believe in computer science.
@return [Hash]
# File lib/chef-api/authentication.rb, line 241 def signature_lines signature = Base64.encode64(encrypted_request) signature.split(/\n/).each_with_index.inject({}) do |hash, (line, index)| hash["#{X_OPS_AUTHORIZATION}-#{index + 1}"] = line hash end end
Digest the given IO, reading in 1024 bytes at one time.
@param [IO] io
the IO (or File object)
@return [String]
# File lib/chef-api/authentication.rb, line 276 def digest_io(io) digester = Digest::SHA1.new while buffer = io.read(1024) digester.update(buffer) end io.rewind Base64.encode64(digester.digest) end
Digest a string.
@param [String] string
the string to digest
@return [String]
# File lib/chef-api/authentication.rb, line 296 def digest_string(string) Base64.encode64(Digest::SHA1.digest(string)) end
Hash the given object.
@param [String, IO] object
a string or IO object to hash
@return [String]
the hashed value
# File lib/chef-api/authentication.rb, line 260 def hash(object) if object.respond_to?(:read) digest_io(object) else digest_string(object) end end