API User Onboarding Workflow
This guide describes the workflow for onboarding API users to the Custody Engine system. API users are programmatic clients that authenticate using cryptographic key pairs rather than traditional username/password credentials.
Overview
The Custody Engine uses a dual-key cryptographic authentication system for API users. This approach provides several security advantages over traditional authentication methods:
Why Cryptographic Authentication?
| Traditional Auth | Cryptographic Auth |
|---|---|
| Passwords can be intercepted | Private keys never leave the client |
| Session tokens can be stolen | Each request is individually signed |
| Server stores sensitive credentials | Server only stores public keys |
Key Benefits:
- Non-repudiation: Every request is cryptographically signed, creating an audit trail that proves who initiated each action
- No knowledge of private keys: The server never sees or stores private keys, eliminating credential theft from server breaches
- End-to-end encryption: Request payloads are encrypted, protecting sensitive transaction data in transit
- Device binding: Each device has unique credentials, enabling granular access control and revocation
The Dual-Key System
- ECDSA (P-256): Used for request signing and verification
- RSA (2048-bit): Used for payload encryption and decryption
Why two key types? Each algorithm is optimized for its specific purpose:
| Key Type | Purpose | Why This Algorithm? |
|---|---|---|
| ECDSA | Digital signatures | Fast signing, compact signatures (64 bytes), widely supported |
| RSA | Encryption | Better suited for encrypting larger payloads, industry standard |
Each API user must generate both key pairs before registration. Upon successful creation, the server returns a device_uuid that uniquely identifies the user's authentication context. This UUID links the user's keys to their identity and must be included in all subsequent requests.
Onboarding Flow Diagram
What is COSE?
COSE (CBOR Object Signing and Encryption) is the message format used by the Custody Engine. It provides:
- Compact binary encoding: More efficient than JSON for cryptographic data
- Standardized structure: RFC 8152 compliant format
- Layered security: Supports both signing and encryption in a single envelope
- Interoperability: Used in WebAuthn, IoT, and other security-critical systems
The SDK handles all COSE encoding/decoding automatically - you work with regular JavaScript objects.
Prerequisites
Before onboarding an API user, ensure you have:
- Admin-level access to create users
- The
@blockstream/ecs-js-sdkpackage installed - Access to the server's public keys (ECDSA and RSA)
Step-by-Step Onboarding Process
Step 1: Generate Cryptographic Key Pairs
The client must generate two key pairs before registration.
| Key | Format | Description |
|---|---|---|
ecdsa.privateKey | Raw bytes (32 bytes) | Private key for signing requests |
ecdsa.publicKeyHex | Hex string (66 chars) | Compressed P-256 public key |
rsa.privateKeyPem | PEM (PKCS#8) | Private key for decrypting responses |
rsa.publicKeyPem | PEM (SPKI) | Public key for server to encrypt responses |
Important: Store the private keys securely. They cannot be recovered if lost.
Step 2: Create the API User
Any user with ability to invite new users can submit a user creation request with the generated public keys:
import { v4 as uuidv4 } from 'uuid'
const createPayload = await broadcastRequest({
action: 'add',
resource: '/users',
details: {
uid: uuidv4(),
email: 'api_user@example.com',
first_name: 'API',
last_name: 'User',
ecdsa_pubkey: userKeys.ecdsa.publicKeyHex,
rsa_pubkey: userKeys.rsa.publicKeyPem,
type: 'api',
},
})Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
uid | UUID | Yes | Unique identifier for the user |
email | String | Yes | Email address (must be unique) |
first_name | String | Yes | User's first name |
last_name | String | Yes | User's last name |
ecdsa_pubkey | Hex String | Yes | Compressed ECDSA public key (66 hex characters) |
rsa_pubkey | PEM String | Yes | RSA public key in PEM format |
type | String | Yes | Must be "api" for API users |
Response
On success, the server returns:
const {
uid, // The user's unique identifier
device_uuid, // Device identifier for authentication
server_ecdsa_pubkey, // Server's ECDSA public key (for verifying responses)
server_rsa_pubkey, // Server's RSA public key (for encrypting requests)
} = createPayload.detailsStep 3: Initialize the Blockstream SDK
With the keys and device UUID, initialize the SDK for authenticated requests:
import { Blockstream } from '@blockstream/ecs-js-sdk'
const userInstance = new Blockstream(
userKeys.rsa.privateKeyPem, // User's RSA private key
userKeys.ecdsa.privateKey, // User's ECDSA private key
device_uuid, // Device UUID from creation response
SERVER_RSA_PUBLIC_KEY_PEM, // Server's RSA public key
SERVER_ECDSA_PUBLIC_KEY, // Server's ECDSA public key
)SDK Constructor Parameters
| Parameter | Type | Description |
|---|---|---|
rsaPrivateKey | PEM String | User's RSA private key for decrypting server responses |
ecdsaPrivateKey | Uint8Array | User's ECDSA private key for signing requests |
deviceUuid | String | Unique device identifier returned during user creation |
serverRsaPublicKey | PEM String | Server's RSA public key for encrypting request payloads |
serverEcdsaPublicKey | Uint8Array | Server's ECDSA public key for verifying response signatures |
Note: server keys are provided by the server during registration with invitation or in invitation response.
Step 4: Make Authenticated Requests
The SDK instance handles all cryptographic operations automatically:
const response = await broadcastRequest(
{
action: 'get',
resource: `/users/${userId}`,
},
{},
userInstance, // Pass the initialized SDK instance
)[!NOTE]
broadcastRequesttakes 3 parameters:request,options, anduserInstance.requestis an object withaction,resourceanddetailsfor the API request.optionsis an optional object for customizing the request to send unprotected request or configure COSE options.userInstanceis the initialized SDK instance that handles authentication and signing.
All requests are:
- Signed with the user's ECDSA private key
- Encrypted with the server's RSA public key
- Wrapped in COSE (CBOR Object Signing and Encryption) format
Key Format Specifications
ECDSA Key Requirements
| Property | Specification |
|---|---|
| Curve | P-256 (secp256r1) |
| Private Key Size | 32 bytes (256 bits) |
| Public Key Format | Compressed (33 bytes / 66 hex characters) |
| Usage | Request signing and response verification |
RSA Key Requirements
| Property | Specification |
|---|---|
| Key Size | 2048 bits |
| Public Key Format | PEM (SPKI encoding) |
| Private Key Format | PEM (PKCS#8 encoding) |
| Algorithm | RSA-OAEP with SHA-256 |
| Usage | Payload encryption and decryption |
Device Management
Each API user can have multiple devices (key pairs). Use these endpoints to manage devices:
| Operation | Endpoint | HTTP Method | Description |
|---|---|---|---|
| List Devices | /users/{userId}/devices | GET | Retrieve all devices for a user |
| Invite Device | /users/{userId}/invite-device | POST | Generate invitation for a new device |
| Delete Device | /users/{userId}/devices/{deviceUuid} | DELETE | Remove a specific device |
| Authorize Device | /account-management/authorize | POST | Complete device registration |
Key Rotation
TODO
Security Considerations
-
Private Key Storage: Never expose private keys in logs, version control, or client-side code. Use secure key management solutions.
-
Key Generation: Always generate keys using cryptographically secure random number generators.
-
Device Tracking: The server tracks
last_access_atfor each device, enabling audit trails and inactive device detection. -
Password Protection: If a password is set during device authorization for non-api useres, all subsequent devices will require it for registration.
Example: Complete Onboarding Flow
import { Blockstream } from '@blockstream/ecs-js-sdk'
import { v4 as uuidv4 } from 'uuid'
import { generateUserKeyPairs } from './helpers/users'
async function onboardApiUser(email: string, firstName: string, lastName: string) {
// 1. Generate key pairs
const userKeys = await generateUserKeyPairs()
// 2. Create the user (as admin)
const createResponse = await broadcastRequest({
action: 'add',
resource: '/users',
details: {
uid: uuidv4(),
email,
first_name: firstName,
last_name: lastName,
ecdsa_pubkey: userKeys.ecdsa.publicKeyHex,
rsa_pubkey: userKeys.rsa.publicKeyPem,
type: 'api',
},
})
if (createResponse.status !== 'success') {
throw new Error(`User creation failed: ${createResponse.message}`)
}
const { device_uuid, uid } = createResponse.details
// 3. Initialize SDK for the new user
const userInstance = new Blockstream(
userKeys.rsa.privateKeyPem,
userKeys.ecdsa.privateKey,
device_uuid,
SERVER_RSA_PUBLIC_KEY_PEM,
SERVER_ECDSA_PUBLIC_KEY,
)
// 4. Verify by fetching user profile
const profileResponse = await broadcastRequest(
{ action: 'get', resource: `/users/${uid}` },
{},
userInstance,
)
return {
userId: uid,
deviceUuid: device_uuid,
instance: userInstance,
keys: {
ecdsa: {
privateKey: userKeys.ecdsa.privateKey,
publicKeyHex: userKeys.ecdsa.publicKeyHex,
},
rsa: {
privateKeyPem: userKeys.rsa.privateKeyPem,
publicKeyPem: userKeys.rsa.publicKeyPem,
},
},
}
}Troubleshooting
| Issue | Possible Cause | Solution |
|---|---|---|
unauthorized response | Invalid or mismatched keys | Verify key pair matches what was registered |
device_uuid missing | User creation failed | Check response status and error message |
| Signature verification failed | Wrong ECDSA key format | Ensure public key is compressed (33 bytes) |
| Decryption failed | RSA key mismatch | Verify PEM format and key size (2048-bit) |
| Invalid COSE message | Back end failed to encrypt response or decrypt request | Enhance error propagation to the client without revealing sensitive details |
Onboarding a Regular User
How a regular (human) user registers their device using an admin-issued invitation and establishes a persistent authenticated session.
Wallet Onboarding Workflow
This guide describes the workflow for creating and configuring wallets in the Custody Engine system. It covers the different wallet types, signer setup, and asset management.