Skip to Content

Request Signature

For all state-changing requests (POST, PUT, PATCH, DELETE), you must include a digital signature. BLOX uses RFC 9421 HTTP Message Signatures to ensure the integrity, authenticity, and non-repudiation of requests.

How it Works

RFC 9421 signatures use asymmetric cryptography. You sign requests with your Private Key, and BLOX verifies them using the Signing Public Key you provided when generating your API key.

AspectDescription
Key TypePublic/Private key pair
SecurityPrivate key never leaves your server
AlgorithmEd25519 / ECDSA

To sign a request, you must:

  1. Canonicalize and hash the request body to generate a Content-Digest.
  2. Construct a Signature Base String containing the request metadata (method, path) and headers.
  3. Sign the base string using your private key.
  4. Send the signature and metadata in the Signature and Signature-Input headers.

Key Pair Generation

Before making signed requests, generate a cryptographic key pair and register the public key when creating your API key.

Supported Algorithms

AlgorithmIdentifierKey FormatBest For
Ed25519ed25519PEMRecommended for most integrations
ECDSA P-256ecdsa-p256-sha256PEMStandard ECDSA
ECDSA secp256k1ecdsa-secp256k1-sha256EVM AddressSign with Ethereum wallet
# Generate private key openssl genpkey -algorithm Ed25519 -out private_key.pem # Extract public key openssl pkey -in private_key.pem -pubout -out public_key.pem

Generating ECDSA P-256 Keys

# Generate private key openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem # Extract public key openssl ec -in private_key.pem -pubout -out public_key.pem

Using an Ethereum Wallet (secp256k1)

If you prefer signing with an EVM wallet, provide your Ethereum address as the public key. Signatures are verified using public key recovery from the signature.

Important: Your private key must never be shared or uploaded. Keep it secure on your server.


Required Headers

All signed requests require these headers in addition to blox-api-key:

HeaderFormatDescription
Content-Digestsha-256=:BASE64:Base64-encoded SHA-256 hash of the canonicalized request body.
Signature-Inputsig1=(...);created=...;keyid=...;alg=...Metadata describing the signature components.
Signaturesig1=:BASE64:The cryptographic signature of the base string.

Content-Digest

Compute SHA-256 hash of the canonicalized body (keys sorted alphabetically) and Base64-encode it:

Content-Digest: sha-256=:X48E9qOokqqrvDts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:

Signature-Input

Defines which components are signed. Required components: @method, @path, content-digest, content-type.

Signature-Input: sig1=(@method @path content-digest content-type);created=1705900000;keyid="your_key_id";alg="ed25519"

Signature

The resulting signature, wrapped in colons:

Signature: sig1=:w7SdqL8L...:

Signature Base String

The signature base string is a deterministic representation of the request:

"@method": POST "@path": /v1/checkout "content-digest": sha-256=:X48E9qOokqqrvDts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: "content-type": application/json "@signature-params": (@method @path content-digest content-type);created=1705900000;keyid="your_key_id";alg="ed25519"

Implementation Examples

Node.js (TypeScript)

import crypto from "crypto"; import canonicalize from "canonicalize"; interface SignatureResult { contentDigest: string; signatureInput: string; signature: string; } function createSignature( method: string, path: string, body: object, apiKeyId: string, privateKeyPem: string ): SignatureResult { // Step 1: Canonicalize and hash the body const canonicalBody = canonicalize(body) ?? JSON.stringify(body); const bodyHash = crypto.createHash("sha256").update(canonicalBody).digest("base64"); const contentDigest = `sha-256=:${bodyHash}:`; // Step 2: Build signature parameters const created = Math.floor(Date.now() / 1000); const signatureParams = `(@method @path content-digest content-type);created=${created};keyid="${apiKeyId}";alg="ed25519"`; // Step 3: Build signature base const signatureBase = [ `"@method": ${method}`, `"@path": ${path}`, `"content-digest": ${contentDigest}`, `"content-type": application/json`, `"@signature-params": ${signatureParams}`, ].join("\n"); // Step 4: Sign with private key const sig = crypto.sign(null, Buffer.from(signatureBase), privateKeyPem); const signatureB64 = sig.toString("base64"); return { contentDigest, signatureInput: `sig1=${signatureParams}`, signature: `sig1=:${signatureB64}:`, }; } // Usage const body = { addressTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f8bD21", amount: 15000, tokenId: "550e8400-e29b-41d4-a716-446655440000", }; const { contentDigest, signatureInput, signature } = createSignature( "POST", "/v1/checkout", body, "your_api_key_id", privateKeyPem ); const response = await fetch("https://api.blox.my/v1/checkout", { method: "POST", headers: { "blox-api-key": "YOUR_API_KEY", "Content-Type": "application/json", "Content-Digest": contentDigest, "Signature-Input": signatureInput, "Signature": signature, }, body: JSON.stringify(body), });

Python

import hashlib import base64 import time import json from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey def canonicalize(obj): """Recursively sort keys and serialize to JSON without whitespace.""" if isinstance(obj, dict): return "{" + ",".join( f'"{k}":{canonicalize(v)}' for k, v in sorted(obj.items()) ) + "}" elif isinstance(obj, list): return "[" + ",".join(canonicalize(item) for item in obj) + "]" else: return json.dumps(obj, separators=(",", ":")) def create_signature(method: str, path: str, body: dict, api_key_id: str, private_key_path: str): # Step 1: Canonicalize and hash the body canonical_body = canonicalize(body) body_hash = base64.b64encode(hashlib.sha256(canonical_body.encode()).digest()).decode() content_digest = f"sha-256=:{body_hash}:" # Step 2: Build signature parameters created = int(time.time()) signature_params = f'(@method @path content-digest content-type);created={created};keyid="{api_key_id}";alg="ed25519"' # Step 3: Build signature base signature_base = "\n".join([ f'"@method": {method}', f'"@path": {path}', f'"content-digest": {content_digest}', '"content-type": application/json', f'"@signature-params": {signature_params}', ]) # Step 4: Load private key and sign with open(private_key_path, "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None) sig = private_key.sign(signature_base.encode()) signature_b64 = base64.b64encode(sig).decode() return { "content_digest": content_digest, "signature_input": f"sig1={signature_params}", "signature": f"sig1=:{signature_b64}:", } # Usage body = { "addressTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f8bD21", "amount": 15000, "tokenId": "550e8400-e29b-41d4-a716-446655440000", } headers = create_signature( "POST", "/v1/checkout", body, "your_api_key_id", "./private_key.pem" )

Algorithm-Specific Notes

Ed25519

  • Sign the signature base directly (no prehashing required)
  • Recommended for simplicity and security
  • Use alg="ed25519" in Signature-Input

ECDSA P-256

  • Sign using SHA-256 as the hash function
  • Use alg="ecdsa-p256-sha256" in Signature-Input

ECDSA secp256k1 (EVM Wallet)

  • Use your Ethereum wallet to sign
  • The keyid is your Ethereum address (e.g., 0x742d35Cc...)
  • Use alg="ecdsa-secp256k1-sha256" in Signature-Input
  • Signature is verified via public key recovery

Troubleshooting

ErrorCauseSolution
Invalid signatureSignature verification failedVerify signature base construction matches exactly
Missing Signature-Input headerHeader not providedInclude Signature-Input header
Missing Content-Digest headerBody hash not providedInclude Content-Digest for POST requests
Clock drift detectedTimestamp too old or in futureEnsure created is within 30 seconds of server time

Common Issues

  1. Key ordering matters — Ensure JSON is canonicalized (keys sorted alphabetically) before hashing
  2. Clock skew — The created timestamp must be within 30 seconds of the server time
  3. Encoding — Use UTF-8 for all string operations
  4. Line endings — Use \n (LF) not \r\n (CRLF) in the signature base
  5. Replay protection — Each signature can only be used once

Security Best Practices

  1. Never share your private key — It should only exist on your server
  2. Rotate keys periodically — Create new keys and deactivate old ones
  3. Use environment variables — Don’t hardcode keys in source code
  4. Monitor API key usage — Check for unusual activity in the dashboard
Last updated