Files
nostr_quantum_preparation/plans/v2-hardened-derivation.md

170 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# V2 Hardened Derivation Scheme — Design Doc
## Status
Proposed. Addresses audit finding [F-M3 (Medium)](../audits/GLM5.2/findings.md) — BIP32 non-hardened leaf indices used for PQ seeds.
## Decision summary
| Question | Decision |
|---|---|
| v2 path scheme | **Per-algorithm coin types in the unregistered SLIP-44 `102XXX'` range** (matches n_signer) |
| Seed pipeline | **FIPS seeded interface**: BIP32 child bytes (exact length) → `keygen(seed)` (matches noble + Rust crates) |
| v1 users | **Full recovery**: v1 derivation retained; same seed derives both v1 and v2 keys; old events verify forever |
| n_signer / Rust signer | Free to migrate to the seeded API later (no users); their DRBG pipeline documented as divergence |
| New coin types | ML-DSA-44 = `102006'`, Falcon-512 = `102007'` (continuing n_signer's range) |
## Problem
V1 derives all keys under `m/44'/1237'/0'/0/` with **non-hardened** leaf children:
| Child | Key |
|---|---|
| 0 | secp256k1 (NIP-06, published as npub) |
| 1 | ML-DSA-44 |
| 2 | ML-DSA-65 |
| 3+4 | SLH-DSA-128s (48-byte seed) |
| 5+6 | Falcon-512 (48-byte seed) |
| 7+8 | ML-KEM-768 (64-byte seed) |
The project's threat model assumes the published secp256k1 key **will** be broken by Shor's algorithm. Post-quantum, any leaked public key (including the one inside an xpub) yields its private key, and holding a node's private key + chain code gives every child below it, hardened or not. So with any xpub leak at or above the change level, a quantum attacker reaches **all 5 PQ seeds** through the identity account. Non-hardened derivation buys nothing here anyway: PQ public keys come from `keygen(seed)`, not scalar multiplication, so watch-only derivation of PQ child pubkeys is impossible.
### Why alternatives were rejected
- **Hardened leaves under account 0** (`m/44'/1237'/0'/0'/{n}'` or `m/44'/1237'/0'/{n}'/0'`): still hangs PQ keys off the identity account; an account-0 xpub leak + quantum reaches everything below account 0.
- **Per-algorithm accounts under 1237'** (`m/44'/1237'/{n}'/0/0`): better (per-key isolation) but a coin-level `1237'` xpub leak + quantum still reaches all 5, and accounts 1'–5' collide with NIP-06 multi-identity use (a wallet identity at account 1 would silently republish the ML-DSA-44 seed as an secp256k1 npub).
- **Per-algorithm coin types** (chosen): PQ keys leave the `1237'` subtree entirely. No compromise of the Nostr coin branch — even the coin-level xpub with quantum — can touch them. Matches n_signer's existing scheme.
## Solution
**V2 scheme: one coin type per PQ algorithm, all-hardened below coin type.**
Coin types 102003'–102005' are n_signer's existing allocations ([`n_signer/documents/derivation_paths.md`](../../n_signer/documents/derivation_paths.md)); 102006'–102007' are new allocations for the two algorithms this project adds. The `102XXX` range is unregistered in SLIP-44 and chosen to avoid collisions with real cryptocurrencies.
| Algorithm | Coin type | Path (account 0) | Seed length |
|---|---|---|---|
| ML-DSA-44 | `102006'` | `m/44'/102006'/0'/0'/0'` | 32 B (one child) |
| ML-DSA-65 | `102003'` | `m/44'/102003'/0'/0'/0'` | 32 B (one child) |
| SLH-DSA-128s | `102004'` | `m/44'/102004'/0'/0'/0'` + `/1'` | 48 B (two children, first 48 of 64) |
| Falcon-512 | `102007'` | `m/44'/102007'/0'/0'/0'` + `/1'` | 48 B (two children, first 48 of 64) |
| ML-KEM-768 | `102005'` | `m/44'/102005'/0'/0'/0'` + `/1'` | 64 B (two children) |
The secp256k1 identity key **stays at NIP-06 `m/44'/1237'/0'/0/0`** — unchanged, standard, published.
### Security properties
| Leak + quantum attacker | Result |
|---|---|
| Published npub only (always broken) | PQ safe |
| Account-0 xpub (standard wallet export) | PQ safe |
| Coin-level `m/44'/1237'` xpub | **PQ safe — PQ keys are not under `1237'` at all** |
| One PQ coin branch's own xpub | 1 algorithm falls (per-algorithm isolation) |
| NIP-06 multi-identity accounts | No collision — wallets never derive `102XXX'` coin types |
### Seed pipeline: FIPS seeded interface
FIPS 203/204/205 define keygen as consuming a fixed-length seed (ML-DSA 32 B, ML-KEM 64 B, SLH-DSA-128s 48 B); the SHAKE expansion happens *inside* keygen. The v2 pipeline is therefore: derive BIP32 children → concatenate/truncate to the exact seed length → `keygen(seed)`. This is what [`derivePQKeysFromSeed()`](../www/js/pq-crypto.mjs) already does via noble, and what Rust PQ crates expose — so JS and Rust implementations agree by construction.
**n_signer divergence:** n_signer feeds the derived child through a SHAKE-256 DRBG into PQClean's `randombytes()` callback (a PQClean API artifact, not a cryptographic choice). Same path + same seed bytes there produce *different* keys than the seeded interface. Since n_signer and the Rust signer have no users, the recommendation (filed separately in those projects) is to migrate them to the seeded API; this project does not replicate the DRBG.
**Falcon caveat:** Falcon (draft FIPS 206) keygen is rejection-sampling-based with no universally implemented seed interface. Even with identical seeds, noble's Falcon keys ≠ PQClean's ≠ Rust's. We pin noble's behavior in test vectors and flag Falcon as per-library in the NIP proposal.
## Compatibility — v1 users can still recover
The root of trust is the **BIP39 seed**, not the path. Verification is path-agnostic: [`verify-app.mjs`](../www/js/verify-app.mjs) checks PQ signatures against pubkeys in the kind 1 event tags and never derives from a seed. Therefore:
1. **Existing v1 events remain fully verifiable forever.** No verifier changes required for old events.
2. **A v1 user's seed still recovers their v1 keys.** V1 derivation code is retained and exposed as a legacy option.
3. **The same seed mints a v2 key-link event at any time**: load seed → derive v2 keys → publish new kind 1 → OTS anchor. The v2 PQ keys are cryptographically independent of the v1 keys (different coin branches), so the v1 xpub-leak scenario no longer matters going forward.
### Version signaling
New kind 1 events include a `derivation_scheme` tag:
```
['derivation_scheme', '2']
```
- Absent tag → v1 (legacy). Informational for display; signature verification is unaffected either way.
- Unknown future values → informational only (fail-open for display; this tag is metadata, not evidence — unlike `digest_version`, which fails closed because it changes what is hashed).
## File-by-file changes
### 1. `www/js/pq-crypto.mjs`
- Add versioned scheme table:
```js
const PQ_DERIVATION_SCHEMES = {
v1: { // legacy — retained for recovery, never default
base: "m/44'/1237'/0'/0", hardenedLeaves: false,
children: { mlDsa44: [1], mlDsa65: [2], slhDsa: [3,4], falcon512: [5,6], mlKem: [7,8] } },
v2: { // per-algorithm coin types (n_signer-compatible)
hardenedLeaves: true,
children: {
mlDsa44: { coin: 102006, indices: [0] },
mlDsa65: { coin: 102003, indices: [0] },
slhDsa: { coin: 102004, indices: [0, 1] },
falcon512: { coin: 102007, indices: [0, 1] },
mlKem: { coin: 102005, indices: [0, 1] },
} },
};
export const PQ_DERIVATION_SCHEME_VERSION = 2;
```
- Refactor [`deriveBIP32Child()`](../www/js/pq-crypto.mjs) and `derivePQSeedFromBIP32()` to take the scheme instead of the hardcoded v1 base.
- `derivePQKeysFromSeed(seed, scheme = 'v2')` — default v2; `'v1'` still works for recovery.
- `buildKind1Announcement()` gains a `derivationScheme` parameter (default 2) and emits the `derivation_scheme` tag.
- `PQ_KEY_INFO` derivation paths become scheme-aware so the UI shows the correct path.
### 2. `www/js/index-app.mjs`
- Default flow derives v2 and shows v2 paths in the UI.
- Add a **v1 recovery mode**: user enters a v1-era seed → app derives v1 keys → matches them against the user's published kind 1 event (by npub) → confirms "these are your v1 keys" → offers to mint a v2 event from the same seed.
### 3. `www/pq-crypto.bundle.js`
- Rebuild via `node build-pq-bundle.js` after source changes.
### 4. `test/vectors/generate-vectors.mjs` + vectors
- Emit `seed-to-pubkeys.v2.json` (same fixed test seed, v2 paths) alongside the pinned v1 file. V1 vectors stay untouched as the legacy conformance reference.
### 5. `test/pq-crypto.test.mjs`
- v2 derivation reproduces the v2 vector.
- v1 derivation still reproduces the v1 vector (regression).
- **Independence test:** v1 and v2 keys from the same seed share no key material (pubkeys differ for every algorithm).
- New events carry `derivation_scheme: '2'`; v1 events omit it.
- Recovery path: v1 seed → v1 keys → match published event tags.
### 6. Docs
- `README.md`: Component 6 gains the v2 scheme and the coin-type isolation rationale; implementation status table updated.
- `explanation.md`, `nip_proposal.md`: replace the wallet-compatibility justification for non-hardened leaves with the v2 scheme; document `derivation_scheme` tag; document v1 legacy/recovery; document the `102XXX'` coin-type registry (102003'–102005' per n_signer, 102006'–102007' new); flag Falcon as per-library.
- `audits/GLM5.2/findings.md` F-M3: annotate as addressed-by-v2 (append status; do not rewrite history).
### 7. `www/js/version.json`
- Bump to `0.2.0` (minor: new derivation scheme, backward compatible).
## What we are explicitly NOT doing
- **Not** deleting or changing v1 derivation (recovery depends on it).
- **Not** re-deriving or re-signing existing events (impossible — and unnecessary, verification is path-agnostic).
- **Not** making `derivation_scheme` fail-closed in the verifier (display metadata, not evidence).
- **Not** moving the secp256k1 identity key off NIP-06 (ecosystem compatibility).
- **Not** replicating n_signer's SHAKE-256 DRBG pipeline (locks us out of the FIPS seeded interface; n_signer/Rust should migrate instead — separate effort, no users to break).
- **Not** claiming cross-implementation Falcon determinism (rejection sampling; pin noble's vectors, flag in NIP).
## Test matrix summary
| Test | Asserts |
|---|---|
| v2 vector reproduction | Same seed → pinned v2 pubkeys |
| v1 vector regression | Same seed → pinned v1 pubkeys (unchanged) |
| v1/v2 independence | No shared pubkeys across schemes |
| Tag emission | New events have `derivation_scheme 2`; legacy path omits it |
| Recovery flow | v1 seed → v1 keys match published event |
| Existing suite | All current tests still pass (no behavioral change to verification) |