Files
n_signer/plans/kb2040_real_nip06_crypto.md

177 lines
10 KiB
Markdown

# 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).