Module: Wire::Webhook
- Defined in:
- lib/wire/webhook.rb
Overview
Webhook verifies inbound webhook signatures.
Header format: “WirePayment-Signature: t=<unix>,v1=<hex>” where
hex = HMAC-SHA256(secret, "<t>.<rawBody>")
Verification runs on the RAW request body, before any JSON parsing.
Constant Summary collapse
- SIGNATURE_HEADER =
"WirePayment-Signature"- DEFAULT_TOLERANCE_SECONDS =
300
Class Method Summary collapse
-
.parse_header(header) ⇒ Object
parse_header extracts t (Integer) and v1 (hex String) from the header.
-
.secure_compare(a, b) ⇒ Object
Constant-time comparison.
-
.verify(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Object
Verify a webhook signature and return the parsed event (Hash).
-
.verify_at(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now:) ⇒ Object
verify_at is the testable core taking an explicit ‘now` (unix seconds).
Class Method Details
.parse_header(header) ⇒ Object
parse_header extracts t (Integer) and v1 (hex String) from the header. Returns [nil, nil] when t is missing or not an integer.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/wire/webhook.rb', line 57 def parse_header(header) t = nil v1 = nil header.to_s.split(",").each do |part| k, v = part.strip.split("=", 2) case k when "t" parsed = Integer(v, exception: false) return [nil, nil] if parsed.nil? t = parsed when "v1" v1 = v end end [t, v1] end |
.secure_compare(a, b) ⇒ Object
Constant-time comparison. Prefers OpenSSL’s fixed-length compare, falling back to a Rack-style XOR comparison on older rubies.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/wire/webhook.rb', line 77 def secure_compare(a, b) a = a.to_s b = b.to_s if OpenSSL.respond_to?(:fixed_length_secure_compare) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) else return false unless a.bytesize == b.bytesize l = a.unpack("C*") res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res.zero? end end |
.verify(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Object
Verify a webhook signature and return the parsed event (Hash).
32 33 34 |
# File 'lib/wire/webhook.rb', line 32 def verify(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS) verify_at(payload, header, secret, tolerance: tolerance, now: Time.now.to_i) end |
.verify_at(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now:) ⇒ Object
verify_at is the testable core taking an explicit ‘now` (unix seconds).
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/wire/webhook.rb', line 37 def verify_at(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now:) t, v1 = parse_header(header) raise SignatureVerificationError, "malformed signature header" if t.nil? || v1.nil? || v1.empty? if (now - t).abs > tolerance raise SignatureVerificationError, "timestamp outside tolerance" end body = payload.to_s expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{body}") unless secure_compare(expected, v1) raise SignatureVerificationError, "signature mismatch" end JSON.parse(body) end |