Skip to content

feat: support webhook signing-key rotation without downtime - #1156

Merged
greatest0fallt1me merged 1 commit into
CalloraOrg:mainfrom
Agbasimere:feature/webhook-signing-key-rotation
Aug 27, 2026
Merged

feat: support webhook signing-key rotation without downtime#1156
greatest0fallt1me merged 1 commit into
CalloraOrg:mainfrom
Agbasimere:feature/webhook-signing-key-rotation

Conversation

@Agbasimere

@Agbasimere Agbasimere commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes: #1147

Summary

Inbound webhook verification now accepts the current signing secret and the previous secret only while the configured rotation grace window is open. HMAC comparison is constant-time across every active key, timestamps outside the skew window are rejected, and nonces are persisted so replays fail. Failure responses never identify which key matched.

Acceptance criteria

  • Both keys work only in the configured rotation window — WebhookStore.getActiveSecrets / expirePreviousSecret; src/webhooks/webhook.deliver.test.ts, src/webhooks/webhook.store.test.ts
  • Old timestamps are rejected — verifyWebhookSignature + skew tests in webhook.signature.test.ts / webhook.deliver.test.ts
  • Reused nonces are rejected — WebhookNonceStore + replay tests
  • Failure responses do not reveal the matched key — generic INVALID_WEBHOOK_SIGNATURE / WEBHOOK_NONCE_REPLAYED message; matchesAnySecret does not short-circuit
  • Tests cover rotation, skew, replay, malformed headers, and deletion

Security and failure modes

  • Nonce is bound into the HMAC (<timestamp>.<nonce>.<rawBody>), so a captured signature cannot be replayed with a fresh nonce.
  • Nonces are scoped per developer and purged on webhook deletion.
  • Signature verification always compares every active secret; errors never include key material, current/previous, or a matched-key index.
  • Invalid signatures do not persist a nonce (no unauthenticated nonce oracle).
  • JSON is parsed from the captured raw body so verification and parsing share the same bytes.

Test plan

  • POST /api/webhooks/:id/rotate-secret then deliver with old and new secrets inside the grace window (both 200)
  • Deliver with the previous secret after previous_expires_at (401, no key identity in the body)
  • Timestamp older/newer than 5 minutes (401 WEBHOOK_TIMESTAMP_OUT_OF_WINDOW)
  • Repeat the same signed request (401 WEBHOOK_NONCE_REPLAYED)
  • Malformed sha256= / nonce headers (400)
  • DELETE /api/webhooks/:id then deliver (404)

Accept current and previous secrets only inside the grace window, compare HMACs in constant time, persist nonces to reject replays, and keep failure responses from identifying which key matched.
@greatest0fallt1me
greatest0fallt1me merged commit 8f3f2b0 into CalloraOrg:main Aug 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[GrantFox][High] Support webhook signing-key rotation

2 participants