Module: Apertur::Signature

Defined in:
lib/apertur/signature.rb

Overview

Webhook signature verification utilities.

Provides constant-time signature verification for three webhook formats used by the Apertur platform.

Class Method Summary collapse

Class Method Details

.secure_compare(a, b) ⇒ Boolean

Constant-time string comparison to prevent timing attacks.

Parameters:

  • a (String)
  • b (String)

Returns:

  • (Boolean)


71
72
73
74
75
76
77
# File 'lib/apertur/signature.rb', line 71

def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
rescue StandardError
  false
end

.verify_event(body, timestamp, signature, secret) ⇒ Boolean

Verify an event webhook signature (HMAC SHA256 method).

The signed payload is “#{timestamp}.#{body}” and the signature header is formatted as sha256=<hex>.

Parameters:

  • body (String)

    the raw request body

  • timestamp (String)

    the X-Apertur-Timestamp header value

  • signature (String)

    the X-Apertur-Signature header value

  • secret (String)

    the webhook signing secret

Returns:

  • (Boolean)

    true if the signature is valid



38
39
40
41
42
43
# File 'lib/apertur/signature.rb', line 38

def verify_event(body, timestamp, signature, secret)
  signature_base = "#{timestamp}.#{body}"
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_base)
  sig = signature.start_with?("sha256=") ? signature[7..] : signature
  secure_compare(expected, sig)
end

.verify_svix(body, svix_id, timestamp, signature, secret) ⇒ Boolean

Verify an event webhook signature (Svix method).

The signed payload is “#{svix_id}.#{timestamp}.#{body}” and the signing key is the secret decoded from hex. The signature header is formatted as v1,<base64>.

Parameters:

  • body (String)

    the raw request body

  • svix_id (String)

    the svix-id header value

  • timestamp (String)

    the svix-timestamp header value

  • signature (String)

    the svix-signature header value (e.g. “v1,base64…”)

  • secret (String)

    the webhook signing secret (hex-encoded)

Returns:

  • (Boolean)

    true if the signature is valid



57
58
59
60
61
62
63
64
# File 'lib/apertur/signature.rb', line 57

def verify_svix(body, svix_id, timestamp, signature, secret)
  signature_base = "#{svix_id}.#{timestamp}.#{body}"
  key = [secret].pack("H*")
  expected = OpenSSL::HMAC.digest("SHA256", key, signature_base)
  expected_b64 = [expected].pack("m0")
  sig = signature.start_with?("v1,") ? signature[3..] : signature
  secure_compare(expected_b64, sig)
end

.verify_webhook(body, signature, secret) ⇒ Boolean

Verify an image delivery webhook signature.

The signature header is formatted as sha256=<hex> and is computed as HMAC-SHA256(body, secret).

Parameters:

  • body (String)

    the raw request body

  • signature (String)

    the signature header value (e.g. “sha256=abc123…”)

  • secret (String)

    the webhook signing secret

Returns:

  • (Boolean)

    true if the signature is valid



22
23
24
25
26
# File 'lib/apertur/signature.rb', line 22

def verify_webhook(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  sig = signature.start_with?("sha256=") ? signature[7..] : signature
  secure_compare(expected, sig)
end