Files
n_signer/plans/kb2040_real_nip06_crypto.md

10 KiB

KB2040 Hidden Signer — Real NIP-06 Derivation + Schnorr Signing (Option B)

Goal

Replace placeholder pseudo_pubkey_hex() 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
SHA-512 YES nostr_sha512
HMAC-SHA256 / HMAC-SHA512 YES utils.c
PBKDF2-HMAC-SHA512 (2048) YES nostr_pbkdf2_hmac_sha512
BIP-39 mnemonic to seed YES nostr_bip39_mnemonic_to_seed
BIP-32 master + path derive YES utils.c
NIP-06 keys-from-mnemonic m/44'/1237'/account'/0/0 YES nostr_derive_keys_from_mnemonic
NIP-01 create+sign event (id + schnorr) YES nostr_create_and_sign_event
RNG abstraction (needs platform impl) YES nostr_platform_random

The ONE thing nostr_core_lib does NOT contain

The actual elliptic-curve engine (bitcoin-core libsecp256k1). nostr_secp256k1.c 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.

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:

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 #included 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 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() 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 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): 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) (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 (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() and the string-splice id/sig hack in sign_event.
  • 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 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).