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
-
.secure_compare(a, b) ⇒ Boolean
Constant-time string comparison to prevent timing attacks.
-
.sign_request(secret, method, path, body, timestamp) ⇒ Hash{String=>String}
Sign an outgoing API request (HMAC SHA256 method).
-
.verify_event(body, timestamp, signature, secret) ⇒ Boolean
Verify an event webhook signature (HMAC SHA256 method).
-
.verify_svix(body, svix_id, timestamp, signature, secret) ⇒ Boolean
Verify an event webhook signature (Svix method).
-
.verify_webhook(body, signature, secret) ⇒ Boolean
Verify an image delivery webhook signature.
Class Method Details
.secure_compare(a, b) ⇒ Boolean
Constant-time string comparison to prevent timing attacks.
96 97 98 99 100 101 102 |
# File 'lib/apertur/signature.rb', line 96 def secure_compare(a, b) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) rescue StandardError false end |
.sign_request(secret, method, path, body, timestamp) ⇒ Hash{String=>String}
Sign an outgoing API request (HMAC SHA256 method).
The signed payload is
"\#{timestamp}.\#{method.upcase}.\#{path}.\#{sha256hex(body)}" and the
signature header is formatted as sha256=<hex>.
81 82 83 84 85 86 87 88 89 |
# File 'lib/apertur/signature.rb', line 81 def sign_request(secret, method, path, body, ) body_hash = OpenSSL::Digest::SHA256.hexdigest(body || "") signature_base = "#{}.#{method.to_s.upcase}.#{path}.#{body_hash}" signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_base) { "X-Aptr-Signature" => "sha256=#{signature}", "X-Aptr-Timestamp" => .to_s } 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>.
38 39 40 41 42 43 |
# File 'lib/apertur/signature.rb', line 38 def verify_event(body, , signature, secret) signature_base = "#{}.#{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>.
57 58 59 60 61 62 63 64 |
# File 'lib/apertur/signature.rb', line 57 def verify_svix(body, svix_id, , signature, secret) signature_base = "#{svix_id}.#{}.#{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).
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 |