Signing, custody and approvals
For security teams and architects.
In short
CryptoRail asks for signatures; it never holds what produces them. Each sending address is an identity: an address, a reference to a signer, and a signing level. The key stays in your HSM, in a cloud KMS key in your account, or behind your own signing service. Every signature that comes back is checked against the identity’s address before anything is sent.
Approvals are separate from signing. Operations above your thresholds wait for a quorum of your approvers, who sign an EIP-712 digest of what the operation does. A signing gate checks, at the last moment, that the payload about to be signed is exactly what they approved. Where the rules demand more, a smart account enforces the quorum on chain, so that even the signing key alone cannot move anything.
Keys never enter CryptoRail: signing happens in your HSM, your cloud KMS or your own signer, and only the development signer ever touches a key.
Every signature is checked against the expected address before anything is sent.
Identities, not keys
An identity never stores key material. For a key in an HSM or a KMS, CryptoRail asks the device for the public key, derives the address from it, and refuses the identity when that address is not the one declared or when the key is on the wrong curve. Mistakes surface when the identity is set up, not after a nonce has been spent.
An identity holds an address and a reference to its signer, never a key; for an HSM or KMS key the address is derived from the device's public key and the curve is checked before the identity is accepted.
Only the development signer's package may touch key material, enforced by a dependency check on every change.
The development signer warns on every start and refuses to load in production unless an explicit override is set.
Three signer levels
One port, three levels. Each identity is bound to exactly one.
flowchart TB
C[CryptoRail prepares the exact transaction]
C -->|32-byte digest| L1[Level 1, digest: HSM or KMS returns r and s]
C -->|unsigned transaction| L2[Level 2, transaction: signer returns the signed bytes]
C -->|bytes, hash, decoded call, deadline| L3[Level 3, external: your signer answers later]
L1 --> V{signature recovers to the identity?}
L2 --> V
L3 -.->|through the inbox| V
V -->|yes| B[broadcast]
V -->|no| X[hard failure and an alert]
| Level | What the signer sees | What CryptoRail does | Typical signers |
|---|---|---|---|
| Digest | 32 bytes, never a transaction | Builds and hashes the transaction, recovers the parity, normalises the signature to the form Ethereum requires, assembles and verifies | PKCS#11 HSMs, AWS KMS |
| Transaction | The whole unsigned transaction | Checks that the bytes returned are the transaction it asked for and that the signature recovers to the identity | Signing services that sign whole transactions |
| External | The exact bytes, their hash, the decoded call, a deadline, the reason for the request | Persists the request, waits, checks the answer field by field, verifies, broadcasts unchanged | Your own signer, a custodian, an air-gapped machine |
Three ways to sign behind one port: a digest signer that returns only r and s, a transaction signer that returns the signed bytes, and an external signer that answers later through an inbox.
EIP-712 typed data signed through the same identities and signers, with the same verification, from the library.
EIP-712 typed-data signing over the HTTP API.
HSMs over PKCS#11
PKCS#11 is the reference institutional signer: the same adapter covers on-premise HSMs and AWS CloudHSM through the vendor’s library. The HSM PIN is a reference into your secret manager, never a value in a file, and it never appears in a message or a log. The adapter is tested on every change against SoftHSM2, the standard software token; each physical vendor is declared supported only after a documented manual test on that vendor’s device, and none has been run yet.
Keys in a hardware security module over PKCS#11 (Thales, Utimaco, Entrust, AWS CloudHSM): tested on every change against SoftHSM2, each vendor declared after a manual test on its device. Built, certification pending
AWS KMS
The KMS adapter holds nothing but a key identifier. The key must be a secp256k1 key; KMS signs the digest and the adapter turns its answer into a signature. The process needs permission to read the public key and to sign with each key, and nothing else. The adapter is tested against the LocalStack emulator; the manual test against a real AWS account, required before it is declared supported, has not been run yet.
Signing keys in AWS KMS, with the curve checked at startup. Built, certification pending
Signing with Azure Key Vault or GCP Cloud KMS, or through Fireblocks. On request
Your own signer
When the key must never be reachable by CryptoRail at all, the identity uses the external level. CryptoRail prepares the transaction, nonce and fee included, and issues a signing request; your system signs on its side and posts the signed bytes back. Nothing goes out but the request.
sequenceDiagram
participant CR as CryptoRail
participant S as Your signer
CR->>CR: prepared, nonce held
CR-->>S: request: exact bytes, hash, decoded call, deadline, reason
Note over S: your own checks and controls
S->>CR: signed bytes, bound to that request
CR->>CR: same request? same bytes? the identity's key?
CR->>CR: broadcast the bytes unchanged
Note over S,CR: or a rejection with a reason, and the nonce is given back
The answer is checked three ways before anything is recorded: it must answer a pending request, the bytes must be the ones requested (destination, calldata, value, nonce, network, fees), and the signature must recover to the identity. A signer cannot answer a request for one payment with another. Because the signer may take time, the request has a deadline; a request nobody answers expires and gives its nonce back, so the identity keeps working. Every request says why it exists, so a custodian’s operator can tell a fee refresh from a second payment.
Your own signer keeps the key: CryptoRail prepares the exact bytes, your system signs them, CryptoRail verifies and broadcasts.
A request to an external signer carries the exact bytes, their hash, the decoded call and a deadline; a request nobody signs expires and gives the nonce back.
Signatures and approvals posted back are authenticated and bound to the one request they answer.
A transaction signed outside CryptoRail is never re-signed on the signer's behalf: if it gets stuck, the new fee goes back as a request flagged fee-refresh, and an approval already given is reused.
Approvals by quorum
Institutional operations wait for people, not for cryptography. If the slow approval produced the signed transaction itself, the nonce and the fee inside it would be frozen for days. CryptoRail therefore separates the two.
sequenceDiagram
participant Y as Your system
participant CR as CryptoRail
participant W as Your workflow tool
participant A as Approvers, k of n
participant G as Signing gate
participant K as HSM or KMS
Y->>CR: submit the operation
CR->>CR: policy: approval needed, 2 of 3
CR->>CR: PENDING_APPROVAL, no nonce, no fee
CR-->>W: approval requested, with digest and decoded call
W->>A: route to the approvers
A->>CR: EIP-712 signatures over the digest
CR->>CR: quorum reached: fee from today's market, next nonce
CR->>G: sign this payload, under this approval
G->>G: digest matches, gas within cap, unexpired, unused, quorum and policy still hold
G->>K: digest
K-->>G: signature
The operation digest covers what the operation does: network, actor, target, calldata, value, gas cap, policy version, expiry and the operation’s id. It deliberately leaves out the nonce and the fee, so waiting for approval never blocks the identity’s queue and a fee replacement never needs a new approval.
CryptoRail verifies approvals, it does not orchestrate them. It exposes pending operations with their digest and a decoded view of the call, accepts signatures and rejections through an authenticated inbox, and publishes an event at every step for your workflow tool to route. It has no approval screen of its own and does not chase approvers.
Operations above a threshold wait for a quorum of approvers, who sign an EIP-712 digest that names neither nonce nor fee.
An operation waiting for approval holds no nonce and no fee, so it can wait days without blocking anything, and a later fee or nonce change never needs a new approval.
Three kinds of approval proof: EIP-712 signatures of registered approvers, an authenticated attestation from an approval system that cannot sign, or verification on chain by the account itself.
Approvers are registered per tenant, their keys rotated and revoked; approvals expire and can be rejected with a reason, and the events of each step reach your workflow system.
The signing gate
Between the approval and the signature the operation is rebuilt at least once, approvers may be revoked and limits may move. The signing gate is where everything is checked again, against the exact payload about to be signed. A refusal names the check that failed and ends the operation with a failure report; every decision, allowed or refused, is recorded.
At the last moment before signing, a signing gate checks the exact payload against what was approved: same digest, gas within the cap, not expired, not used by another transaction, quorum still valid, policy still allowing it.
Every approval, signature and decision is recorded with who, when and what, and every change made through the API lands in an audit log.
The gate is software, in a process of its own: in a split deployment it is the only process with signer credentials, it reaches your signer and the database and nothing else, and every other process asks it through the database. Whoever controls that one process still controls what the gate lets through. For the flows where that is not acceptable, the next section moves the check onto the chain.
In a split deployment the signing gate is a process of its own, the only one with signer credentials: it reaches the database and your signer and nothing else, and re-checks every request before it signs.
On-chain enforcement with guarded smart accounts
For the operations that matter most, the actor can be a guarded smart account: an ERC-4337
account whose own validation counts the approvers’ signatures. The identity that submits its
operations, HSM or not, becomes a mere executor. The weights are chosen so that the arithmetic
does the enforcing: every approver has weight one, the executor n - k + 1, and the threshold is
n + 1.
flowchart LR
E[executor alone] --> F1[below the threshold: refused on chain]
AP[all approvers, no executor] --> F2[one short: refused on chain]
EK1[executor and k - 1 approvers] --> F3[one short: refused on chain]
EK[executor and k approvers] --> OK[threshold reached: executed]
With two of three approvers, for example, the approvers weigh one each, the executor two, and the threshold is four: the executor with one approver reaches three and is refused; with two it reaches four. An operation submitted by the executor alone, or one approval short, is refused by the chain itself, whatever any software believed. Each approval flow gets its own nonce lane in the account, so pending approvals never block each other.
When required, approvals are enforced on chain by a smart account: the signing key alone cannot move anything.
Only the canonical EntryPoint and public account and paymaster contracts from audited lineages, pinned per network; no custom contract.
Two reservations stand, both recorded in the decision record for the account contracts (ADR-0003). The account’s weighted validator version is audited by lineage (its predecessor was audited), and the owner has yet to decide on it for production use. And the guardian set of a deployed account is fixed at deployment, so rotating an approver after its first operation leaves the account and the registry apart until renewal is built.
What it does not do
- Custody of your end customers' keys. Not supported
- It does not hold a key, except in the development signer, which refuses production unless explicitly overridden, and in a pilot sandbox, which holds test keys and is never supported for value.
- It has no approval screen and does not notify approvers. Your workflow tool routes the requests; CryptoRail verifies what comes back.
- The gate does not make a compromised gate harmless. It is isolated from the API and the worker, but it is software holding the credentials; on-chain enforcement is the answer where that is not enough.
- No HSM vendor and no real AWS account has been certified yet. Both adapters are tested on every change against emulators; each declaration waits for its manual test.
- An attested approval proves that a system said yes, not who approved. It exists for workflow tools that cannot sign, and the record says plainly that it is weaker.
- It does not sign arbitrary messages (EIP-191): that is not built. EIP-712 signing over the HTTP API is on the roadmap; today it is available from the library.
Evidence
Every claim above links to its evidence on the evidence page. The main ones:
| Subject | Requirements | Journeys |
|---|---|---|
| Identities, verification, key boundary | FR-SIG-01, FR-SIG-02, FR-SIG-09, FR-SIG-12, NFR-SEC-01, NFR-SEC-02 | J12, J16 |
| Signer levels and typed data | FR-SIG-03, FR-SIG-04, FR-SIG-05, FR-SIG-13 | J08, J16, J20 |
| PKCS#11 and AWS KMS | FR-SIG-10, FR-SIG-11, FR-SIG-12 | J18, J40 |
| Your own signer | FR-SIG-05, FR-SIG-06, FR-SIG-07, FR-SIG-08, FR-SIG-22, NFR-SEC-03 | J20, J25, J42 |
| Approvals and the signing gate | FR-APR-01 to FR-APR-10 | J13, J19, J41, J49 |
| On-chain enforcement | FR-APR-11, FR-SPN-06, NFR-SEC-09, ADR-0003 | J33 |