Q: seed derivation, and what the unhardened path costs a C firmware - #108
Q: seed derivation, and what the unhardened path costs a C firmware#108TheCryptoDonkey wants to merge 4 commits into
Conversation
LUD-25's Seed-recoverable note secrets section: every secret this vault generates for a note is drawn from the hardware RNG, and a seed phrase backs up nothing that was never derived from it. Restore onto a fresh device today and every outstanding note is gone, silently, until someone tries to redeem one whose preimage no longer exists anywhere. This is the derivation core only -- nothing calls it yet, and no note behaviour changes. It is deliberately the first piece, because it is what decides whether the section is implementable on a device this small. Every level of the path is hardened, which diverges from the section as drafted (139' and i' hardened, d1..d4 not). Unhardened CKDpriv hashes the parent PUBLIC key, so it needs a secp256k1 point multiplication, and this firmware carries no EC code at all. Hardened CKDpriv needs HMAC-SHA512 -- already linked, monocypher does OTA signature checks with it -- plus 256-bit modular addition, which is what bn_add_mod_n is. That is the whole difference between this file and porting an EC implementation into the reference vault. Nothing is lost by it. Unhardened derivation buys child PUBLIC keys without the private key, and a note secret has no public counterpart here: SERVICE only ever sees sha256(secret). LUD-05 leaves those levels unhardened because a linkingKey is a real signing keypair; the shape does not carry over. Two details the spec still has to pin, or two wallets will derive two trees for one mint and a restore will quietly find nothing: the domain levels are masked to 31 bits (a hardened index has no 32nd), and the domain hashed is the withdraw endpoint's host AND path, since a note is the whole URL. Vectors are BIP-32's own published ones (including test vector 3, whose master key begins with a zero byte), plus LUD-25 path vectors generated by an independent Python implementation of the same scheme.
4870adc to
5a11e29
Compare
## Why Independent of the seed-derivation question in #108, and deliberately separate from it: this changes nothing about how note secrets are derived, only how the one secret they all descend from is generated. The note seed is the single value every note depends on for the life of the device, and the TRNG behind it comes with a caveat this repo already documents. `README.md`'s security posture says Espressif's full-entropy guarantee for `esp_fill_random()` is conditional on Wi-Fi or BT having been active, `main.c` starts BLE before the first secret for exactly that reason, and the startup self-test catches a catastrophically stuck source rather than a merely poor one. That is a bet on firmware ordering staying correct forever, and it is a bigger bet for a seed than for one note. ## What The seed is drawn from both ends, and neither can choose it: 1. `entropy_begin` — the vault draws 32 bytes and publishes only their sha256. It is now committed. 2. the host — the wallet gathers real human randomness and sends it over. 3. `entropy_finish` — the vault mixes the two, and reveals its own contribution so the wallet can check it against the commitment from step 1. The commitment is the point of the ordering. Without it, a vault with a weak or backdoored TRNG could wait to see the host's bytes and grind its own until the seed landed somewhere it liked. With it, the vault is bound before the host speaks, and the host moves second against a value already fixed. Because the mix is a hash, neither contribution can cancel the other: an all-zero host leaves the seed exactly as good as the TRNG alone, a dead TRNG leaves it as good as the host's. It takes both being bad to get a bad seed. A rejected contribution closes the session rather than leaving the commitment live, or the same grinding just points the other way — a host retrying against a pinned device value samples seeds until one suits it. ## Scope Nothing calls it yet and no behaviour changes; `new_note` still uses the RNG directly. It is offered as the piece that would sit under a seed, whichever derivation the spec settles on. Portable, RNG injected, so the adversarial properties are what the tests assert rather than something argued in a comment: a vault cannot swap its contribution, a degenerate host cannot weaken the seed, a rejected contribution cannot be retried, and an all-zero draw refuses to start. ## Verification - native suite 992/992, ASan + UBSan 992/992, `CC=gcc-16` per the Makefile's own warning about Apple clang - firmware builds for `t-display` - rebased onto current `main` Co-authored-by: TheCryptoDonkey <TheCryptoDonkey@users.noreply.github.com>
…y lacks The header claimed the module diverges from the draft in one place. It diverges in two: the draft's cashHashingKey is 139'/0 and this uses 139'/0'. That one changes cashHashingKey, so d1..d4, so every secret - a wallet following the draft finds none of this firmware's notes even if the d1..d4 hardening question is settled tomorrow. Worth declaring where the divergence is declared. It also said the firmware carries no EC code at all. It carries curve25519, in monocypher, for Ed25519 OTA signature checks; what it has none of is secp256k1, which is the claim the argument actually rests on. And SHA-512 and HMAC-SHA512 are not needed BY the OTA check, they are linked in with monocypher-ed25519.c for it.
|
Three things from implementing this on the vault firmware, all questions The restore walk. We ended up asking The title question, or the part not already in the description: I could Not derivation, but next door. None of this is urgent for us. Happy to move any of it to lnurl/luds#301 if |
|
You answered this by shipping, and the answer goes against this PR.
and Withdrawing the hardening ask, and the masking one with it. Both were The useful half of this PR survived, though, and it turned out better than Closing this one. Thanks for the answer, even if it was the one that cost me |
Read this as a question, not a feature
You said you'd bring your signer's implementation across into the reference
vault. This is not an attempt to get in front of that. It exists because
implementing the section here answers one question that is much harder to
answer by reading: what the path costs a firmware with no elliptic-curve
code in it.
Take it, bin it, or take just the argument and write your own. If you'd rather
do it yourself, say so and I'll close this.
The one thing worth deciding before either of us writes more
The section hardens
139'andi'but notd1..d4, and its cashHashingKeyis
139'/0where this PR uses139'/0'(that second one changescashHashingKey, so
d1..d4, so every secret - the two schemes miss eachother's notes even once the
d1..d4question is settled). Unhardened CKDprivhashes
serP(point(kpar)), the parent public key, so computing one needsa secp256k1 point multiplication.
Your signer has secp256k1. This firmware has none. It does carry curve25519,
in monocypher, for the Ed25519 OTA signature check, but that is a different
curve with nothing reusable here. What it does have to hand is SHA-512 and
HMAC-SHA512, linked in with monocypher-ed25519.c for that OTA check, plus its
own SHA-256.
Hardened CKDpriv needs exactly those, plus 256-bit modular addition. That is
the whole of
bn_add_mod_nin this PR, and the whole difference between 300lines of arithmetic and porting an EC implementation into the reference vault.
Nothing is lost by hardening. Unhardened derivation buys one thing: deriving
child public keys without the private key. A note secret has no public
counterpart in this protocol,
SERVICEonly ever seessha256(secret), sothe capability has nothing to act on. LUD-05 leaves those levels unhardened
because a linkingKey is a real signing keypair; the shape does not carry over
to opaque 32-byte preimages, and it costs exactly the devices most likely to
be holding these notes.
So this PR implements the hardened variant, which means it does not match
the section as written. If you keep
d1..d4unhardened,derive.cis wrongand I'll rewrite it or close this. That is the decision I'm asking for.
Two details that need pinning either way
Whoever writes it, and whatever you decide above:
bit, and
d1..d4come out of a hash, so roughly half of them overflow. Thismasks with
HARDENED - 1. It costs 4 bits out of 128 that were only everseparating domains.
note is the whole URL, and two mints can share a hostname, so hashing the
host alone gives them one tree between them.
Get either wrong in two implementations and the trees differ, and a restore
finds nothing while reporting no error at all.
Verification
Native suite 1026/1026, ASan + UBSan clean,
CC=gcc-16per the Makefile's ownwarning about Apple clang. Firmware builds for both boards. Nothing calls it
and no behaviour changes.
The BIP-32 vectors are the published ones from the BIP, including test vector
3 whose master key begins with a zero byte, which is the case implementations
get wrong by dropping the left pad. The LUD-25 path vectors were generated by
an independent Python implementation of the same scheme, so a match is two
implementations agreeing rather than one repeating itself.
Seed generation moved out to #110, which is independent of all of the above.