ESP32-S3 FROST threshold signing device security documentation.
- Device operates air-gapped (USB serial only, no network)
- Physical attacker may have brief access but not unlimited time
- Side-channel attacks are possible but require specialized equipment
- Flash storage may be extracted for offline analysis
- FROST secret shares (partial private keys)
- Signing nonces (single-use, critical to prevent key extraction)
- Policy bundles (authorization rules)
- Full physical compromise with unlimited time and equipment
- Supply chain attacks on ESP32 hardware
- Attacks on the coordinator/CLI software
- Shares encrypted with AES-256-GCM before flash storage
- Key derived via HKDF-SHA256 from eFuse MAC + optional PIN
- PIN limitation: PIN adds entropy but does not protect against offline brute-force if flash is extracted (no hardware-enforced rate limiting)
- Each slot uses unique 12-byte random nonce
- 16-byte GCM tag detects tampering
- Storage V2 binds group name as AAD
- Session-based nonce management with strict state machine
- 30-second session timeout
- Terminal states (COMPLETE, FAILED, EXPIRED) trigger immediate zeroization
- Session IDs validated against all-zero and all-ones patterns
- Duplicate commitment/share detection per session
- Warden-signed policy bundles with Schnorr signature verification
- Policy hash binding prevents substitution attacks
- Constant-time hash comparison (ct_compare)
- Rules evaluated before signing (max_amount, max_fee)
secure_memzero()for sensitive data (Xtensa assembly, compiler-safe)- Session secrets cleared on state transitions
- DKG secrets cleared after finalize
- Storage buffers cleared after read/write
ct_compare(): timing-independent comparisonct_is_zero(): timing-independent zero checkct_select32(),ct_select_bytes(): branchless conditional selectionct_cswap32(): branchless conditional swap
secresult_t: 32-bit result type with distinct bit patterns- TRUE = 0xAAAAAAAA, FALSE = 0x55555555
- Error codes use repeated byte patterns (e.g., 0x1E1E1E1E)
- Minimum Hamming distance of 4 between values
SECRESULT_IS_TRUE()requires exact match, not just non-zero
- Self-test runs 3 rounds at startup, requires 2/3 pass
- Device refuses to start if RNG fails self-test
rng_fill_checked()validates each random output- Monobit test: rejects if >50% bytes are 0x00 or 0xFF
- Bit distribution test: expects bits within 25% of 50/50
- Transition test: expects transitions within 25% of expected
- Single retry on failure, then abort
- Health degradation tracked after 5 cumulative failures
Note: These are lightweight operational checks, not NIST SP 800-90B compliant. SP 800-90B specifies different tests (Repetition Count, Adaptive Proportion) with dynamic thresholds. Custom checks were chosen for embedded constraints and fail-fast behavior rather than certification compliance.
- RNG failure aborts signing operations
- DKG round1 checks
rng_is_healthy()before proceeding
- Maximum 4 concurrent sessions
- Sessions bound to specific message and participants
- Constant-time participant validation
- Commitments and shares deduplicated by signer index
- Single global session (no concurrent DKG)
- State machine: IDLE -> ROUND1 -> ROUND2 -> COMPLETE
- Peer index validation with deduplication
- ZK proof verification for round1 commitments
- JSON-RPC over USB CDC, 16KB max message
- cJSON parser with bounded string fields
- Base64 validation for PSBT input
- Rate limiting: 1s delay after 5 consecutive errors
- No shell access, no firmware update over serial
- Session ID: 32 bytes from hardware RNG
- Session timeout: 30 seconds (prevents stale accumulation)
- Constant-time session lookup (prevents timing attacks)
- Maximum 4 concurrent sessions (bounded resource usage)
- Consumed session ring buffer prevents replay
- NIP-44 encryption for share transport
- Event kind separation (DKG vs signing)
- Relay trust model: relays see encrypted blobs only
- Group names: alphanumeric, underscore, hyphen only
- Hex strings: strict character validation
- Threshold/participants: 2-16 range
- Index: 1 to participant_count
- Direct flash partition access (not NVS)
- 512-byte aligned slot-based storage
- Migration markers for crash recovery
- Corrupt slot detection and clearing
| Operation | Library | Purpose |
|---|---|---|
| FROST signing | secp256k1-frost | Threshold Schnorr signatures |
| AES-256-GCM | mbedtls | Share encryption at rest |
| HKDF-SHA256 | mbedtls | Storage key derivation |
| SHA256 | mbedtls | Message hashing |
| Schnorr verify | secp256k1 | Policy signature verification |
| NIP-44 | noscrypt | Nostr event encryption |
At boot, the following self-tests run before the device accepts commands:
- RNG self-test: 3 rounds, requires 2/3 pass (device restarts on failure)
- Storage init: Verifies partition access
- Crypto init: Derives storage key from device ID
Failure modes:
- RNG failure: Device restarts automatically
- Storage failure: Continues with warning (storage ops unavailable)
- Crypto init failure: Share operations unavailable
Before submitting PRs that touch security-sensitive code:
- No new uses of
atoi(),sprintf(), or unbounded string ops - All new buffers have explicit size limits
- Secrets are zeroized after use (
secure_memzero) - New RPC methods validate all parameters
- No timing side channels in security-critical comparisons
- Error messages do not leak sensitive information
ESP-IDF Secure Boot v2 can be enabled for production deployments:
- RSA-3072/PSS signature verification of bootloader and application
- Anti-rollback protection via eFuse version counter
- Optional flash encryption for firmware at rest
- See
docs/SECURE_BOOT.mdfor implementation details
Build with secure boot:
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.secureboot" build- MAC address readable, used in key derivation
- PIN not rate-limited at hardware level; a weak or short PIN provides no meaningful protection against offline brute-force when flash can be extracted
- Single-threaded, no concurrent request handling
- Secure boot requires careful key management (key loss = bricked device)
Report security issues to security@privkey.io with:
- Description of the vulnerability
- Steps to reproduce
- Potential impact assessment
Do not disclose publicly until a fix is available.