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