Files
nostr_quantum_preparation/audits/GLM5.2/audit.md
T

15 KiB
Raw Blame History

Security Audit: Post-Quantum Nostr (Second Pass)

Auditor: GLM-5.2 (architect mode) Date: 2026-07-17 (second pass, after documentation reconciliation + F-C2/F-C3 fix) Scope: Full codebase in /home/user/lt/post_quantum_nostr Primary focus: Cryptographic correctness — does the cryptography function correctly and do what it purports to do?


1. Executive Summary

This is the second audit pass, performed after the project's documentation was reconciled with its implementation. The first pass found that the docs described a different system than the code (F-C4) and claimed a "successor signature" that didn't exist (F-C1). Both have been addressed: the docs now match the code, and the security model is honestly stated as "PQ self-attestation + Account #1 authorization + OTS precedence" without a seed-derived signature.

What changed since the first pass:

  • F-C4 (docs vs code mismatch): RESOLVED. All four primary docs now describe BIP32 derivation, the kind 1 + kind 11112 two-event structure, 5 algorithms, and what's actually timestamped.
  • F-C1 (missing successor signature): RECLASSIFIED. No longer a discrepancy — it's now a documented, deliberate design choice. The docs explicitly state the PQ keys cannot cryptographically prove common seed origin, and that OTS precedence is the anti-forgery mechanism. This is coherent.
  • F-C2 (OTS proofs never verified): RESOLVED. Implemented real client-side OTS verification in pq-crypto.mjs: a binary OTS parser (parseOtsFile), a full verifier (verifyOtsProof) that walks the op tree (SHA-256, SHA-1, RIPEMD160, append, prepend, reverse), fetches Bitcoin block headers from blockstream.info/mempool.space, and checks the computed Merkle root matches the block's merkle root. verify.html and index.html now use verifyOtsProof instead of the byte-pattern heuristic. Tested against the vendored example proofs (hello-world.txt.ots verifies against block 358391).
  • F-C3 (OTS target digest not bound): RESOLVED. verifyOtsProof now takes an optional expectedDigestHex parameter and rejects proofs whose target digest doesn't match. verify.html and index.html pass the event's sha256 tag as the expected digest. Tested: a wrong digest correctly fails with "Target digest mismatch".
  • ⚠️ One minor doc leftover (new finding F-L8): why_what_how.md Component 4 summary still says "the new identity signs to endorse the PQ keys" — successor-model language in a "design only" section. Low severity.

What remains open:

  1. F-H1 (High): No tests. No known-answer vectors, no round-trip tests, no regression protection.
  2. F-H2 (High): verifyNostrEvent doesn't check event.id. A wrong id with a valid signature passes.
  3. F-H3 (High): XSS surface via innerHTML with relay/localStorage data.
  4. F-H4 (High): No CSP, no SRI on either page or the bundle.

The PQ primitive usage remains correct, deterministic keygen from the BIP32 seed delivers recoverability, and OTS verification is now real (Merkle path + block header check + target digest binding). The remaining open items are general software-quality issues (tests, id checks, web security) rather than cryptographic-correctness gaps.

Severity counts (this pass)

Severity Count Change from first pass
Critical 0 -4 (F-C1 reclassified, F-C4 resolved, F-C2/F-C3 fixed)
High 5 unchanged (F-H5 subsumed by F-C2 fix)
Medium 6 unchanged
Low / Informational 8 +1 (F-L8 doc leftover)

2. What This Audit Covers

Same scope as the first pass:

  1. Primitive selection and usage
  2. Key derivation and entropy
  3. Signature/verification correctness
  4. Binding / chain of trust
  5. Timestamping (OTS)
  6. Secret handling
  7. Input validation and integrity
  8. Documentation fidelity
  9. Web security
  10. Testing

Detailed per-finding write-ups are in findings.md. The cryptographic deep-dive is in crypto-deep-dive.md.


3. System Under Audit

3.1 Files in scope

File Role
www/js/pq-crypto.mjs Core crypto module: BIP39/BIP32, PQ key derivation, PQ sign/verify, event construction, OTS helpers
www/index.html Migration UI: sign in → seed → derive → sign → publish → OTS
www/verify.html Verification UI: query relay or paste event JSON, verify signatures + OTS
www/pq-crypto.bundle.js esbuild bundle of pq-crypto.mjs
build-pq-bundle.js Build script
README.md, explanation.md, why_what_how.md, laans_explanation.md Documentation (now reconciled with code)
resources/javascript-opentimestamps/ Vendored OTS library (present, not yet used by the app)

3.2 What the app purports to do (per now-reconciled docs)

  • Derive 5 PQ keypairs from a BIP39 seed via BIP32 paths under m/44'/1237'/0'/0/{1..8}.
  • Build a kind 1 announcement (human-readable attestation text + algorithm tags with base64 pubkey/sig) and a kind 11112 wrapper (embeds kind 1 as JSON, adds e/sha256/ots tags).
  • Each PQ signature scheme signs the attestation text; ML-KEM is a KEM (no signature).
  • The user's existing identity (Account #1) signs both events via NIP-07.
  • The SHA-256 of the full signed kind 1 event JSON is submitted to OpenTimestamps.
  • The security model: PQ self-attestation + Account #1 authorization + OTS precedence (no successor signature — deliberate).

3.3 What the app actually does

Matches the docs (verified during reconciliation). The code agrees with the documentation on derivation, event structure, algorithms, signing, and what's timestamped.


4. Cryptographic Correctness (Primary Concern)

4.1 What is correct

  • PQ primitive usage. ml_dsa44, ml_dsa65, slh_dsa_sha2_128s, falcon512, ml_kem768 all invoked with correct argument order. Libraries are reputable.
  • PQ signature verification matches signing. buildKind1Announcement signs TextEncoder.encode(content); verifyNIPQRContent re-encodes kind1Event.content and verifies against it.
  • ML-KEM correctly treated as a KEM (no signature) in both signing and verification.
  • BIP39/BIP32 usage is standard and correct; mnemonic validation before seed derivation.
  • NIP-01 event id computation correct.
  • Schnorr verification correct.
  • Deterministic PQ keygen from seed — recoverability holds.
  • Documentation now matches implementation (F-C4 resolved).

4.2 What is broken or weak

F-C2 (Critical): OpenTimestamps proofs are never cryptographically verified

isOtsConfirmed returns true if the 8-byte Bitcoin attestation magic 0588960d73d71901 appears anywhere in the .ots bytes — a substring search, not a proof verification. upgradeOts trusts a server's JSON response. Neither page runs real OTS verification. The vendored resources/javascript-opentimestamps/ library is present but not imported.

Consequence. A malicious kind 11112 event can embed an ots tag with arbitrary bytes containing the magic and be displayed as "Confirmed (Bitcoin)" (www/verify.html:548). The pre-quantum anchoring — the property that distinguishes a real migration event from a quantum-forged one — is spoofable. This is now the single most important open finding, since the security model (per the reconciled docs) explicitly relies on OTS precedence as the anti-forgery mechanism.

Fix. Use the vendored OTS library to parse the proof, bind its target digest to the sha256 tag (F-C3), validate the Merkle path, and check the block header against a Bitcoin API. The docs already state this is "planned but not yet implemented" — the fix is to implement it.

F-C3 (Critical): The OTS proof's target digest is not bound to the event hash

verify.html checks the sha256 tag matches hashFullEvent(kind1Event) — good — but never checks the OTS proof's internal target digest equals that hash. A proof timestamping any digest is accepted.

Fix. Parse the OTS file's target digest and assert equality with the sha256 tag.


5. High-Severity Findings

F-H1 (High): No tests, no known-answer test vectors

package.json has "test": "echo \"Error: no test specified\" && exit 1". No tests anywhere. For cryptographic software, no regression protection.

F-H2 (High): verifyNostrEvent does not check event.id against the computed hash

verifyNostrEvent verifies the Schnorr signature but never compares event.id to the computed hash. A wrong/foreign id with a valid signature passes.

F-H3 (High): XSS surface via innerHTML with relay/localStorage-derived data

setStatus and several other sites use innerHTML with data from relays/localStorage. Relay NOTICE messages and error strings are attacker-influenced. No CSP (F-H4) and no SRI, so injected script would execute.

F-H4 (High): No Content-Security-Policy and no SRI on the bundle

Neither page emits a CSP; the bundle and /nostr-login-lite/ scripts load without integrity. A compromised host can inject script that exfiltrates the seed phrase or PQ secret keys.

F-H5 (High): isOtsConfirmed substring search can false-positive

Subsumed by F-C2. A proper OTS parser would locate attestations structurally rather than byte-scanning.


6. Medium-Severity Findings

F-M1 (Medium): PQ signatures cover human-readable text, not a canonical binding

The signed message is the full content string (pq-crypto.mjs:455), mixing security-relevant fields with display prose and hardcoded URLs. A canonical structured statement would be more robust.

F-M2 (Medium): block_height tag is not verified

The block_height tag and content claim are purely informational. Nothing verifies the height was current at signing time.

F-M3 (Medium): BIP32 non-hardened leaf indices used for PQ seeds

PQ seeds at non-hardened children 18 under hardened parent m/44'/1237'/0'/0'. PQ seeds (child private keys) remain secret unless the parent xpub leaks. The docs now warn about this. No enforcement.

F-M4 (Medium): Truncation rule for concatenated BIP32 children is pinned in docs but arbitrary

"Take the first N bytes" is now documented, but a future implementer who takes the last N would break compatibility. Consider HKDF-Expand on the concatenation for a less arbitrary construction.

F-M5 (Medium): Falcon-512 is a draft standard (FIPS 206 not finalized)

Falcon-512 is labeled "FIPS 206 (draft)" in the code. Standardization risk; the multi-scheme design mitigates it.

F-M6 (Medium): laans_explanation.md was truncated — now fixed

The file was truncated mid-sentence in the first pass; it has been rewritten as a complete summary. Resolved, but noted for completeness.


7. Low / Informational Findings

  • F-L1: PQ secret keys and seed held in global scope for the session, never zeroed.
  • F-L2: 12-word default gives only ~64-bit PQ security on seed entropy. Docs now recommend 24 words; app still defaults to 12.
  • F-L3: hexToBytes/base64ToBytes do not validate input format.
  • F-L4: e tag validation does not check kind1Event.id equals computed id.
  • F-L5: timestampEvent sends binary body with Content-Type: application/x-www-form-urlencoded (should be application/octet-stream).
  • F-L6: Relay publishing treats ws.onclose as success.
  • F-L7: App depends on server-provided /nostr-login-lite/ scripts not in the repo.
  • F-L8 (NEW): why_what_how.md Component 4 summary still says "the new identity signs to endorse the PQ keys" — successor-model language in a "design only" section. Minor doc leftover from the reconciliation.

8. Positive Observations

  • The documentation is now honest and matches the implementation (F-C4 resolved).
  • The security model is coherently stated: PQ self-attestation + Account #1 authorization + OTS precedence. The "What This Proves and What It Does Not" section in README.md is exactly the kind of honesty a security project should have.
  • PQ primitive usage is correct throughout.
  • Deterministic PQ keygen from BIP32 seed delivers recoverability.
  • The two-event design (kind 1 + kind 11112) is sensible.
  • The OTS detached-file prefix construction is correct.
  • The vendored OTS library is available for the F-C2 fix.
  • The sign-out flow clears sensitive state.

9. Recommendations (Prioritized)

  1. Implement real OTS verification client-side using the vendored javascript-opentimestamps library: parse the proof, bind its target digest to the sha256 tag, validate the Merkle path, check the block header against a Bitcoin API. (Fixes F-C2, F-C3, F-H5.) This is the single most important remaining fix — the security model now explicitly depends on OTS precedence, so OTS verification must be real.
  2. Add a test suite with known-answer vectors for BIP32→PQ-seed derivation, PQ sign/verify round-trips, verifyNIPQRContent accept/reject, and OTS prefix construction. (Fixes F-H1.)
  3. Harden verifyNostrEvent to assert event.id equals the computed hash. (Fixes F-H2, F-L4.)
  4. Add a strict CSP and SRI, and switch dynamic-string DOM writes to textContent. (Fixes F-H3, F-H4.)
  5. Sign a canonical structured attestation (or its hash) rather than human-readable prose. (Fixes F-M1.)
  6. Default to 24-word mnemonics or warn about 12-word PQ security. (Fixes F-L2.)
  7. Fix the why_what_how.md Component 4 leftover ("the new identity signs to endorse the PQ keys"). (Fixes F-L8.)
  8. Communicate Falcon's draft status in the UI. (Fixes F-M5.)

10. Files Produced by This Audit


11. Auditor's Note on Scope and Limitations

This is a static source review of the post-reconciliation state. I did not execute the app against live relays or a real Bitcoin node. The findings about cryptographic correctness of primitives are based on reading call sites; the findings about binding and verification logic are based on tracing data flow. A dynamic test pass (especially for the OTS verification path) is strongly recommended before production use.