181 lines
17 KiB
Markdown
181 lines
17 KiB
Markdown
# Remediation Plan: Reconcile Documentation with Implementation
|
||
|
||
**Date:** 2026-07-17
|
||
**Scope:** Address audit findings F-C1 and F-C4 (and related F-M1, F-M4, F-M6) by adjusting all documentation to match what the code actually does.
|
||
**Direction chosen by user:** Adjust docs to match code; drop the successor-signature security claim; accept the "PQ self-attestation + Account #1 authorization + OTS precedence" model.
|
||
|
||
This plan covers **only the documentation reconciliation** (F-C1 doc side + F-C4). The remaining audit findings (F-C2 real OTS verification, F-C3 digest binding, F-H1–F-H5, etc.) are **out of scope for this plan** and will be addressed in a follow-up "code does what it says" audit pass, as the user stated.
|
||
|
||
---
|
||
|
||
## Background: What the code actually does
|
||
|
||
Before listing changes, here is the canonical description of the implemented system that all docs must match:
|
||
|
||
### Derivation
|
||
- BIP39 mnemonic (12 words, 128 bits) → BIP39 seed (PBKDF2-HMAC-SHA512, 2048 iterations, 64 bytes).
|
||
- BIP32 master key from the seed.
|
||
- All keys derived under `m/44'/1237'/0'/0/` (NIP-06 base path, account 0, change 0):
|
||
- child 0 → secp256k1 keypair (NIP-06 standard). **Derived but NOT used to sign anything in the current implementation.**
|
||
- child 1 → 32-byte seed → ML-DSA-44 keypair
|
||
- child 2 → 32-byte seed → ML-DSA-65 keypair
|
||
- children 3+4 → 64 bytes concatenated, first 48 used → SLH-DSA-128s keypair
|
||
- children 5+6 → 64 bytes concatenated, first 48 used → Falcon-512 keypair
|
||
- children 7+8 → 64 bytes concatenated → ML-KEM-768 keypair
|
||
- PQ keygen is deterministic: same mnemonic → same PQ keypairs. Recoverable from the seed phrase.
|
||
|
||
### Event structure (two events)
|
||
1. **Kind 1 announcement** (human-readable text note):
|
||
- `content`: human-readable attestation statement (prose naming the user's npub, hex pubkey, block height, and listing the 5 PQ algorithms).
|
||
- `tags`: `['block_height', '<height>']` plus one `['algorithm', '<algo>', '<base64 pubkey>', '<base64 signature>']` tag per PQ signature scheme. ML-KEM-768 tag has only the pubkey (no signature — it's a KEM).
|
||
- Each PQ signature scheme signs `TextEncoder.encode(content)` (the human-readable text).
|
||
- Signed with the user's **existing** Nostr identity (Account #1) via `window.nostr.signEvent` (NIP-07). This is the only secp256k1 signature on the kind 1 event.
|
||
|
||
2. **Kind 11112 wrapper** (replaceable event, 10000–19999 range):
|
||
- `content`: `JSON.stringify(kind1Event)` (the full signed kind 1 event embedded as a JSON string).
|
||
- `tags`: `['e', '<kind1 event id>']`, `['sha256', '<hex SHA-256 of the full signed kind 1 event JSON>']`, `['ots', '<base64 .ots proof>']`.
|
||
- Signed with the user's existing identity (Account #1) via `window.nostr.signEvent`.
|
||
|
||
### What is OpenTimestamped
|
||
- The **SHA-256 of the full signed kind 1 event JSON** (including its `id` and `sig`), computed by `hashFullEvent(kind1Event)`. This is the digest submitted to the OTS calendar and recorded in the `sha256` tag.
|
||
|
||
### Security model (the one docs must describe)
|
||
- The PQ keys **self-attest**: each PQ signature scheme signs the human-readable attestation statement, which names the user's npub and the block height.
|
||
- The user's **existing** secp256k1 identity (Account #1) authorizes the migration by signing both the kind 1 and kind 11112 events.
|
||
- There is **no successor signature**. The seed-derived secp256k1 key (Account #2, child 0) is derived but not used to sign. The docs must **not** claim that the seed-derived key endorses the PQ keys.
|
||
- **Pre-quantum anchoring** is provided by OpenTimestamps on the kind 1 event hash. After a quantum break, a forged migration event cannot be backdated to before the real event's OTS anchor (re-mining historical Bitcoin blocks is infeasible even with a quantum computer). The real event is distinguishable from a forged one by OTS precedence — the event with the earliest valid OTS proof wins.
|
||
- **What is NOT proven** (docs must be honest about this): the PQ keys cannot *cryptographically* prove they share a common seed origin. They prove only that 5 PQ keypairs exist and each signed the attestation text. Common-origin is asserted by the attestation text and authorized by Account #1, not proven by a seed-derived signature. The user has decided this is acceptable.
|
||
|
||
### Algorithms (5, not 3)
|
||
ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512, ML-KEM-768.
|
||
|
||
---
|
||
|
||
## Changes by file
|
||
|
||
### 1. [`README.md`](README.md)
|
||
|
||
**Component 1 (lines ~103–147): PQ Key Derivation**
|
||
- Replace the HKDF derivation description (lines 126–137) with the BIP32 derivation description matching [`explanation.md`](explanation.md:39) and the code.
|
||
- Update the mermaid diagram (lines 109–124): change `HKDF derivation` labels to `BIP32 derivation` for all PQ keys. Remove the `SYMKEY` node if it's not implemented (it isn't — no symmetric storage key is derived in the code). Keep `BIP32 derivation - secp256k1 keypair`.
|
||
- Add the derivation path table (child indices 0–8, seed lengths, concatenation/truncation rule for >32-byte seeds) matching [`explanation.md`](explanation.md:41) and [`pq-crypto.mjs`](www/js/pq-crypto.mjs:49).
|
||
- Document the truncation rule explicitly: "for 48-byte and 64-byte seeds, two 32-byte BIP32 children are concatenated (64 bytes); for 48-byte seeds the first 48 bytes are used." (F-M4)
|
||
- Keep the seed-entropy table (12 vs 24 words) as-is — it's accurate.
|
||
|
||
**Component 2 (lines ~150–265): Key-Link Events**
|
||
- Replace the event format JSON (lines 202–233) with the actual two-event structure: kind 1 announcement (text content + `algorithm` tags) and kind 11112 wrapper (JSON content + `e`/`sha256`/`ots` tags). Show real tag format: `['algorithm', '<algo>', '<base64 pubkey>', '<base64 signature>']`.
|
||
- Update the mermaid diagram (lines 169–196): show 5 PQ schemes (add ML-DSA-44, Falcon-512). Show the kind 1 + kind 11112 two-event structure. Remove the "secp256k1 signature covers the entire content" claim about a successor; the only secp256k1 signature is Account #1's on the Nostr events.
|
||
- **Remove the `secp256k1_signature` / successor-signature claim** (lines 229–235). Replace with: "The user's existing Nostr identity (the secp256k1 key people already know) signs the kind 1 and kind 11112 events, authorizing the migration. The seed-derived secp256k1 key is not used to sign."
|
||
- Update the "ML-KEM Cannot Sign" section (lines 237–243): keep the KEM explanation, but remove the claim that "the secp256k1 signature covers the ML-KEM public key in the content" — in the actual format, the ML-KEM pubkey is in a tag, and Account #1's signature covers the whole event (which includes the tags). Restate accurately.
|
||
- Add a new subsection **"What This Proves and What It Does Not"** stating honestly:
|
||
- Proves: each PQ key signed the attestation; Account #1 authorized the migration; the event is OTS-anchored pre-quantum.
|
||
- Does NOT prove: that the PQ keys share a common seed origin (no successor signature). Common-origin is asserted in the attestation text, not cryptographically proven by a seed-derived key.
|
||
- Update the "Revocation" section (lines 258–264): keep, but adjust "signed by the remaining valid PQ keys" to reflect the actual signing model.
|
||
|
||
**Component 3 (lines ~268–305): OpenTimestamps**
|
||
- Update "What Should Be OpenTimestamped" to reflect that the code timestamps the **SHA-256 of the full signed kind 1 event JSON** (not "the key-link event" generically, and not the kind 11112). Clarify the two-event relationship: the kind 11112 wrapper *carries* the OTS proof; the kind 1 event *is* what's timestamped.
|
||
- Add a note that OTS verification is currently a byte-pattern heuristic and real client-side verification is planned (this is honest about the current state — F-C2 is out of scope for this plan but the docs shouldn't overclaim). **Actually**: since the user wants docs to match what the code *does*, and the code does NOT do real OTS verification, the docs should describe the current behavior (byte-pattern check + server upgrade) and not claim full verification. Mark the real-verification as future work.
|
||
|
||
**Component 4 (lines ~309–435): Migrating Existing Users with Raw nsec**
|
||
- This section describes approaches (A/B/C) that are **not implemented** in the current code (no nsec encryption, no kind 30078 storage event, no 36-word phrase). Options:
|
||
- (a) Mark this entire component as "Design only — not yet implemented" clearly at the top.
|
||
- (b) Remove it.
|
||
- Recommend (a): keep the design discussion but label it unimplemented, since the code only does the seed-generation → PQ-derivation → sign → publish → OTS flow for users who already have a Nostr identity. The raw-nsec migration is future work.
|
||
|
||
**Component 5 (lines ~438–491): Quantum-Safe Self-Storage**
|
||
- Not implemented in the code (no OTP, no symmetric storage key derivation, no kind 30078 encryption). Mark as "Design only — not yet implemented."
|
||
|
||
**Component 6 (lines ~494–543): Deterministic Wallet Compartmentalization**
|
||
- This is analysis/commentary, not implemented features. It's accurate as background. Keep, but ensure it doesn't claim the app uses hardened PQ derivation (it uses non-hardened children 1–8 under a hardened parent). The existing text is mostly fine; just verify it doesn't contradict the actual paths.
|
||
|
||
**The Complete Migration Flow (lines ~547–609)**
|
||
- Update the mermaid diagram and user-experience steps to match the actual flow: sign in → generate seed → derive PQ keys (5) → PQ keys sign attestation text → Account #1 signs kind 1 → hash kind 1 → OTS submit → build kind 11112 with OTS proof → Account #1 signs kind 11112 → publish both → poll for OTS confirmation → republish kind 11112 with confirmed proof.
|
||
- Remove the "Encrypt old nsec" / "kind 30078" steps (not implemented) or mark them as future.
|
||
- Remove "successor signature" from the flow.
|
||
|
||
**Implementation Status (lines ~652–682)**
|
||
- Update the "Existing Infrastructure" table: the app uses `@noble/post-quantum` (not liboqs), `@scure/bip39/bip32`, and a vendored-but-not-yet-used `javascript-opentimestamps`. Remove the `nostr_core_lib` references (that's a different project).
|
||
- Update "To Be Built" to reflect what's actually missing: real OTS client-side verification, tests, the raw-nsec migration, the storage encryption.
|
||
|
||
**References** — keep, they're accurate.
|
||
|
||
---
|
||
|
||
### 2. [`explanation.md`](explanation.md)
|
||
|
||
This file is **mostly correct** (it describes BIP32, which matches the code). Changes needed:
|
||
|
||
- **Step 6 (lines ~97–106):** The link statement shown ("Identity <Account #1> is migrating to successor <Account #2>...") does not match the actual `content` in [`buildKind1Announcement`](www/js/pq-crypto.mjs:435). Replace with the actual attestation text (the "I am signaling that the post-quantum public keys listed in the tags of this event were generated by me..." text).
|
||
- **Step 8 (lines ~122–130): "Sign the statement with Account #2's secp256k1 key"** — **REMOVE this step entirely.** The code does not do this. Replace with a note: "The seed-derived secp256k1 key (Account #2) is not used to sign in the current implementation. The only secp256k1 signature is Account #1's on the Nostr event (Step 10)."
|
||
- **Step 9 (lines ~132–169): "Build the NIP-QR event content"** — the JSON structure shown (`statement`, `successor_pubkey`, `successor_signature`, `pq_keys[]`) does not match the code. Replace with the actual structure: kind 1 event with text `content` + `algorithm` tags.
|
||
- **Step 10 (lines ~171–193):** Update the event kind from 30078 to kind 1 (announcement) + kind 11112 (wrapper). Show the actual two-event structure.
|
||
- **Step 11:** Update to reflect publishing both events.
|
||
- **Summary table "Who Signs What" (lines ~201–211):** Remove the "Account #2 secp256k1 key" row. Remove the "successor_signature" column. Update ML-KEM row to reflect that its pubkey is in a tag covered by Account #1's event signature (not a successor signature).
|
||
- **Summary: BIP32 Key Derivation Tree (lines ~213–259):** Remove the "signs the link statement (Schnorr)" annotation under child 0 (Account #2). Add 5 algorithms (it currently shows the right 5 actually — verify ML-DSA-44 and Falcon are both present; they are in the table at line 265). Update the tree to show child 0 as "derived but not used for signing in current implementation."
|
||
- **The 5 PQ Algorithms table (lines ~263–273):** Already lists 5 algorithms. Verify it matches the code (it does). Keep.
|
||
|
||
---
|
||
|
||
### 3. [`why_what_how.md`](why_what_how.md)
|
||
|
||
- **"The six components" (lines ~56–68):** Component 1 says "PQ keys are derived from it deterministically using HKDF with algorithm-specific labels." Change to BIP32. Component 2 says "ML-DSA-65, SLH-DSA-128s, and ML-KEM-768" (3 algorithms) — change to all 5. Remove "successor" language if present.
|
||
- **"The cryptographic chain of trust" diagram (lines ~83–103):** Remove the Account #2 signature step. Show: Account #1 signs the event; PQ keys sign the attestation; OTS anchors. No successor.
|
||
- **"The NIP-QR event" (lines ~105–129):** Replace the kind 30078 example with the actual kind 1 + kind 11112 structure. Remove `successor` tag, `successor_signature` field. Show `algorithm` tags and `e`/`sha256`/`ots` tags.
|
||
- **"PQ key derivation from seed" (lines ~131–143):** Replace the HKDF pseudocode with the BIP32 path table.
|
||
- **"The algorithms" table (lines ~158–164):** Add ML-DSA-44 and Falcon-512 (currently only 3 listed).
|
||
- **"Post-quantum scenario" (lines ~171–181):** Update step 1–6 to remove successor-signature reasoning. The argument becomes: attacker forges Account #1 → can forge kind 1 events → but cannot backdate before the real event's OTS anchor → real event wins by OTS precedence. This is still valid and is the core argument.
|
||
- **"The demo" (lines ~183–193):** Update the algorithm list to 5. Already mentions `@noble/post-quantum` — good.
|
||
|
||
---
|
||
|
||
### 4. [`laans_explanation.md`](laans_explanation.md)
|
||
|
||
- The file is truncated mid-sentence (F-M6). Either complete it or remove it.
|
||
- The existing content (lines 1–16) describes "5 different nsec npub key pairs" and a link statement referencing "Account #1" and "Account #2" with the successor model.
|
||
- **Recommendation:** Rewrite this file to be a short, accurate summary matching the implementation, or delete it if `explanation.md` already covers the same ground (it does, more thoroughly). If kept, update the link statement text and remove the successor reference.
|
||
|
||
---
|
||
|
||
## Execution order
|
||
|
||
1. **README.md** — largest and most visible; do first.
|
||
2. **explanation.md** — mostly correct; targeted edits to Steps 6–11 and summary tables.
|
||
3. **why_what_how.md** — targeted edits to components, chain-of-trust diagram, event format, derivation, algorithms.
|
||
4. **laans_explanation.md** — rewrite or delete.
|
||
5. **Consistency pass** — re-read all four docs and cross-check against [`pq-crypto.mjs`](www/js/pq-crypto.mjs) and [`index.html`](www/index.html) to ensure every claim about derivation, event format, algorithms, signing, and OTS matches the code. Specifically verify:
|
||
- Derivation scheme = BIP32 (not HKDF) everywhere.
|
||
- Event structure = kind 1 + kind 11112 (not kind 30078) everywhere.
|
||
- Algorithms = 5 (not 3) everywhere.
|
||
- No successor signature claimed anywhere.
|
||
- What is timestamped = SHA-256 of full signed kind 1 event JSON, everywhere.
|
||
- Security model = PQ self-attestation + Account #1 authorization + OTS precedence, everywhere.
|
||
- Unimplemented components (raw-nsec migration, storage encryption) marked as design-only.
|
||
|
||
## Out of scope (for a follow-up plan)
|
||
|
||
These audit findings are NOT addressed by this documentation reconciliation and will be tackled when the user does the "code does what it says" pass:
|
||
- F-C2: real client-side OTS verification
|
||
- F-C3: bind OTS target digest to event hash
|
||
- F-H1: add tests
|
||
- F-H2: `verifyNostrEvent` check `event.id`
|
||
- F-H3: XSS / `innerHTML`
|
||
- F-H4: CSP / SRI
|
||
- F-H5: `isOtsConfirmed` substring heuristic (subsumed by F-C2)
|
||
- F-M1: canonical structured attestation (optional improvement)
|
||
- F-M2: block_height verification
|
||
- F-M3: xpub hygiene documentation (may be partially addressed in this pass)
|
||
- F-M5: Falcon draft-status UI warning
|
||
- F-L1–F-L7: low-severity items
|
||
|
||
Note: F-C1's *code* side (the missing successor signature) is intentionally NOT being fixed — the user decided the current security model is acceptable. The docs are being changed to match. F-C4 is fully addressed by this plan.
|
||
|
||
---
|
||
|
||
## Mermaid diagram guidance
|
||
|
||
To avoid Mermaid parsing errors (per architect-mode instructions), when updating diagrams:
|
||
- Do not use double quotes `""` inside square brackets `[]`.
|
||
- Do not use parentheses `()` inside square brackets `[]`.
|
||
- Use single quotes or no quotes inside node labels.
|
||
- Example: `EVENT[Kind 1 Announcement - text content plus algorithm tags]` is safe; `EVENT["Kind 1 (announcement)"]` is not.
|