Signing & EnQlave
Qustody never holds signing keys. When a transaction clears policy and screening it parks in PENDING_SIGNATURE and waits for a signature produced by your signer — the EnQlave desktop app, or a service you build against the contract below. Signatures use Quantum Chain's quantum-safe signature scheme.
Key & signature formats
| Item | Size | Encoding in API payloads |
|---|---|---|
| Public key | 1952 bytes | hex string (3904 hex chars) |
| Signature | 3293 bytes | hex string (6586 hex chars) |
| Signing digest | 32 bytes | hex, Keccak-256 of the unsigned transaction |
| Address | 20 bytes | 0x-hex — last 20 bytes of Keccak-256(publicKey) |
Length checks are exact and hard: any other size is rejected. Always submit unprefixed lowercase hex for signatures and public keys — the wallet-registration endpoint tolerates a 0x prefix, the signature-submission endpoint does not.
Option A — EnQlave (recommended)
EnQlave is the desktop signer for Linux and Windows. It generates and stores Qeys (signing keys) locally, registers wallets with the ownership proof automatically, displays every pending transaction for review, and submits signatures — you never touch the wire format. Verify downloads against the published SHA256SUMS.
Option B — your own signer (pull flow)
GET/v1/transactions/{id}/signing_payload
{
"transactionId": "…",
"signingHash": "ab34…", // 32-byte digest, hex, no 0x — this is what you sign
"sourceAddress": "…", // source wallet ID
"destinationAddress": "0x…",
"assetId": "QC",
"sourceVaultId": "…",
"amount": "1000000000000000000", // present for transfers
"note": "…", "metadata": {…}
}
signing payload expired — re-create the transaction rather than caching digests.POST/v1/transactions/{id}/signature
{
"signature": "<hex, 3293 bytes>", // required, no 0x prefix
"publicKey": "<hex, 1952 bytes>" // required, no 0x prefix
}
Verification rules, in order:
- The transaction must currently be
PENDING_SIGNATURE(else409 INVALID_TX_STATE, code1301). - The payload must not be expired.
publicKeymust exactly match the wallet's registered key — a valid signature from any other key returns403 SIGNATURE_MISMATCH(code1302).- The signature must verify over the stored 32-byte digest.
On success the transaction moves SIGNED → BROADCASTING → CONFIRMING.
Fail-closed broadcast assembly
Before broadcast, Qustody rebuilds the unsigned transaction from the stored intent only, requires the rebuilt hash to equal the digest you signed, then re-verifies the assembled raw transaction field by field. Neither a compromised database record nor a tampered payload can redirect funds — the signature simply won't match, and the transaction fails.
Option C — hosted signer endpoint (push flow)
Instead of polling, Qustody can call your signer over HTTPS when a signature is needed (30-second timeout). Your endpoint receives:
{
"requestId": "…",
"signingHash": "<base64>", // 32-byte digest — base64 in the push flow
"sourceAddress": "0x…",
"destinationAddress": "0x…", // present for transfers
"amount": "1000000000000000000",
"metadata": {"vaultId": "…", "assetId": "QC", "policy_version": "…"}
}
And must respond with one of:
// sign immediately (200) {"signature": "<hex>", "publicKey": "<hex>", "status": "signed"} // defer for human approval (202) — submit later via POST …/signature {"status": "pending_approval", "approvalId": "…"} // refuse (200) {"status": "rejected"}
If your signer enforces its own policy versions, a policy_version mismatch is retried once with a recomputed version.
Registering keys
Whichever signer you run, each wallet's public key must be registered up front — either generated via …/wallets/generate or registered with the ownership-proof challenge Keccak256("qustody:register:" + lowercase(address)). Details in Vaults & Wallets. Signature submissions are only accepted from the registered key.

