EthereumPhone/PQ1
Contribute to EthereumPhone/PQ1 development by creating an account on GitHub.
15 stars · 2 forks · 0 watchers
README
PQ1 A post-quantum ERC-4337 hardware wallet (the PQ1) where every primitive that protects the seed — at rest, in transit between chips, in firmware updates, in transaction signing — is a NIST PQC standard or a Grover-resistant symmetric primitive. The secure elements' channel layers (which we cannot replace) are symmetric-rooted — no public-key handshake ever crosses a bus — so even against a future CRQC the strongest attack on recorded traffic is depth-limited Grover key search (NIST Category 1, the same floor as the SPHINCS+C10 signatures themselves). Target hardware: STM32U585 (Cortex-M33, TrustZone) + Infineon OPTIGA Trust M V3 + NXP EdgeLock SE050. No single die, no single vendor, and no future cryptographically-relevant quantum computer (CRQC) should recover the seed from harvested traffic or extracted ciphertext. Build one yourself: every part is off-the-shelf — DIY.md has the ~$150 bill of materials (Mouser links included), the wiring, and the first-flash guide.
Firmware-format authority correction — 2026-07-15. The existing PQFW_V1/75-byte proof and implementation are legacy bench evidence only. Historical V4/80-byte text remains in some owner documents, while the more-specific Draft 1.1 describes V6/121 bytes as a research candidate with no implementation authority. No replacement schema is currently selected. Reconcile those owners before implementation or formalization; later V4 passages in this README are historical candidate text, not current approval.
Status — 2026-04, pre-production bring-up. All-C10 cutover complete. Every transaction is signed with SPHINCS+C10 (W+C_F+C, h=18, d=2, a=11, k=13, w=8, l=43, target_sum=205, sig=4008) — hash-based, no lattice or number-theoretic assumptions, no classical fallback. The same primitive signs both Type 1 (bootstrap → slot registration) and Type 2 (slot → user tx); there is no FORS+C and no secp256k1/P-256/Ed25519 anywhere. The firmware boots and runs on a real B-U585I-IOT02A with the OPTIGA Trust M V3 Shield + NXP OM-SE050ARD on Arduino R3 headers, and on QEMU mps2-an505. Dual-SE XOR entropy split, three-way PIN-attempt consumption (MCU + OPTIGA + SE050), both SE drivers, and the Tier-1 SAES-CMAC(DHUK) KDF are validated end-to-end on silicon; the boot counter check is directionally MCU→OPTIGA E120 because the SE050 attempt count is not peek-readable. On-chain contracts (PQSmartWallet + factory + PQMultiOwnable) target EntryPoint v0.6 behind cheap ERC-1967 proxies at a deterministic CREATE2 address keyed on sha256(masterPkSeed‖masterPkRoot). SHA-256 throughout the PQ stack (routed to the STM32U585 HASH peripheral); Keccak-256 only for EVM-mandated hashes. No devices have shipped and no on-chain wallets hold funds. Anything described below as "frozen" or "a hard fork to change" is the shape the team intends to commit to at launch — domain tags, the C10 parameter set, the CREATE2 salt, and the EntryPoint version can all still be changed cleanly before first shipment. The bring-up branch carries known production-invariant regressions; see CLAUDE.md "Pre-Production Caveats".
┌──────────────────────────────────────────────────┐ │ STM32U585 (Cortex-M33) │ │ │ │ ┌───────────────── SECURE WORLD ───────────────┐ │ ┌──── NON-SECURE WORLD ────┐ │ │ │ │ │ │ │ │ PIN → gated_unlock (page-124 pre-commit) │ │ │ USB HID / LCD forward │ │ │ → SE-derived auth via hw::secret_keys │ │ │ Companion app drives │ ┌──────────┐ │ │ → SAES-CMAC(DHUK, label) [Tier 1] │ │ │ (chain_id, slot_index, │ │ OPTIGA │◄──┼──┤ │ │ │ flags) per sign call │ │Trust M V3│ │ │ OPTIGA.unlock(K_O) → half_O │◄┼───┼──►┌──────────────────┐ │ │(Shielded │ │ │ (Shielded Conn AES-128-CCM-8; │ │ │ │ NSC gateway │ │ │ Conn, │ │ │ E120 LUC + F1D0 AuthRef silicon-gated) │ │ │ │ sign·unlock· │ │ │ E120 LUC)│ │ │ │ │ │ │ status·fw-update │ │ └──────────┘ │ │ SE050.unlock(K_E) → half_E │ │ │ └──────────────────┘ │ ┌──────────┐ │ │ (SCP03 AES-CMAC + AES-CBC; admin UserID │ │ │ │ │ SE050 │◄──┼──┤ keys: BHK prod; DHUK fallback; OTP dev) │ │ │ no secrets, ever │ │ (SCP03 │ │ │ │ │ └──────────────────────────┘ │ + admin │ │ │ E = HKDF(half_O ⊕ half_E) │ │ │ UID) │ │ │ bip39_seed ← PBKDF2-SHA512(BIP-39(E)) │ │ └──────────┘ │ │ master = HMAC-SHA512("sphincs-c6-v1", seed) │ │ │ │ master_sk / slot_sk ← sphincs_c10::keygen │ │ │ │ type1_sig ← C10.sign(master_sk, userOpHash) │ │ │ │ type2_sig ← C10.sign(slot_sk, userOpHash) │ │ │ │ verify-before-release (FI guard, both sigs) │ │ │ │ zeroize on lock/timeout/tamper/brownout │ │ │ │ │ │ │ │ TRNG / HASH / SAES (DHUK) / TAMP / BOR │ │ │ │ Secure-only inactivity TIM · MCU PIN counter │ │ │ └────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘ ▲ │ FSBL (current: legacy bench code; │ target: approved + WRP-protected) │ verifies C10+SHA-256 on bench; production │ rollback/factory authority remains blocked
Design Properties Each item below is implemented today (QEMU and/or real STM32U585), partial, or planned. See Implementation Status for the per-item state.
Post-quantum signatures, one primitive everywhere — SPHINCS+C10 for both Type 1 (bootstrap slot registration) and Type 2 (per-slot user tx). The on-chain contract has a single c10Verifier immutable wired to both dispatch paths. Per-chain caps MAX_BOOTSTRAP_USES = MAX_SLOT_USES = 65,536 are immutable; combined ≈ 2³² user txns/chain before that chain is permanently frozen. Per-KEY margin (corrected 2026-07-26): slot keys are chain-bound so 65,536 is a true per-key cap (birthday floor 96 b); the bootstrap key is chain-independent (invariant #6, for cross-chain address stability), so its per-key budget across C chains is C × 65,536 and its generic floor degrades as 96 − 2·log₂ C (94 b at 2 chains). Documented as the P14 caveat in Quantitative.lean; realistic bootstrap usage is tens of signatures (slot rotations only). (Implemented; forge test covers both paths.) Post-quantum firmware signing (pre-production) — the existing V1/75-byte signer and FSBL verify SPHINCS+C10 artifacts on the bench, but their rollback/try-once backend is rejected and compile-blocked from production. Historical Draft 0.9/V4 is preserved as research evidence. Draft 1.1 proposes slot-bound manifest v6 and an exact 121-byte PQFW_V6 preimage, but remains an unapproved research candidate; journal/ECC/OTP, FLASH, RAM/stack, release-policy, factory, and silicon gates remain open. Symmetric-only SE tunnels (no harvestable handshake) — no public-key exchange crosses I²C. The rdp2-self-lock candidate implements a journaled first-field transport→final transition before the seed wizard: SE050 credentials become BHK-rooted and the OPTIGA PBS binds a persisted fresh TRNG salt to the DHUK derivation. This is not yet a production-approved ceremony: the authenticated factory handoff/receipt, old/new/KVN recovery proof, exact E140 order, and silicon evidence remain OPEN and production-blocking. Once that ceremony is closed, the accepted residual is Grover key search on AES-128 session keys (~2⁶⁴ serial, NIST Category 1 — the same floor as SPHINCS+C10 itself), per recorded session, requiring a physical tap during a live unlock, twice over thanks to the XOR split. (An ML-KEM-1024 inner wrap was prototyped and descoped 2026-07-07 — owner decision; see docs/security/ml-kem-inner-wrap.md.) TrustZone isolation — signing key, PIN state, key derivation, and crypto confined to the secure world. The NSC gateway (sign / batch-sign / off-chain sign / unlock / lock / status / wallet-address / init-code / firmware-update) is the only crossing point, with NS pointer validation and TOCTOU defense (NS buffers copied to S-stack before parse). On silicon the gateway runs through real ARMv8-M CMSE veneers (make e2e-hw); QEMU uses a shared-memory mailbox (workaround for a QEMU 8.2.2 MPC S-alias bug). Dual secure elements (split entropy) — BIP-39 entropy is XOR-split: half_O on OPTIGA, half_E on SE050. Either chip alone reveals zero bits. E = HKDF(half_O ⊕ half_E) happens only in S-SRAM during unlock, then zeroized. (Validated on silicon; both on I2C1 at 0x30 / 0x48.) Three-way PIN-attempt enforcement — every ordinary wrong attempt charges FI-hardened MCU page 124, OPTIGA E120 LUC, and the SE050 silicon UserID. Page 124 and SE050 enforce the user-facing 10-attempt bound; E120 is a separate 32-lifetime-attempt anti-extraction backstop. Boot checks the readable counters directionally (E120_used > page124_used means rollback/tamper); the SE050 attempt attribute is policy-denied and is not a boot input. CMD_GET_REMAINING uses page 124 plus runtime driver mirrors, not a three-way silicon read receipt. Silicon evidence from make pin-gate-hw-counter-e2e covers per-attempt consumption/desync recovery within one run, and make pin-gate-wipe-e2e covers lockout/wipe. A reboot-based silicon receipt for both directional boot-ordering cases remains open; those branches are not claimed as hardware-E2E-validated. Three-tier DHUK + BHK + OTP key hierarchy — Tier 1 (DHUK, SAES-CMAC(DHUK, label‖counter)) is landed behind saes-dhuk. At RDP0 the DHUK is an ST-substituted constant shared across boards (per-die uniqueness only at RDP ≥ 1). The Tier-2 BHK lifecycle and page-127 salted first-boot journal exist in the quarantined rdp2-self-lock candidate; production review and silicon evidence remain open. Trusted-display clear-signing — every signable artifact is decoded and rendered in S-world before confirm by native on-device decoders. Safe EIP-712 SafeTx and CoW Swap EIP-712 GPv2Order are verified in-world (secure/src/tx/eip712/{safe,cowswap}/) and decoded locally. ERC-20 transfers and CoW order legs render symbol/decimals from a Merkle-verified metadata bundle; accepted ERC-7730 descriptors, including supported Aave v3 operations, render field-level pages. Allowlisted MultiSendCallOnly DELEGATECALL batches are strictly decoded record-by-record. Only genuinely absent opaque calls/records may reach loud blind pages; registry-known/Bloom-positive calls without valid bound semantics, incomplete Aave descriptors, malformed or prohibited batches, other delegatecalls, and page-budget overflow refuse. The clear-signing guarantee covers structured on-chain and typed-data dispatch. Explicit EIP-1271 RAW32 remains a separate ! BLIND RAW32 off-chain tier and must never be used by a companion to downgrade a typed-data request. Boot-time self-test & measurement — hw::hash::init_clock() runs a SHA-256("abc") KAT (halt on mismatch); make saes-self-test-hw runs the SAES round-trip + 8-byte DHUK fingerprint. The secure-world image hash is rendered as 8 BIP-39 words on the NV3007 LCD for trustless comparison against fwmeasure. Hardening hooks — STM32U585 TAMP (Trezor-port; log-only on this branch, production flips to trigger_lockout_wipe()), TIM2 CH1 PWM consumption mask (PA5), UI-capture screenshot-hash harness. All feature-gated; CI keeps them out of production. No heap — #![no_std], stack-only, no Vec/Box/String. zeroize on every secret; subtle for constant-time compares; // SAFETY: on every unsafe.
Quick Start make play # interactive QEMU — laptop arrow keys drive the two wallet buttons make run # mock-SE smoke test in QEMU make e2e # automated unified-sign e2e in QEMU (mock SE) make e2e-hw # automated e2e on real STM32U585 (dual-SE) via probe-rs make test-key-speed # DWT-timed signing bench on real hardware make measure # print the 8 BIP-39 measurement words for this build
Key Action
← Left button — back / scroll down
→ Right button — next / scroll up
←+→ Confirm (press both together)
Esc / Ctrl-C Cancel / quit
Build a dual-SE nonshipping bench firmware (the explicit feature makes the legacy rollback backend visible and is forbidden in production): make FEATURES="dual-se,stm32u585,ui-lcd,saes-dhuk,usb,legacy-fw-rollback-unsafe" all Expected real-hardware key-speed (hw-sha256, auto under stm32u585): first-sign ≤ 3 s (master keygen + slot keygen + 2× sign); Type-2 cached slot ≈ 1.1 s; second-chain first-sign ≈ 2.5 s. Substantially higher means the HASH peripheral isn't being used. HW probe-rs gotcha. probe-rs doesn't implement semihosting SYS_READC, so ui-semihosting PIN prompts hang on real silicon. Use make test-key-speed (no reads) or make play-hw-display (arrow keys via the probe-rs handshake). QEMU is unaffected. Prerequisites
Rust nightly (rust-toolchain.toml), arm-none-eabi-ld, QEMU with mps2-an505 Hardware: B-U585I-IOT02A + OPTIGA Trust M V3 Shield (TRUSTMV3SHIELDTOBO1) + NXP OM-SE050ARD on Arduino R3 headers, driven via ST-LINK + probe-rs
Project Structure sphincs_rust/ ├── secure/ TrustZone SECURE world (main.rs, crypto.rs, sau.rs, nsc/, aa/, tx/, │ optiga/, se050/, dual_se.rs, fw_update/, measured_boot.rs, ui/, hw/) ├── nonsecure/ NON-SECURE world: USB HID + APDU v2 router, NS gateway caller, e2e runner, │ generated erc20_db.bin / names_db.bin ├── shared/ Cross-world #[repr(C)] types, NscStatus, CMD constants, wire-format sizes ├── proto/ pqsigner-proto — protocol constants/enums/sizes (source of truth for Solidity) ├── sphincs-c10/ SPHINCS+C10 signing library (no_std, SHA-256) ├── bip39/ 24-word English BIP-39 (no_std) ├── domain/ tx-core/ aa/ tx/ hal/ pqsigner-erc7730/ pure-logic workspace crates ├── contracts/smart-wallet/ Foundry project — PQSmartWallet, Factory, PQMultiOwnable, │ verifiers/SPHINCsC10Asm.sol (stateless Yul C10 verifier) ├── fsbl/ first-stage bootloader (legacy bench link: 32 KiB; Draft 1.1 resources OPEN) ├── fwsign/ fwmeasure/ fw-manifest/ host signer/verifier, measurement tool, manifest chain ├── dbgen/ host ERC20/names/selectors/ERC-7730 DB + Merkle-tree builder ├── tools/ webhid_test.html, wallet_run_hw.py, … └── docs/ architecture.md, HARDENING.md, threat-model.md, …
See CLAUDE.md for the full per-file map and the non-negotiable invariants. Authenticated companion databases The full catalogue blobs stay on the host/companion. Secure firmware pins only their 32-byte Merkle roots (secure/src/db_roots.rs) and verifies every companion-supplied lookup bundle before its metadata can reach the display.
DB Source NS artifact Secure anchor
ERC20 metadata secure/data/erc20.json tools/companion-stub/erc20_db.bin ERC20_DB_ROOT
Address names secure/data/names.json tools/companion-stub/names_db.bin NAMES_DB_ROOT
Function selectors secure/data/selectors.json tools/companion-stub/selectors_db.bin SELECTOR_DB_ROOT
ERC-7730 descriptors secure/data/erc7730-registry/{registry,ercs}/*/.json + secure/data/erc7730/policy.toml tools/companion-stub/erc7730_db.bin ERC7730_DESCRIPTORS_ROOT
cargo run -p dbgen reads the pinned inputs, builds the SHA-256 Merkle trees, appends per-entry proofs, and writes the .bin files plus db_roots.rs. All generated files are committed. nonsecure/build.rs checks the small E2E blobs used by the QEMU companion stub; production firmware embeds no catalogue blob. The trust chain is fully offline: firmware-signing key → root in secure flash → Merkle proof walk → verification. Catalogue changes must go through the pinned vendor/install workflow, regenerate with dbgen, and pass make check-codegen. Cryptographic Primitives Every primitive that touches a secret, with PQ status. Classical entries are display-only (never reach the seed), a residual SE-vendor surface we wrap with planned PQ confidentiality, or a planned migration.
Where Primitive Size PQ Notes
Tx signing (Type 1 + 2) SPHINCS+C10 (W+C_F+C, h=18 d=2 a=11 k=13 w=8 l=43 target_sum=205) sig 4008 B, pk 32 B ✅ One primitive for bootstrap and per-slot. Verifier SPHINCsC10Asm.sol runs in-EVM via the SHA-256 precompile. The on-chain SignatureWrapper(ownerIndex, sig) is 4128 B (4008 padded to 4032 + 3×32 header)
Firmware signing SPHINCS+C10 (same params) sig 4008 B ⚠️ Legacy V1 bench path exists. Draft 1.1 proposes manifest-v6 but is not implementation-approved; production remains blocked on its journal/ECC/OTP, FLASH, RAM/stack, release-policy, factory, and silicon gates. No classical fallback
OPTIGA wire Shielded Connection: TLS-PRF + AES-128-CCM-8; candidate final PBS is DHUK-derived and binds a persisted TRNG salt tag 8 B ⚠️ Symmetric-rooted (no Shor surface). Candidate code exists; authenticated factory handoff, recovery proof, E140 ordering, and silicon evidence remain production-blocking. Only after that closure does the accepted Grover-2⁶⁴ tapped-session residual apply
OPTIGA PIN gate AuthRef (0xF1D0) + E120 LUC (silicon-monotonic) — ✅ Trezor-parity; immune to PBS extraction. Hardware-cleared by Change=Auto(F1D0) over transient auth on success
SE050 wire SCP03 (AES-CMAC + AES-CBC); candidate final credentials use the BHK derivation axis k 16/32 B ⚠️ Symmetric-rooted (no Shor surface). The journaled transport→BHK candidate exists, but its handoff and old/new/KVN recovery contract plus silicon evidence remain unapproved. Session keys have no forward secrecy
SE050 PIN gate UserID auth (constant-time, max 10) — ✅ Hardware retry counter; surfaces only via SW=0x63Cx
SE050 admin PIN Current helper: SAES-CMAC(BHK,…); DHUK fallback; OTP only in explicit dev/legacy builds 16 B ⚠️ Derived on demand and never stored as a flash PIN. Page 126 holds the wrapped BHK. The candidate first boot rekeys the transport UserID to this final helper; production validation and recovery authority remain OPEN
MCU PIN counter Page-124 quad-word programs 10-attempt cap ✅ FI-hardened pre-commit in nsc::gated_unlock: bump before touching the SE driver, with post-bump readback (+1 or InternalError)
SE chip attestation ECDSA over a vendor curve — ❌ Proof-of-presence only; cryptographic device identity will be a pinned SPHINCS+C10 cert (planned)
Tier 1 root STM32U585 DHUK via SAES KEYSEL=001; SAES-CMAC(DHUK, label‖counter) 16 B/block ✅ DHUK never CPU-visible. RDP0 = ST constant; per-die uniqueness at RDP ≥ 1
Tier 2 root (planned) BHK — TRNG-burnt, DHUK-wrapped, TAMP-backup-loaded, SECCFGR-locked 32 B ✅ Defense in depth; planned to host SE050 SCP03
Factory transport root Per-device OTP master, TRNG-generated and burned by the factory before shipment 32 B ✅ Derives only the initial SE050 SCP03/admin and OPTIGA PBS transport credentials. Field firmware must never auto-burn it or reuse it as a final device root; the candidate first boot replaces those transport credentials after RDP-2.
BIP-39 → C10 master PBKDF2-HMAC-SHA512 (2048) → HMAC-SHA512("sphincs-c6-v1", seed) (acct 0) / …("sphincs-c6-v1-acct", seed‖acct_be4) (accts 1..=255) 64 B ✅ "c6" tag is historical (carried through the C10 cutover). Acct 0 reproduces the legacy derivation byte-for-byte
Slo
