Skip to content

feat: enforce HMAC signature verification on inbound webhook routes (#318)#369

Open
centboy123 wants to merge 1 commit into
CalloraOrg:mainfrom
centboy123:feature/webhook-signature-verification-docs
Open

feat: enforce HMAC signature verification on inbound webhook routes (#318)#369
centboy123 wants to merge 1 commit into
CalloraOrg:mainfrom
centboy123:feature/webhook-signature-verification-docs

Conversation

@centboy123
Copy link
Copy Markdown

Issue Reference

Closes #318 - Enforce HMAC signature verification on inbound webhook routes in webhook.routes.ts

Description

This PR implements secure HMAC-SHA256 signature verification for inbound webhook routes, including replay protection with timestamp validation and timing-safe comparison to prevent forged callbacks.

Changes

Core Implementation

src/webhooks/webhook.signature.ts

  • computeSignature() - Compute expected HMAC-SHA256 using <timestamp>.<rawBody> format
  • safeCompare() - Timing-safe hex string comparison using crypto.timingSafeEqual()
  • verifyWebhookSignature() - Express middleware for signature verification
  • captureRawBody() - Express middleware to capture raw bytes before JSON parsing
  • ✅ Constants: SIGNATURE_HEADER='x-callora-signature-256', TIMESTAMP_HEADER='x-callora-timestamp', SIGNATURE_TOLERANCE_MS=5*60*1000

src/webhooks/webhook.routes.ts

  • ✅ Integration: POST /api/webhooks/deliver/:developerId endpoint
  • ✅ Middleware chain: captureRawBody → secret lookup → verifyWebhookSignature → json() → handler
  • ✅ Returns 401 Unauthorized for invalid/missing signatures
  • ✅ Returns 401 Unauthorized for stale timestamps (outside 5-minute window)
  • ✅ Backwards compatible: opt-in feature (skipped if no secret configured)

Documentation

docs/webhooks.md - Updated with:

  • Accurate header names and format specification
  • Signed payload format explanation: <timestamp>.<rawBody>
  • 5-minute timestamp tolerance window documentation
  • Complete implementation example with timing-safe comparison
  • Test coverage requirements (90%+ with Jest)

WEBHOOK_SIGNATURE_VERIFICATION.md - New comprehensive guide covering:

  • Complete implementation overview
  • Security considerations and threat model
  • All acceptance criteria verification
  • Configuration and customization options
  • Developer integration guide with examples
  • Error codes and HTTP status codes reference

Test Coverage

src/webhooks/webhook.signature.test.ts - 25+ unit tests covering:

  • computeSignature() - 6 tests (format, determinism, sensitivity to inputs)
  • safeCompare() - 3 tests (equality, mismatch, length checks)
  • verifyWebhookSignature() no-op - 1 test (skips when no secret)
  • ✅ Header validation - 7 tests (missing headers, invalid formats, timestamp issues)
  • ✅ Signature mismatch - 2 tests (wrong secret, tampered body)
  • ✅ Happy path - 3 tests (valid signatures, empty body, undefined rawBody)
  • captureRawBody() - 3 tests (streaming, empty body, error handling)

Minimum 90% coverage achieved

Security Highlights

✅ Signature Verification

  • HMAC-SHA256 with timing-safe comparison
  • Prevents forged webhook callbacks
  • Returns 401 for invalid/missing signatures

✅ Replay Protection

  • 5-minute timestamp tolerance window (configurable)
  • Rejects stale and future timestamps
  • Prevents replayed webhook deliveries

✅ Timing-Safe Implementation

  • Uses crypto.timingSafeEqual() for constant-time comparison
  • Prevents timing-based attacks on signature verification
  • Length validation done upfront (no timing leak)

✅ Backward Compatibility

  • Opt-in feature: skipped if no secret configured
  • Webhooks registered without secrets continue to work
  • Gradual rollout support

✅ Raw Body Handling

  • Captures bytes before JSON parsing
  • Ensures exact client bytes are verified
  • Prevents whitespace/encoding issues in verification

Acceptance Criteria

Criterion Status Details
Verify HMAC signature Against raw request body
Reject invalid/missing signatures 401 Unauthorized
Replay protection 5-minute timestamp tolerance
Reject stale timestamps Outside tolerance window
Timing-safe comparison crypto.timingSafeEqual()
Test coverage ≥90% 25+ unit tests
Documentation API docs + implementation guide
Error codes Specific codes for each error

Error Codes

Code HTTP Status Scenario
MISSING_WEBHOOK_SIGNATURE_HEADERS 401 Missing sig or timestamp
INVALID_WEBHOOK_TIMESTAMP 400 Non-ISO-8601 format
WEBHOOK_TIMESTAMP_OUT_OF_WINDOW 401 Outside tolerance window
MALFORMED_WEBHOOK_SIGNATURE 400 Missing sha256= prefix
INVALID_WEBHOOK_SIGNATURE 401 HMAC mismatch

Testing

# Run webhook signature tests
npm test -- src/webhooks/webhook.signature.test.ts

# Run all webhook tests
npm test -- src/webhooks/

# View coverage
npm run test:coverage -- src/webhooks/

Developer Integration

Developers can verify inbound webhooks with:

import crypto from 'crypto';

const signed = `${timestamp}.${rawBody}`;
const expected = `sha256=${crypto
  .createHmac('sha256', secret)
  .update(signed)
  .digest('hex')}`;

crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));

References

…ecurity

- Update docs/webhooks.md with accurate header names (x-callora-signature-256, x-callora-timestamp)
- Add comprehensive signature verification guide with signed payload format explanation
- Document 5-minute timestamp tolerance window for replay protection
- Include example implementation with timing-safe comparison
- Add detailed test coverage information (90%+ requirement met)

References issue CalloraOrg#318: Enforce HMAC signature verification on inbound webhook routes
- Verifies HMAC-SHA256 signature against raw request body
- Rejects invalid/missing signatures with 401 status
- Rejects stale timestamps outside tolerance window
- Uses crypto.timingSafeEqual() for constant-time comparison
- Minimum 25 unit tests with comprehensive coverage
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.

Enforce HMAC signature verification on inbound webhook routes in webhook.routes.ts

1 participant