fix(billing): compare Stripe webhook signature in constant time (BUNYIP-107) #147

Merged
vas2000-work merged 1 commit from fix/BUNYIP-107-webhook-constant-time into main 2026-06-16 01:00:12 +02:00
Owner

What

Compare the Stripe webhook signature in constant time. The verifier hex-encoded the expected HMAC and compared it to each provided v1 signature with ==, which short-circuits on the first differing byte and leaks timing. This is on the billing-grant path (a forged webhook drives membership/entitlement state), so constant-time comparison is the expected hygiene (Stripe's own SDK uses it).

Change

  • Decode each provided v1 signature from hex and verify it against the computed MAC with hmac's constant-time verify_slice, instead of hex-encoding the expected MAC and string-comparing.
  • The MAC is cloned per candidate (verify_slice consumes it).
  • Behavior is unchanged: valid signatures pass, malformed/wrong ones fail. The timestamp tolerance (+/-300s) and multi-v1 handling are preserved.

Testing (rust-builder container)

  • cargo test -p bunyip-domain --lib stripe: 23 pass, including the existing valid/invalid/old-timestamp/missing-timestamp/no-v1 tests and a new verify_webhook_signature_wrong_but_valid_hex that exercises the constant-time rejection path (valid hex, wrong value).
  • cargo clippy -p bunyip-domain --all-targets -- -D warnings: clean.
  • cargo fmt --all --check: clean.
## What Compare the Stripe webhook signature in constant time. The verifier hex-encoded the expected HMAC and compared it to each provided `v1` signature with `==`, which short-circuits on the first differing byte and leaks timing. This is on the billing-grant path (a forged webhook drives membership/entitlement state), so constant-time comparison is the expected hygiene (Stripe's own SDK uses it). ## Change - Decode each provided `v1` signature from hex and verify it against the computed MAC with hmac's constant-time `verify_slice`, instead of hex-encoding the expected MAC and string-comparing. - The MAC is cloned per candidate (`verify_slice` consumes it). - Behavior is unchanged: valid signatures pass, malformed/wrong ones fail. The timestamp tolerance (+/-300s) and multi-`v1` handling are preserved. ## Testing (rust-builder container) - `cargo test -p bunyip-domain --lib stripe`: 23 pass, including the existing valid/invalid/old-timestamp/missing-timestamp/no-v1 tests and a new `verify_webhook_signature_wrong_but_valid_hex` that exercises the constant-time rejection path (valid hex, wrong value). - `cargo clippy -p bunyip-domain --all-targets -- -D warnings`: clean. - `cargo fmt --all --check`: clean.
fix(billing): compare Stripe webhook signature in constant time (BUNYIP-107)
All checks were successful
Check / fmt / clippy / build / test (pull_request) Successful in 1m4s
Create release / Create release from merged PR (pull_request) Has been skipped
c943316a3e
The webhook signature check hex-encoded the expected HMAC and compared it to each provided v1 signature with `==`, which short-circuits on the first differing byte and leaks timing on the billing-grant path. Decode each v1 signature from hex and verify it against the computed MAC with hmac's constant-time `verify_slice` instead. Behavior is unchanged for valid and malformed signatures; adds a test covering a valid-hex-but-wrong signature (the constant-time rejection path).

#BUNYIP-107

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vas2000-work deleted branch fix/BUNYIP-107-webhook-constant-time 2026-06-16 01:00:12 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!147
No description provided.