Skip to content

PQ signing in the TEE (2d-hsm)

Production POA uses 2d-hsm for block signatures. Producer, agent-wallet and bridge signing are separate roles; see Signing and key custody for the current deployment boundaries.

This page is the architecture overview for that service. The normative vsock wire format lives in vsock-api-wire-format-spec-draft.md; the normative AuthorizationTicket ABI, signed preimage, and contextHash rules live in authorization-tickets-precompile-spec-draft.md.

ResponsibilityNotes
Block producer PQ signaturesML-DSA-65 (FIPS 204, parameter set ML-DSA-65) over the 32-byte block digest on the hot path (~2s cadence).
AuthorizationTicket signaturesCanonical ticketHash (Keccak256 + Solidity-aligned preimage); types 0 recovery and 1 hard-fork activation.
Network-as-second-factorARM_FOR_PRODUCTION requires a cryptographically verified RecentChainProof (Producer Chain Attestation v1, Ed25519) before the enclave arms.
Attestation surfaceGET_MEASUREMENT returns TEE measurement, attestation, and pq_pubkey bound together in remote attestation.

The producer profile does not implement bridge signing. The separate agent_gateway profile supports POA SIGN_BRIDGE_LOCK with the bridge-operator key purpose; Ethereum bridgeOut remains a separate signer/vault policy. See key custody. The producer key is a third cryptographic role in the wider system, with its own namespace and signing path.

The 2D host process is untrusted. It may craft vsock frames, replay old proofs, or lie about chain tip. The enclave must fail closed: reject malformed wire, reject tickets when not armed (for hard forks), reject stale or forged RecentChainProofs, and refuse to sign when no operational ML-DSA-65 key is installed.

TEE — 2d-hsm enclave

Untrusted 2D host

vsock: framed CBOR

Host client / orchestrator

enclave-protocol

state + vsock CBOR

ML-DSA-65 signer

sealed at boot

Pinned ProducerAttestationTrust

sealed / attested config

Critical rule: ProducerAttestationTrust (the Ed25519 key that verifies chain proofs) is loaded inside the enclave from sealed config or attested provisioning. It must never be supplied by the host in an ARM_FOR_PRODUCTION payload.

All messages use a 4-byte big-endian length prefix, one protocol-version byte, one message-type byte, then CBOR payload (max 1 MiB). Inner ARM / GET_STATUS / SIGN bodies use integer map keys per the spec.

CommandPurpose
GET_MEASUREMENTRemote attestation package + pq_pubkey + static supported_ticket_types + pq_signing_ready.
ARM_FOR_PRODUCTIONBind armed state to authorized_state after verifying RecentChainProof + measurement consistency.
GET_STATUSArmed metadata, pending hard-fork height, last known block from proof.
SIGN_AUTHORIZATION_TICKETSign canonical ticketHash; hard-fork (type 1) requires prior arm + stateful dispatch.

Dispatch split in the reference crate:

  • Stateless dispatch_command — recovery tickets (type 0) and GET_MEASUREMENT only; hard-fork and arm return explicit errors directing callers to the stateful path.
  • Stateful dispatch_command_with_state — arming, GET_STATUS, and hard-fork signing with EnclaveState + pinned trust.
ItemProduction value
AlgorithmML-DSA (FIPS 204), parameter set ML-DSA-65
pq_pubkey1952 bytes
signature3309 bytes (pure ML-DSA over raw 32-byte ticketHash)
Chain proofEd25519 detached signature over domain-separated preimage (format v1)

pq_signing_ready: true only after install_sealed_pq_signer succeeds at enclave boot. Default builds ship with no embedded secret key; SIGN_AUTHORIZATION_TICKET returns PqSigningUnavailable until provisioned. Hosts detect mock-era peers via pq_signing_ready == false and 64-byte PQ signatures (dev-only test-support + demo-mock-sign).

Sealed key (TASK-1): Production platform sealing uses seal v1 — ChaCha20Poly1305 AEAD with a measurement-bound key derived from a 32-byte provisioning root via SHA3-256 (2d-hsm-pq-seal-v1-key domain). The provisioning root is derived from the SEV-SNP firmware via the snp-derive-root boot helper (SNP_GET_DERIVED_KEY ioctl → SHA3-256 domain-separated), written to /run/twod-hsm/pq-seal-root.bin at boot, and read by the enclave via the release-safe platform-root-from-boot-file feature (fixed path, not a host-settable env var). The v0 XOR format is #[cfg(test)]-only; non-test ml-dsa-65 builds accept v1 and reject v0. The sealed-boot ceremony + snp-derive-root selftest are validated on SEV-SNP staging hardware; safety rests on measured boot (the NixOS image + snp-derive-root oneshot are part of the measured SNP launch).Sealing binds the configured SNP measurement. Binding to a firmware measurement alone does not establish guest kernel, disk or code integrity. Verify measured-boot coverage and recovery evidence for the specific deployment.

The signing backend is a property of the deployed build, separate from the wire protocol: the public key remains 1952 bytes and the ML-DSA-65 signature 3309 bytes. A constant-time backend feature flag is not evidence that its acceptance gate passed or that the running VM uses it.

Before claiming resistance to timing attacks, check the release manifest, the dudect/ctgrind harness evidence from the isolated measurement host, and the reviewer verdict for that build. The harness must keep input generation and signing randomness in separate RNG domains so bookkeeping cannot masquerade as a secret-dependent timing result. Record any residual side-channel risk with the deployed version.

2d-hsm enclaveUntrusted host2d-hsm enclaveUntrusted hostGET_MEASUREMENTmeasurement, attestation, pq_pubkey, pq_signing_readyARM_FOR_PRODUCTION + RecentChainProofVerify Ed25519 proof vs pinned trustCheck height / measurement / pubkey rulesarmedSIGN_AUTHORIZATION_TICKET (type 1)Require armed + matching pq_pubkey + fork fieldsML-DSA-65 signature + ticketHash

Hard-fork tickets must use handle_sign_authorization_ticket_with_state after a valid arm. Recovery tickets (type 0) may use the stateless path during bootstrap, but pq_pubkey in the ticket must still match the installed signer when a real key is active.

Hard-fork authorization is scoped to the producer epoch, not just to the PQ key. A ticket signed by producer A in an earlier epoch must not be replayable after the chain rotates A → B → A. The on-chain/precompile side therefore recomputes the hard-fork contextHash from the current producer’s key and activation height and rejects a mismatch. This page intentionally stays at the architecture level; the exact byte-level preimage is the 2d-hsm AuthorizationTicket spec linked above.

Producer recovery is also not live-by-default in the native chain yet. The finalized-tip downtime gate exists in the 2d precompile code, but record_finalized_tip/2 is not wired into the block executor’s finality path. Until that integration lands, native PRODUCER_RECOVERY acceptance remains fail-closed (no_finalized_tip) rather than advertising recovery as an active production feature. The Solidity reference models this with an explicit relay/height stand-in, not the final native source of truth.

The impl/rust/enclave-protocol crate (high-risk per project AGENTS.md) currently includes:

  • Framing, canonical ticketHash, and Solidity cross-check tests
  • EnclaveState / arming monotonicity and hard-fork session rules
  • Producer Chain Attestation v1 verification
  • ML-DSA-65 signing with seal v1 production key install (ChaCha20Poly1305 AEAD, measurement-bound, SNP-derived root via snp-derive-root)

Producer enclave signing is integrated with Elixir. Proof freshness between arm and sign, recovery and the constant-time backend acceptance gate require evidence for the specific release.

Production POA uses 2d-hsm for block signatures. Producer, agent-wallet and bridge signing are separate roles; see Signing and key custody for the current deployment boundaries.