Module: Zeridion::Flare::Webhook
- Defined in:
- lib/zeridion_flare/webhook.rb
Overview
Verify outbound webhook signatures from Zeridion Flare.
Each delivery is signed with HMAC-SHA256 over <unix_timestamp>.<raw_body>
using the subscription secret. The signature is sent in the
X-Zeridion-Signature header in the form t=<unix_timestamp>,v1=<hex>.
Class Method Summary collapse
-
.secure_compare(a, b) ⇒ Object
Constant-time string comparison (defends against timing-attack signature-guessing).
-
.verify(payload, signature_header, secret, tolerance_seconds: nil) ⇒ Boolean
True iff the signature is valid (and within tolerance, if set).
Class Method Details
.secure_compare(a, b) ⇒ Object
Constant-time string comparison (defends against timing-attack signature-guessing). OpenSSL.secure_compare is available in Ruby 3.0+; we fall back to a hand-rolled loop for older runtimes if it's missing.
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/zeridion_flare/webhook.rb', line 59 def self.secure_compare(a, b) return false if a.bytesize != b.bytesize if OpenSSL.respond_to?(:secure_compare) OpenSSL.secure_compare(a, b) else l = a.unpack("C*") r = 0 i = -1 b.each_byte { |v| r |= v ^ l[i += 1] } r.zero? end end |
.verify(payload, signature_header, secret, tolerance_seconds: nil) ⇒ Boolean
Returns true iff the signature is valid (and within tolerance, if set).
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/zeridion_flare/webhook.rb', line 22 def self.verify(payload, signature_header, secret, tolerance_seconds: nil) return false if signature_header.nil? || signature_header.empty? return false if secret.nil? || secret.empty? = nil signatures = [] signature_header.split(",").each do |part| trimmed = part.strip eq = trimmed.index("=") next if eq.nil? || eq.zero? key = trimmed[0, eq] value = trimmed[(eq + 1)..] case key when "t" = Integer(value, 10) if value.match?(/\A\d+\z/) when "v1" signatures << value.downcase end end return false if .nil? || signatures.empty? if tolerance_seconds now = Time.now.to_i return false if (now - ).abs > tolerance_seconds end signing_input = "#{}.#{payload}" expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signing_input) signatures.any? { |provided| secure_compare(expected, provided) } end |