Module: AgentAdmit::Webhook
- Defined in:
- lib/agentadmit/webhook.rb
Overview
Verification for inbound AgentAdmit alert webhooks.
AgentAdmit signs every alert webhook delivery with the app’s webhook signing secret (whsec_…, returned once when the webhook URL is configured). The signature arrives in the X-AgentAdmit-Signature header:
X-AgentAdmit-Signature: t=<unix_ts>,v1=<hex hmac-sha256>
where the HMAC input is “t.raw_body” keyed with the full whsec_ secret. Always verify against the raw request body (request.raw_post in Rails), before any JSON parsing.
Constant Summary collapse
- SIGNATURE_HEADER =
Header AgentAdmit signs alert webhook deliveries with.
"X-AgentAdmit-Signature"- DEFAULT_TOLERANCE_SECONDS =
Default maximum clock skew (seconds) allowed for replay protection.
300
Class Method Summary collapse
-
.secure_compare(expected, candidate) ⇒ Object
private
Constant-time string comparison (portable — OpenSSL.secure_compare is not available on every openssl gem version).
-
.valid_signature?(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) ⇒ Boolean
Boolean form of Webhook.verify_signature.
-
.verify_signature(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) ⇒ void
Verify the X-AgentAdmit-Signature header on an inbound alert webhook.
Class Method Details
.secure_compare(expected, candidate) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Constant-time string comparison (portable — OpenSSL.secure_compare is not available on every openssl gem version).
102 103 104 105 106 107 108 109 |
# File 'lib/agentadmit/webhook.rb', line 102 def secure_compare(expected, candidate) return false unless expected.bytesize == candidate.bytesize diff = 0 expected_bytes = expected.unpack("C*") candidate.each_byte.with_index { |byte, i| diff |= byte ^ expected_bytes[i] } diff.zero? end |
.valid_signature?(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) ⇒ Boolean
Boolean form of verify_signature.
89 90 91 92 93 94 |
# File 'lib/agentadmit/webhook.rb', line 89 def valid_signature?(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) verify_signature(payload, header, secret, tolerance: tolerance, now: now) true rescue WebhookSignatureError false end |
.verify_signature(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) ⇒ void
This method returns an undefined value.
Verify the X-AgentAdmit-Signature header on an inbound alert webhook.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/agentadmit/webhook.rb', line 54 def verify_signature(payload, header, secret, tolerance: DEFAULT_TOLERANCE_SECONDS, now: nil) raise WebhookSignatureError, "Webhook signing secret is required" if secret.nil? || secret.empty? raise WebhookSignatureError, "Missing X-AgentAdmit-Signature header" if header.nil? || header.empty? = nil candidates = [] header.split(",").each do |part| key, _, value = part.strip.partition("=") case key when "t" raise WebhookSignatureError, "Malformed signature header" unless value.match?(/\A\d+\z/) = Integer(value) when "v1" candidates << value end end raise WebhookSignatureError, "Malformed signature header" if .nil? || candidates.empty? if tolerance.positive? && ((now || Time.now.to_i) - ).abs > tolerance raise WebhookSignatureError, "Signature timestamp outside tolerance window" end expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{}.#{payload}") matched = candidates.any? { |candidate| secure_compare(expected, candidate) } raise WebhookSignatureError, "Webhook signature verification failed" unless matched end |