This implementation adds explicit signature versioning and bounded timestamp validation semantics to webhook deliveries, enabling safe evolution of signing algorithms without breaking existing consumers.
Implementation:
- Added
X-Webhook-Signature-Versionheader to all signed webhook deliveries - Header value is an integer (currently "1" for HMAC-SHA256)
- Stored in database via
WebhookDelivery.signature_versioncolumn
Example Header:
X-Webhook-Signature: sha256=abc123...def456
X-Webhook-Signature-Version: 1
Location: docs/WEBHOOK_INTEGRATION.md
Documented Semantics:
- Format: ISO 8601 with microseconds (
2026-04-29T14:30:45.123456) - Timezone: UTC (Zulu time)
- Immutability: Same across all retry attempts and signature versions
- Location: Top-level
timestampfield in JSON payload
Strategy:
- New signature algorithm can be introduced as v2, v3, etc.
- Receivers read
X-Webhook-Signature-Versionheader to select verification logic - During migration phase: dual-sign with both old and new algorithms
- Old receivers work unchanged (ignore headers they don't understand)
- New receivers upgrade incrementally
File: alembic/versions/0016_webhook_signature_versioning.py
- Adds
signature_versioncolumn towebhook_deliveriestable - Default value: 1
- Nullable: False
File: app/services/webhook_signing.py (NEW)
sign_payload_v1(): HMAC-SHA256 signingverify_signature_v1(): HMAC-SHA256 verification (constant-time)sign_payload(): Polymorphic signing with version supportverify_signature(): Polymorphic verification with safe fallback- Comprehensive module docstring with versioning and timestamp semantics
File: docs/WEBHOOK_INTEGRATION.md (NEW)
- 300+ lines covering:
- Signature verification with code examples (Python & Node.js)
- Timestamp validation patterns and recommendations
- Version evolution strategy
- Security best practices
- Operational monitoring guidance
File: tests/test_webhook_signature_versioning.py (NEW)
- 40+ test cases in 10 test classes:
TestSignatureVersioningV1: HMAC-SHA256 core testsTestSignatureVersioning: Polymorphic signing testsTestWebhookHeadersWithSignatureVersion: Header generationTestWebhookDeliveryWithSignatureVersion: Model storageTestTimestampValidationSemantics: Idempotency/freshnessTestSignatureVersionEvolution: Future-proofingTestWebhookSigningIntegration: End-to-end scenarios
File: app/models/webhook.py
- Added
signature_version: Column(Integer, default=1, nullable=False)toWebhookDelivery
File: app/services/webhook_service.py
- Imports: Now uses
webhook_signingmodule instead of inline HMAC _build_headers(): Acceptssignature_versionparameter, includes version headercreate_delivery(): Accepts and storessignature_versionparametertrigger_sla_violation_webhooks(): Documents versioning behavior, passes through versiondispatch_delivery(): Uses storedsignature_versionfrom delivery record
File: app/api/v1/endpoints/webhooks.py
- Added
signature_version: inttoWebhookDeliveryResponsePydantic schema
POST https://consumer.example.com/webhook HTTP/1.1
Content-Type: application/json
X-Webhook-Event: sla.violation
X-Webhook-Timestamp: 2026-04-29T14:30:45.123456
X-Webhook-Signature: sha256=abc123def456...
X-Webhook-Signature-Version: 1
{
"schema_version": "1",
"event": "sla.violation",
"timestamp": "2026-04-29T14:30:45.123456",
"data": { ... }
}import hmac
import hashlib
def verify_webhook(request_body: str, headers: dict, secret: str) -> bool:
# Read signature version (enables future algorithm evolution)
sig_version = int(headers.get('X-Webhook-Signature-Version', '1'))
signature_header = headers.get('X-Webhook-Signature', '')
if sig_version == 1:
# HMAC-SHA256 verification
provided_sig = signature_header.replace('sha256=', '')
expected_sig = hmac.new(
secret.encode(),
request_body.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, provided_sig)
elif sig_version == 2:
# Future algorithm (e.g., EdDSA)
return verify_v2(signature_header, request_body, secret)
else:
# Unknown version - fail securely
return FalseConsumers can deduplicate retried webhooks:
class DeliveredWebhook(db.Model):
webhook_id = Column(UUID, primary_key=True)
timestamp = Column(DateTime, primary_key=True)
event_type = Column(String)
payload_hash = Column(String)
processed_at = Column(DateTime)
# Query to check if webhook was already processed
existing = db.query(DeliveredWebhook).filter(
DeliveredWebhook.webhook_id == webhook_id,
DeliveredWebhook.timestamp == datetime.fromisoformat(payload['timestamp'])
).first()
if existing:
return 200 # Already processed, return successBenefits:
- Retry-safe: Same delivery won't be processed twice
- Consumer-controlled: Each can set its own deduplication window
- Audit trail: Track when each event occurred vs processed
- Graceful failure recovery: Replayed deliveries have same timestamp
- HMAC-SHA256 signature generation (consistent, deterministic)
- Signature verification (valid/invalid/tampered/wrong-secret)
- Timing-safe comparison (prevents timing attacks)
- Unsupported version handling (fails safely)
- Header generation with version metadata
- Timestamp immutability across retries
- Idempotency deduplication patterns
- Freshness validation calculations
- Future algorithm evolution path
- Integration scenarios (end-to-end signing)
-
Database Migration:
alembic upgrade 0016_webhook_sig_versioning
-
Syntax Check:
python -m py_compile app/services/webhook_signing.py python -m py_compile app/services/webhook_service.py
-
Run Tests:
pytest tests/test_webhook_signature_versioning.py -v
-
Integration Test:
pytest tests/test_contract_parity.py -k webhook -v
-
Phase 0 (Current):
- Existing webhooks continue to work
- New field added with
default=1 - All new deliveries automatically get
signature_version=1
-
Phase 1 (Future): If shifting to EdDSA:
- Create v2 signing function
- Deploy dual-signing (v1 + v2)
- New deliveries include both headers
-
Phase 2:
- Monitor version adoption via metrics
- Announce v1 deprecation timeline
-
Phase 3:
- Support window for v2 migration
- Provide consumer upgrade guidance
-
Phase 4:
- Remove v1 support
✅ Constant-Time Comparison: Uses hmac.compare_digest() to prevent timing attacks
✅ Versioning Fails Safe: Unknown versions return False (not crash)
✅ Timestamp Immutability: Same timestamp across retries prevents manipulation
✅ Explicit Headers: Version info enables transparent algorithm evolution
✅ Future Extensibility: Can add v2, v3, etc. without changes to payload structure
✅ Existing webhooks: Continue to work unchanged
✅ New deliveries: Automatically include version headers
✅ Old consumers: Ignore new headers (header-safe protocol)
✅ Database: Migration adds column with sensible default
✅ No payload changes: Timestamp already present, unchanged structure
- Algorithm Transition Timeline: When should v2 be introduced? (6-12 months ideal for adoption)
- Monitoring: Should we track version distribution in metrics?
- Deprecation Period: How long to support old algorithms during transition?
- Consumer Communication: When/how to notify users about versioning?
- Signature Standard: RFC 5869 (HMAC-based Key Derivation)
- Timestamp Format: ISO 8601 (RFC 3339), UTC timezone
- Timing Attacks: OWASP timing-attack prevention
- Consumer Examples: See
docs/WEBHOOK_INTEGRATION.md