Signature Verification
Every webhook request from Tower Crown includes a cryptographic signature. Verifying it lets you confirm the request genuinely came from us and that the body wasn't modified in transit.
The Signature Header
Each delivery includes the X-Towercrown-Signature header in this format:
X-Towercrown-Signature: t=1782691200,v1=5d2f627a8db3c1f2e4a9b8...
| Part | Description |
|---|---|
t | Unix timestamp (seconds) of when the request was sent. |
v1 | HMAC-SHA256 hex digest of the signing payload. |
How the Signature is Computed
Tower Crown computes the signature like this:
signing_payload = t + "." + raw_request_body
signature = HMAC-SHA256(secret, signing_payload)
Where secret is the plaintext value of your signing secret (starting with whsec_), and raw_request_body is the exact bytes of the JSON body as received.
Use the raw body bytes, not a parsed and re-serialized version. Re-serializing JSON can change key ordering or whitespace and break the comparison.
Verification Steps
1. Extract the timestamp and signature
Split X-Towercrown-Signature by , and extract:
t. the timestamp valuev1. the signature hex string
2. Guard against replay attacks
Check that the timestamp is within an acceptable window of your current system time (5 minutes is a reasonable threshold). Reject anything older.
3. Reconstruct the signing payload
signing_payload = t + "." + raw_body
4. Compute HMAC-SHA256
Compute HMAC-SHA256(secret, signing_payload) using your stored signing secret as the key. Encode the result as a lowercase hex string.
5. Compare signatures
Use a constant-time comparison function to compare your computed signature against v1. This prevents timing-based attacks.
Never use a simple === or == comparison for cryptographic values. Use constant-time equality (e.g. crypto.timingSafeEqual in Node.js).
Example. Node.js / Express
const crypto = require('crypto');
function verifyTowerCrownSignature(req, res, next) {
const signatureHeader = req.headers['x-towercrown-signature'];
const secret = process.env.TOWERCROWN_WEBHOOK_SECRET;
if (!signatureHeader || !secret) {
return res.status(401).send('Missing signature or secret');
}
const parts = Object.fromEntries(
signatureHeader.split(',').map(part => part.split('='))
);
const { t: timestamp, v1: receivedSignature } = parts;
if (!timestamp || !receivedSignature) {
return res.status(400).send('Malformed signature header');
}
// Reject stale requests (> 5 minutes old)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
return res.status(400).send('Signature timestamp expired');
}
// Reconstruct and verify
const signingPayload = `${timestamp}.${req.rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signingPayload)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expected, 'hex')
);
if (!isValid) {
return res.status(403).send('Invalid signature');
}
next();
}
Make sure you pass the raw body buffer to your HMAC computation, not req.body. In Express, this means using express.raw({ type: 'application/json' }) before the express.json() middleware, and reading from req.rawBody.
Example. Python
import hmac
import hashlib
import time
def verify_tower_crown_signature(headers: dict, raw_body: bytes, secret: str) -> bool:
sig_header = headers.get("x-towercrown-signature", "")
parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
timestamp = parts.get("t")
received = parts.get("v1")
if not timestamp or not received:
return False
# Reject stale requests
if abs(time.time() - int(timestamp)) > 300:
return False
signing_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
secret.encode(), signing_payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)