V2 hardened derivation: PQ keys move to per-algorithm coin types (102003'-102007', all-hardened) outside the Nostr 1237' subtree, closing audit F-M3. Adds derivation_scheme tag on new events, v1 recovery mode for legacy seeds, v2 conformance vectors, 10 new tests (147 pass), updated docs/NIP proposal, and upload-test.sh staging deploy. Deployed to production.
This commit is contained in:
@@ -274,6 +274,22 @@ The NIP-06 path `m/44'/1237'/account'/0/0` has hardened derivation at three leve
|
||||
|
||||
**Recommendations:** use separate accounts for separate identities; never publish xpubs (chain codes are the keys to the kingdom); use 24-word mnemonics; don't reuse keys across services; assume published keys *will* be broken and plan for containment, not prevention.
|
||||
|
||||
### V2 hardened derivation (current)
|
||||
|
||||
Post-quantum keys are no longer derived under the Nostr coin branch at all. Each algorithm gets its own **coin type** in the unregistered SLIP-44 `102XXX'` range, all-hardened below the coin type (see [`plans/v2-hardened-derivation.md`](plans/v2-hardened-derivation.md)):
|
||||
|
||||
| Algorithm | Coin type | Path |
|
||||
|---|---|---|
|
||||
| ML-DSA-44 | `102006'` | `m/44'/102006'/0'/0'/0'` |
|
||||
| ML-DSA-65 | `102003'` | `m/44'/102003'/0'/0'/0'` |
|
||||
| SLH-DSA-128s | `102004'` | `m/44'/102004'/0'/0'/0'` + `/1'` |
|
||||
| Falcon-512 | `102007'` | `m/44'/102007'/0'/0'/0'` + `/1'` |
|
||||
| ML-KEM-768 | `102005'` | `m/44'/102005'/0'/0'/0'` + `/1'` |
|
||||
|
||||
The secp256k1 identity key stays at NIP-06 `m/44'/1237'/0'/0/0`. Because the PQ keys live outside the `1237'` subtree entirely, **no compromise of the Nostr coin branch — even a coin-level xpub leak combined with a quantum computer — can reach them**, and a leak of any single PQ branch's extended key compromises exactly one algorithm. Coin types 102003'–102005' match n_signer and the Rust signer; 102006'–102007' are this project's allocations.
|
||||
|
||||
**V1 legacy and recovery.** Events created before v2 used non-hardened children under `m/44'/1237'/0'/0/` (audit F-M3). Those events verify forever (verification is path-agnostic), and the app retains a v1 recovery mode: enter a v1-era seed, re-derive its v1 keys, confirm they match the published event, then re-run the same seed through the v2 flow to publish upgraded keys. New events carry a `derivation_scheme` tag (`2`); absent tag means v1.
|
||||
|
||||
---
|
||||
|
||||
## What Remains Unsolved
|
||||
@@ -296,6 +312,9 @@ The current implementation is a static web app (`www/`) that performs the full p
|
||||
|---|---|---|
|
||||
| BIP39 seed phrase generation (24-word default, 12-word option, with optional user entropy) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| BIP32 key derivation (secp256k1 + 5 PQ keypairs) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| V2 hardened derivation (per-algorithm 102XXX' coin types, `derivation_scheme` tag) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| V1 legacy recovery mode (re-derive v1 keys from a v1-era seed) | Implemented | `www/index.html` |
|
||||
| V2 conformance vectors (seed → pubkeys, per-scheme) | Implemented | `test/vectors/seed-to-pubkeys.v2.json` |
|
||||
| PQ signing (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| ML-KEM-768 keygen (KEM, no signing) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| Kind 1 announcement event construction | Implemented | `www/js/pq-crypto.mjs` |
|
||||
|
||||
@@ -147,6 +147,8 @@ and have callers require `id` presence.
|
||||
|
||||
**Fix.** Document the requirement never to publish the xpub at `m/44'/1237'/0'/0'` (done). Consider hardened indices for PQ children if wallet-compatibility is not required.
|
||||
|
||||
**Status (2026-08-21): ADDRESSED by derivation scheme v2.** PQ keys now derive under per-algorithm coin types in the unregistered SLIP-44 `102XXX'` range (all-hardened below coin type), entirely outside the Nostr `1237'` subtree — so no xpub leak within the Nostr coin branch can reach PQ keys, and a single PQ branch leak costs one algorithm. V1 derivation is retained for recovery of v1-era seeds; new events carry a `derivation_scheme` tag. See `plans/v2-hardened-derivation.md` and `test/vectors/seed-to-pubkeys.v2.json`.
|
||||
|
||||
---
|
||||
|
||||
## F-M4 (Medium) — Truncation rule for concatenated BIP32 children is arbitrary
|
||||
|
||||
+12
-8
@@ -41,16 +41,20 @@ This is the root of the HD wallet tree. From here, we derive all keys — both s
|
||||
|
||||
## Step 4: Derive all keys from BIP32 paths
|
||||
|
||||
All keys are derived under the NIP-06 base path `m/44'/1237'/0'/0/`, using different child indices:
|
||||
The secp256k1 identity key is derived at the NIP-06 path `m/44'/1237'/0'/0/0`. The five PQ keys are derived under **per-algorithm coin types** in the unregistered SLIP-44 `102XXX'` range, all-hardened below the coin type (the v2 scheme — see below for why):
|
||||
|
||||
| Child index | Algorithm | Seed length | BIP32 path | How derived |
|
||||
| Algorithm | Coin type | Seed length | BIP32 path | How derived |
|
||||
|---|---|---|---|---|
|
||||
| 0 | secp256k1 (NIP-06) | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32 — private key used directly |
|
||||
| 1 | ML-DSA-44 | 32 bytes | `m/44'/1237'/0'/0/1` | Single child — 32-byte private key is the PQ seed |
|
||||
| 2 | ML-DSA-65 | 32 bytes | `m/44'/1237'/0'/0/2` | Single child — 32-byte private key is the PQ seed |
|
||||
| 3+4 | SLH-DSA-128s | 48 bytes | `m/44'/1237'/0'/0/3` + `m/44'/1237'/0'/0/4` | Two children concatenated (64 bytes), first 48 used |
|
||||
| 5+6 | Falcon-512 | 48 bytes | `m/44'/1237'/0'/0/5` + `m/44'/1237'/0'/0/6` | Two children concatenated (64 bytes), first 48 used |
|
||||
| 7+8 | ML-KEM-768 | 64 bytes | `m/44'/1237'/0'/0/7` + `m/44'/1237'/0'/0/8` | Two children concatenated (64 bytes) |
|
||||
| secp256k1 (NIP-06) | 1237' | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32 — private key used directly |
|
||||
| ML-DSA-44 | 102006' | 32 bytes | `m/44'/102006'/0'/0'/0'` | Single child — 32-byte private key is the PQ seed |
|
||||
| ML-DSA-65 | 102003' | 32 bytes | `m/44'/102003'/0'/0'/0'` | Single child — 32-byte private key is the PQ seed |
|
||||
| SLH-DSA-128s | 102004' | 48 bytes | `m/44'/102004'/0'/0'/0'` + `/1'` | Two children concatenated (64 bytes), first 48 used |
|
||||
| Falcon-512 | 102007' | 48 bytes | `m/44'/102007'/0'/0'/0'` + `/1'` | Two children concatenated (64 bytes), first 48 used |
|
||||
| ML-KEM-768 | 102005' | 64 bytes | `m/44'/102005'/0'/0'/0'` + `/1'` | Two children concatenated (64 bytes) |
|
||||
|
||||
**Why coin types instead of child indices under 1237'?** The v1 scheme placed PQ seeds at non-hardened children under the NIP-06 account. Post-quantum, any leaked public key (including one inside an xpub) yields its private key, and a node's private key + chain code gives every child below it — so a single xpub leak inside the Nostr coin branch could reach all five PQ seeds. Moving each algorithm to its own coin branch means no compromise of the `1237'` subtree can touch PQ keys at all, and a leak of one branch's extended key costs exactly one algorithm. Coin types 102003'–102005' match n_signer and the Rust signer; 102006'–102007' are this project's allocations.
|
||||
|
||||
**V1 legacy (recovery only).** Events created before v2 used non-hardened children 1–8 under `m/44'/1237'/0'/0/`. Those events verify forever, and the app can re-derive v1 keys from a v1-era seed (recovery mode) so their owner can confirm ownership and then publish upgraded v2 keys from the same seed. New events carry a `derivation_scheme` tag (`2`); absent tag means v1.
|
||||
|
||||
### Why some algorithms need two children
|
||||
|
||||
|
||||
+24
-10
@@ -63,18 +63,30 @@ flowchart TD
|
||||
|
||||
## PQ key derivation from a BIP39 seed
|
||||
|
||||
All PQ keys are derived from a BIP39 seed via **BIP32 hierarchical deterministic derivation**, the same standard NIP-06 uses for secp256k1 keys. PQ keys live at fixed child indices under the NIP-06 base path.
|
||||
All PQ keys are derived from a BIP39 seed via **BIP32 hierarchical deterministic derivation**, the same standard NIP-06 uses for secp256k1 keys.
|
||||
|
||||
**Base path:** `m/44'/1237'/0'/0/` (NIP-06 account 0, change 0)
|
||||
### Derivation scheme v2 (current)
|
||||
|
||||
| Child index/indices | Algorithm | Seed length needed | BIP32 path | Derivation |
|
||||
Each PQ algorithm is derived under its own **coin type** in the unregistered SLIP-44 `102XXX'` range, all-hardened below the coin type. The secp256k1 identity key stays at the NIP-06 path.
|
||||
|
||||
| Algorithm | Coin type | Seed length needed | BIP32 path | Derivation |
|
||||
|---|---|---|---|---|
|
||||
| 0 | secp256k1 (NIP-06) | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32; private key used directly |
|
||||
| 1 | ML-DSA-44 | 32 bytes | `m/44'/1237'/0'/0/1` | Single child; 32-byte private key is the PQ seed |
|
||||
| 2 | ML-DSA-65 | 32 bytes | `m/44'/1237'/0'/0/2` | Single child; 32-byte private key is the PQ seed |
|
||||
| 3 + 4 | SLH-DSA-128s | 48 bytes | `m/44'/1237'/0'/0/3` + `m/44'/1237'/0'/0/4` | Two children concatenated (64 bytes), first 48 used |
|
||||
| 5 + 6 | Falcon-512 | 48 bytes | `m/44'/1237'/0'/0/5` + `m/44'/1237'/0'/0/6` | Two children concatenated (64 bytes), first 48 used |
|
||||
| 7 + 8 | ML-KEM-768 | 64 bytes | `m/44'/1237'/0'/0/7` + `m/44'/1237'/0'/0/8` | Two children concatenated (64 bytes) |
|
||||
| secp256k1 (NIP-06) | 1237' | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32; private key used directly |
|
||||
| ML-DSA-44 | 102006' | 32 bytes | `m/44'/102006'/0'/0'/0'` | Single child; 32-byte private key is the PQ seed |
|
||||
| ML-DSA-65 | 102003' | 32 bytes | `m/44'/102003'/0'/0'/0'` | Single child; 32-byte private key is the PQ seed |
|
||||
| SLH-DSA-128s | 102004' | 48 bytes | `m/44'/102004'/0'/0'/0'` + `m/44'/102004'/0'/0'/1'` | Two children concatenated (64 bytes), first 48 used |
|
||||
| Falcon-512 | 102007' | 48 bytes | `m/44'/102007'/0'/0'/0'` + `m/44'/102007'/0'/0'/1'` | Two children concatenated (64 bytes), first 48 used |
|
||||
| ML-KEM-768 | 102005' | 64 bytes | `m/44'/102005'/0'/0'/0'` + `m/44'/102005'/0'/0'/1'` | Two children concatenated (64 bytes) |
|
||||
|
||||
The coin-type registry: 102003' (ML-DSA-65), 102004' (SLH-DSA-128s), and 102005' (ML-KEM-768) are shared with the n_signer and Rust signer projects; 102006' (ML-DSA-44) and 102007' (Falcon-512) are allocated by this proposal. The `102XXX` range is unregistered in SLIP-44 and chosen to avoid collisions with real cryptocurrencies.
|
||||
|
||||
**Rationale.** PQ keys sit outside the Nostr coin branch (`1237'`) entirely. Post-quantum, any leaked public key — including one inside an extended public key — yields its private key, and a node's private key plus chain code yields every child below it, hardened or not. Deriving PQ keys under per-algorithm coin types means (a) no compromise of the Nostr subtree, even a coin-level xpub leak combined with a quantum computer, can reach PQ keys, and (b) a leak of any single PQ branch's extended key compromises exactly one algorithm.
|
||||
|
||||
**Falcon-512 caveat.** Falcon's keygen is rejection-sampling-based with no universally implemented seed interface; identical seeds produce different keys across libraries. The conformance vectors pin `@noble/post-quantum`'s behavior; implementations using other Falcon libraries will derive different (but equally valid) Falcon keys and MUST NOT be treated as non-conforming on that basis alone.
|
||||
|
||||
### Derivation scheme v1 (legacy)
|
||||
|
||||
Events created before this scheme was introduced derived PQ keys at non-hardened child indices under `m/44'/1237'/0'/0/` (children 1–8, same concatenation rule). Verification is path-agnostic — v1 events remain valid forever — and implementations SHOULD retain v1 derivation so owners of v1-era seeds can recover their keys and publish upgraded v2 events from the same seed. New events MUST use v2 and include the `derivation_scheme` tag (below); an absent tag indicates v1.
|
||||
|
||||
### Concatenation and truncation rule (normative)
|
||||
|
||||
@@ -134,6 +146,7 @@ pending timestamp on the Bitcoin blockchain via OpenTimestamps.
|
||||
- `["block_height", "<height>"]` — the Bitcoin block height at signing time, as a decimal string.
|
||||
- `["algorithm", "<algorithm-id>", "<base64 pubkey>", "<base64 signature>"]` — one tag per PQ **signature** scheme. The signature is over `TextEncoder.encode(content)`.
|
||||
- `["algorithm", "ml-kem-768", "<base64 pubkey>"]` — for ML-KEM-768. ML-KEM is a KEM, not a signature scheme, so it has no signature field. Its ownership is asserted by the attestation text and authorized by the attesting identity's secp256k1 signature over the kind 1 event (which covers the tags, including the ML-KEM pubkey).
|
||||
- `["derivation_scheme", "<version>"]` — the derivation scheme version that produced the PQ keys (`"2"` for the current per-algorithm coin-type scheme). Informational metadata: verification is path-agnostic, so verifiers MUST NOT reject events over this tag, and unknown values are display-only. An absent tag indicates v1 (legacy).
|
||||
|
||||
### Algorithm identifiers
|
||||
|
||||
@@ -168,7 +181,8 @@ A future NIP revision MAY define a new policy version that drops a scheme later
|
||||
["algorithm", "ml-dsa-65", "<base64 pubkey>", "<base64 signature>"],
|
||||
["algorithm", "slh-dsa-128s", "<base64 pubkey>", "<base64 signature>"],
|
||||
["algorithm", "falcon-512", "<base64 pubkey>", "<base64 signature>"],
|
||||
["algorithm", "ml-kem-768", "<base64 pubkey>"]
|
||||
["algorithm", "ml-kem-768", "<base64 pubkey>"],
|
||||
["derivation_scheme", "2"]
|
||||
],
|
||||
"sig": "<attesting identity Schnorr signature>"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nostr_quantum_preparation",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# 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) |
|
||||
+164
-7
@@ -1523,7 +1523,7 @@ describe('F-D6: cross-implementation conformance vectors', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('F-D6: implementation reproduces the pinned seed-to-pubkeys vector', () => {
|
||||
test('F-D6: implementation reproduces the pinned v1 seed-to-pubkeys vector (legacy regression)', () => {
|
||||
const vecPath = join(vectorsDir, 'seed-to-pubkeys.v1.json');
|
||||
const vec = JSON.parse(readFileSync(vecPath, 'utf8'));
|
||||
const seed = m.mnemonicToSeed(vec.mnemonic);
|
||||
@@ -1534,16 +1534,173 @@ describe('F-D6: cross-implementation conformance vectors', () => {
|
||||
assert.equal(m.bytesToHex(secpKp.publicKey), vec.derivedPublicKeys.secp256k1.publicKeyHex,
|
||||
'secp256k1 pubkey should match the pinned vector');
|
||||
|
||||
const pqKeys = m.derivePQKeysFromSeed(seed);
|
||||
// v1 vector must be reproduced with the EXPLICIT legacy scheme — the
|
||||
// default is v2 now, and v1 keys must never change (recovery depends on it).
|
||||
const pqKeys = m.derivePQKeysFromSeed(seed, 'v1');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlDsa44.publicKey), vec.derivedPublicKeys['ml-dsa-44'].publicKeyHex,
|
||||
'ml-dsa-44 pubkey should match the pinned vector');
|
||||
'ml-dsa-44 pubkey should match the pinned v1 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlDsa65.publicKey), vec.derivedPublicKeys['ml-dsa-65'].publicKeyHex,
|
||||
'ml-dsa-65 pubkey should match the pinned vector');
|
||||
'ml-dsa-65 pubkey should match the pinned v1 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.slhDsa.publicKey), vec.derivedPublicKeys['slh-dsa-128s'].publicKeyHex,
|
||||
'slh-dsa-128s pubkey should match the pinned vector');
|
||||
'slh-dsa-128s pubkey should match the pinned v1 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.falcon512.publicKey), vec.derivedPublicKeys['falcon-512'].publicKeyHex,
|
||||
'falcon-512 pubkey should match the pinned vector');
|
||||
'falcon-512 pubkey should match the pinned v1 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlKem.publicKey), vec.derivedPublicKeys['ml-kem-768'].publicKeyHex,
|
||||
'ml-kem-768 pubkey should match the pinned vector');
|
||||
'ml-kem-768 pubkey should match the pinned v1 vector');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// V2 HARDENED DERIVATION SCHEME (per-algorithm coin types)
|
||||
// ============================================================================
|
||||
//
|
||||
// See plans/v2-hardened-derivation.md. V2 moves PQ keys off the Nostr coin
|
||||
// branch (1237') entirely, into per-algorithm coin types in the unregistered
|
||||
// SLIP-44 102XXX' range, all-hardened below the coin type. This isolates PQ
|
||||
// keys from any compromise of the Nostr subtree (audit F-M3) and matches the
|
||||
// coin-type registry shared with n_signer / the Rust signer.
|
||||
|
||||
describe('V2 hardened derivation scheme', () => {
|
||||
const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
test('v2: seed-to-pubkeys.v2.json vector is loadable and well-formed', () => {
|
||||
const vecPath = join(vectorsDir, 'seed-to-pubkeys.v2.json');
|
||||
let vec;
|
||||
try {
|
||||
vec = JSON.parse(readFileSync(vecPath, 'utf8'));
|
||||
} catch (e) {
|
||||
assert.fail(`Could not load ${vecPath}: ${e.message}`);
|
||||
}
|
||||
assert.equal(vec.vectorType, 'nostr-pq-link-seed-to-pubkeys');
|
||||
assert.equal(vec.vectorVersion, 2);
|
||||
assert.equal(vec.derivationScheme, 2);
|
||||
assert.equal(typeof vec.mnemonic, 'string');
|
||||
assert.equal(typeof vec.bip39SeedHex, 'string');
|
||||
for (const alg of ['ml-dsa-44', 'ml-dsa-65', 'slh-dsa-128s', 'falcon-512', 'ml-kem-768']) {
|
||||
assert.ok(vec.derivedPublicKeys[alg], `should have ${alg} entry`);
|
||||
assert.equal(typeof vec.derivedPublicKeys[alg].publicKeyHex, 'string');
|
||||
assert.ok(vec.derivedPublicKeys[alg].derivationPath.includes("102"), `${alg} path should use a 102XXX coin type`);
|
||||
}
|
||||
});
|
||||
|
||||
test('v2: implementation reproduces the pinned v2 vector', () => {
|
||||
const vecPath = join(vectorsDir, 'seed-to-pubkeys.v2.json');
|
||||
const vec = JSON.parse(readFileSync(vecPath, 'utf8'));
|
||||
const seed = m.mnemonicToSeed(vec.mnemonic);
|
||||
assert.equal(m.bytesToHex(seed), vec.bip39SeedHex, 'BIP39 seed should match the v2 vector');
|
||||
|
||||
const pqKeys = m.derivePQKeysFromSeed(seed, 'v2');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlDsa44.publicKey), vec.derivedPublicKeys['ml-dsa-44'].publicKeyHex,
|
||||
'ml-dsa-44 pubkey should match the pinned v2 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlDsa65.publicKey), vec.derivedPublicKeys['ml-dsa-65'].publicKeyHex,
|
||||
'ml-dsa-65 pubkey should match the pinned v2 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.slhDsa.publicKey), vec.derivedPublicKeys['slh-dsa-128s'].publicKeyHex,
|
||||
'slh-dsa-128s pubkey should match the pinned v2 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.falcon512.publicKey), vec.derivedPublicKeys['falcon-512'].publicKeyHex,
|
||||
'falcon-512 pubkey should match the pinned v2 vector');
|
||||
assert.equal(m.bytesToHex(pqKeys.mlKem.publicKey), vec.derivedPublicKeys['ml-kem-768'].publicKeyHex,
|
||||
'ml-kem-768 pubkey should match the pinned v2 vector');
|
||||
});
|
||||
|
||||
test('v2: default scheme is v2 (no argument = v2 keys)', () => {
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
const byDefault = m.derivePQKeysFromSeed(seed);
|
||||
const explicitV2 = m.derivePQKeysFromSeed(seed, 'v2');
|
||||
assert.equal(m.bytesToHex(byDefault.mlDsa65.publicKey), m.bytesToHex(explicitV2.mlDsa65.publicKey),
|
||||
'default derivation must be v2');
|
||||
// numeric form also accepted
|
||||
const numericV2 = m.derivePQKeysFromSeed(seed, 2);
|
||||
assert.equal(m.bytesToHex(numericV2.mlDsa65.publicKey), m.bytesToHex(explicitV2.mlDsa65.publicKey),
|
||||
'numeric scheme 2 must equal v2');
|
||||
});
|
||||
|
||||
test('v2: v1 and v2 keys from the same seed are independent (no shared key material)', () => {
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
const v1Keys = m.derivePQKeysFromSeed(seed, 'v1');
|
||||
const v2Keys = m.derivePQKeysFromSeed(seed, 'v2');
|
||||
assert.notEqual(m.bytesToHex(v1Keys.mlDsa44.publicKey), m.bytesToHex(v2Keys.mlDsa44.publicKey), 'ml-dsa-44');
|
||||
assert.notEqual(m.bytesToHex(v1Keys.mlDsa65.publicKey), m.bytesToHex(v2Keys.mlDsa65.publicKey), 'ml-dsa-65');
|
||||
assert.notEqual(m.bytesToHex(v1Keys.slhDsa.publicKey), m.bytesToHex(v2Keys.slhDsa.publicKey), 'slh-dsa-128s');
|
||||
assert.notEqual(m.bytesToHex(v1Keys.falcon512.publicKey), m.bytesToHex(v2Keys.falcon512.publicKey), 'falcon-512');
|
||||
assert.notEqual(m.bytesToHex(v1Keys.mlKem.publicKey), m.bytesToHex(v2Keys.mlKem.publicKey), 'ml-kem-768');
|
||||
});
|
||||
|
||||
test('v2: secp256k1 NIP-06 identity key is unchanged across schemes', () => {
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
const kp = m.deriveSecp256k1FromSeed(seed);
|
||||
// NIP-06 path is scheme-independent; both v1 and v2 users keep the same identity key
|
||||
const vec1 = JSON.parse(readFileSync(join(vectorsDir, 'seed-to-pubkeys.v1.json'), 'utf8'));
|
||||
if (vec1.mnemonic === TEST_MNEMONIC) {
|
||||
assert.equal(m.bytesToHex(kp.publicKey), vec1.derivedPublicKeys.secp256k1.publicKeyHex);
|
||||
}
|
||||
});
|
||||
|
||||
test('v2: unknown scheme throws', () => {
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
assert.throws(() => m.derivePQKeysFromSeed(seed, 'v3'), /Unknown derivation scheme/);
|
||||
assert.throws(() => m.derivePQKeysFromSeed(seed, 99), /Unknown derivation scheme/);
|
||||
});
|
||||
|
||||
test('v2: derivation paths use per-algorithm coin types, all hardened', () => {
|
||||
assert.equal(m.pqDerivationPath('mlDsa44', 'v2'), "m/44'/102006'/0'/0'/0'");
|
||||
assert.equal(m.pqDerivationPath('mlDsa65', 'v2'), "m/44'/102003'/0'/0'/0'");
|
||||
assert.equal(m.pqDerivationPath('slhDsa', 'v2'), "m/44'/102004'/0'/0'/0' + m/44'/102004'/0'/0'/1'");
|
||||
assert.equal(m.pqDerivationPath('falcon512', 'v2'), "m/44'/102007'/0'/0'/0' + m/44'/102007'/0'/0'/1'");
|
||||
assert.equal(m.pqDerivationPath('mlKem', 'v2'), "m/44'/102005'/0'/0'/0' + m/44'/102005'/0'/0'/1'");
|
||||
// v1 paths unchanged (legacy)
|
||||
assert.equal(m.pqDerivationPath('mlDsa44', 'v1'), "m/44'/1237'/0'/0/1");
|
||||
assert.equal(m.pqDerivationPath('mlKem', 'v1'), "m/44'/1237'/0'/0/7 + m/44'/1237'/0'/0/8");
|
||||
// algorithm-id helper
|
||||
assert.equal(m.derivationPathForAlgorithm('ml-dsa-65'), "m/44'/102003'/0'/0'/0'");
|
||||
assert.equal(m.derivationPathForAlgorithm('ml-dsa-65', 'v1'), "m/44'/1237'/0'/0/2");
|
||||
});
|
||||
|
||||
test('v2: PQ_DERIVATION_SCHEME_VERSION is 2', () => {
|
||||
assert.equal(m.PQ_DERIVATION_SCHEME_VERSION, 2);
|
||||
});
|
||||
|
||||
test('v2: kind 1 announcement carries derivation_scheme tag', async () => {
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
const pqKeys = m.derivePQKeysFromSeed(seed, 'v2');
|
||||
const kp = m.deriveSecp256k1FromSeed(seed);
|
||||
const ev = m.buildKind1Announcement(m.bytesToHex(kp.publicKey), 800000, pqKeys, 'v2');
|
||||
const tag = ev.tags.find(t => t[0] === 'derivation_scheme');
|
||||
assert.ok(tag, 'v2 event must have a derivation_scheme tag');
|
||||
assert.equal(tag[1], '2');
|
||||
|
||||
// v1 events (legacy path) omit the tag
|
||||
const v1Keys = m.derivePQKeysFromSeed(seed, 'v1');
|
||||
const ev1 = m.buildKind1Announcement(m.bytesToHex(kp.publicKey), 800000, v1Keys, 'v1');
|
||||
assert.ok(!ev1.tags.some(t => t[0] === 'derivation_scheme'), 'v1 event must NOT have the tag');
|
||||
});
|
||||
|
||||
test('v2: v1 recovery flow — v1 seed re-derives v1 keys that verify against a v1 event', async () => {
|
||||
// Simulate the recovery path: a v1-era seed produces keys whose signatures
|
||||
// verify inside a kind 1 announcement built the v1 way.
|
||||
const seed = m.mnemonicToSeed(TEST_MNEMONIC);
|
||||
const v1Keys = m.derivePQKeysFromSeed(seed, 'v1');
|
||||
const kp = m.deriveSecp256k1FromSeed(seed);
|
||||
const ev = m.buildKind1Announcement(m.bytesToHex(kp.publicKey), 800000, v1Keys, 'v1');
|
||||
|
||||
// Each algorithm tag must verify against the re-derived v1 public keys
|
||||
const algTag = (id) => ev.tags.find(t => t[0] === 'algorithm' && t[1] === id);
|
||||
const statement = new TextEncoder().encode(ev.content);
|
||||
|
||||
const t44 = algTag('ml-dsa-44');
|
||||
assert.ok(m.verifyMLDSA44(m.base64ToBytes(t44[3]), statement, v1Keys.mlDsa44.publicKey),
|
||||
'recovered v1 ml-dsa-44 key must verify its own event signature');
|
||||
const t65 = algTag('ml-dsa-65');
|
||||
assert.ok(m.verifyMLDSA65(m.base64ToBytes(t65[3]), statement, v1Keys.mlDsa65.publicKey),
|
||||
'recovered v1 ml-dsa-65 key must verify its own event signature');
|
||||
const tSlh = algTag('slh-dsa-128s');
|
||||
assert.ok(m.verifySLHDSA(m.base64ToBytes(tSlh[3]), statement, v1Keys.slhDsa.publicKey),
|
||||
'recovered v1 slh-dsa-128s key must verify its own event signature');
|
||||
const tFal = algTag('falcon-512');
|
||||
assert.ok(m.verifyFalcon(m.base64ToBytes(tFal[3]), statement, v1Keys.falcon512.publicKey),
|
||||
'recovered v1 falcon-512 key must verify its own event signature');
|
||||
// KEM has no signature — pubkey presence is the check
|
||||
const tKem = algTag('ml-kem-768');
|
||||
assert.equal(tKem[2], m.bytesToBase64(v1Keys.mlKem.publicKey),
|
||||
'recovered v1 ml-kem-768 pubkey must match the event tag');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,14 +26,6 @@ import { fileURLToPath } from 'node:url';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const VECTORS_DIR = __dirname;
|
||||
|
||||
const PQ_DERIVATION_PATHS = {
|
||||
secp256k1: [0],
|
||||
mlDsa44: [1],
|
||||
mlDsa65: [2],
|
||||
slhDsa: [3, 4],
|
||||
falcon512: [5, 6],
|
||||
mlKem: [7, 8],
|
||||
};
|
||||
const PQ_SEED_LENGTHS = {
|
||||
mlDsa44: 32,
|
||||
mlDsa65: 32,
|
||||
@@ -42,26 +34,43 @@ const PQ_SEED_LENGTHS = {
|
||||
mlKem: 64,
|
||||
};
|
||||
|
||||
// V1 (legacy): non-hardened children under the NIP-06 account 0 change level.
|
||||
const V1_PATHS = {
|
||||
mlDsa44: ["m/44'/1237'/0'/0/1"],
|
||||
mlDsa65: ["m/44'/1237'/0'/0/2"],
|
||||
slhDsa: ["m/44'/1237'/0'/0/3", "m/44'/1237'/0'/0/4"],
|
||||
falcon512: ["m/44'/1237'/0'/0/5", "m/44'/1237'/0'/0/6"],
|
||||
mlKem: ["m/44'/1237'/0'/0/7", "m/44'/1237'/0'/0/8"],
|
||||
};
|
||||
|
||||
// V2 (default): per-algorithm coin types in the unregistered SLIP-44 102XXX'
|
||||
// range, all-hardened below the coin type. See plans/v2-hardened-derivation.md.
|
||||
const V2_PATHS = {
|
||||
mlDsa44: ["m/44'/102006'/0'/0'/0'"],
|
||||
mlDsa65: ["m/44'/102003'/0'/0'/0'"],
|
||||
slhDsa: ["m/44'/102004'/0'/0'/0'", "m/44'/102004'/0'/0'/1'"],
|
||||
falcon512: ["m/44'/102007'/0'/0'/0'", "m/44'/102007'/0'/0'/1'"],
|
||||
mlKem: ["m/44'/102005'/0'/0'/0'", "m/44'/102005'/0'/0'/1'"],
|
||||
};
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function deriveBIP32Child(bip39Seed, childIndices) {
|
||||
function deriveBIP32Child(bip39Seed, path) {
|
||||
const hdKey = HDKey.fromMasterSeed(bip39Seed);
|
||||
const path = `m/44'/1237'/0'/0/${childIndices.join('/')}`;
|
||||
const child = hdKey.derive(path);
|
||||
if (!child.privateKey) throw new Error(`Failed to derive at ${path}`);
|
||||
return child.privateKey;
|
||||
}
|
||||
|
||||
function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) {
|
||||
if (childIndices.length === 1) {
|
||||
const seed = deriveBIP32Child(bip39Seed, childIndices);
|
||||
return seed;
|
||||
function derivePQSeedFromBIP32(bip39Seed, paths, requiredLength) {
|
||||
if (paths.length === 1) {
|
||||
return deriveBIP32Child(bip39Seed, paths[0]);
|
||||
} else {
|
||||
let combined = new Uint8Array(0);
|
||||
for (const idx of childIndices) {
|
||||
const child = deriveBIP32Child(bip39Seed, [idx]);
|
||||
for (const path of paths) {
|
||||
const child = deriveBIP32Child(bip39Seed, path);
|
||||
const newCombined = new Uint8Array(combined.length + child.length);
|
||||
newCombined.set(combined);
|
||||
newCombined.set(child, combined.length);
|
||||
@@ -81,18 +90,30 @@ const seed = mnemonicToSeedSync(MNEMONIC, '');
|
||||
const secpHd = HDKey.fromMasterSeed(seed).derive("m/44'/1237'/0'/0/0");
|
||||
const secpPub = secpHd.publicKey;
|
||||
|
||||
// PQ keys
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(seed, PQ_DERIVATION_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44);
|
||||
// PQ keys — v1 (legacy) paths
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(seed, V1_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44);
|
||||
const mlDsa44Keys = ml_dsa44.keygen(mlDsa44Seed);
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(seed, PQ_DERIVATION_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65);
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(seed, V1_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65);
|
||||
const mlDsa65Keys = ml_dsa65.keygen(mlDsa65Seed);
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(seed, PQ_DERIVATION_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa);
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(seed, V1_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa);
|
||||
const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed);
|
||||
const falconSeed = derivePQSeedFromBIP32(seed, PQ_DERIVATION_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512);
|
||||
const falconSeed = derivePQSeedFromBIP32(seed, V1_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512);
|
||||
const falconKeys = falcon512.keygen(falconSeed);
|
||||
const mlKemSeed = derivePQSeedFromBIP32(seed, PQ_DERIVATION_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem);
|
||||
const mlKemSeed = derivePQSeedFromBIP32(seed, V1_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem);
|
||||
const mlKemKeys = ml_kem768.keygen(mlKemSeed);
|
||||
|
||||
// PQ keys — v2 (per-algorithm coin types, all hardened)
|
||||
const v2MlDsa44Seed = derivePQSeedFromBIP32(seed, V2_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44);
|
||||
const v2MlDsa44Keys = ml_dsa44.keygen(v2MlDsa44Seed);
|
||||
const v2MlDsa65Seed = derivePQSeedFromBIP32(seed, V2_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65);
|
||||
const v2MlDsa65Keys = ml_dsa65.keygen(v2MlDsa65Seed);
|
||||
const v2SlhDsaSeed = derivePQSeedFromBIP32(seed, V2_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa);
|
||||
const v2SlhDsaKeys = slh_dsa_sha2_128s.keygen(v2SlhDsaSeed);
|
||||
const v2FalconSeed = derivePQSeedFromBIP32(seed, V2_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512);
|
||||
const v2FalconKeys = falcon512.keygen(v2FalconSeed);
|
||||
const v2MlKemSeed = derivePQSeedFromBIP32(seed, V2_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem);
|
||||
const v2MlKemKeys = ml_kem768.keygen(v2MlKemSeed);
|
||||
|
||||
const vector = {
|
||||
vectorType: 'nostr-pq-link-seed-to-pubkeys',
|
||||
vectorVersion: 1,
|
||||
@@ -129,7 +150,8 @@ const vector = {
|
||||
notes: [
|
||||
'The truncation rule is normative: for 48-byte seeds, two BIP32 children are concatenated (64 bytes) and the FIRST 48 bytes are used. For 64-byte seeds, all 64 bytes are used.',
|
||||
'A future implementer who takes the last 48 bytes, or concatenates in the opposite order, will produce different keys and break seed-phrase recoverability.',
|
||||
'A second implementation should run its own keygen from the same mnemonic and compare against the publicKeyHex values in this file.'
|
||||
'A second implementation should run its own keygen from the same mnemonic and compare against the publicKeyHex values in this file.',
|
||||
'LEGACY: this v1 vector pins the pre-v2 derivation (non-hardened children under m/44\'/1237\'/0\'/0/). It must never change — v1 seed recovery depends on it. New derivations use seed-to-pubkeys.v2.json.'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -137,8 +159,62 @@ const outPath = join(VECTORS_DIR, 'seed-to-pubkeys.v1.json');
|
||||
writeFileSync(outPath, JSON.stringify(vector, null, 2) + '\n');
|
||||
console.log(`Wrote ${outPath}`);
|
||||
console.log('secp256k1 pubkey:', bytesToHex(secpPub));
|
||||
console.log('ml-dsa-44 pubkey:', bytesToHex(mlDsa44Keys.publicKey));
|
||||
console.log('ml-dsa-65 pubkey:', bytesToHex(mlDsa65Keys.publicKey));
|
||||
console.log('slh-dsa-128s pubkey:', bytesToHex(slhDsaKeys.publicKey));
|
||||
console.log('falcon-512 pubkey:', bytesToHex(falconKeys.publicKey));
|
||||
console.log('ml-kem-768 pubkey:', bytesToHex(mlKemKeys.publicKey));
|
||||
console.log('ml-dsa-44 pubkey (v1):', bytesToHex(mlDsa44Keys.publicKey));
|
||||
console.log('ml-dsa-65 pubkey (v1):', bytesToHex(mlDsa65Keys.publicKey));
|
||||
console.log('slh-dsa-128s pubkey (v1):', bytesToHex(slhDsaKeys.publicKey));
|
||||
console.log('falcon-512 pubkey (v1):', bytesToHex(falconKeys.publicKey));
|
||||
console.log('ml-kem-768 pubkey (v1):', bytesToHex(mlKemKeys.publicKey));
|
||||
|
||||
// ── V2 vector: per-algorithm coin types, all hardened ────────────────────────
|
||||
const v2Vector = {
|
||||
vectorType: 'nostr-pq-link-seed-to-pubkeys',
|
||||
vectorVersion: 2,
|
||||
derivationScheme: 2,
|
||||
description: 'V2 hardened derivation: per-algorithm coin types in the unregistered SLIP-44 102XXX range, all-hardened below the coin type. PQ keys are outside the Nostr coin branch (1237\'), so no compromise of the Nostr subtree can reach them (audit F-M3).',
|
||||
mnemonic: MNEMONIC,
|
||||
bip39SeedHex: bytesToHex(seed),
|
||||
coinTypes: {
|
||||
'ml-dsa-44': 102006,
|
||||
'ml-dsa-65': 102003,
|
||||
'slh-dsa-128s': 102004,
|
||||
'falcon-512': 102007,
|
||||
'ml-kem-768': 102005,
|
||||
},
|
||||
derivedPublicKeys: {
|
||||
'ml-dsa-44': {
|
||||
derivationPath: "m/44'/102006'/0'/0'/0'",
|
||||
publicKeyHex: bytesToHex(v2MlDsa44Keys.publicKey)
|
||||
},
|
||||
'ml-dsa-65': {
|
||||
derivationPath: "m/44'/102003'/0'/0'/0'",
|
||||
publicKeyHex: bytesToHex(v2MlDsa65Keys.publicKey)
|
||||
},
|
||||
'slh-dsa-128s': {
|
||||
derivationPath: "m/44'/102004'/0'/0'/0' + m/44'/102004'/0'/0'/1' (concatenated, first 48 bytes used)",
|
||||
publicKeyHex: bytesToHex(v2SlhDsaKeys.publicKey)
|
||||
},
|
||||
'falcon-512': {
|
||||
derivationPath: "m/44'/102007'/0'/0'/0' + m/44'/102007'/0'/0'/1' (concatenated, first 48 bytes used)",
|
||||
publicKeyHex: bytesToHex(v2FalconKeys.publicKey)
|
||||
},
|
||||
'ml-kem-768': {
|
||||
derivationPath: "m/44'/102005'/0'/0'/0' + m/44'/102005'/0'/0'/1' (concatenated, all 64 bytes used)",
|
||||
publicKeyHex: bytesToHex(v2MlKemKeys.publicKey)
|
||||
}
|
||||
},
|
||||
notes: [
|
||||
'Same truncation rule as v1: two hardened children concatenated, first 48 bytes for 48-byte seeds, all 64 for 64-byte seeds.',
|
||||
'Coin types 102003\u2013102005 match n_signer / the Rust signer; 102006\u2013102007 are this project\u2019s allocations for ML-DSA-44 and Falcon-512.',
|
||||
'Falcon-512 is per-library: its keygen is rejection-sampling-based with no universal seed interface, so this vector pins @noble/post-quantum\u2019s behavior specifically.',
|
||||
'The secp256k1 identity key is unchanged: NIP-06 m/44\'/1237\'/0\'/0/0 (see the v1 vector).'
|
||||
]
|
||||
};
|
||||
|
||||
const v2OutPath = join(VECTORS_DIR, 'seed-to-pubkeys.v2.json');
|
||||
writeFileSync(v2OutPath, JSON.stringify(v2Vector, null, 2) + '\n');
|
||||
console.log(`Wrote ${v2OutPath}`);
|
||||
console.log('ml-dsa-44 pubkey (v2):', bytesToHex(v2MlDsa44Keys.publicKey));
|
||||
console.log('ml-dsa-65 pubkey (v2):', bytesToHex(v2MlDsa65Keys.publicKey));
|
||||
console.log('slh-dsa-128s pubkey (v2):', bytesToHex(v2SlhDsaKeys.publicKey));
|
||||
console.log('falcon-512 pubkey (v2):', bytesToHex(v2FalconKeys.publicKey));
|
||||
console.log('ml-kem-768 pubkey (v2):', bytesToHex(v2MlKemKeys.publicKey));
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"notes": [
|
||||
"The truncation rule is normative: for 48-byte seeds, two BIP32 children are concatenated (64 bytes) and the FIRST 48 bytes are used. For 64-byte seeds, all 64 bytes are used.",
|
||||
"A future implementer who takes the last 48 bytes, or concatenates in the opposite order, will produce different keys and break seed-phrase recoverability.",
|
||||
"A second implementation should run its own keygen from the same mnemonic and compare against the publicKeyHex values in this file."
|
||||
"A second implementation should run its own keygen from the same mnemonic and compare against the publicKeyHex values in this file.",
|
||||
"LEGACY: this v1 vector pins the pre-v2 derivation (non-hardened children under m/44'/1237'/0'/0/). It must never change — v1 seed recovery depends on it. New derivations use seed-to-pubkeys.v2.json."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"vectorType": "nostr-pq-link-seed-to-pubkeys",
|
||||
"vectorVersion": 2,
|
||||
"derivationScheme": 2,
|
||||
"description": "V2 hardened derivation: per-algorithm coin types in the unregistered SLIP-44 102XXX range, all-hardened below the coin type. PQ keys are outside the Nostr coin branch (1237'), so no compromise of the Nostr subtree can reach them (audit F-M3).",
|
||||
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"bip39SeedHex": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
|
||||
"coinTypes": {
|
||||
"ml-dsa-44": 102006,
|
||||
"ml-dsa-65": 102003,
|
||||
"slh-dsa-128s": 102004,
|
||||
"falcon-512": 102007,
|
||||
"ml-kem-768": 102005
|
||||
},
|
||||
"derivedPublicKeys": {
|
||||
"ml-dsa-44": {
|
||||
"derivationPath": "m/44'/102006'/0'/0'/0'",
|
||||
"publicKeyHex": "2c9763a5b69e423e6930b0430b1dd13db7ff14696a84bba4fe46ab8a1b2dee581b262caf3f31e2d4e14302b4f57c2f16d3d53f44c98a5e6a033a7e4066c3552a9dfce3fa5b0802599e1fbdb13d4e5e4f14aba08ef93f4b7ba254693952fc39405f74460a52e5b714ea1350ce1e4589534168c23c145e192cff98093398f7930fa747c56cac51a044dc2067527e53b7752eb9114279ca4a97aabb0e9b43ae3d321eb462cfdf2b51d7d93b926df6d02bfa4875c10e95cd2ea08ad1a345602cbc2cb12b22671a677911f72373e3f25e113cd7c34888c022afc79c24f8be57bb4eb3c3ffdb08a96bb80bc07a36ae947eb561178f9bab4c24f49e8188392a20476759a3462323b8a9d04edcd1b88b7734c94b0178215b3908cc55e753c2405ab37d12ca13ec15173b0694a51043dd98093929dad48460aabd3b3e80453bda0a4d0ebe0a5b76654e45900737d87f109997b5e34ef7141004c9537c01a246fc4c04268378cd30935d8e7d2b3847f910821bf9357dc858bb059270822a6f9a4f980b57f8bf4de69b1e55058888e7f6d6524220174b991302f55bfa0f5d979b374fb2c9394aba4e2a1bfcd55fc6495c397482b845b484d68ab1d4b58953f3d79708e0c4647b8deb3fdc89acbc83773d4eaac7b8e47ba30a443e827e180f686b0ae5484e9e780cb529ba3df65128597989cefed5891a01e66f1e4834ef960e819d92a24ee00a84d60878434717503d6f457c465b63302bbd4820df3a37edc66a23124aba228dc12ca6294ca99e1e8669d5a742122ef97ec42e7041e48198b3924f65528d2a766c7032a13ab50094e4cd1b2c5ad06dd1dcb59f70cd0b865a40b1e2198ca413750f6473a001fd908d5ddc640553786847c8dde4f65bef059cc132c8b75a68e2e2d8e4b08f437e1834ae7bb0d7f5e944030749fa73ad786e69cf361d242d1b8ce646ada8bf8e1dd19f87cf513d972f0b590139c1ab20ad44be86b49f5e6c38d5712355ebb484ed2b0f224ed8c8ad7eaa01389d9a5799950edfcb2aab19bf0fdfaaa7759b8deca275b031ebbbaf93e33840fa49c7833fd470917525447db2d0b926a82352cf7960d416d68a4defa51466e0123166fc567348c8aba1ae4d5c0fdc10aa017aa64f88d792ae61690cb4852a79b2d660e1cd405efe3b2f7e5d75edb338e5119a950925b6bdffb057502d241504b00a89dc2700753213912c0a543521674973ebd95aee5c9177962e33813240d44e8f5da85c059166dc1a5c8af10622b30eefd2cd1553ebba63a68f900bce19835cdba115177d22c85d2fd0502c4bb21f1d78bd14f480bbe8548ae02701bdce6f72060b87125faef235acee9ed229935071e4ac4951901f171cfe82f22987269a786fdca03b6ca2c6944b58d2756707a655c225da187d09d8263048f6ea29b248857f9cba4c7ec06698f5a049cff52f6c87a60078464fec9fb36048e5e7e93a4c201d0eecfecbcd9bb2364578ece831d56702854cbb33487fcfcdac49f8bcf4a88d884a890ce3907ec03c8ff11cb73b426ce2ebf6119929151959fededfa0c46ed3649220899b4996064f32f2bc995bf048e197d25ee64cade03a15663cd1f59e6deb11a9269e35c4523816e393be9e547f7879593c4fd253f1cafffa0530457cc3c58270ee2c1ce96ee4607f3026274120a064a7344d6321fdaf6821e1b19e8cef2684ad9e5aed4f34fa28846252c9ac1f7dc4bba3837ed0569a7d242e4a3c9f0956cdaa0087364ea797a75e85e044ead9eaced7cee38678cec7ab4f024c69b71c0858513bd2adeeb283a3e73dda8b2a413e4a5f56fd121275de5960df49b26555e7c651afea746f820b59c85c96a2"
|
||||
},
|
||||
"ml-dsa-65": {
|
||||
"derivationPath": "m/44'/102003'/0'/0'/0'",
|
||||
"publicKeyHex": "ea4ce61a7606342f76b5fa2462c27c010f5cd3e5467bb65961d68e87288856ee8339a05093eae7f6cd3f1e5b8a8685da980b7a5cc20597bfaf89cf604bdbe1820b3e58d457f684d8484cd4c87deaf2eafa2d654557f9c0c1b1c733772847bc25be39e98fdbe7768039ff6e87c2548a8841b88a6c17b84581f03aa4171fa737786d6639b9354e761703b7a68d6e49e41585f7ec1992e5b9d083b6a7a1d07b65a714bdfa3d7df23b432845264c0c786b9f1ec214f66b54ffa5b65b0d9ede667e40d10136e643aec6bb25cd1cae873d245fde196fb5e2c081e10a69e2dfd3b160adf1cc5a0deefb1a5256d9b6ba15033a89d5fe8b0eef5afc8c1fb1ce6d1c63d177127ead73ba469c30b6ac2502f12aa96f9d930c0287bf793b740b3fc13c0a7d61c170cba75ad7e63f93a6937c369bbdae4d35035583f5478b943e1c21014b2d356449872ee978f0210fa4d4661548d34b2a8ad3f1686fa834f5c431d9e8e55725974cf84acfbcbf2f559dc43f00cf81d53a5c87b7d158162733006721d43fe581ec278331b78a87587011c752b782153ef5d09ccee9e00742d4cf0cc07d343df557790e7674915cc631113ae2fd4921c477c1bf42214d6ab5a42cea1c596f72d17101e0467a8fc54c0a2e19dc5db7d623196ea68f320bda3a8c519ce30666ea0c963de4ee9036beb1a025263aedac8137f5348ceb5762dfc8b689dbfdd889c531920a01e19dc6a40a183bcc4b8dd2100c72746532d9db2c3f668b5e21df428e9b74194464e66f0636516c8c79295f0ec0c08d227bccdb83a92fd4dfcdd552b972e7c3b0530bfe5310603c64e6c2972350762448d448e15279becc2582654a1b398619ce6f2450546b1c602dd1a3f640e878089e43cd6a1594bf1daaf4005aee41ab91b60e9dca9dab461c95d20986085c344a3fc9c3801548484b09e42a507ae44de14efbc471083e7e2992d210c8b022b2d3d840319f59c3554defd027afdf824b955a84385724f1aef727c06d6dda723bf82dff4e7e45265ab56d5d492b2104c6cc363632d905cf4b92e5253e9d10a6496e85959b0e637ee21ca6a8c63716d9ee8e722960113b013a393459079ec53248ea1df90f28bef28f182bdcfb93e94ba455390693eebac77fc670ffb8a0e23585a8b6743a20ebf5555738a23efc24ec681bb3beeeda82e346487c3295f5ea25464f6de5b96f9eebe4539a42392b598365724e786d9a39a5266e4e1a2e1527f5161b3d2f76404b9b52b5d158835f50ecbd6011b172bf55157eb5ae661a75593d6af0adbeaf755244f7d5204e2bc20ad2a2384ea83156df29fcb5cb3ab105fbc4489ddebc395eaf7fed7e71ec07294cb004639c17cc6d8ca5e7a6472208768f82fe25999c551d58552114d8af3dc9b15242dc784654f58517d06d76f99ecddf9c38c5b20d672cc7f6981514ef8f781378c5db2a9d0410428fe97b33708fe946c60aa35e08d85de2d5dbf8659867074960d9b1c345949c1ee3625ed47935007bf496f856c9a40d7ab0ada31d6466730c073e534de7e28292c40237d4db41cffa97836459a585d957dbc7f9983557f4d4894b342a85539343461ab1d86e5d25389959e7ebf3bda57df9a8de8b2096162b5b73388cb4297434355ab36fb884f67c5f3dbd7dcdd75aca4a660219d0e52af7ed2dfa0d8c50bbf3088c947f05a6373dc18321c3e33717780a9e074cea762bda740e520e28ea750014bdc407ff7e581fdcf589f84c26e1b7cdfd76e21fd63b8a2dffba9fe2e3351708f9281f0344a96ef2b10c19709e980fa152f926569bc80cad4a4741d6349ffe52a2162c97e08c47d26a56727f84ab4df26be45318be6286015cb68ad0a26905b321fbe1a889151eed0e8b5a30e7132a4d04c7243804e34addfb7e1e68eb82f3ea647a181e731982de413dd77742d623cf78fdf497ea263452926890f6a4f5d7e9c1b5e07e05544bc21496ef09ecbc32e6852dbd5d5529a32227455d02334c292abdcd9e55b4e1d173c3d3bb44e42205d12465ef56f6ca15c2eac1e18d2afeb4393ab69c55084f37217d078894a44cb43fb2d884a8b44cd39587db1b72f1f4622014945ac1f2d317316a1fe6cae7134b7028fe21055b50a2d7de38281617ba340130f2933ff93c755aac68cc8938ff12efd5055cf646764e4ef4868d6e1cc54e1087798c9bf5c7be5ac8d59983a82eb2aae3157c4f0cb00e33d296a2c1b1165f985a5a104ebcb25845831d796df16e70c70a4b40706240f2ea747c751c3fff24b44a9ba38ab7c98aebba68bf0a128d6251fea943ac86e1ff838c67166a13264ac512f4e708719f5df6ce6cb5622ec21add51a071c7b1b4c67801d487238e1eab8f7806ff5590bd79ce55b4891570a5cda6158c9d4db71ec1db65081525a980b5c19ebfcc72376493f360378994f3e791b59cce41ab980820203b94a70bfe6b011c77b9adf7211a54affa5880f15fc8bbb00f833d85fba1b4d17135e3d70662c588198f9fca09f87c01430ddcad975974a36f5dd76fd0c5b89b7be76b6d4740fde86b2e33e6919af86b68368dae5ad3f4dd0beae887dc6a13ac051095b4cb8504ae617f6fecf6f90c3bd9798d658952a5e71b3904520b22b88d1e40bd51bb02fdf864ef96942dd42a757944092c582eb69f39b75b4bdf9280bf49d0b0a6e41ac2c3194cd0bd9d28b896dc353e15266824b4768df4f7153e0a33e670125a8518073dfcdcbfe0f452d193b870a72c6bc34cafa7e28f18b9d7a1b7"
|
||||
},
|
||||
"slh-dsa-128s": {
|
||||
"derivationPath": "m/44'/102004'/0'/0'/0' + m/44'/102004'/0'/0'/1' (concatenated, first 48 bytes used)",
|
||||
"publicKeyHex": "193676e5236af19da41c0175581ee7f888b51633a1d125ad8f32a344b60af970"
|
||||
},
|
||||
"falcon-512": {
|
||||
"derivationPath": "m/44'/102007'/0'/0'/0' + m/44'/102007'/0'/0'/1' (concatenated, first 48 bytes used)",
|
||||
"publicKeyHex": "0958504aa577d76896d84902d64f2e3cf298a7b7c5949dfcacd73dc4e1b34e3fa154d01e57c4ed17eb503e629c0f649dc8500be4807866282573a0cc6a98ea27a28cd113411a076a34350bdf1a263bc1069932a4194c384060af06ab6b79378530ebf378fed2181a1e7142ed9dd54eecf2a0391b9640260d1602e82bcec2080b641120409a66b8cebc2817e34c1fb9cd77c1657a2e5cf1115c6f17844814b702aa742d0cab72631f22aee2fa984b4e818adaba139a2e5f3aca8eb53b9fa7a4fe305239e6412ea436c997efb2436202244fa4461b0c1cda0925a58f7acbe4b5a8958c5eeb89f88bbb88b9cf15c90db59fed0406e719c9be4cd718a3876e1e9a00a9850fbd396d90c6c6224982f8a2d7b6473926755c13cbc9f748c942e4ea5a38a8b94ff5fd8f322aedbce7752264135541c648a47657545fd5d99d934366f37173dc701bb91938ef0689507c758a091f756004f3d19044d42cb2940980a60696153370e74d9b1f902ff6b84e856ffc7d0b6117e08f18569759101c057610659f8cb67d12fc85cf0a24a5121016068f7ba521389009db745d282c2b8dc917a4d194955de5b4b6409d365241e7ad68f970a3ae1e3909b0d426e46625b427a24c153a5fdd0501b05e77244e71578c908eafdacf9a52e16b4d8902c13bc168837e5079ec8103791ca541e74a37b119c786291e141d4167a1a949bd8f6ac7a4bd4d0c6c8d51b04353da62cdd8a98e8f7c04446825439ce80dbe1808776542154cb9497894ed0ab57c35502b2d534a3895038b3b639a8947652d922ded9dc1b2d2e6624c19c8340bc90baad91213c822512001f7d3a906b3d4c4e0e255fb04eeb822feafc595e0e2bbee0eff1a6ddfd991cff22c951333e2e6daa1f4e06455d61761b5105a1ce8f89591d475d41fad09b6f248b96f323fd2b0e18853ee29f0abf9171e4f9a900d574ea76e28aee1a45edf011b8cf009e183c72e3e7b4107a9d0215182709865bc50114088e6521007567684cee58a9504d927be41f0b0aafdf01c9551a12e38f6a203da7ffdd0c3cc14ba49b83ec9ac2d801ed282752fced71c0abf794d5ea83ef5d7f56064a4099197219829439d98197a898fc46cdd8630b29f555ca4510b8e8a78ac2c3b5a62a0aa7f483a7a345ad972b2de6484d01a31f86fc9cf6ee836e27b62aace33b6f0c3145b644f5d2ea665231aed69d6c490222346138d73c0de998f3e15387256ad7b6d1cf2f8eb83cb2b76cf206a41f864dc5d2398c5c9"
|
||||
},
|
||||
"ml-kem-768": {
|
||||
"derivationPath": "m/44'/102005'/0'/0'/0' + m/44'/102005'/0'/0'/1' (concatenated, all 64 bytes used)",
|
||||
"publicKeyHex": "0980c83da4b081fa8bf69b3e00ddbd47824da54410bfd539fe58783c9b66679c272f32adff0278902a92d2c5a6f5649d5a928797d893b8ba789f67870786aa90a260eed4b7eaa43e6beac4e82c5c14192b44cac0521c4584c02a10740e6f54cd86f3293f10c285c20f33f4af0a87afae853043ca4afc99baaca3a79d483befea212a1b25c12cac8672a238257ba9a876dc6558eb6c397a1694a6e9bcdcf6b121789f8a64a7ced10cea2b5e3ac15bf9807e00ad37b7b1b46dd5c88f21b93482b94ae884744172e2603d15550df77988bac53a5ad22cd357992b2a7795e516a5763a1f093be33a86634cc81f1c33c1f61104629c9c2022040988a44778a52844618241ce0a90790041e461b2cc5212a8220177e08a9bbca3a608663a558287f91095d21321233e9d675599b82867c082096519fea29107a36d6304a183e0c38a590f3c272a06e36e8e62ba6a70b31d3acfb1c52d9205cd5713a96f40a9b0683badd3bcb1e974ade1a68e6a74e1f2097856bc09b75212a503bc97cd90c7ba1a625c4b5829eed65213880f9dcb99a4604eb3a479a0a08f4e28c5b29c9e8773991f2847e310be826bb1fa968b1bb21601c25ec778ae62d1582f94c83fb63749d5206ef89507fa2c331a18e5f11adbabcce14a5b36b39f6c1a3a21c733c9412083e089171ca69a3362dc9c7198b83bf9a26174879f0ed32f2afbcbfa4ac92dac74234a1017197a422220e68834fb2b9ddda915002bc392b69db3985236898a7f602b306c05b26b85f4c9553a7630c1b68890f97bafc96a9e0b06b3f3222cf47f6118894fa469ef8386c14172b2fc19e030574f34c170f33e85849923072cc131508055436fb7c7f88c3e7eeb0c41c02d48d1c759d72c1609240e027d6a6a314784ae924c40257c6bed08508ba907d06cc3db8b619c5594413311ad914895a3946ab31b6f35373a286721d3107fa108da0766af2b2ab2813d030254ea635dd1252153e210ee74c2120722727683d5874dae0b9d6e5bbd10939d543c95d227722903c99402c48a2341d48b0ebd143e5ff0500e910c9f071d5f4a198f0068e241388e4b23fd0582269158c3e0af13d889c2f1afeaf308d6c3cbcfe653345c0384d08a399b6902c550721963e397112ec31f430773c68900aea96b6832709eb918ca3483affb6866488e4fa726b7550f2ed106a9d71bcca0928885947f935255b56e18a54ce18a5257934bc02b3ea531978d61501e435203458f51d1bd58d555c1134cebd6ccd5e19b3545a3545b21aea4919123996be824e11b2f312b43b3f52db7d08d98156fb2dcc8986b78192a97419102290123660346dd6ccd89544f0aa246ddca1cf586266d187e227872cb9c6b503c8546249eff344a3ea9c697031858c88a09201a8194a0ba1a9db2663a936762e3c50293e8a591ebc84a9c128ce2cf8ff941b883bb448075af7868325b906c75b51e5394a6e617bcbaa80a839e81a0489210b7c3c74c260a7d1025b982e192d5b192c0f91e32c079fde60b100501e25564e29368e9591abaa80a7f79cc375489c64980bf9785959c590a50265c32097b5866dda92542198068237ab0d73367f99c846b8d4649b8878520b4bc40252c0719967bfd1a2d08f234788f82ef24b5a6f1f59bf0829d7461754110f0f21088f547beb0"
|
||||
}
|
||||
},
|
||||
"notes": [
|
||||
"Same truncation rule as v1: two hardened children concatenated, first 48 bytes for 48-byte seeds, all 64 for 64-byte seeds.",
|
||||
"Coin types 102003–102005 match n_signer / the Rust signer; 102006–102007 are this project’s allocations for ML-DSA-44 and Falcon-512.",
|
||||
"Falcon-512 is per-library: its keygen is rejection-sampling-based with no universal seed interface, so this vector pins @noble/post-quantum’s behavior specifically.",
|
||||
"The secp256k1 identity key is unchanged: NIP-06 m/44'/1237'/0'/0/0 (see the v1 vector)."
|
||||
]
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Upload the Nostr Quantum Preparation web app to the TEST endpoint.
|
||||
#
|
||||
# Usage: ./upload-test.sh
|
||||
#
|
||||
# Uploads the contents of www/ to ubuntu@laantungir.net:html/quantum-prep-test/
|
||||
# The site will be accessible at https://laantungir.net/quantum-prep-test/
|
||||
#
|
||||
# This is the staging deployment for the v2 hardened derivation scheme.
|
||||
# The production site (https://laantungir.net/quantum-prep/) is NOT touched.
|
||||
# Once the v2 flow is confirmed working here, run ./upload.sh to promote it.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SERVER="ubuntu@laantungir.net"
|
||||
REMOTE_DIR="html/quantum-prep-test"
|
||||
LOCAL_DIR="$(dirname "$0")/www"
|
||||
|
||||
echo "🔒 Nostr Quantum Preparation — Upload (TEST endpoint)"
|
||||
echo "====================================================="
|
||||
echo "Server: $SERVER"
|
||||
echo "Remote: $REMOTE_DIR"
|
||||
echo "Local: $LOCAL_DIR"
|
||||
echo ""
|
||||
echo "⚠️ This deploys to the TEST endpoint only."
|
||||
echo " Production (quantum-prep) is NOT modified."
|
||||
echo ""
|
||||
|
||||
# Ensure the bundle is up to date
|
||||
echo "📦 Building PQ crypto bundle..."
|
||||
node "$(dirname "$0")/build-pq-bundle.js"
|
||||
echo ""
|
||||
|
||||
# Create the remote directory if it doesn't exist.
|
||||
# /var/www/html is root-owned, so creating a NEW top-level directory needs
|
||||
# sudo; we then chown it to ubuntu so subsequent rsyncs work normally.
|
||||
echo "📁 Ensuring remote directory exists..."
|
||||
ssh "$SERVER" "sudo mkdir -p $REMOTE_DIR && sudo chown ubuntu:ubuntu $REMOTE_DIR"
|
||||
echo ""
|
||||
|
||||
# Upload files using rsync
|
||||
echo "📤 Uploading files..."
|
||||
rsync -avz --delete \
|
||||
--exclude='*.map' \
|
||||
"$LOCAL_DIR/" \
|
||||
"$SERVER:$REMOTE_DIR/"
|
||||
echo ""
|
||||
|
||||
echo "✅ Upload complete!"
|
||||
echo "🧪 Test site: https://laantungir.net/quantum-prep-test"
|
||||
echo ""
|
||||
echo "To promote to production after confirmation: ./upload.sh"
|
||||
+16
-5
@@ -488,6 +488,17 @@
|
||||
</div>
|
||||
<textarea class="pq-textarea" id="pqSeedInput" style="min-height: 80px; width: 100%; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 12px; font-size: 14px; color: var(--primary-color); font-family: monospace; margin: 10px 0;" placeholder="abandon ability able about above absent absorb abstract absurd abuse access accident..."></textarea>
|
||||
<div id="pqSeedValidation" style="font-size: 13px; margin: 5px 0;"></div>
|
||||
<label class="pq-checkbox-row" style="font-size: 13px;">
|
||||
<input type="checkbox" id="pqSeedV1Recovery" />
|
||||
This is a v1-era seed (created before the v2 hardened scheme) — derive its v1 keys for recovery
|
||||
</label>
|
||||
<div class="pq-info-text" id="pqSeedV1Note" style="display: none; font-size: 12px; color: var(--accent-color);">
|
||||
<strong>v1 recovery mode:</strong> your keys will be derived with the legacy v1 paths
|
||||
(non-hardened children under m/44'/1237'/0'/0/) so you can confirm they match your published
|
||||
key-link event. After confirming, you can re-run the flow with the same seed to mint a new
|
||||
v2 event — the v2 keys are independent of the v1 keys, so the old xpub-leak risk no longer
|
||||
applies going forward.
|
||||
</div>
|
||||
<button class="pq-button" id="pqSeedOwnContinueBtn" disabled>Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,32 +516,32 @@
|
||||
<div class="pq-checklist" id="pqKeyList" style="margin-bottom: 15px;">
|
||||
<div class="pq-checklist-item" id="pqKeyMlDsa44">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
|
||||
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · <span class="pq-derive-path" data-alg="ml-dsa-44">m/44'/102006'/0'/0'/0'</span>
|
||||
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub" style="margin-top: 2px;"></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pq-checklist-item" id="pqKeyMlDsa65">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
|
||||
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · <span class="pq-derive-path" data-alg="ml-dsa-65">m/44'/102003'/0'/0'/0'</span>
|
||||
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub" style="margin-top: 2px;"></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pq-checklist-item" id="pqKeySlhDsa">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
|
||||
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · <span class="pq-derive-path" data-alg="slh-dsa-128s">m/44'/102004'/0'/0'/0' + /1'</span>
|
||||
<div class="pq-key-pubkey" id="pqKeySlhDsaPub" style="margin-top: 2px;"></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pq-checklist-item" id="pqKeyFalcon">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
|
||||
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · <span class="pq-derive-path" data-alg="falcon-512">m/44'/102007'/0'/0'/0' + /1'</span>
|
||||
<div class="pq-key-pubkey" id="pqKeyFalconPub" style="margin-top: 2px;"></div>
|
||||
<div style="font-size: 11px; color: var(--accent-color); margin-top: 4px;">⚠ Draft standard — may need re-issuing if FIPS 206 changes</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pq-checklist-item" id="pqKeyMlKem">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
|
||||
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · <span class="pq-derive-path" data-alg="ml-kem-768">m/44'/102005'/0'/0'/0' + /1'</span>
|
||||
<div class="pq-key-pubkey" id="pqKeyMlKemPub" style="margin-top: 2px;"></div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+31
-3
@@ -4,6 +4,7 @@
|
||||
isValidMnemonic,
|
||||
deriveSecp256k1FromSeed,
|
||||
derivePQKeysFromSeed,
|
||||
derivationPathForAlgorithm,
|
||||
buildKind1Announcement,
|
||||
buildProofCarrier,
|
||||
computeEventId,
|
||||
@@ -42,6 +43,9 @@
|
||||
let pqSecpKeys = null;
|
||||
let pqEvent = null; // kind 9999 proof carrier event
|
||||
let kind1Event = null; // kind 1 announcement event
|
||||
// Derivation scheme for this run: 'v2' (default, hardened coin-type paths)
|
||||
// or 'v1' (legacy recovery of a pre-v2 seed). See plans/v2-hardened-derivation.md.
|
||||
let pqScheme = 'v2';
|
||||
|
||||
/* ================================================================
|
||||
DOM REFERENCES
|
||||
@@ -638,6 +642,11 @@
|
||||
pqDeriveProgress.style.width = '0%';
|
||||
pqDeriveContinueBtn.classList.add('pq-hidden');
|
||||
|
||||
// Show the paths for the active scheme in the key checklist
|
||||
document.querySelectorAll('.pq-derive-path').forEach(el => {
|
||||
try { el.textContent = derivationPathForAlgorithm(el.dataset.alg, pqScheme); } catch (_) { /* leave default */ }
|
||||
});
|
||||
|
||||
try {
|
||||
pqSeed = mnemonicToSeed(pqMnemonic);
|
||||
pqDeriveProgress.style.width = '15%';
|
||||
@@ -645,7 +654,7 @@
|
||||
pqDeriveProgress.style.width = '30%';
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
pqKeys = derivePQKeysFromSeed(pqSeed);
|
||||
pqKeys = derivePQKeysFromSeed(pqSeed, pqScheme);
|
||||
pqDeriveProgress.style.width = '60%';
|
||||
|
||||
setKeyIcon('pqKeyMlDsa44', 'Done');
|
||||
@@ -664,7 +673,10 @@
|
||||
document.getElementById('pqKeyMlKemPub').textContent = bytesToBase64(pqKeys.mlKem.publicKey).substring(0, 60) + '...';
|
||||
|
||||
pqDeriveProgress.style.width = '100%';
|
||||
setStatus(pqDeriveStatus, 'success', 'All 5 post-quantum keys derived successfully!');
|
||||
setStatus(pqDeriveStatus, 'success',
|
||||
pqScheme === 'v1'
|
||||
? 'All 5 post-quantum keys derived with legacy v1 paths (recovery mode). Verify these match your published event, then re-run with v2 to upgrade.'
|
||||
: 'All 5 post-quantum keys derived successfully (v2 hardened scheme)!');
|
||||
pqDeriveContinueBtn.classList.remove('pq-hidden');
|
||||
setStepDone(3);
|
||||
} catch (error) {
|
||||
@@ -693,7 +705,7 @@
|
||||
|
||||
// ---- Phase 1: Build and sign the kind 1 announcement event ----
|
||||
setStatus(pqSignStatus, 'info', ' Building kind 1 announcement event...');
|
||||
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys);
|
||||
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys, pqScheme);
|
||||
|
||||
// Strip the non-NIP-01 helper field 'statementBytes' before sending to
|
||||
// the signer. buildKind1Announcement includes it for internal use, but
|
||||
@@ -991,6 +1003,15 @@
|
||||
const pqSeedInput = document.getElementById('pqSeedInput');
|
||||
const pqSeedValidation = document.getElementById('pqSeedValidation');
|
||||
const pqSeedOwnContinueBtn = document.getElementById('pqSeedOwnContinueBtn');
|
||||
// v1 recovery toggle: derive legacy v1 keys from a pre-v2 seed
|
||||
const pqSeedV1Recovery = document.getElementById('pqSeedV1Recovery');
|
||||
const pqSeedV1Note = document.getElementById('pqSeedV1Note');
|
||||
if (pqSeedV1Recovery) {
|
||||
pqSeedV1Recovery.addEventListener('change', () => {
|
||||
pqScheme = pqSeedV1Recovery.checked ? 'v1' : 'v2';
|
||||
if (pqSeedV1Note) pqSeedV1Note.style.display = pqSeedV1Recovery.checked ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
pqSeedInput.addEventListener('input', () => {
|
||||
const value = pqSeedInput.value.trim();
|
||||
if (!value) {
|
||||
@@ -1011,6 +1032,7 @@
|
||||
});
|
||||
pqSeedOwnContinueBtn.addEventListener('click', () => {
|
||||
pqMnemonic = pqSeedInput.value.trim();
|
||||
// Scheme was set by the v1-recovery checkbox; default v2 otherwise.
|
||||
setStepDone(2);
|
||||
derivePQKeys();
|
||||
});
|
||||
@@ -1163,6 +1185,12 @@
|
||||
pqSecpKeys = null;
|
||||
pqEvent = null;
|
||||
kind1Event = null; // F-L1: clear all secret references
|
||||
pqScheme = 'v2'; // reset derivation scheme to the default
|
||||
// Reset the v1-recovery toggle UI
|
||||
const v1Toggle = document.getElementById('pqSeedV1Recovery');
|
||||
if (v1Toggle) v1Toggle.checked = false;
|
||||
const v1Note = document.getElementById('pqSeedV1Note');
|
||||
if (v1Note) v1Note.style.display = 'none';
|
||||
userEntropyChunks = [];
|
||||
entropyCollected = 0;
|
||||
lastMouseX = null;
|
||||
|
||||
+175
-51
@@ -10,8 +10,23 @@
|
||||
*
|
||||
* Uses @noble/post-quantum (pure JS, no WASM needed)
|
||||
*
|
||||
* BIP32 Derivation Paths (all under m/44'/1237'/0'/0/):
|
||||
* 0 — secp256k1 (NIP-06 standard, Account #2)
|
||||
* Derivation schemes (see plans/v2-hardened-derivation.md):
|
||||
*
|
||||
* V2 (default) — per-algorithm coin types in the unregistered SLIP-44
|
||||
* 102XXX' range, all-hardened below coin type. PQ keys are NOT under the
|
||||
* Nostr coin branch (1237'), so no compromise of the Nostr subtree (even a
|
||||
* coin-level xpub leak + quantum) can reach them. Coin types 102003'–102005'
|
||||
* match n_signer/the Rust signer; 102006'–102007' are this project's
|
||||
* allocations for ML-DSA-44 and Falcon-512.
|
||||
* ML-DSA-44 m/44'/102006'/0'/0'/0' (32-byte seed)
|
||||
* ML-DSA-65 m/44'/102003'/0'/0'/0' (32-byte seed)
|
||||
* SLH-DSA-128s m/44'/102004'/0'/0'/0' + /1' (48-byte seed)
|
||||
* Falcon-512 m/44'/102007'/0'/0'/0' + /1' (48-byte seed)
|
||||
* ML-KEM-768 m/44'/102005'/0'/0'/0' + /1' (64-byte seed)
|
||||
*
|
||||
* V1 (legacy, retained for recovery only) — all keys under
|
||||
* m/44'/1237'/0'/0/ with non-hardened leaf children:
|
||||
* 0 — secp256k1 (NIP-06 standard)
|
||||
* 1 — ML-DSA-44 (32-byte seed)
|
||||
* 2 — ML-DSA-65 (32-byte seed)
|
||||
* 3+4 — SLH-DSA-128s (48-byte seed, two 32-byte children concatenated)
|
||||
@@ -37,26 +52,83 @@ import { DEFAULT_POLICY, knownAlgorithms, isMandatorySignature, isKem } from './
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* BIP32 derivation paths for all keys.
|
||||
* Versioned PQ derivation schemes.
|
||||
*
|
||||
* Base path: m/44'/1237'/0'/0/ (NIP-06 account 0, change 0)
|
||||
* Child indices:
|
||||
* 0 — secp256k1 (NIP-06 standard)
|
||||
* 1 — ML-DSA-44
|
||||
* 2 — ML-DSA-65
|
||||
* 3, 4 — SLH-DSA-128s (two children, concatenated for 48-byte seed)
|
||||
* 5, 6 — Falcon-512 (two children, concatenated for 48-byte seed)
|
||||
* 7, 8 — ML-KEM-768 (two children, concatenated for 64-byte seed)
|
||||
* V2 (default): per-algorithm coin types (102XXX' range), all-hardened below
|
||||
* the coin type. Each algorithm gets its own coin branch, so a leak of any
|
||||
* one branch's extended key compromises exactly one algorithm, and no leak
|
||||
* within the Nostr coin branch (1237') can reach PQ keys at all.
|
||||
*
|
||||
* V1 (legacy): all PQ seeds at non-hardened children 1–8 under the NIP-06
|
||||
* account 0 change level. Retained ONLY so v1-era seeds can recover their
|
||||
* v1 keys; never used for new derivations. See audit F-M3.
|
||||
*
|
||||
* `children` maps each algorithm to either:
|
||||
* - v1: an array of child indices under the shared base path
|
||||
* - v2: { coin, indices } — hardened children under m/44'/coin'/0'/0'
|
||||
*/
|
||||
const PQ_DERIVATION_PATHS = {
|
||||
secp256k1: [0], // 32 bytes (standard NIP-06)
|
||||
mlDsa44: [1], // 32 bytes
|
||||
mlDsa65: [2], // 32 bytes
|
||||
slhDsa: [3, 4], // 64 bytes concatenated, take first 48
|
||||
falcon512: [5, 6], // 64 bytes concatenated, take first 48
|
||||
mlKem: [7, 8], // 64 bytes concatenated
|
||||
const PQ_DERIVATION_SCHEMES = {
|
||||
v1: {
|
||||
version: 1,
|
||||
base: "m/44'/1237'/0'/0",
|
||||
hardenedLeaves: false,
|
||||
children: {
|
||||
mlDsa44: [1],
|
||||
mlDsa65: [2],
|
||||
slhDsa: [3, 4],
|
||||
falcon512: [5, 6],
|
||||
mlKem: [7, 8],
|
||||
},
|
||||
},
|
||||
v2: {
|
||||
version: 2,
|
||||
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] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The derivation scheme version used for new key derivations and announced
|
||||
* in the kind 1 event via the `derivation_scheme` tag.
|
||||
*/
|
||||
export const PQ_DERIVATION_SCHEME_VERSION = 2;
|
||||
|
||||
/**
|
||||
* Resolve a scheme identifier ('v1' | 'v2' | 1 | 2) to its definition.
|
||||
* @param {string|number} scheme
|
||||
* @returns {object} scheme definition
|
||||
*/
|
||||
function resolveScheme(scheme) {
|
||||
const key = typeof scheme === 'number' ? `v${scheme}` : scheme;
|
||||
const def = PQ_DERIVATION_SCHEMES[key];
|
||||
if (!def) {
|
||||
throw new Error(`Unknown derivation scheme: ${scheme}. Supported: 'v1', 'v2'`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full derivation path for one algorithm under a scheme (for display/tests).
|
||||
* @param {string} alg - algorithm key in scheme.children
|
||||
* @param {string|number} scheme
|
||||
* @returns {string} e.g. "m/44'/102003'/0'/0'/0'" (v2) or "m/44'/1237'/0'/0/2" (v1)
|
||||
*/
|
||||
export function pqDerivationPath(alg, scheme = 'v2') {
|
||||
const def = resolveScheme(scheme);
|
||||
const child = def.children[alg];
|
||||
if (!child) throw new Error(`Unknown algorithm: ${alg}`);
|
||||
if (def.version === 1) {
|
||||
return child.map(i => `${def.base}/${i}`).join(' + ');
|
||||
}
|
||||
return child.indices.map(i => `m/44'/${child.coin}'/0'/0'/${i}'`).join(' + ');
|
||||
}
|
||||
|
||||
// Seed lengths required by each algorithm's keygen()
|
||||
const PQ_SEED_LENGTHS = {
|
||||
mlDsa44: 32,
|
||||
@@ -149,15 +221,14 @@ export function isValidMnemonic(mnemonic) {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Derive a BIP32 child private key at a given path.
|
||||
* Derive a BIP32 child private key at an explicit full path.
|
||||
*
|
||||
* @param {Uint8Array} bip39Seed - 64-byte BIP39 seed
|
||||
* @param {number[]} childIndices - child indices under m/44'/1237'/0'/0/
|
||||
* @param {string} path - full derivation path, e.g. "m/44'/102003'/0'/0'/0'"
|
||||
* @returns {Uint8Array} 32-byte private key
|
||||
*/
|
||||
function deriveBIP32Child(bip39Seed, childIndices) {
|
||||
function deriveBIP32Child(bip39Seed, path) {
|
||||
const hdKey = HDKey.fromMasterSeed(bip39Seed);
|
||||
const path = `m/44'/1237'/0'/0/${childIndices.join('/')}`;
|
||||
const child = hdKey.derive(path);
|
||||
if (!child.privateKey) {
|
||||
throw new Error(`Failed to derive private key at path ${path}`);
|
||||
@@ -165,6 +236,25 @@ function deriveBIP32Child(bip39Seed, childIndices) {
|
||||
return child.privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full derivation paths for one algorithm under a scheme.
|
||||
*
|
||||
* v1: children are indices under the shared base m/44'/1237'/0'/0/
|
||||
* v2: children are hardened indices under m/44'/<coin>'/0'/0'
|
||||
*
|
||||
* @param {object} schemeDef - resolved scheme definition
|
||||
* @param {string} alg - algorithm key in scheme.children
|
||||
* @returns {string[]} full paths (one per child index)
|
||||
*/
|
||||
function schemePathsFor(schemeDef, alg) {
|
||||
const child = schemeDef.children[alg];
|
||||
if (!child) throw new Error(`Unknown algorithm: ${alg}`);
|
||||
if (schemeDef.version === 1) {
|
||||
return child.map(idx => `${schemeDef.base}/${idx}`);
|
||||
}
|
||||
return child.indices.map(i => `m/44'/${child.coin}'/0'/0'/${i}'`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a seed of the required length from BIP32 child keys.
|
||||
*
|
||||
@@ -173,14 +263,14 @@ function deriveBIP32Child(bip39Seed, childIndices) {
|
||||
* For 64-byte seeds: derive two children, concatenate (64 bytes).
|
||||
*
|
||||
* @param {Uint8Array} bip39Seed - 64-byte BIP39 seed
|
||||
* @param {number[]} childIndices - child indices to derive
|
||||
* @param {string[]} paths - full child paths to derive
|
||||
* @param {number} requiredLength - required seed length
|
||||
* @returns {Uint8Array} seed bytes
|
||||
*/
|
||||
function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) {
|
||||
if (childIndices.length === 1) {
|
||||
function derivePQSeedFromBIP32(bip39Seed, paths, requiredLength) {
|
||||
if (paths.length === 1) {
|
||||
// Single child — 32 bytes
|
||||
const seed = deriveBIP32Child(bip39Seed, childIndices);
|
||||
const seed = deriveBIP32Child(bip39Seed, paths[0]);
|
||||
if (seed.length !== requiredLength) {
|
||||
throw new Error(`Seed length mismatch: got ${seed.length}, expected ${requiredLength}`);
|
||||
}
|
||||
@@ -188,8 +278,8 @@ function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) {
|
||||
} else {
|
||||
// Multiple children — concatenate and truncate
|
||||
let combined = new Uint8Array(0);
|
||||
for (const idx of childIndices) {
|
||||
const child = deriveBIP32Child(bip39Seed, [idx]);
|
||||
for (const path of paths) {
|
||||
const child = deriveBIP32Child(bip39Seed, path);
|
||||
const newCombined = new Uint8Array(combined.length + child.length);
|
||||
newCombined.set(combined);
|
||||
newCombined.set(child, combined.length);
|
||||
@@ -234,14 +324,12 @@ export function deriveSecp256k1FromSeed(seed, accountIndex = 0) {
|
||||
/**
|
||||
* Derive all PQ keypairs from a BIP39 seed using BIP32 derivation paths.
|
||||
*
|
||||
* Paths (under m/44'/1237'/0'/0/):
|
||||
* 1 — ML-DSA-44
|
||||
* 2 — ML-DSA-65
|
||||
* 3+4 — SLH-DSA-128s
|
||||
* 5+6 — Falcon-512
|
||||
* 7+8 — ML-KEM-768
|
||||
* V2 (default) — per-algorithm coin types, all-hardened (see module header).
|
||||
* V1 (legacy) — non-hardened children under m/44'/1237'/0'/0/. Pass 'v1'
|
||||
* ONLY to recover keys for a v1-era seed; never for new derivations.
|
||||
*
|
||||
* @param {Uint8Array} bip39Seed - 64-byte BIP39 seed
|
||||
* @param {string|number} [scheme='v2'] - 'v1' | 'v2' | 1 | 2
|
||||
* @returns {{
|
||||
* mlDsa44: {publicKey: Uint8Array, secretKey: Uint8Array},
|
||||
* mlDsa65: {publicKey: Uint8Array, secretKey: Uint8Array},
|
||||
@@ -250,25 +338,27 @@ export function deriveSecp256k1FromSeed(seed, accountIndex = 0) {
|
||||
* mlKem: {publicKey: Uint8Array, secretKey: Uint8Array}
|
||||
* }}
|
||||
*/
|
||||
export function derivePQKeysFromSeed(bip39Seed) {
|
||||
// ML-DSA-44 (32-byte seed, path child 1)
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44);
|
||||
export function derivePQKeysFromSeed(bip39Seed, scheme = 'v2') {
|
||||
const schemeDef = resolveScheme(scheme);
|
||||
|
||||
// ML-DSA-44 (32-byte seed)
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, 'mlDsa44'), PQ_SEED_LENGTHS.mlDsa44);
|
||||
const mlDsa44Keys = ml_dsa44.keygen(mlDsa44Seed);
|
||||
|
||||
// ML-DSA-65 (32-byte seed, path child 2)
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65);
|
||||
// ML-DSA-65 (32-byte seed)
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, 'mlDsa65'), PQ_SEED_LENGTHS.mlDsa65);
|
||||
const mlDsa65Keys = ml_dsa65.keygen(mlDsa65Seed);
|
||||
|
||||
// SLH-DSA-128s (48-byte seed, paths children 3+4 concatenated)
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa);
|
||||
// SLH-DSA-128s (48-byte seed, two children concatenated)
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, 'slhDsa'), PQ_SEED_LENGTHS.slhDsa);
|
||||
const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed);
|
||||
|
||||
// Falcon-512 (48-byte seed, paths children 5+6 concatenated)
|
||||
const falconSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512);
|
||||
// Falcon-512 (48-byte seed, two children concatenated)
|
||||
const falconSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, 'falcon512'), PQ_SEED_LENGTHS.falcon512);
|
||||
const falconKeys = falcon512.keygen(falconSeed);
|
||||
|
||||
// ML-KEM-768 (64-byte seed, paths children 7+8 concatenated)
|
||||
const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem);
|
||||
// ML-KEM-768 (64-byte seed, two children concatenated)
|
||||
const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, 'mlKem'), PQ_SEED_LENGTHS.mlKem);
|
||||
const mlKemKeys = ml_kem768.keygen(mlKemSeed);
|
||||
|
||||
return {
|
||||
@@ -614,9 +704,10 @@ export const NIP_QR_KIND = 9999;
|
||||
* @param {string} hexPubkey - The user's Nostr hex pubkey (Account #1)
|
||||
* @param {number} blockHeight - Current Bitcoin block height for pre-quantum anchoring
|
||||
* @param {object} pqKeys - PQ keypairs from derivePQKeysFromSeed()
|
||||
* @param {string|number} [derivationScheme='v2'] - scheme the keys were derived with
|
||||
* @returns {{kind: number, content: string, tags: Array, pubkey: string, created_at: number, statementBytes: Uint8Array}}
|
||||
*/
|
||||
export function buildKind1Announcement(hexPubkey, blockHeight, pqKeys) {
|
||||
export function buildKind1Announcement(hexPubkey, blockHeight, pqKeys, derivationScheme = 'v2') {
|
||||
const npub = hexToNpub(hexPubkey);
|
||||
|
||||
// Human-readable attestation statement (signed by each PQ key)
|
||||
@@ -658,6 +749,14 @@ Created at: https://laantungir.net/quantum-prep/`;
|
||||
['algorithm', 'falcon-512', bytesToBase64(pqKeys.falcon512.publicKey), bytesToBase64(falconSig)],
|
||||
['algorithm', 'ml-kem-768', bytesToBase64(pqKeys.mlKem.publicKey)]
|
||||
];
|
||||
// Informational metadata: which derivation scheme produced these keys.
|
||||
// Absent tag = v1 (legacy events predate the tag). Not evidence — signature
|
||||
// verification is path-agnostic — so verifiers treat unknown values as
|
||||
// display-only.
|
||||
const schemeVersion = resolveScheme(derivationScheme).version;
|
||||
if (schemeVersion >= 2) {
|
||||
tags.push(['derivation_scheme', String(schemeVersion)]);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 1,
|
||||
@@ -1543,7 +1642,8 @@ export const PQ_KEY_INFO = {
|
||||
fips: 'FIPS 204',
|
||||
type: 'signature',
|
||||
securityLevel: 'Category 2 (~AES-128)',
|
||||
derivationPath: "m/44'/1237'/0'/0/1"
|
||||
coinType: 102006,
|
||||
derivationPath: "m/44'/102006'/0'/0'/0'"
|
||||
},
|
||||
'ml-dsa-65': {
|
||||
name: 'ML-DSA-65 (Dilithium)',
|
||||
@@ -1552,7 +1652,8 @@ export const PQ_KEY_INFO = {
|
||||
fips: 'FIPS 204',
|
||||
type: 'signature',
|
||||
securityLevel: 'Category 3 (~AES-192)',
|
||||
derivationPath: "m/44'/1237'/0'/0/2"
|
||||
coinType: 102003,
|
||||
derivationPath: "m/44'/102003'/0'/0'/0'"
|
||||
},
|
||||
'slh-dsa-128s': {
|
||||
name: 'SLH-DSA-128s (SPHINCS+)',
|
||||
@@ -1561,7 +1662,8 @@ export const PQ_KEY_INFO = {
|
||||
fips: 'FIPS 205',
|
||||
type: 'signature',
|
||||
securityLevel: 'Category 1 (~AES-128, hash-based)',
|
||||
derivationPath: "m/44'/1237'/0'/0/3+4"
|
||||
coinType: 102004,
|
||||
derivationPath: "m/44'/102004'/0'/0'/0' + /1'"
|
||||
},
|
||||
'falcon-512': {
|
||||
name: 'Falcon-512',
|
||||
@@ -1570,7 +1672,8 @@ export const PQ_KEY_INFO = {
|
||||
fips: 'FIPS 206 (draft)',
|
||||
type: 'signature',
|
||||
securityLevel: 'Category 1 (~AES-128, lattice-based)',
|
||||
derivationPath: "m/44'/1237'/0'/0/5+6"
|
||||
coinType: 102007,
|
||||
derivationPath: "m/44'/102007'/0'/0'/0' + /1'"
|
||||
},
|
||||
'ml-kem-768': {
|
||||
name: 'ML-KEM-768 (Kyber)',
|
||||
@@ -1579,10 +1682,31 @@ export const PQ_KEY_INFO = {
|
||||
fips: 'FIPS 203',
|
||||
type: 'kem',
|
||||
securityLevel: 'Category 3 (~AES-192)',
|
||||
derivationPath: "m/44'/1237'/0'/0/7+8"
|
||||
coinType: 102005,
|
||||
derivationPath: "m/44'/102005'/0'/0'/0' + /1'"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derivation path for an algorithm id (as used in event tags / PQ_KEY_INFO
|
||||
* keys) under a given scheme. Defaults to v2.
|
||||
*
|
||||
* @param {string} algorithmId - e.g. 'ml-dsa-44'
|
||||
* @param {string|number} [scheme='v2']
|
||||
* @returns {string} display path
|
||||
*/
|
||||
export function derivationPathForAlgorithm(algorithmId, scheme = 'v2') {
|
||||
const algKey = {
|
||||
'ml-dsa-44': 'mlDsa44',
|
||||
'ml-dsa-65': 'mlDsa65',
|
||||
'slh-dsa-128s': 'slhDsa',
|
||||
'falcon-512': 'falcon512',
|
||||
'ml-kem-768': 'mlKem',
|
||||
}[algorithmId];
|
||||
if (!algKey) throw new Error(`Unknown algorithm id: ${algorithmId}`);
|
||||
return pqDerivationPath(algKey, scheme);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OPENTIMESTAMPS (NIP-03)
|
||||
// ============================================================================
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.1.7",
|
||||
"VERSION_NUMBER": "0.1.7",
|
||||
"BUILD_DATE": "2026-07-31T10:14:20.015Z"
|
||||
"VERSION": "v0.1.8",
|
||||
"VERSION_NUMBER": "0.1.8",
|
||||
"BUILD_DATE": "2026-08-23T12:05:43.823Z"
|
||||
}
|
||||
|
||||
+92
-32
@@ -9627,20 +9627,49 @@ function isKem(algorithmId, policy = DEFAULT_POLICY) {
|
||||
}
|
||||
|
||||
// www/js/pq-crypto.mjs
|
||||
var PQ_DERIVATION_PATHS = {
|
||||
secp256k1: [0],
|
||||
// 32 bytes (standard NIP-06)
|
||||
mlDsa44: [1],
|
||||
// 32 bytes
|
||||
mlDsa65: [2],
|
||||
// 32 bytes
|
||||
slhDsa: [3, 4],
|
||||
// 64 bytes concatenated, take first 48
|
||||
falcon512: [5, 6],
|
||||
// 64 bytes concatenated, take first 48
|
||||
mlKem: [7, 8]
|
||||
// 64 bytes concatenated
|
||||
var PQ_DERIVATION_SCHEMES = {
|
||||
v1: {
|
||||
version: 1,
|
||||
base: "m/44'/1237'/0'/0",
|
||||
hardenedLeaves: false,
|
||||
children: {
|
||||
mlDsa44: [1],
|
||||
mlDsa65: [2],
|
||||
slhDsa: [3, 4],
|
||||
falcon512: [5, 6],
|
||||
mlKem: [7, 8]
|
||||
}
|
||||
},
|
||||
v2: {
|
||||
version: 2,
|
||||
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] }
|
||||
}
|
||||
}
|
||||
};
|
||||
var PQ_DERIVATION_SCHEME_VERSION = 2;
|
||||
function resolveScheme(scheme) {
|
||||
const key = typeof scheme === "number" ? `v${scheme}` : scheme;
|
||||
const def = PQ_DERIVATION_SCHEMES[key];
|
||||
if (!def) {
|
||||
throw new Error(`Unknown derivation scheme: ${scheme}. Supported: 'v1', 'v2'`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
function pqDerivationPath(alg, scheme = "v2") {
|
||||
const def = resolveScheme(scheme);
|
||||
const child = def.children[alg];
|
||||
if (!child) throw new Error(`Unknown algorithm: ${alg}`);
|
||||
if (def.version === 1) {
|
||||
return child.map((i) => `${def.base}/${i}`).join(" + ");
|
||||
}
|
||||
return child.indices.map((i) => `m/44'/${child.coin}'/0'/0'/${i}'`).join(" + ");
|
||||
}
|
||||
var PQ_SEED_LENGTHS = {
|
||||
mlDsa44: 32,
|
||||
mlDsa65: 32,
|
||||
@@ -9675,26 +9704,33 @@ function mnemonicToSeed(mnemonic, passphrase = "") {
|
||||
function isValidMnemonic(mnemonic) {
|
||||
return validateMnemonic(mnemonic, wordlist);
|
||||
}
|
||||
function deriveBIP32Child(bip39Seed, childIndices) {
|
||||
function deriveBIP32Child(bip39Seed, path) {
|
||||
const hdKey = HDKey.fromMasterSeed(bip39Seed);
|
||||
const path = `m/44'/1237'/0'/0/${childIndices.join("/")}`;
|
||||
const child = hdKey.derive(path);
|
||||
if (!child.privateKey) {
|
||||
throw new Error(`Failed to derive private key at path ${path}`);
|
||||
}
|
||||
return child.privateKey;
|
||||
}
|
||||
function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) {
|
||||
if (childIndices.length === 1) {
|
||||
const seed = deriveBIP32Child(bip39Seed, childIndices);
|
||||
function schemePathsFor(schemeDef, alg) {
|
||||
const child = schemeDef.children[alg];
|
||||
if (!child) throw new Error(`Unknown algorithm: ${alg}`);
|
||||
if (schemeDef.version === 1) {
|
||||
return child.map((idx) => `${schemeDef.base}/${idx}`);
|
||||
}
|
||||
return child.indices.map((i) => `m/44'/${child.coin}'/0'/0'/${i}'`);
|
||||
}
|
||||
function derivePQSeedFromBIP32(bip39Seed, paths, requiredLength) {
|
||||
if (paths.length === 1) {
|
||||
const seed = deriveBIP32Child(bip39Seed, paths[0]);
|
||||
if (seed.length !== requiredLength) {
|
||||
throw new Error(`Seed length mismatch: got ${seed.length}, expected ${requiredLength}`);
|
||||
}
|
||||
return seed;
|
||||
} else {
|
||||
let combined = new Uint8Array(0);
|
||||
for (const idx of childIndices) {
|
||||
const child = deriveBIP32Child(bip39Seed, [idx]);
|
||||
for (const path of paths) {
|
||||
const child = deriveBIP32Child(bip39Seed, path);
|
||||
const newCombined = new Uint8Array(combined.length + child.length);
|
||||
newCombined.set(combined);
|
||||
newCombined.set(child, combined.length);
|
||||
@@ -9718,16 +9754,17 @@ function deriveSecp256k1FromSeed(seed, accountIndex = 0) {
|
||||
publicKey: child.publicKey
|
||||
};
|
||||
}
|
||||
function derivePQKeysFromSeed(bip39Seed) {
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44);
|
||||
function derivePQKeysFromSeed(bip39Seed, scheme = "v2") {
|
||||
const schemeDef = resolveScheme(scheme);
|
||||
const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, "mlDsa44"), PQ_SEED_LENGTHS.mlDsa44);
|
||||
const mlDsa44Keys = ml_dsa44.keygen(mlDsa44Seed);
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65);
|
||||
const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, "mlDsa65"), PQ_SEED_LENGTHS.mlDsa65);
|
||||
const mlDsa65Keys = ml_dsa65.keygen(mlDsa65Seed);
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa);
|
||||
const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, "slhDsa"), PQ_SEED_LENGTHS.slhDsa);
|
||||
const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed);
|
||||
const falconSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512);
|
||||
const falconSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, "falcon512"), PQ_SEED_LENGTHS.falcon512);
|
||||
const falconKeys = falcon512.keygen(falconSeed);
|
||||
const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem);
|
||||
const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, schemePathsFor(schemeDef, "mlKem"), PQ_SEED_LENGTHS.mlKem);
|
||||
const mlKemKeys = ml_kem768.keygen(mlKemSeed);
|
||||
return {
|
||||
mlDsa44: mlDsa44Keys,
|
||||
@@ -9910,7 +9947,7 @@ function validateSignerOutput(signedEvent, template, expectedPubkey) {
|
||||
return signedEvent;
|
||||
}
|
||||
var NIP_QR_KIND = 9999;
|
||||
function buildKind1Announcement(hexPubkey, blockHeight, pqKeys) {
|
||||
function buildKind1Announcement(hexPubkey, blockHeight, pqKeys, derivationScheme = "v2") {
|
||||
const npub = hexToNpub(hexPubkey);
|
||||
const content = `I am signaling that the post-quantum public keys listed in the tags of this event were generated by me and I hold the private keys. I may use these keys in the future as successors to my current Nostr identity.
|
||||
|
||||
@@ -9944,6 +9981,10 @@ Created at: https://laantungir.net/quantum-prep/`;
|
||||
["algorithm", "falcon-512", bytesToBase64(pqKeys.falcon512.publicKey), bytesToBase64(falconSig)],
|
||||
["algorithm", "ml-kem-768", bytesToBase64(pqKeys.mlKem.publicKey)]
|
||||
];
|
||||
const schemeVersion = resolveScheme(derivationScheme).version;
|
||||
if (schemeVersion >= 2) {
|
||||
tags.push(["derivation_scheme", String(schemeVersion)]);
|
||||
}
|
||||
return {
|
||||
kind: 1,
|
||||
content,
|
||||
@@ -10486,7 +10527,8 @@ var PQ_KEY_INFO = {
|
||||
fips: "FIPS 204",
|
||||
type: "signature",
|
||||
securityLevel: "Category 2 (~AES-128)",
|
||||
derivationPath: "m/44'/1237'/0'/0/1"
|
||||
coinType: 102006,
|
||||
derivationPath: "m/44'/102006'/0'/0'/0'"
|
||||
},
|
||||
"ml-dsa-65": {
|
||||
name: "ML-DSA-65 (Dilithium)",
|
||||
@@ -10495,7 +10537,8 @@ var PQ_KEY_INFO = {
|
||||
fips: "FIPS 204",
|
||||
type: "signature",
|
||||
securityLevel: "Category 3 (~AES-192)",
|
||||
derivationPath: "m/44'/1237'/0'/0/2"
|
||||
coinType: 102003,
|
||||
derivationPath: "m/44'/102003'/0'/0'/0'"
|
||||
},
|
||||
"slh-dsa-128s": {
|
||||
name: "SLH-DSA-128s (SPHINCS+)",
|
||||
@@ -10504,7 +10547,8 @@ var PQ_KEY_INFO = {
|
||||
fips: "FIPS 205",
|
||||
type: "signature",
|
||||
securityLevel: "Category 1 (~AES-128, hash-based)",
|
||||
derivationPath: "m/44'/1237'/0'/0/3+4"
|
||||
coinType: 102004,
|
||||
derivationPath: "m/44'/102004'/0'/0'/0' + /1'"
|
||||
},
|
||||
"falcon-512": {
|
||||
name: "Falcon-512",
|
||||
@@ -10513,7 +10557,8 @@ var PQ_KEY_INFO = {
|
||||
fips: "FIPS 206 (draft)",
|
||||
type: "signature",
|
||||
securityLevel: "Category 1 (~AES-128, lattice-based)",
|
||||
derivationPath: "m/44'/1237'/0'/0/5+6"
|
||||
coinType: 102007,
|
||||
derivationPath: "m/44'/102007'/0'/0'/0' + /1'"
|
||||
},
|
||||
"ml-kem-768": {
|
||||
name: "ML-KEM-768 (Kyber)",
|
||||
@@ -10522,9 +10567,21 @@ var PQ_KEY_INFO = {
|
||||
fips: "FIPS 203",
|
||||
type: "kem",
|
||||
securityLevel: "Category 3 (~AES-192)",
|
||||
derivationPath: "m/44'/1237'/0'/0/7+8"
|
||||
coinType: 102005,
|
||||
derivationPath: "m/44'/102005'/0'/0'/0' + /1'"
|
||||
}
|
||||
};
|
||||
function derivationPathForAlgorithm(algorithmId, scheme = "v2") {
|
||||
const algKey = {
|
||||
"ml-dsa-44": "mlDsa44",
|
||||
"ml-dsa-65": "mlDsa65",
|
||||
"slh-dsa-128s": "slhDsa",
|
||||
"falcon-512": "falcon512",
|
||||
"ml-kem-768": "mlKem"
|
||||
}[algorithmId];
|
||||
if (!algKey) throw new Error(`Unknown algorithm id: ${algorithmId}`);
|
||||
return pqDerivationPath(algKey, scheme);
|
||||
}
|
||||
var OTS_CALENDAR_SERVERS = [
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
@@ -11047,6 +11104,7 @@ function parseProofArchive(archiveJson) {
|
||||
export {
|
||||
CANONICAL_DIGEST_VERSION,
|
||||
NIP_QR_KIND,
|
||||
PQ_DERIVATION_SCHEME_VERSION,
|
||||
PQ_KEY_INFO,
|
||||
base64ToBytes,
|
||||
buildKind11112Wrapper,
|
||||
@@ -11059,6 +11117,7 @@ export {
|
||||
canonicalEventDigest,
|
||||
clearPendingOts,
|
||||
computeEventId,
|
||||
derivationPathForAlgorithm,
|
||||
derivePQKeysFromSeed,
|
||||
deriveSecp256k1FromSeed,
|
||||
generateSeedPhrase,
|
||||
@@ -11073,6 +11132,7 @@ export {
|
||||
mnemonicToSeed,
|
||||
parseOtsFile,
|
||||
parseProofArchive,
|
||||
pqDerivationPath,
|
||||
savePendingOts,
|
||||
selectCanonicalProofCarrier,
|
||||
signWithFalcon,
|
||||
|
||||
Reference in New Issue
Block a user