Class: ChefAPI::Authentication

Inherits:
Object
  • Object
show all
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



71
72
73
74
75
76
77
# File 'lib/chef-api/authentication.rb', line 71

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)


41
42
43
44
45
46
47
48
49
# File 'lib/chef-api/authentication.rb', line 41

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



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/chef-api/authentication.rb', line 147

def canonical_key
  return @canonical_key if @canonical_key

  ChefAPI::Log.info "Parsing private key..."

  if @key.nil?
    ChefAPI::Log.warn "No private key given!"
    raise "No private key given!"
  end

  if @key.is_a?(OpenSSL::PKey::RSA)
    ChefAPI::Log.debug "Detected private key is an OpenSSL Ruby object"
    @canonical_key = @key
  elsif @key =~ /(.+)\.pem$/ || File.exist?(File.expand_path(@key))
    ChefAPI::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
    ChefAPI::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)


203
204
205
# File 'lib/chef-api/authentication.rb', line 203

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)


181
182
183
# File 'lib/chef-api/authentication.rb', line 181

def canonical_path
  @canonical_path ||= hash(@path.squeeze("/").gsub(%r{(/)+$}, "")).chomp
end

#canonical_requestString

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

Returns:

  • (String)


212
213
214
215
216
217
218
219
220
# File 'lib/chef-api/authentication.rb', line 212

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)


191
192
193
# File 'lib/chef-api/authentication.rb', line 191

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)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/chef-api/authentication.rb', line 112

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)


227
228
229
# File 'lib/chef-api/authentication.rb', line 227

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



96
97
98
99
100
101
102
103
# File 'lib/chef-api/authentication.rb', line 96

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)


239
240
241
242
243
244
245
# File 'lib/chef-api/authentication.rb', line 239

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