Class: CoreLibrary::HmacSignatureVerifier

Inherits:
SignatureVerifier
  • Object
show all
Defined in:
lib/apimatic-core/security/signature_verification/hmac_signature_verifier.rb

Overview

HmacSignatureVerifier ===

Verifies HMAC signatures for incoming requests.

Works with Rack::Request or any object exposing Rack-like ‘env` or `headers`/`raw_body`.

Example:

verifier = HmacSignatureVerifier.new(
  secret_key: "supersecret",
  signature_header: "X-Signature"
)
result = verifier.verify(rack_request)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret_key:, signature_header:, canonical_message_builder: nil, hash_algorithm: 'sha256', encoder: HexEncoder.new, signature_value_template: DIGEST_PLACEHOLDER) ⇒ HmacSignatureVerifier

Returns a new instance of HmacSignatureVerifier.

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/apimatic-core/security/signature_verification/hmac_signature_verifier.rb', line 49

def initialize(secret_key:, signature_header:, canonical_message_builder: nil, hash_algorithm: 'sha256',
               encoder: HexEncoder.new, signature_value_template: DIGEST_PLACEHOLDER)
  raise ArgumentError, 'secret_key must be a non-empty string' unless secret_key.is_a?(String) && !secret_key.empty?

  unless signature_header.is_a?(String) && !signature_header.strip.empty?
    raise ArgumentError,
          'signature_header must be a non-empty string'
  end

  @secret_key = secret_key
  @signature_header_lc = signature_header.strip.downcase.tr('_', '-')
  @canonical_message_builder = canonical_message_builder
  @hash_alg = hash_algorithm
  @encoder = encoder
  @signature_value_template = signature_value_template
end

Class Method Details

.fixed_length_secure_compare(a, b) ⇒ Boolean

Compares two strings in constant time to prevent timing attacks. Uses OpenSSL.fixed_length_secure_compare when available (Ruby ≥ 3 with openssl gem ≥ 3.x); falls back to a pure Ruby implementation otherwise.

Parameters:

  • a (String)

    the first string to compare

  • b (String)

    the second string to compare

Returns:

  • (Boolean)

    true if the strings are equal, false otherwise

Raises:

  • (ArgumentError)

    if the inputs are of unequal length



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/apimatic-core/security/signature_verification/hmac_signature_verifier.rb', line 112

def self.fixed_length_secure_compare(a, b)
  if RUBY_VERSION >= '3.0' && OpenSSL.respond_to?(:fixed_length_secure_compare)
    OpenSSL.fixed_length_secure_compare(a, b)
  else
    raise ArgumentError, 'inputs must be same length' unless a.bytesize == b.bytesize

    res = 0
    a.bytes.zip(b.bytes) { |x, y| res |= (x ^ y) }
    res.zero?
  end
end

Instance Method Details

#verify(request) ⇒ CoreLibrary::SignatureVerificationResult

Verifies the HMAC signature for the request.

Parameters:

  • request (Rack::Request, #env, #headers, #raw_body)

Returns:

  • (CoreLibrary::SignatureVerificationResult)


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/apimatic-core/security/signature_verification/hmac_signature_verifier.rb', line 70

def verify(request)
  headers = RackRequestHelper.extract_headers_hash(request)
  provided_signature = headers[@signature_header_lc]

  if provided_signature.nil?
    return CoreLibrary::SignatureVerificationResult.failed(
      ["Signature header '#{@signature_header_lc}' is missing"]
    )
  end

  message = resolve_message_bytes(request)
  digest = OpenSSL::HMAC.digest(@hash_alg, @secret_key, message)
  encoded_digest = @encoder.encode(digest) unless @encoder.nil?

  expected_signature =
    if @signature_value_template.include?(DIGEST_PLACEHOLDER)
      @signature_value_template.gsub(DIGEST_PLACEHOLDER, encoded_digest)
    else
      @signature_value_template
    end

  if HmacSignatureVerifier.fixed_length_secure_compare(provided_signature, expected_signature)
    CoreLibrary::SignatureVerificationResult.passed
  else
    CoreLibrary::SignatureVerificationResult.failed(
      ['Signature mismatch']
    )
  end
rescue StandardError => e
  CoreLibrary::SignatureVerificationResult.failed(
    ["Signature verification failed: #{e.message}"]
  )
end