class Mixlib::Authentication::SignatureVerification

Public Class Methods

new(request = nil) click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 55
def initialize(request = nil)
  @auth_request = HTTPAuthenticationRequest.new(request) if request

  @valid_signature, @valid_timestamp, @valid_content_hash = false, false, false

  @hashed_body = nil
end

Public Instance Methods

authenticate_request(user_secret, time_skew = (15 * 60)) click to toggle source

Takes the request, boils down the pieces we are interested in, looks up the user, generates a signature, and compares to the signature in the request

Headers

X-Ops-Sign: algorithm=sha1;version=1.0; X-Ops-UserId: <user_id> X-Ops-Timestamp: X-Ops-Content-Hash: X-Ops-Authorization-#{line_number}

# File lib/mixlib/authentication/signatureverification.rb, line 78
def authenticate_request(user_secret, time_skew = (15 * 60))
  Mixlib::Authentication.logger.trace "Initializing header auth : #{request.inspect}"

  @user_secret       = user_secret
  @allowed_time_skew = time_skew # in seconds

  begin
    parts = parse_signing_description

    # version 1.0 clients don't include their algorithm in the
    # signing description, so default to sha1
    parts[:algorithm] ||= "sha1"

    verify_signature(parts[:algorithm], parts[:version])
    verify_timestamp
    verify_content_hash

  rescue StandardError => se
    raise AuthenticationError, "Failed to authenticate user request. Check your client key and clock: #{se.message}", se.backtrace
  end

  if valid_request?
    SignatureResponse.new(user_id)
  else
    nil
  end
end
authenticate_user_request(request, user_lookup, time_skew = (15 * 60)) click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 63
def authenticate_user_request(request, user_lookup, time_skew = (15 * 60))
  @auth_request = HTTPAuthenticationRequest.new(request)
  authenticate_request(user_lookup, time_skew)
end
headers() click to toggle source

The authorization header is a Base64-encoded version of an RSA signature. The client sent it on multiple header lines, starting at index 1 - X-Ops-Authorization-1, X-Ops-Authorization-2, etc. Pull them out and concatenate.

# File lib/mixlib/authentication/signatureverification.rb, line 126
def headers
  @headers ||= request.env.inject({}) { |memo, kv| memo[$2.tr("-", "_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo }
end
valid_content_hash?() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 114
def valid_content_hash?
  @valid_content_hash
end
valid_request?() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 118
def valid_request?
  valid_signature? && valid_timestamp? && valid_content_hash?
end
valid_signature?() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 106
def valid_signature?
  @valid_signature
end
valid_timestamp?() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 110
def valid_timestamp?
  @valid_timestamp
end

Private Instance Methods

assert_required_headers_present() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 132
def assert_required_headers_present
  MANDATORY_HEADERS.each do |header|
    unless headers.key?(header)
      raise MissingAuthenticationHeader, "required authentication header #{header.to_s.upcase} missing"
    end
  end
end
hashed_body(digest = Digest::SHA1) click to toggle source

The request signature is based on any file attached, if any. Otherwise it's based on the body of the request.

# File lib/mixlib/authentication/signatureverification.rb, line 181
def hashed_body(digest = Digest::SHA1)
  unless @hashed_body
    # TODO: tim: 2009-112-28: It'd be nice to remove this special case, and
    # always hash the entire request body. In the file case it would just be
    # expanded multipart text - the entire body of the POST.
    #
    # Pull out any file that was attached to this request, using multipart
    # form uploads.
    # Depending on the server we're running in, multipart form uploads are
    # handed to us differently.
    # - In Passenger (Cookbooks Community Site), the File is handed to us
    #   directly in the params hash. The name is whatever the client used,
    #   its value is therefore a File or Tempfile.
    #   e.g. request['file_param'] = File
    #
    # - In Merb (Chef server), the File is wrapped. The original parameter
    #   name used for the file is used, but its value is a Hash. Within
    #   the hash is a name/value pair named 'file' which actually
    #   contains the Tempfile instance.
    #   e.g. request['file_param'] = { :file => Tempfile }
    file_param = request.params.values.find { |value| value.respond_to?(:read) }

    # No file_param; we're running in Merb, or it's just not there..
    if file_param.nil?
      hash_param = request.params.values.find { |value| value.respond_to?(:has_key?) } # Hash responds to :has_key? .
      unless hash_param.nil?
        file_param = hash_param.values.find { |value| value.respond_to?(:read) } # File/Tempfile responds to :read.
      end
    end

    # Any file that's included in the request is hashed if it's there. Otherwise,
    # we hash the body.
    if file_param
      Mixlib::Authentication.logger.trace "Digesting file_param: '#{file_param.inspect}'"
      @hashed_body = digester.hash_file(file_param, digest)
    else
      body = request.raw_post
      Mixlib::Authentication.logger.trace "Digesting body: '#{body}'"
      @hashed_body = digester.hash_string(body, digest)
    end
  end
  @hashed_body
end
timestamp_within_bounds?(time1, time2) click to toggle source

Compare the request timestamp with boundary time

Parameters

time1<Time>

minuend

time2<Time>

subtrahend

# File lib/mixlib/authentication/signatureverification.rb, line 232
def timestamp_within_bounds?(time1, time2)
  time_diff = (time2 - time1).abs
  is_allowed = (time_diff < @allowed_time_skew)
  Mixlib::Authentication.logger.trace "Request time difference: #{time_diff}, within #{@allowed_time_skew} seconds? : #{!!is_allowed}"
  is_allowed
end
verify_content_hash() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 168
def verify_content_hash
  @valid_content_hash = (content_hash == hashed_body)

  # Keep the trace messages lined up so it's easy to scan them
  Mixlib::Authentication.logger.trace("Expected content hash is: '#{hashed_body}'")
  Mixlib::Authentication.logger.trace(" Request Content Hash is: '#{content_hash}'")
  Mixlib::Authentication.logger.trace("           Hashes match?: #{@valid_content_hash}")

  @valid_content_hash
end
verify_signature(algorithm, version) click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 140
def verify_signature(algorithm, version)
  candidate_block = canonicalize_request(algorithm, version)
  signature = Base64.decode64(request_signature)
  @valid_signature = case version
                     when "1.3"
                       digest = validate_sign_version_digest!(algorithm, version)
                       @user_secret.verify(digest.new, signature, candidate_block)
                     else
                       request_decrypted_block = @user_secret.public_decrypt(signature)
                       (request_decrypted_block == candidate_block)
                     end

  # Keep the trace messages lined up so it's easy to scan them
  Mixlib::Authentication.logger.trace("Verifying request signature:")
  Mixlib::Authentication.logger.trace(" Expected Block is: '#{candidate_block}'")
  Mixlib::Authentication.logger.trace("Decrypted block is: '#{request_decrypted_block}'")
  Mixlib::Authentication.logger.trace("Signatures match? : '#{@valid_signature}'")

  @valid_signature
rescue => e
  Mixlib::Authentication.logger.trace("Failed to verify request signature: #{e.class.name}: #{e.message}")
  @valid_signature = false
end
verify_timestamp() click to toggle source
# File lib/mixlib/authentication/signatureverification.rb, line 164
def verify_timestamp
  @valid_timestamp = timestamp_within_bounds?(Time.parse(timestamp), Time.now)
end