Blockstream Enterprise
Onboarding

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 AuthCryptographic Auth
Passwords can be interceptedPrivate keys never leave the client
Session tokens can be stolenEach request is individually signed
Server stores sensitive credentialsServer only stores public keys

Key Benefits:

  1. Non-repudiation: Every request is cryptographically signed, creating an audit trail that proves who initiated each action
  2. No knowledge of private keys: The server never sees or stores private keys, eliminating credential theft from server breaches
  3. End-to-end encryption: Request payloads are encrypted, protecting sensitive transaction data in transit
  4. 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 TypePurposeWhy This Algorithm?
ECDSADigital signaturesFast signing, compact signatures (64 bytes), widely supported
RSAEncryptionBetter 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

rect [rgb(240, 248, 255)] rect [rgb(255, 248, 240)] rect [rgb(240, 255, 240)] rect [rgb(255, 240, 255)] Generate ECDSA key pair (P-256) Generate RSA key pair (2048-bit) POST /users(with public keys, type: "api") Create user record Generate device_uuid Return device_uuid + server public keys Share device_uuid Initialize Blockstream SDK(user keys + device_uuid + server keys) Sign request (ECDSA) Encrypt payload (RSA) COSE-wrapped request Verify signature Decrypt payload COSE-wrapped response Step 2: User Creation Step 3: SDK Initialization Step 4: Authenticated Requests Client Admin Server

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:

  1. Admin-level access to create users
  2. The @blockstream/ecs-js-sdk package installed
  3. 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.

KeyFormatDescription
ecdsa.privateKeyRaw bytes (32 bytes)Private key for signing requests
ecdsa.publicKeyHexHex string (66 chars)Compressed P-256 public key
rsa.privateKeyPemPEM (PKCS#8)Private key for decrypting responses
rsa.publicKeyPemPEM (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

FieldTypeRequiredDescription
uidUUIDYesUnique identifier for the user
emailStringYesEmail address (must be unique)
first_nameStringYesUser's first name
last_nameStringYesUser's last name
ecdsa_pubkeyHex StringYesCompressed ECDSA public key (66 hex characters)
rsa_pubkeyPEM StringYesRSA public key in PEM format
typeStringYesMust 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.details

Step 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

ParameterTypeDescription
rsaPrivateKeyPEM StringUser's RSA private key for decrypting server responses
ecdsaPrivateKeyUint8ArrayUser's ECDSA private key for signing requests
deviceUuidStringUnique device identifier returned during user creation
serverRsaPublicKeyPEM StringServer's RSA public key for encrypting request payloads
serverEcdsaPublicKeyUint8ArrayServer'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]
broadcastRequest takes 3 parameters: request, options, and userInstance. request is an object with action, resource and details for the API request. options is an optional object for customizing the request to send unprotected request or configure COSE options. userInstance is 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

PropertySpecification
CurveP-256 (secp256r1)
Private Key Size32 bytes (256 bits)
Public Key FormatCompressed (33 bytes / 66 hex characters)
UsageRequest signing and response verification

RSA Key Requirements

PropertySpecification
Key Size2048 bits
Public Key FormatPEM (SPKI encoding)
Private Key FormatPEM (PKCS#8 encoding)
AlgorithmRSA-OAEP with SHA-256
UsagePayload encryption and decryption

Device Management

Each API user can have multiple devices (key pairs). Use these endpoints to manage devices:

OperationEndpointHTTP MethodDescription
List Devices/users/{userId}/devicesGETRetrieve all devices for a user
Invite Device/users/{userId}/invite-devicePOSTGenerate invitation for a new device
Delete Device/users/{userId}/devices/{deviceUuid}DELETERemove a specific device
Authorize Device/account-management/authorizePOSTComplete device registration

Key Rotation

TODO

Security Considerations

  1. Private Key Storage: Never expose private keys in logs, version control, or client-side code. Use secure key management solutions.

  2. Key Generation: Always generate keys using cryptographically secure random number generators.

  3. Device Tracking: The server tracks last_access_at for each device, enabling audit trails and inactive device detection.

  4. 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

IssuePossible CauseSolution
unauthorized responseInvalid or mismatched keysVerify key pair matches what was registered
device_uuid missingUser creation failedCheck response status and error message
Signature verification failedWrong ECDSA key formatEnsure public key is compressed (33 bytes)
Decryption failedRSA key mismatchVerify PEM format and key size (2048-bit)
Invalid COSE messageBack end failed to encrypt response or decrypt requestEnhance error propagation to the client without revealing sensitive details

On this page