Validating Webhooks
BlockWyre signs all outbound webhooks using RS512 (RSA-SHA512) detached JWS signatures. This allows you to verify that webhooks are authentic and haven't been tampered with, without needing a shared secret.
How It Works
- BlockWyre signs each webhook payload with an RSA private key
- The signature is sent as a detached JWS in the
X-BlockWyre-Signatureheader - You fetch BlockWyre's public key from the JWKS endpoint to verify the signature
JWKS Endpoint
GET /.well-known/jwks.json
This endpoint is public (no authentication required) and returns BlockWyre's current public keys in JWK Set format.
Response:
{
"keys": [
{
"kty": "RSA",
"kid": "blockwyre-webhook-key-1",
"alg": "RS512",
"use": "sig",
"n": "...",
"e": "AQAB"
}
]
}
Caching: The endpoint returns Cache-Control: public, max-age=3600. Cache the JWKS for up to 1 hour and refresh on a kid miss (indicating key rotation).
Signature Format
The signature is a detached JWS in compact serialization:
<base64url-header>..<base64url-signature>
Note the empty payload segment (two consecutive dots). The actual payload is the raw HTTP request body.
Header (decoded):
{
"alg": "RS512",
"kid": "blockwyre-webhook-key-1"
}
Signature Version
| Version | Format | Algorithm |
|---|---|---|
20260201 (current) | Detached JWS | RS512 |
Verification Steps
- Fetch the JWKS from
GET /.well-known/jwks.json(cache for 1 hour, refresh onkidmiss) - Extract the
kidfrom the JWS header (base64url-decode the part before..) - Find the matching key in the JWKS by
kid - Reconstruct the full JWS by inserting the raw request body (base64url-encoded) between the two dots
- Verify the RS512 signature using the public key from JWKS
- Validate the timestamp from
X-BlockWyre-Timestamp— reject if older than 5 minutes
Code Examples
Node.js
const jose = require('jose');
// Cache the JWKS - refresh every hour or on kid miss
let jwksCache = null;
let jwksCacheTime = 0;
async function getJWKS(baseUrl) {
const now = Date.now();
if (jwksCache && now - jwksCacheTime < 3600000) {
return jwksCache;
}
const response = await fetch(`${baseUrl}/.well-known/jwks.json`);
jwksCache = await response.json();
jwksCacheTime = now;
return jwksCache;
}
async function verifyWebhook(req, baseUrl) {
const signature = req.headers['x-blockwyre-signature'];
const signatureVersion = req.headers['x-blockwyre-signature-version'];
const timestamp = req.headers['x-blockwyre-timestamp'];
// Check signature version
if (signatureVersion !== '20260209') {
throw new Error(`Unsupported signature version: ${signatureVersion}`);
}
// Reject stale webhooks (> 5 minutes)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
// Validate detached JWS format
const parts = signature.split('..');
if (parts.length !== 2) {
throw new Error('Invalid signature format');
}
// Reconstruct full JWS with payload
const rawBody = typeof req.body === 'string'
? req.body
: JSON.stringify(req.body);
const encodedPayload = Buffer.from(rawBody).toString('base64url');
const fullJWS = `${parts[0]}.${encodedPayload}.${parts[1]}`;
// Fetch JWKS and verify
const jwks = await getJWKS(baseUrl);
const { payload } = await jose.compactVerify(
fullJWS,
jose.createLocalJWKSet(jwks)
);
return JSON.parse(new TextDecoder().decode(payload));
}
Replay Attack Prevention
Always validate the X-BlockWyre-Timestamp header to prevent replay attacks. Reject any webhook with a timestamp older than 5 minutes from your server's current time.
Additionally, you can track the X-BlockWyre-Webhook-ID header to detect and reject duplicate deliveries.
Key Rotation
BlockWyre may rotate signing keys periodically. When this happens:
- A new key pair is generated with a new
kid - The JWKS endpoint is updated to include the new public key
- New webhooks are signed with the new key
- The old public key remains in the JWKS for a transition period
Your implementation should handle this automatically if you:
- Look up keys by
kid(not by index) - Refresh the JWKS cache when you encounter an unknown
kid
Support and Resources
If you need assistance or have any questions, our support team is here to help. You can contact our support team at support@blockwyre.com.
Stay Updated
Stay up-to-date with the latest news, updates, and features from BlockWyre by following us on social media:
- Instagram: BlockWyre Instagram
- Twitter: BlockWyre Twitter
- Facebook: BlockWyre Facebook
- LinkedIn: BlockWyre LinkedIn
We are excited to have you on board and look forward to seeing how you leverage BlockWyre's powerful tools to enhance your financial operations. Happy integrating!