Class: Philiprehberger::WebhookSignature::Verifier

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/webhook_signature/verifier.rb

Overview

Verifies HMAC webhook signatures with replay prevention.

Constant Summary collapse

DEFAULT_TOLERANCE =

5 minutes

300
SUPPORTED_ALGORITHMS =
{
  sha256: 'SHA256',
  sha512: 'SHA512'
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(secret, algorithm: :sha256) ⇒ Verifier

Returns a new instance of Verifier.

Parameters:

  • secret (String)

    the shared secret key

  • algorithm (Symbol) (defaults to: :sha256)

    HMAC digest algorithm (:sha256 or :sha512)

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 18

def initialize(secret, algorithm: :sha256)
  raise ArgumentError, 'Secret must be a non-empty string' if secret.nil? || secret.empty?

  unless SUPPORTED_ALGORITHMS.key?(algorithm)
    raise ArgumentError,
          "Unsupported algorithm: #{algorithm.inspect}. Supported algorithms: #{SUPPORTED_ALGORITHMS.keys.map(&:inspect).join(', ')}"
  end

  @secret = secret
  @algorithm = algorithm
  @digest_name = SUPPORTED_ALGORITHMS.fetch(algorithm)
end

Instance Method Details

#valid?(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE) ⇒ Boolean

Boolean wrapper around verify!.

Parameters:

  • payload (String)

    the raw payload body

  • timestamp (Integer)

    the timestamp from the signature

  • signature (String)

    the hex-encoded HMAC signature

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds (nil to skip replay check)

Returns:

  • (Boolean)


92
93
94
95
96
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 92

def valid?(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE)
  verify!(payload, timestamp: timestamp, signature: signature, tolerance: tolerance)
rescue VerificationError
  false
end

#valid_header?(payload, header:, tolerance: DEFAULT_TOLERANCE) ⇒ Boolean

Boolean wrapper around verify_header!.

Parameters:

  • payload (String)

    the raw payload body

  • header (String)

    the header value in “t=TIMESTAMP,v1=SIGNATURE” format

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds

Returns:

  • (Boolean)


104
105
106
107
108
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 104

def valid_header?(payload, header:, tolerance: DEFAULT_TOLERANCE)
  verify_header!(payload, header: header, tolerance: tolerance)
rescue VerificationError
  false
end

#verify(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE) ⇒ Boolean

Verify a payload against a signature.

Parameters:

  • payload (String)

    the raw payload body

  • timestamp (Integer)

    the timestamp from the signature

  • signature (String)

    the hex-encoded HMAC signature

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds (nil to skip replay check)

Returns:

  • (Boolean)

    true if valid



38
39
40
41
42
43
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 38

def verify(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE)
  return false if tolerance && stale?(timestamp, tolerance)

  expected = compute_signature(payload, timestamp)
  secure_compare(expected, signature)
end

#verify!(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE) ⇒ true

Verify and raise on failure.

Parameters:

  • payload (String)

    the raw payload body

  • timestamp (Integer)

    the timestamp from the signature

  • signature (String)

    the hex-encoded HMAC signature

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds (nil to skip replay check)

Returns:

  • (true)

Raises:



63
64
65
66
67
68
69
70
71
72
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 63

def verify!(payload, timestamp:, signature:, tolerance: DEFAULT_TOLERANCE)
  if tolerance && stale?(timestamp, tolerance)
    raise VerificationError, "Signature timestamp is too old (tolerance: #{tolerance}s)"
  end

  expected = compute_signature(payload, timestamp)
  raise VerificationError, 'Signature mismatch' unless secure_compare(expected, signature)

  true
end

#verify_header(payload, header:, tolerance: DEFAULT_TOLERANCE) ⇒ Boolean

Verify a signature header string.

Parameters:

  • payload (String)

    the raw payload body

  • header (String)

    the header value in “t=TIMESTAMP,v1=SIGNATURE” format

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds

Returns:

  • (Boolean)

    true if valid



51
52
53
54
55
56
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 51

def verify_header(payload, header:, tolerance: DEFAULT_TOLERANCE)
  parsed = parse_header(header)
  return false unless parsed

  verify(payload, timestamp: parsed[:timestamp], signature: parsed[:signature], tolerance: tolerance)
end

#verify_header!(payload, header:, tolerance: DEFAULT_TOLERANCE) ⇒ true

Verify a header string or raise on failure.

Parameters:

  • payload (String)

    the raw payload body

  • header (String)

    the header value in “t=TIMESTAMP,v1=SIGNATURE” format

  • tolerance (Integer, nil) (defaults to: DEFAULT_TOLERANCE)

    max age in seconds

Returns:

  • (true)

Raises:



81
82
83
84
85
86
# File 'lib/philiprehberger/webhook_signature/verifier.rb', line 81

def verify_header!(payload, header:, tolerance: DEFAULT_TOLERANCE)
  parsed = parse_header(header)
  raise VerificationError, 'Invalid header format' unless parsed

  verify!(payload, timestamp: parsed[:timestamp], signature: parsed[:signature], tolerance: tolerance)
end