Signature Verification
If webhook signing is enabled for your integration, signature verification is mandatory.
Do not treat it as an optional hardening step. It is part of the public webhook contract.
Signing inputs
Red Cloud sends these headers:
X-RedCloud-TimestampX-RedCloud-Signature
Verification formula:
HMAC_SHA256(secret, "${timestamp}.${rawBody}")
Where:
secretis your webhook signing secrettimestampis the exact value ofX-RedCloud-TimestamprawBodyis the exact request body bytes delivered to your endpoint
Verification rules
Your handler should:
- read the raw body before mutating or re-serializing it
- extract
X-RedCloud-Timestamp - extract
X-RedCloud-Signature - compute the expected HMAC from
timestamp.rawBody - compare signatures using constant-time comparison
- reject the request if the signature is invalid
Common implementation mistake
The most common failure is verifying against parsed JSON instead of the raw body.
Wrong:
Parse JSON -> stringify object -> compute HMAC
Correct:
Read raw bytes -> compute HMAC -> compare -> then parse JSON
Timestamp handling
The timestamp is part of the signature input. Treat it as a required header.
Recommended handling:
- reject requests with a missing timestamp
- reject requests with a missing signature
- optionally enforce a replay window in your own system if your security model requires it
If you enforce a replay window, keep your server clocks in sync and make sure the policy is documented internally for support teams.
Node.js example
import crypto from "node:crypto";
function verifyWebhookSignature({ rawBody, timestamp, signature, secret }) {
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
const actual = Buffer.from(signature, "utf8");
const wanted = Buffer.from(expected, "utf8");
if (actual.length !== wanted.length) {
return false;
}
return crypto.timingSafeEqual(actual, wanted);
}
Python example
import hashlib
import hmac
def verify_webhook_signature(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
payload = timestamp.encode("utf-8") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Handler sequencing recommendation
For a production webhook handler:
- capture raw body
- verify signature
- parse JSON
- validate required fields
- apply idempotent event handling
- persist the accepted event
- return
200
Do not return 200 before the event is accepted by your internal processing path.
Troubleshooting signature failures
Check:
- the raw body is actually raw and unmodified
- the correct secret is being used
- the timestamp header is included in the signed payload
- hex encoding matches what your verifier expects
- no middleware rewrites the body before verification
Likely causes:
- body parser runs before signature verification
- wrong secret in the environment
- accidental whitespace or newline mutation
- signature compared against parsed JSON instead of the raw body