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