class Cyclid::HMAC::Signer

Helper class that provides signing capabilites for the hmac strategies.

@author Felix Gilcher <felix.gilcher@asquera.de>

Constants

DEFAULT_OPTS

HMAC defaults

Attributes

algorithm[RW]
default_opts[RW]
secret[RW]

Public Class Methods

new(algorithm = 'sha256', default_opts = {}) click to toggle source

create a new HMAC instance

@param [String] algorithm The hashing-algorithm to use. See the openssl documentation for valid values. @param [Hash] default_opts The default options for all calls that take opts

@option default_opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names @option default_opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication @option default_opts [String] :auth_header ('Authorization') The name of the authorization header to use @option default_opts [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature. @option default_opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce @option default_opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header @option default_opts [Bool] :query_based (false) Whether to use query based authentication @option default_opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date` @option default_opts [Hash] :extra_auth_params ({}) Additional parameters to inject in the auth parameter @option default_opts [Array<Symbol>] :ignore_params ([]) Params to ignore for signing

# File lib/cyclid/hmac.rb, line 64
def initialize(algorithm = 'sha256', default_opts = {})
  self.algorithm = algorithm
  default_opts[:nonce_header] ||= 'X-%{scheme}-Nonce' % { scheme: (default_opts[:auth_scheme] || 'HMAC') }
  default_opts[:alternate_date_header] ||= 'X-%{scheme}-Date' % { scheme: (default_opts[:auth_scheme] || 'HMAC') }
  self.default_opts = DEFAULT_OPTS.merge(default_opts)
end

Public Instance Methods

canonical_representation(params) click to toggle source

generates the canonical representation for a given request

@param [Hash] params the parameters to create the representation with @option params [String] :method The HTTP Verb of the request @option params [String] :date The date of the request as it was formatted in the request @option params [String] :nonce ('') The nonce given in the request @option params [String] :path The path portion of the request @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param. @option params [Hash] :headers ({}) All headers given in the request (optional and required) @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication @option params [Hash] :extra_auth_params ({}) Additional parameters to inject in the auth parameter @option params [Array<Symbol>] :ignore_params ([]) Params to ignore for signing @option params [String] :auth_header ('Authorization') The name of the authorization header to use @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature. @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header @option params [Bool] :query_based (false) Whether to use query based authentication @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`

@return [String] the canonical representation

# File lib/cyclid/hmac.rb, line 154
def canonical_representation(params)
  rep = ''

  rep << "#{params[:method].upcase}\n"
  rep << "date:#{params[:date]}\n"
  rep << "nonce:#{params[:nonce]}\n"

  (params[:headers] || {}).sort.each do |pair|
    name, value = *pair
    rep << "#{name.downcase}:#{value}\n"
  end

  rep << params[:path]

  p = (params[:query] || {}).dup

  unless p.empty?
    query = p.sort.map do |key, value|
      '%{key}=%{value}' % {
        key: Rack::Utils.unescape(key.to_s),
        value: Rack::Utils.unescape(value.to_s)
      }
    end.join('&')
    rep << "?#{query}"
  end

  rep
end
generate_signature(params) click to toggle source

Generate the signature from a hash representation

returns nil if no secret or an empty secret was given

@param [Hash] params the parameters to create the representation with @option params [String] :secret The secret to generate the signature with @option params [String] :method The HTTP Verb of the request @option params [String] :date The date of the request as it was formatted in the request @option params [String] :nonce ('') The nonce given in the request @option params [String] :path The path portion of the request @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param. @option params [Hash] :headers ({}) All headers given in the request (optional and required) @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication @option params [Hash] :extra_auth_params ({}) Additional parameters to inject in the auth parameter @option params [Array<Symbol>] :ignore_params ([]) Params to ignore for signing @option params [String] :auth_header ('Authorization') The name of the authorization header to use @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature. @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header @option params [Bool] :query_based (false) Whether to use query based authentication @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`

@return [String] the signature

# File lib/cyclid/hmac.rb, line 95
def generate_signature(params)
  secret = params.delete(:secret)

  # jruby stumbles over empty secrets, we regard them as invalid anyways, so we return an empty digest if no scret was given
  if '' == secret.to_s
    nil
  else
    OpenSSL::HMAC.hexdigest(algorithm, secret, canonical_representation(params))
  end
end
sign_request(url, secret, opts = {}) click to toggle source

sign the given request

@param [String] url The url of the request @param [String] secret The shared secret for the signature @param [Hash] opts Options for the signature generation

@option opts [String] :nonce ('') The nonce to use in the signature @option opts [String, strftime] :date (Time.now) The date to use in the signature @option opts [Hash] :headers ({}) A list of optional headers to include in the signature @option opts [String,Symbol] :method ('GET') The HTTP method to use in the signature

@option opts [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names @option opts [String] :auth_param ('auth') The name of the authentication param to use for query based authentication @option opts [Hash] :extra_auth_params ({}) Additional parameters to inject in the auth parameter @option opts [Array<Symbol>] :ignore_params ([]) Params to ignore for signing @option opts [String] :auth_header ('Authorization') The name of the authorization header to use @option opts [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature. @option opts [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce @option opts [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header @option opts [Bool] :query_based (false) Whether to use query based authentication @option opts [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`

# File lib/cyclid/hmac.rb, line 205
def sign_request(url, secret, opts = {})
  opts = default_opts.merge(opts)

  uri = parse_url(url)
  headers = opts[:headers] || {}

  date = opts[:date] || Time.now.gmtime
  date = date.gmtime.strftime('%a, %d %b %Y %T GMT') if date.respond_to? :strftime

  method = opts[:method] ? opts[:method].to_s.upcase : 'GET'

  query_values = Rack::Utils.parse_nested_query(uri.query)

  if query_values
    query_values.delete_if do |k, _v|
      opts[:ignore_params].one? { |param| (k == param) || (k == param.to_s) }
    end
  end

  signature = generate_signature(secret: secret, method: method, path: uri.path, date: date, nonce: opts[:nonce], query: query_values, headers: opts[:headers], ignore_params: opts[:ignore_params])

  if opts[:query_based]
    auth_params = opts[:extra_auth_params].merge('date' => date,
                                                 'signature' => signature)
    auth_params[:nonce] = opts[:nonce] unless opts[:nonce].nil?

    query_values ||= {}
    query_values[opts[:auth_param]] = auth_params
    uri.query = Rack::Utils.build_nested_query(query_values)
  else
    headers[opts[:auth_header]]   = opts[:auth_header_format] % opts.merge(signature: signature)
    headers[opts[:nonce_header]]  = opts[:nonce] unless opts[:nonce].nil?

    if opts[:use_alternate_date_header]
      headers[opts[:alternate_date_header]] = date
    else
      headers['Date'] = date
    end
  end

  [headers, uri.to_s]
end
validate_signature(signature, params) click to toggle source

compares the given signature with the signature created from a hash representation

@param [String] signature the signature to compare with @param [Hash] params the parameters to create the representation with @option params [String] :secret The secret to generate the signature with @option params [String] :method The HTTP Verb of the request @option params [String] :date The date of the request as it was formatted in the request @option params [String] :nonce ('') The nonce given in the request @option params [String] :path The path portion of the request @option params [Hash] :query ({}) The query parameters given in the request. Must not contain the auth param. @option params [Hash] :headers ({}) All headers given in the request (optional and required) @option params [String] :auth_scheme ('HMAC') The name of the authorization scheme used in the Authorization header and to construct various header-names @option params [String] :auth_param ('auth') The name of the authentication param to use for query based authentication @option params [Hash] :extra_auth_params ({}) Additional parameters to inject in the auth parameter @option params [Array<Symbol>] :ignore_params ([]) Params to ignore for signing @option params [String] :auth_header ('Authorization') The name of the authorization header to use @option params [String] :auth_header_format ('%{auth_scheme} %{signature}') The format of the authorization header. Will be interpolated with the given options and the signature. @option params [String] :nonce_header ('X-#{auth_scheme}-Nonce') The header name for the request nonce @option params [String] :alternate_date_header ('X-#{auth_scheme}-Date') The header name for the alternate date header @option params [Bool] :query_based (false) Whether to use query based authentication @option params [Bool] :use_alternate_date_header (false) Use the alternate date header instead of `Date`

@return [Bool] true if the signature matches

# File lib/cyclid/hmac.rb, line 129
def validate_signature(signature, params)
  compare_hashes(signature, generate_signature(params))
end

Private Instance Methods

compare_hashes(presented, computed) click to toggle source

compares two hashes in a manner that's invulnerable to timing sidechannel attacks (see issue #16) by comparing them characterwise up to the end in all cases, no matter where the mismatch happens short circuits if the length does not match since this does not allow timing sidechannel attacks.

# File lib/cyclid/hmac.rb, line 253
def compare_hashes(presented, computed)
  if computed.length == presented.length
    computed.chars.zip(presented.chars).map { |x, y| x == y }.all?
  else
    false
  end
end
parse_url(url) click to toggle source

parse url if url parameter is string do nothing if parameter is URI

# File lib/cyclid/hmac.rb, line 262
def parse_url(url)
  return url if url.is_a?(URI)
  URI.parse(url)
end