Class: ChefAPI::Authentication

Inherits:
Object
  • Object
show all
Includes:
Logify
Defined in:
lib/chef-api/authentication.rb

Constant Summary collapse

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.

false
SIGNATURE =
'algorithm=sha1;version=1.0;'.freeze
X_OPS_SIGN =

Headers

'X-Ops-Sign'.freeze
X_OPS_USERID =
'X-Ops-Userid'.freeze
X_OPS_TIMESTAMP =
'X-Ops-Timestamp'.freeze
X_OPS_CONTENT_HASH =
'X-Ops-Content-Hash'.freeze
X_OPS_AUTHORIZATION =
'X-Ops-Authorization'.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user, key, verb, path, body) ⇒ Authentication

Create a new Authentication object for signing. Creating an instance will not run any validations or perform any operations (this is on purpose).

Parameters:

  • user (String)

    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”.

  • key (String, OpenSSL::PKey::RSA)

    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)

  • verb (Symbol, String)

    the verb for the request (e.g. :get)

  • path (String)

    the “path” part of the URI (e.g. /path/to/resource)

  • body (String, IO)

    the body to sign for the request, as a raw string or an IO object to be read in chunks



72
73
74
75
76
77
78
# 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

Class Method Details

.from_options(options = {}) ⇒ Object

Create a new signing object from the given options. All options are required.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :user (String)
  • :key (String, OpenSSL::PKey::RSA)
  • verb (String, Symbol)
  • :path (String)
  • :body (String, IO)

See Also:

  • ((#initialize)


42
43
44
45
46
47
48
49
50
# 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

Instance Method Details

#canonical_keyOpenSSL::PKey::RSA

TODO:

Handle errors when the file cannot be read due to insufficient permissions

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.

Returns:

  • (OpenSSL::PKey::RSA)

    the RSA private key as an OpenSSL object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# 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_methodString

The uppercase verb.

Examples:

:get #=> "GET"

Returns:

  • (String)


205
206
207
# File 'lib/chef-api/authentication.rb', line 205

def canonical_method
  @canonical_method ||= @verb.to_s.upcase
end

#canonical_pathString

The canonical path, with duplicate and trailing slashes removed. This value is then hashed.

Examples:

"/zip//zap/foo" #=> "/zip/zap/foo"

Returns:

  • (String)


183
184
185
# File 'lib/chef-api/authentication.rb', line 183

def canonical_path
  @canonical_path ||= hash(@path.squeeze('/').gsub(/(\/)+$/,'')).chomp
end

#canonical_requestString

The canonical request, from the path, body, user, and current timestamp.

Returns:

  • (String)


214
215
216
217
218
219
220
221
222
# 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_timestampString

The iso8601 timestamp for this request. This value must be cached so it is persisted throughout this entire request.

Returns:

  • (String)


193
194
195
# File 'lib/chef-api/authentication.rb', line 193

def canonical_timestamp
  @canonical_timestamp ||= Time.now.utc.iso8601
end

#content_hashString, IO

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.

Returns:

  • (String, IO)


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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_requestString

The canonical request, encrypted by the given private key.

Returns:

  • (String)


229
230
231
# File 'lib/chef-api/authentication.rb', line 229

def encrypted_request
  canonical_key.private_encrypt(canonical_request)
end

#headersHash

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' => '...'
  # ...
}

Returns:

  • (Hash)

    the signing headers



97
98
99
100
101
102
103
104
# 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_linesHash

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.

Returns:

  • (Hash)


241
242
243
244
245
246
247
# 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