ESP32-S3 air-gapped FROST threshold signing device for Keep.
- Quick Start
- Hardware
- Prerequisites
- Build & Flash
- Usage
- Bitcoin PSBT Signing
- Policy Enforcement
- Distributed Key Generation (DKG)
- Features
- JSON-RPC API
- Testing
- License
Flash firmware directly from your browser - no tools required:
Requires Chrome or Edge.
pip install esptoolDownload the latest keep-merged.bin from Releases:
esptool.py --chip esp32s3 --port /dev/ttyACM0 write_flash 0x0 keep-merged.bincargo install --git https://github.com/privkeyio/keep keep-clikeep frost hardware ping --device /dev/ttyACM0- ESP32-S3 with USB Serial JTAG support
- 8MB Flash, 8MB PSRAM recommended
- Tested on ESP32-S3-DevKitC-1-N8R8
For building from source:
mkdir -p ~/esp && cd ~/esp
git clone -b v5.4.1 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf && ./install.sh esp32s3
source export.shcd ~/projects # or your preferred directory
git clone -b esp-idf-support https://github.com/privkeyio/secp256k1-frost
git clone https://github.com/privkeyio/keep-esp32
git clone https://github.com/privkeyio/keep
git clone https://github.com/ElementsProject/libwally-core
git clone -b esp-idf-support https://github.com/privkeyio/noscrypt
git clone https://github.com/privkeyio/libnostr-cYour directory structure should look like:
~/projects/
├── secp256k1-frost/ # FROST crypto library
├── keep-esp32/ # This repo (ESP32 firmware)
├── keep/ # Keep CLI and core library
├── libwally-core/ # Bitcoin primitives (PSBT, sighash)
├── noscrypt/ # NIP-44 crypto (symlinked in components/)
└── libnostr-c/ # Nostr client library (symlinked in components/)
cd ~/projects/keep
cargo build --release -p keep-cli
# Binary at: ./target/release/keeppip install pyserialcd ~/projects/keep-esp32
source ~/esp/esp-idf/export.sh
idf.py build
idf.py -p /dev/ttyACM0 flash monitor# Add keep to PATH for convenience
export PATH="$PATH:~/projects/keep/target/release"
# Test device connection (USB CDC)
keep frost hardware ping --device /dev/ttyACM0
# List shares stored on device
keep frost hardware list --device /dev/ttyACM0First, generate and split a keyset using the keep CLI:
# Generate a 2-of-3 threshold keyset
keep frost generate --threshold 2 --shares 3 --name mygroup
# View your shares
keep frost list
# Export share #1 to hardware device
keep frost hardware import --device /dev/ttyACM0 --group mygroup --share 1Threshold signing requires multiple participants. The CLI coordinates via Nostr relay:
# Start signing session (waits for other signers on relay)
keep frost network sign \
--group mygroup \
--message $(echo -n "hello" | sha256sum | cut -d' ' -f1) \
--relay wss://nos.lol \
--hardware /dev/ttyACM0 \
--threshold 2 \
--participants 3The device supports Bitcoin PSBT (BIP-174) parsing and Taproot sighash extraction for threshold signing.
CLI parses PSBT → Device extracts sighash → FROST signing → CLI adds signature → Signed PSBT
| Method | Description |
|---|---|
bitcoin_parse |
Parse PSBT, return summary (inputs, outputs, amounts, fees) |
bitcoin_sign |
Extract Taproot sighash for a specific input |
# Parse PSBT on device (via JSON-RPC)
{"id":1,"method":"bitcoin_parse","params":{"psbt":"cHNidP8BAF4..."}}
# Response: {"id":1,"result":{"inputs":1,"outputs":2,"total_in_sats":100000,"fee_sats":1000}}
# Get sighash for FROST signing
{"id":2,"method":"bitcoin_sign","params":{"psbt":"cHNidP8BAF4...","input_idx":0}}
# Response: {"id":2,"result":{"input_idx":0,"sighash":"abc123..."}}- CLI parses PSBT and sends to device for verification
- Device extracts Taproot sighash via
bitcoin_sign - CLI coordinates FROST signing with
frost_commit/frost_sign - CLI aggregates signature shares from all participants
- CLI adds final Schnorr signature to PSBT
The device never sees the full private key - only its threshold share participates in signing.
The device supports Warden policy bundles for transaction authorization. Policies define spending rules (whitelists, limits, etc.) that are enforced before signing.
- Warden creates and signs a policy bundle with Schnorr signature
- Policy bundle is synced to device via
policy_updateRPC over USB - Device verifies signature and stores bundle in flash
- Before signing, device evaluates transaction against policy rules
# Check current policy status
{"id":1,"method":"policy_get"}
# Response: {"id":1,"result":{"has_policy":true,"version":1,"warden_pubkey":"...","policy_hash":"..."}}
# Upload signed policy bundle (hex-encoded)
{"id":2,"method":"policy_update","params":{"bundle":"01..."}}
# Response: {"id":2,"result":{"ok":true}}| Rule | Type | Description |
|---|---|---|
max_amount |
integer | Maximum total output amount in sats |
max_fee |
integer | Maximum transaction fee in sats |
Example policy rules JSON:
{"max_amount": 1000000, "max_fee": 10000}| Field | Size | Description |
|---|---|---|
| version | 1 byte | Bundle format version |
| warden_pubkey | 32 bytes | Warden's x-only public key |
| policy_hash | 32 bytes | SHA256 of policy rules |
| rules_len | 4 bytes | Length of rules data |
| rules | 2048 bytes | Policy rules (JSON) |
| created_at | 8 bytes | Unix timestamp |
| signature | 64 bytes | Schnorr signature over bundle |
See Warden documentation for policy creation and management.
Generate threshold keys without any single party knowing the full private key. Each participant runs the command on their own device:
# Participant 1
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 1 \
--relay wss://nos.lol \
--hardware /dev/ttyACM0
# Participant 2 (on second device)
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 2 \
--relay wss://nos.lol \
--hardware /dev/ttyACM0
# Participant 3 (on third device)
keep frost network dkg \
--group mygroup \
--threshold 2 \
--participants 3 \
--index 3 \
--relay wss://nos.lol \
--hardware /dev/ttyACM0All participants must start within 5 minutes. On success, each device stores its share and displays the group public key.
- FROST Threshold Signatures: Two-round Schnorr threshold signing (secp256k1)
- Bitcoin PSBT: Parse PSBTs and compute Taproot sighashes (BIP-174, BIP-341)
- Policy Enforcement: Warden-signed policy bundles with Schnorr signature verification
- Air-Gapped: No network - USB serial JSON-RPC only
- Secure Storage: Direct partition-backed share storage (persists across firmware updates)
- Multi-Group: Store up to 8 signing shares for different groups
- Nostr Coordination: NIP-44 encrypted event protocol for DKG and signing
| Method | Description |
|---|---|
ping |
Health check, returns version |
list_shares |
List stored group identifiers |
import_share |
Import FROST share for a group |
export_share |
Export encrypted share for backup (requires passphrase) |
delete_share |
Remove share from storage |
get_share_pubkey |
Get public key for stored share |
get_share_info |
Get share metadata (pubkey, index, threshold, participants) |
| Method | Description |
|---|---|
frost_commit |
Round 1: Generate nonce commitment |
frost_sign |
Round 2: Generate signature share |
| Method | Description |
|---|---|
dkg_init |
Initialize DKG session |
dkg_round1 |
Generate commitment and ZK proof |
dkg_round1_peer |
Receive and validate peer commitment |
dkg_round2 |
Generate shares for all participants |
dkg_receive_share |
Receive encrypted share from peer |
dkg_finalize |
Derive final share and store |
| Method | Description |
|---|---|
bitcoin_parse |
Parse PSBT, return summary |
bitcoin_sign |
Extract sighash for input |
| Method | Description |
|---|---|
policy_update |
Store signed policy bundle from Warden |
policy_get |
Get current policy bundle metadata |
python3 scripts/test_all_rpc.pypython3 test/hardware/test_hardware.pypython3 scripts/monitor_serial.pyRequires secp256k1-frost to be built first:
# Build secp256k1-frost
cd ~/projects/secp256k1-frost
mkdir -p build && cd build
cmake .. && make
# Run native tests
cd ~/projects/keep-esp32/test/native
mkdir -p build && cd build
cmake .. && make
./test_frost
./test_session
./test_storage
./test_secure_elementAGPL-3.0