v0.0.34 - Add qrexec bridge subcommand and --bridge-source-trusted flag for persistent-signer qrexec transport
This commit is contained in:
176
plans/kb2040_real_nip06_crypto.md
Normal file
176
plans/kb2040_real_nip06_crypto.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# KB2040 Hidden Signer — Real NIP-06 Derivation + Schnorr Signing (Option B)
|
||||
|
||||
## Goal
|
||||
|
||||
Replace placeholder [`pseudo_pubkey_hex()`](firmware/kb2040_hidden_signer/kb2040_hidden_signer.ino:344)
|
||||
so that:
|
||||
|
||||
- `get_public_key` returns a **distinct, real** secp256k1 x-only pubkey per
|
||||
`nostr_index` (host shows 5+ accounts again instead of 1 deduped entry).
|
||||
- `sign_event` returns a **real NIP-01 event id** + **valid BIP-340 Schnorr
|
||||
signature** that verifies against the pubkey.
|
||||
|
||||
## Decision: use `resources/nostr_core_lib` exclusively
|
||||
|
||||
Audit of what nostr_core_lib provides **on its own** (pure C, NO mbedTLS):
|
||||
|
||||
| Need | In nostr_core_lib? | Reference |
|
||||
|---|---|---|
|
||||
| SHA-256 (+ streaming) | YES | [`nostr_sha256`](resources/nostr_core_lib/nostr_core/utils.c:298) |
|
||||
| SHA-512 | YES | [`nostr_sha512`](resources/nostr_core_lib/nostr_core/utils.c:584) |
|
||||
| HMAC-SHA256 / HMAC-SHA512 | YES | [`utils.c`](resources/nostr_core_lib/nostr_core/utils.c:474) |
|
||||
| PBKDF2-HMAC-SHA512 (2048) | YES | [`nostr_pbkdf2_hmac_sha512`](resources/nostr_core_lib/nostr_core/utils.c:814) |
|
||||
| BIP-39 mnemonic to seed | YES | [`nostr_bip39_mnemonic_to_seed`](resources/nostr_core_lib/nostr_core/utils.c:1402) |
|
||||
| BIP-32 master + path derive | YES | [`utils.c`](resources/nostr_core_lib/nostr_core/utils.c:1445) |
|
||||
| NIP-06 keys-from-mnemonic m/44'/1237'/account'/0/0 | YES | [`nostr_derive_keys_from_mnemonic`](resources/nostr_core_lib/nostr_core/nip006.c:59) |
|
||||
| NIP-01 create+sign event (id + schnorr) | YES | [`nostr_create_and_sign_event`](resources/nostr_core_lib/nostr_core/nip001.h:16) |
|
||||
| RNG abstraction (needs platform impl) | YES | [`nostr_platform_random`](resources/nostr_core_lib/nostr_core/nostr_platform.h:14) |
|
||||
|
||||
### The ONE thing nostr_core_lib does NOT contain
|
||||
|
||||
The actual **elliptic-curve engine (bitcoin-core libsecp256k1)**.
|
||||
[`nostr_secp256k1.c`](resources/nostr_core_lib/nostr_core/crypto/nostr_secp256k1.c:1)
|
||||
is only a thin wrapper that includes `secp256k1.h` and calls
|
||||
`secp256k1_context_create`, `secp256k1_keypair_create`,
|
||||
`secp256k1_schnorrsig_sign32`, `secp256k1_ec_seckey_tweak_add`, etc. No EC point
|
||||
math is vendored inside nostr_core_lib — confirmed by `REQUIRES secp256k1` in
|
||||
[`CMakeLists.txt`](resources/nostr_core_lib/CMakeLists.txt:27).
|
||||
|
||||
**Conclusion:** "use nostr_core_lib exclusively" covers ALL hashing, BIP-39/32,
|
||||
NIP-06 derivation, and NIP-01 signing. But the library still needs
|
||||
**libsecp256k1 linked in** as its crypto backend (exactly as the ESP build did
|
||||
via the IDF `secp256k1` component). On Arduino RP2040 we must supply
|
||||
libsecp256k1 ourselves — nostr_core_lib cannot run without it.
|
||||
|
||||
## CONFIRMED: where libsecp256k1 already lives (provenance)
|
||||
|
||||
The earlier ESP-IDF firmware did NOT download secp256k1 via `idf_component.yml`.
|
||||
It was a **vendored local ESP-IDF component**, already present in this repo:
|
||||
|
||||
- Component sources: [`firmware/feather_s3_tft/components/secp256k1`](firmware/feather_s3_tft/components/secp256k1)
|
||||
(full bitcoin-core libsecp256k1 checkout incl. include/ and src/).
|
||||
- Headers: [`firmware/feather_s3_tft/components/secp256k1/include/secp256k1.h`](firmware/feather_s3_tft/components/secp256k1/include/secp256k1.h),
|
||||
plus `secp256k1_schnorrsig.h`, `secp256k1_extrakeys.h`, `secp256k1_ecdh.h`.
|
||||
- ESP build wiring: [`REQUIRES secp256k1`](firmware/feather_s3_tft/main/CMakeLists.txt:25)
|
||||
and [`REQUIRES secp256k1`](firmware/cyd_esp32_2432s028/main/CMakeLists.txt:33).
|
||||
- ESP compile config (the flags we must mirror on RP2040):
|
||||
[`components/secp256k1/CMakeLists.txt`](firmware/feather_s3_tft/components/secp256k1/CMakeLists.txt:1)
|
||||
builds only `src/secp256k1.c`, `src/precomputed_ecmult.c`,
|
||||
`src/precomputed_ecmult_gen.c` with these defines:
|
||||
`SECP256K1_BUILD=1`, `SECP256K1_WIDEMUL_INT64=1`, `ECMULT_WINDOW_SIZE=15`,
|
||||
`COMB_BLOCKS=11`, `COMB_TEETH=6`, `ENABLE_MODULE_ECDH=1`,
|
||||
`ENABLE_MODULE_EXTRAKEYS=1`, `ENABLE_MODULE_SCHNORRSIG=1`.
|
||||
|
||||
**Implication:** we already own the exact EC backend source on this machine. The
|
||||
KB2040 task is to compile those same `.c` files (3 sources + the module wiring)
|
||||
under `arduino-cli` with equivalent defines — no new download required, and the
|
||||
RP2040 (32-bit, like Xtensa LX6) can reuse the same `WIDEMUL_INT64` path.
|
||||
|
||||
## Build approach (Arduino RP2040 / arduino-cli)
|
||||
|
||||
`arduino-cli` auto-compiles `.c/.cpp` files placed in the sketch dir and in a
|
||||
`src/` subfolder, and respects `#include` paths relative to the sketch. Key
|
||||
constraint: Arduino has NO `CMakeLists.txt`, so secp256k1's compile-time `-D`
|
||||
defines must be supplied via a vendored config header that is `#include`d before
|
||||
the secp sources (secp256k1 supports `USE_EXTERNAL_DEFAULT_CALLBACKS` and a
|
||||
`libsecp256k1-config.h` style header).
|
||||
|
||||
Steps:
|
||||
|
||||
1. Create a sketch-local sources dir `firmware/kb2040_hidden_signer/src/` and add
|
||||
the nostr_core subset:
|
||||
- `utils.c` (hash/HMAC/PBKDF2/BIP-39/BIP-32)
|
||||
- `nip006.c`, `nip001.c`, `nip019.c`, `nostr_common.c`,
|
||||
`crypto/nostr_secp256k1.c`
|
||||
- `cJSON.c` only if the NIP-01 cJSON signer path is used (see Open Items).
|
||||
2. Vendor secp256k1 from the existing repo copy at
|
||||
[`firmware/feather_s3_tft/components/secp256k1`](firmware/feather_s3_tft/components/secp256k1)
|
||||
into the sketch tree:
|
||||
- sources: `src/secp256k1.c`, `src/precomputed_ecmult.c`,
|
||||
`src/precomputed_ecmult_gen.c`
|
||||
- headers: put `include/` on the include path
|
||||
- add a config header replicating the ESP defines (since no CMake `-D`).
|
||||
3. Add `platform/rp2040.c` implementing
|
||||
[`nostr_platform_random()`](resources/nostr_core_lib/nostr_core/nostr_platform.h:14)
|
||||
via Arduino-Pico RNG (`get_rand_32()` / ROSC entropy), and feed real entropy
|
||||
into the context-randomize step (the stub at
|
||||
[`nostr_secp256k1_context_create`](resources/nostr_core_lib/nostr_core/crypto/nostr_secp256k1.c:38)
|
||||
currently uses a fixed pattern).
|
||||
4. Resolve the mbedTLS question: the ESP CMake listed `REQUIRES mbedtls`, but
|
||||
nostr_core_lib has its own SHA/HMAC/PBKDF2 in `utils.c`. Confirm the compiled
|
||||
subset does NOT include mbedtls headers; if any module does, swap to the
|
||||
nostr_core equivalents so the Arduino build has zero mbedtls dependency.
|
||||
5. Call `nostr_init()` / `nostr_secp256k1_context_create()` once in `setup()`.
|
||||
|
||||
## RPC wiring changes in the sketch
|
||||
|
||||
- On mnemonic load ([`signer_apply_seed`](firmware/kb2040_hidden_signer/kb2040_hidden_signer.ino:360)):
|
||||
validate via `nostr_bip39_mnemonic_validate`, keep caching the mnemonic string
|
||||
for per-index derivation.
|
||||
- `get_public_key`: call
|
||||
[`nostr_derive_keys_from_mnemonic(mnemonic, nostr_index, priv, pub)`](resources/nostr_core_lib/nostr_core/nip006.c:59)
|
||||
(note signature: `int account`, writes 32-byte x-only pub to `public_key+...`
|
||||
internally), then hex-encode the 32-byte x-only pub.
|
||||
- `sign_event`: derive priv for `nostr_index`, then either reuse
|
||||
[`nostr_create_and_sign_event`](resources/nostr_core_lib/nostr_core/nip001.h:16)
|
||||
(cJSON-based, computes id + schnorr) or compute id32 (SHA-256 of canonical
|
||||
array) + schnorr directly via the wrapper. Return `{...event, id, pubkey, sig}`.
|
||||
- Remove [`pseudo_pubkey_hex()`](firmware/kb2040_hidden_signer/kb2040_hidden_signer.ino:344)
|
||||
and the string-splice id/sig hack in
|
||||
[`sign_event`](firmware/kb2040_hidden_signer/kb2040_hidden_signer.ino:513).
|
||||
- Zeroize derived private keys after each operation.
|
||||
|
||||
## Verify
|
||||
|
||||
1. `nostr-login-lite`: 5+ distinct npubs discovered (no dedup collapse).
|
||||
2. Cross-check one pubkey vs host NIP-06 derivation of the same mnemonic/index.
|
||||
3. Sign a NIP-42 auth event; confirm the signature verifies and login completes.
|
||||
4. Re-test the WebUSB demo for dual-transport parity (CDC + vendor).
|
||||
|
||||
## Risk / size notes
|
||||
|
||||
- libsecp256k1 context needs RAM; RP2040 has 264 KB. `ECMULT_WINDOW_SIZE=15`
|
||||
matches the ESP build; lower it if RAM is tight. Precomputed tables live in
|
||||
flash (KB2040 has 8 MB), so flash is not the constraint.
|
||||
- PBKDF2 2048 iters HMAC-SHA512 on 125 MHz RP2040 is fast (<100 ms).
|
||||
- cJSON uses malloc — ensure heap headroom, or bypass cJSON for the compact id
|
||||
serialization to save RAM.
|
||||
- Mirror the ESP `secure_memzero` pattern for secret cleanup.
|
||||
|
||||
## Open items to confirm with user
|
||||
|
||||
1. **secp256k1 source location:** copy the 3 secp `.c` sources + `include/` from
|
||||
[`firmware/feather_s3_tft/components/secp256k1`](firmware/feather_s3_tft/components/secp256k1)
|
||||
into `firmware/kb2040_hidden_signer/src/secp256k1/`, OR reference them via a
|
||||
relative include path. (Recommend copying so the sketch is self-contained for
|
||||
`arduino-cli`.)
|
||||
2. **cJSON on-device?** nostr_core_lib's NIP-01 signer uses cJSON. Use it
|
||||
(simplest, matches library) or hand-serialize the id to save RAM?
|
||||
3. **Derivation caching:** cache the mnemonic string and derive on demand
|
||||
(recommended, via `nostr_derive_keys_from_mnemonic`) vs cache `seed[64]`.
|
||||
|
||||
---
|
||||
|
||||
## Actionable TODO (for Code mode)
|
||||
|
||||
1. Vendor secp256k1: copy `src/secp256k1.c`, `src/precomputed_ecmult.c`,
|
||||
`src/precomputed_ecmult_gen.c` + `include/` from
|
||||
`firmware/feather_s3_tft/components/secp256k1` into
|
||||
`firmware/kb2040_hidden_signer/src/secp256k1/`.
|
||||
2. Add `secp256k1` config header replicating ESP defines
|
||||
(`WIDEMUL_INT64`, `ECMULT_WINDOW_SIZE=15`, `COMB_BLOCKS=11`, `COMB_TEETH=6`,
|
||||
`ENABLE_MODULE_EXTRAKEYS`, `ENABLE_MODULE_SCHNORRSIG`, `ENABLE_MODULE_ECDH`).
|
||||
3. Vendor nostr_core subset into `firmware/kb2040_hidden_signer/src/nostr_core/`
|
||||
(`utils.c`, `nip006.c`, `nip001.c`, `nip019.c`, `nostr_common.c`,
|
||||
`crypto/nostr_secp256k1.c`) and confirm no mbedtls includes remain.
|
||||
4. Add `src/platform/rp2040.c` implementing `nostr_platform_random()` from
|
||||
Arduino-Pico RNG; feed real entropy into context randomize.
|
||||
5. Init `nostr_secp256k1_context_create()` in `setup()`.
|
||||
6. Replace `get_public_key` body with per-index
|
||||
`nostr_derive_keys_from_mnemonic` + x-only hex encode.
|
||||
7. Replace `sign_event` body with real NIP-01 id + BIP-340 schnorr; delete
|
||||
`pseudo_pubkey_hex()` and the string-splice hack; zeroize privkeys.
|
||||
8. `arduino-cli compile` (TinyUSB FQBN); fix include/define errors; check RAM/flash.
|
||||
9. Flash to KB2040 (correct ttyACM port).
|
||||
10. Verify in nostr-login-lite: 5+ distinct npubs; signature verifies; login OK.
|
||||
11. Verify WebUSB demo still works (dual-transport parity).
|
||||
Reference in New Issue
Block a user