class ChefAPI::Authentication

Constants

SIGNATURE
SIGN_FULL_BODY

@todo: Enable this in the future when Mixlib::Authentication supports signing the full request body instead of just the uploaded file parameter.

X_OPS_AUTHORIZATION
X_OPS_CONTENT_HASH
X_OPS_SIGN

Headers

X_OPS_TIMESTAMP
X_OPS_USERID

Public Class Methods

from_options(options = {}) click to toggle source

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
new(user, key, verb, path, body) click to toggle source

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

Public Instance Methods

canonical_key() click to toggle source

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
canonical_method() click to toggle source

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
canonical_path() click to toggle source

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
canonical_request() click to toggle source

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
canonical_timestamp() click to toggle source

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
content_hash() click to toggle source

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
encrypted_request() click to toggle source

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
headers() click to toggle source

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
signature_lines() click to toggle source

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

Private Instance Methods

digest_io(io) click to toggle source

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_string(string) click to toggle source

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(object) click to toggle source

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