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.

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.



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