# Message Signing & Webhook Verification BCB signs all webhooks and selected API messages to guarantee **authenticity**, **integrity**, and **replay protection**. Two signing methods are supported: - **HMAC-SHA256** – symmetric signing with a shared secret (legacy, still supported) - **RSA-PSS + SHA-256** – asymmetric signing with public/private keys (recommended for new integrations) Both methods use the **same signature string format**, **same timestamp/nonce headers**, and **same replay-protection rules**. ## Common Elements ### 1. Signature String (Canonicalization) Both HMAC and RSA sign the same canonical string constructed in this exact order: ``` {timestamp}{nonce}{method}{path}{body} ``` - **`timestamp`**: Unix epoch seconds (from `Bcb-Timestamp` header). - **`nonce`**: Unique identifier, typically a GUID (from `Bcb-Nonce` header). - **`method`**: HTTP verb in uppercase (`GET`, `POST`, etc.). - **`path`**: The absolute path starting with `/`, without the query string. - **For webhooks**: This is the path component of the webhook URL that BCB calls on your server (e.g., `/webhooks/payments`). - **`body`**: The raw request/response payload. - Use an empty string if there is no body. ### 2. Encoding and Body Rules To ensure consistent signatures, follow these rules strictly: - **Encoding**: Use **UTF-8** for all string-to-byte conversions. - **Raw Body**: Use the **raw bytes exactly as received** over the wire. - **Do NOT** re-serialize JSON (e.g., changing field order). - **Do NOT** normalize whitespace or pretty-print. - If the body is empty, use an empty string `""`. - **Compression**: Signature payload is the **logical HTTP body after decompression** (i.e., the raw UTF-8 string that your application/middleware receives). If BCB sends a gzipped response, your verifier must use the uncompressed content for signature verification. ### 3. Base64 vs Base64url - **`Bcb-Signature`**: This header always contains a standard **Base64** encoded signature. - **JWKS Fields**: Fields within the `/.well-known/jwks.json` (like `n` and `e`) are **Base64url** encoded as per the RFC 7517 spec. - **Copy/Paste Warning**: Ensure your libraries handle the conversion if you are manually extracting keys. ### 4. Standard Headers | Header | Description | Example | | --- | --- | --- | | `Bcb-Signature` | Base64-encoded signature | `a8f3b2...` | | `Bcb-Timestamp` | Unix epoch seconds | `1702987654` | | `Bcb-Nonce` | Unique request identifier | `abc-123-def-456` | | `Bcb-Signature-Version` | Indicates algorithm and key version | `rsa-v1` | **Note on `Bcb-Signature-Version`**: For RSA, this value serves two purposes: 1. It identifies the algorithm used (**RSA-PSS + SHA-256**). 2. It is the **literal Key ID (`kid`)**. You must use this value to look up the correct public key in the BCB `jwks.json` endpoint. **Operational Behavior**: - **Rotation**: When BCB rotates its signing key, the header value will change (e.g., from `rsa-v1` to `rsa-v2`). - **Grace Period**: During rotation, the `jwks.json` endpoint will publish **both** keys. - **Caching**: Clients should cache the JWKS response (5-minute TTL recommended) but **must refetch immediately** if they receive a message with an unknown `kid`. ## Replay Protection All signed messages **must** pass these checks before processing: 1. **Timestamp tolerance**: `|now - timestamp| ≤ 300` seconds (5 minutes). 2. **Nonce uniqueness**: Reject if `{timestamp}:{nonce}` has been seen before (cache for 5 minutes). 3. **Signature validity**: Rebuild the signature string and verify against the header. ## HMAC Signing (Symmetric) ### How It Works - BCB and the client share a **secret key** (provisioned during onboarding). - Both parties compute: `signature = Base64(HMACSHA256(signature_string, secret))`. ### Verification Steps (Node.js Example) ```javascript const crypto = require('crypto'); function verifyHmacSignature(req, rawBody, sharedSecret) { const signatureB64 = req.headers['bcb-signature']; const timestamp = req.headers['bcb-timestamp']; const nonce = req.headers['bcb-nonce']; if (!signatureB64 || !timestamp || !nonce) return false; const method = req.method.toUpperCase(); const path = req.originalUrl.split('?')[0]; const payload = rawBody ?? ''; const signatureString = `${timestamp}${nonce}${method}${path}${payload}`; const expectedB64 = crypto.createHmac('sha256', sharedSecret) .update(signatureString, 'utf8') .digest('base64'); // Safely compare signature bytes const sig = Buffer.from(signatureB64, 'base64'); const exp = Buffer.from(expectedB64, 'base64'); if (sig.length !== exp.length) return false; return crypto.timingSafeEqual(sig, exp); } ``` ## RSA Signing (Asymmetric) ### How It Works - **Bank → Client**: BCB signs with its private key; you verify with BCB's public key (from JWKS). - **Client → Bank**: You sign with your private key; BCB verifies with your public key. - Signing algorithm: **RSA-PSS + SHA-256**. ### Fetching Bank Public Keys (JWKS) ```http GET /v1/.well-known/jwks.json ``` Always use the `kid` from the `Bcb-Signature-Version` header to select the correct key from the JWKS response. ### Verification Steps (Node.js Example) ```javascript const crypto = require('crypto'); async function verifyRsaSignature(req, rawBody, publicKeyPem) { const signature = req.headers['bcb-signature']; const timestamp = req.headers['bcb-timestamp']; const nonce = req.headers['bcb-nonce']; const method = req.method.toUpperCase(); const path = req.originalUrl.split('?')[0]; const payload = rawBody ?? ''; const signatureString = `${timestamp}${nonce}${method}${path}${payload}`; const verify = crypto.createVerify('RSA-SHA256'); verify.update(signatureString, 'utf8'); return verify.verify({ key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, signature, 'base64'); } ``` ## Key Management For instructions on how to rotate your own keys and use the Self-Service API, see the [Key Management Guide](/guides/key-management).