class Ey::Hmac::Adapter
This class is responsible for forming the canonical string to used to sign requests @abstract override methods {#method}, {#path}, {#body}, {#content_type} and {#content_digest}
Constants
- AUTHORIZATION_REGEXP
Attributes
Public Class Methods
@param [Object] request signer-specific request implementation @option options [Integer] :version signature version @option options [Integer] :ttl (nil) duration during which HMAC is valid after signed date @option options [String] :authorization_header ('Authorization') Authorization header key. @option options [String] :server ('EyHmac') service name prefixed to {#authorization}. set to {#service} @option options [Symbol] :sign_with (:sha_256) outgoing signature digest algorithm. See {OpenSSL::Digest#new} @option options [Array] :accepted_digests ([:sha_256]) accepted incoming signature digest algorithm. See {OpenSSL::Digest#new}
# File lib/ey-hmac/adapter.rb, line 18 def initialize(request, options={}) @request, @options = request, options @ttl = options[:ttl] @authorization_header = options[:authorization_header] || 'Authorization' @service = options[:service] || 'EyHmac' @sign_with = options[:sign_with] || :sha256 @accept_digests = Array(options[:accept_digests] || :sha256) end
Public Instance Methods
@see Ey::Hmac#authenticate!
# File lib/ey-hmac/adapter.rb, line 116 def authenticated!(&block) key_id, signature_value = check_signature! key_secret = block.call(key_id) unless key_secret raise Ey::Hmac::MissingSecret, "Failed to find secret matching #{key_id.inspect}" end check_ttl! calculated_signatures = accept_digests.map { |ad| signature(key_secret, ad) } matching_signature = calculated_signatures.any? { |cs| secure_compare(signature_value, cs) } unless matching_signature raise Ey::Hmac::SignatureMismatch, "Calculated signature #{signature_value} does not match #{calculated_signatures.inspect} using #{canonicalize.inspect}" end true end
Check {#authorization_signature} against calculated {#signature} @yieldparam key_id [String] public HMAC key @return [Boolean] true if block yields matching private key and signature matches, else false @see authenticated!
# File lib/ey-hmac/adapter.rb, line 109 def authenticated?(options={}, &block) authenticated!(&block) rescue Ey::Hmac::Error false end
@abstract @return [String] request body. @return [NilClass] if there is no body or the body is empty
# File lib/ey-hmac/adapter.rb, line 74 def body raise NotImplementedError end
In order for the server to correctly authorize the request, the client and server MUST AGREE on this format
default canonical string formation is '{#method}\n{#content_type}\n{#content_digest}\n{#date}\n{#path}' @return [String] canonical string used to form the {#signature}
# File lib/ey-hmac/adapter.rb, line 32 def canonicalize [method, content_type, content_digest, date, path].join("\n") end
@abstract Digest of body. Default is MD5. @return [String] digest of body
# File lib/ey-hmac/adapter.rb, line 67 def content_digest raise NotImplementedError end
@abstract @return [String] value of the Content-Type header in {#request}
# File lib/ey-hmac/adapter.rb, line 80 def content_type raise NotImplementedError end
@abstract @return [String] value of the Date header in {#request}. @see Time#http_date
# File lib/ey-hmac/adapter.rb, line 87 def date raise NotImplementedError end
@abstract @return [String] upcased request verb. i.e. 'GET'
# File lib/ey-hmac/adapter.rb, line 54 def method raise NotImplementedError end
@abstract @return [String] request path. i.e. '/blogs/1'
# File lib/ey-hmac/adapter.rb, line 60 def path raise NotImplementedError end
@abstract Add {#signature} header to request. Typically this is 'Authorization' or 'WWW-Authorization' @return [String] calculated {#authorization} @see Ey::Hmac#sign!
# File lib/ey-hmac/adapter.rb, line 101 def sign!(key_id, key_secret) raise NotImplementedError end
@param [String] key_secret private HMAC key @param [String] signature digest hash function. Defaults to sign_with
@return [String] HMAC signature of {#request}
# File lib/ey-hmac/adapter.rb, line 39 def signature(key_secret, digest = self.sign_with) Base64.strict_encode64( OpenSSL::HMAC.digest( OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize)).strip end
Protected Instance Methods
# File lib/ey-hmac/adapter.rb, line 165 def check_signature! authorization_match = AUTHORIZATION_REGEXP.match(authorization_signature) unless authorization_match raise Ey::Hmac::MissingAuthorization, "Failed to parse authorization_signature #{authorization_signature}" end [authorization_match[1], authorization_match[2]] end
# File lib/ey-hmac/adapter.rb, line 153 def check_ttl! if @ttl && date expiry = Time.parse(date).to_i + @ttl current_time = Time.now.to_i unless expiry > current_time raise Ey::Hmac::ExpiredHmac, "Signature has expired passed #{expiry}. Current time is #{current_time}" end end end
Constant time string comparison. pulled from github.com/rack/rack/blob/master/lib/rack/utils.rb#L399
# File lib/ey-hmac/adapter.rb, line 143 def secure_compare(a, b) return false unless a.bytesize == b.bytesize l = a.unpack("C*") r, i = 0, -1 b.each_byte { |v| r |= v ^ l[i+=1] } r == 0 end