Skip to main content

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...
PartDescription
tUnix timestamp (seconds) of when the request was sent.
v1HMAC-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.

info

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 value
  • v1. 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.

danger

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();
}
note

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)