Skip to content

PQ signing in the TEE (2d-hsm)

The bridge operator HSM topology explains where bridge operator keys live: orchestrator, Vault + OPA, and NetHSM namespaces on three hosts. The block producer uses a separate long-term post-quantum key for block headers and for on-chain AuthorizationTickets (producer recovery and hard-fork activation). That producer path is what the 2d-hsm reference enclave implements: a small PQ signing service inside AMD SEV-SNP (or Nitro Enclaves), with a length-prefixed CBOR protocol over vsock to the untrusted 2D host.

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 enclave does not implement bridge bridge_lock / bridgeOut policy; that remains on the bridge operator topology. 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).

The producer’s ML-DSA-65 signing path is being migrated off the original PQClean-derived (pqcrypto) backend onto a constant-time-hardened RustCrypto ml-dsa backend, so that the secret-dependent signing path has no data-dependent branching or timing observable to the untrusted host. This is a security-assumption change to the signer only: the vsock wire format, the 1952-byte pq_pubkey, and the 3309-byte signature are unchanged, and the backend sits behind a backend-agnostic signing API.

The migration is staged and not yet live in production:

  • Backend selector (TASK-34.4, landed). The crate can compile against the CT backend behind the ml-dsa-65-ct feature, but production still signs with the legacy backend. The CT backend is deploy-banned — a compile_error! blocks it from any production/optimized profile — until the side-channel gate below is cleared.
  • Side-channel acceptance gate (TASK-34.3). A dudect/ctgrind timing harness measures the signing core. The provisional host harness and evidence (Stage-3A) have landed; the final constant-time ruling (Stage-3B) requires floor-meeting measurements collected on an isolated CT rig plus a reviewer ruling. No constant-time guarantee is claimed until that ruling is recorded.
  • Harness RNG domain contract. To keep the measurement honest, the harness keeps input-generation and signing-randomness in separate RNG domains and advances the signing RNG independently of the measured class, so RNG bookkeeping cannot alias into a fixed-vs-random timing signal.

Until Stage-3B passes and the flip to the CT backend is made (Stage 5), the timing side-channel of the legacy backend remains a documented, waived residual rather than a positive constant-time result.

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)

Still deferred: live chain-tip refresh between arm and sign (the RecentChainProof is verified at arm time but not re-checked per-block), Elixir host shim, real vsock transport in production deployment, and the constant-time backend migration (TASK-34 — CT backend selectable behind ml-dsa-65-ct, legacy active in production, pending the Stage-3B side-channel ruling; see below).

In bridge operator HSM topology, the producer key is shown reaching NetHSM’s producer namespace directly from the block-producer host, bypassing bridge Vault/OPA. 2d-hsm is the dedicated minimal enclave design for that producer role on the 2D chain: PQ tickets and block signing inside one auditable service, with vsock as the only host interface. Pre-mainnet rehearsal may still colocate VMs on one EPYC chassis; the logical boundary (host untrusted, enclave holds the PQ secret) is the same whether the image runs as SEV-SNP software-NetHSM or a future physical HSM behind the same API.