10 KiB
KB2040 Hidden Signer — Real NIP-06 Derivation + Schnorr Signing (Option B)
Goal
Replace placeholder pseudo_pubkey_hex()
so that:
get_public_keyreturns a distinct, real secp256k1 x-only pubkey pernostr_index(host shows 5+ accounts again instead of 1 deduped entry).sign_eventreturns 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:
- Component sources:
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, plussecp256k1_schnorrsig.h,secp256k1_extrakeys.h,secp256k1_ecdh.h. - ESP build wiring:
REQUIRES secp256k1andREQUIRES secp256k1. - ESP compile config (the flags we must mirror on RP2040):
components/secp256k1/CMakeLists.txtbuilds onlysrc/secp256k1.c,src/precomputed_ecmult.c,src/precomputed_ecmult_gen.cwith 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 #included before
the secp sources (secp256k1 supports USE_EXTERNAL_DEFAULT_CALLBACKS and a
libsecp256k1-config.h style header).
Steps:
- 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.ccJSON.conly if the NIP-01 cJSON signer path is used (see Open Items).
- Vendor secp256k1 from the existing repo copy at
firmware/feather_s3_tft/components/secp256k1into 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).
- sources:
- Add
platform/rp2040.cimplementingnostr_platform_random()via Arduino-Pico RNG (get_rand_32()/ ROSC entropy), and feed real entropy into the context-randomize step (the stub atnostr_secp256k1_context_createcurrently uses a fixed pattern). - Resolve the mbedTLS question: the ESP CMake listed
REQUIRES mbedtls, but nostr_core_lib has its own SHA/HMAC/PBKDF2 inutils.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. - Call
nostr_init()/nostr_secp256k1_context_create()once insetup().
RPC wiring changes in the sketch
- On mnemonic load (
signer_apply_seed): validate vianostr_bip39_mnemonic_validate, keep caching the mnemonic string for per-index derivation. get_public_key: callnostr_derive_keys_from_mnemonic(mnemonic, nostr_index, priv, pub)(note signature:int account, writes 32-byte x-only pub topublic_key+...internally), then hex-encode the 32-byte x-only pub.sign_event: derive priv fornostr_index, then either reusenostr_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 insign_event. - Zeroize derived private keys after each operation.
Verify
nostr-login-lite: 5+ distinct npubs discovered (no dedup collapse).- Cross-check one pubkey vs host NIP-06 derivation of the same mnemonic/index.
- Sign a NIP-42 auth event; confirm the signature verifies and login completes.
- 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=15matches 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_memzeropattern for secret cleanup.
Open items to confirm with user
- secp256k1 source location: copy the 3 secp
.csources +include/fromfirmware/feather_s3_tft/components/secp256k1intofirmware/kb2040_hidden_signer/src/secp256k1/, OR reference them via a relative include path. (Recommend copying so the sketch is self-contained forarduino-cli.) - 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?
- Derivation caching: cache the mnemonic string and derive on demand
(recommended, via
nostr_derive_keys_from_mnemonic) vs cacheseed[64].
Actionable TODO (for Code mode)
- Vendor secp256k1: copy
src/secp256k1.c,src/precomputed_ecmult.c,src/precomputed_ecmult_gen.c+include/fromfirmware/feather_s3_tft/components/secp256k1intofirmware/kb2040_hidden_signer/src/secp256k1/. - Add
secp256k1config 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). - 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. - Add
src/platform/rp2040.cimplementingnostr_platform_random()from Arduino-Pico RNG; feed real entropy into context randomize. - Init
nostr_secp256k1_context_create()insetup(). - Replace
get_public_keybody with per-indexnostr_derive_keys_from_mnemonic+ x-only hex encode. - Replace
sign_eventbody with real NIP-01 id + BIP-340 schnorr; deletepseudo_pubkey_hex()and the string-splice hack; zeroize privkeys. arduino-cli compile(TinyUSB FQBN); fix include/define errors; check RAM/flash.- Flash to KB2040 (correct ttyACM port).
- Verify in nostr-login-lite: 5+ distinct npubs; signature verifies; login OK.
- Verify WebUSB demo still works (dual-transport parity).