Self-Service Key Management API
This guide covers how to manage your RSA public keys for asymmetric message signing using the BCB Self-Service API.
Overview
BCB allows you to manage your own RSA public keys to ensure zero-downtime rotation. You can store up to two public keys per ApiClient:
- Primary key: The active key currently used to verify your inbound requests.
- Secondary key: A staging slot used for testing and safe rotation.
All endpoints require authentication with strong client credentials and the manage-credentials scope.
Rotation Paths
You can choose between two paths for rotating your keys, depending on your safety requirements:
Path A: Simple Rotation (2 Steps)
Best for rapid development or non-critical environments.
- Upload:
PUT /v1/credentials/{clientId}/keys/secondary - Promote:
POST /v1/credentials/{clientId}/keys/promote
Path B: Safe Rotation (4 Steps - Recommended)
Industry standard for production environments. Prevents accidental lockout by verifying the key works before making it primary.
- Upload:
PUT /v1/credentials/{clientId}/keys/secondary - Challenge:
POST /v1/credentials/{clientId}/keys/secondary/challenge - Verify:
POST /v1/credentials/{clientId}/keys/secondary/verify(Proof-of-Possession) - Promote:
POST /v1/credentials/{clientId}/keys/promote
API Endpoints
1. View Key Metadata
Returns fingerprints, algorithms, and verification status.
GET /v1/credentials/{clientId}/keys
Authorization: Bearer {access_token}
2. Upload Secondary Key
Uploads a PEM-formatted RSA public key.
PUT /v1/credentials/{clientId}/keys/secondary
Content-Type: application/json
Authorization: Bearer {access_token}
{
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkq...\n-----END PUBLIC KEY-----"
}
3. Request Proof-of-Possession Challenge
Returns a Base64 string that you must sign to prove ownership.
Format: The decoded challenge is a dot-separated string: {clientId}.{nonce}.{expiry}.{keyFingerprint}.
POST /v1/credentials/{clientId}/keys/secondary/challenge
Authorization: Bearer {access_token}
Response:
{
"challenge": "YWJjLTEyMy1kZWYtNDU2LjE3MDI5ODc2NTQuYTNmMmIxYzQ...",
"expiresUtc": "2024-01-15T10:35:00Z"
}
4. Verify Proof-of-Possession
Submit the signature for the challenge.
POST /v1/credentials/{clientId}/keys/secondary/verify
Content-Type: application/json
Authorization: Bearer {access_token}
{
"challenge": "YWJjLTEyMy1kZWYtNDU2LjE3MDI5ODc2NTQuYTNmMmIxYzQ...",
"signature": "dGhpcyBpcyBhIHNpZ25hdHVyZQ..."
}
5. Promote Secondary to Primary
Makes the secondary key the new active primary key.
POST /v1/credentials/{clientId}/keys/promote
Authorization: Bearer {access_token}
Response: Key metadata showing the new primary key fingerprint.
Important Logic:
- Copy vs Swap: This operation copies the secondary key into the primary slot and clears the secondary slot.
- Verification Requirement: While the API allows promoting an unverified key (Simple Path), it is strongly recommended to complete the PoP flow first. The server check for "verified" is reflected in the
secondaryKeyVerifiedfield of the metadata. - Immediate Effect: Once promoted, the old primary key is discarded and cannot be used for verification anymore.
6. Delete Secondary Key
Removes the staged secondary key. Useful for aborting a rotation or correcting a mistake.
DELETE /v1/credentials/{clientId}/keys/secondary
Authorization: Bearer {access_token}
Response: Key metadata showing the secondary slot is now empty.
Proof-of-Possession (PoP) Clarity
What exactly do I sign?
The challenge received from the /challenge endpoint is a Base64-encoded string. You must sign the decoded raw bytes of that challenge.
- Decoding: Decode the challenge string from Base64 to get the raw challenge bytes.
- Signing: Use RSA-PSS + SHA-256 to sign those raw bytes with your private key.
- Verification: Submit the resulting signature as a standard Base64 encoded string.
Do NOT sign the Base64 string itself. The server decodes your submitted challenge before verifying the signature against the original raw bytes.
Complete Key Rotation Tutorial (Safe Path)
- Generate Keys: Create a new RSA-3072 or RSA-4096 pair.
- Upload: Use
PUT /keys/secondaryto upload the new public key. - Challenge: Use
POST /keys/secondary/challengeto get the challenge string. - Sign Locally:
const crypto = require('crypto');
// challenge received from API (Base64 string)
const signature = crypto.createSign('RSA-SHA256')
.update(Buffer.from(challenge, 'base64')) // Decode and sign raw bytes
.sign({
key: privateKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
}, 'base64');
- Verify: Use
POST /keys/secondary/verifywith the signature. - Promote: Use
POST /keys/promoteto swap the keys.
Troubleshooting
- "Challenge has expired": Challenges are valid for 5 minutes. Request a new one.
- "Signature verification failed": Ensure you are using RSA-PSS padding and signing the decoded raw bytes of the Base64 challenge string. Do NOT sign the Base64 string literal.
- Base64 vs Base64url:
- Signatures submitted to the API must be standard Base64.
- Public keys in
JWKSuse Base64url. Be careful not to swap them when manually testing.