Skip to content
Last updated

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

HeaderDescriptionExample
Bcb-SignatureBase64-encoded signaturea8f3b2...
Bcb-TimestampUnix epoch seconds1702987654
Bcb-NonceUnique request identifierabc-123-def-456
Bcb-Signature-VersionIndicates algorithm and key versionrsa-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)

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)

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)

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.