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.
| Aspect | Description |
|---|---|
| Key Type | Public/Private key pair |
| Security | Private key never leaves your server |
| Algorithm | Ed25519 / ECDSA |
To sign a request, you must:
- Canonicalize and hash the request body to generate a Content-Digest.
- Construct a Signature Base String containing the request metadata (method, path) and headers.
- Sign the base string using your private key.
- Send the signature and metadata in the
SignatureandSignature-Inputheaders.
Key Pair Generation
Before making signed requests, generate a cryptographic key pair and register the public key when creating your API key.
Supported Algorithms
| Algorithm | Identifier | Key Format | Best For |
|---|---|---|---|
| Ed25519 | ed25519 | PEM | Recommended for most integrations |
| ECDSA P-256 | ecdsa-p256-sha256 | PEM | Standard ECDSA |
| ECDSA secp256k1 | ecdsa-secp256k1-sha256 | EVM Address | Sign with Ethereum wallet |
Generating Ed25519 Keys (Recommended)
# Generate private key
openssl genpkey -algorithm Ed25519 -out private_key.pem
# Extract public key
openssl pkey -in private_key.pem -pubout -out public_key.pemGenerating 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.pemUsing 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:
| Header | Format | Description |
|---|---|---|
Content-Digest | sha-256=:BASE64: | Base64-encoded SHA-256 hash of the canonicalized request body. |
Signature-Input | sig1=(...);created=...;keyid=...;alg=... | Metadata describing the signature components. |
Signature | sig1=: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
keyidis your Ethereum address (e.g.,0x742d35Cc...) - Use
alg="ecdsa-secp256k1-sha256"in Signature-Input - Signature is verified via public key recovery
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Invalid signature | Signature verification failed | Verify signature base construction matches exactly |
Missing Signature-Input header | Header not provided | Include Signature-Input header |
Missing Content-Digest header | Body hash not provided | Include Content-Digest for POST requests |
Clock drift detected | Timestamp too old or in future | Ensure created is within 30 seconds of server time |
Common Issues
- Key ordering matters — Ensure JSON is canonicalized (keys sorted alphabetically) before hashing
- Clock skew — The
createdtimestamp must be within 30 seconds of the server time - Encoding — Use UTF-8 for all string operations
- Line endings — Use
\n(LF) not\r\n(CRLF) in the signature base - Replay protection — Each signature can only be used once
Security Best Practices
- Never share your private key — It should only exist on your server
- Rotate keys periodically — Create new keys and deactivate old ones
- Use environment variables — Don’t hardcode keys in source code
- Monitor API key usage — Check for unusual activity in the dashboard