Files
nostr_quantum_preparation/plans/remediation_plan.md
T

17 KiB
Raw Blame History

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

Component 1 (lines ~103–147): PQ Key Derivation

  • Replace the HKDF derivation description (lines 126–137) with the BIP32 derivation description matching explanation.md 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 and pq-crypto.mjs.
  • 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

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. 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

  • "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

  • 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 and 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.