Files
nostr_quantum_preparation/audits/Fable5/remediation_progress.md
T

9.7 KiB

Fable5 Audit Remediation Progress

Date: 2026-07-25 Companion to: audit.md and mitigation_plan.md Test status: 137/137 passing (105 original + 32 new F-Dx tests)

Summary

All Fable5 audit findings have been remediated. The work proceeded in six phases (F-0 through F-6), matching the mitigation plan. No findings remain open.

Phase-by-phase status

Phase Findings Status Tests added
F-0 L-1, L-4, M-2 (doc), M-5 (doc) Done 0 (docs/comments only)
F-1 H-1 (canonical OTS digest) Done 11 (F-D1)
F-2 H-2 (anchor availability) Done 6 (F-D2)
F-3 M-1 (algorithm policy) Done 4 (F-D3) + 1 updated
F-4 M-3, M-4, L-2 Done 2 (F-D4/L-2)
F-5 L-3 (strict base64) Done 7 (F-D5)
F-6 conformance vectors Done 2 (F-D6)

What changed

Phase F-0 — Documentation and claim hygiene

  • README.md: fixed 12-word → 24-word drift in the migration flow mermaid + user-experience steps + implementation-status table.
  • why_what_how.md: fixed 12-word → 24-word.
  • www/js/pq-crypto.mjs: fixed the wrong pending-attestation-tag comment (83df830d1c9d4a5183dfe30d2ef90c8e); the code constant was already correct.
  • Renamed kind11112* internal parameter names to proofCarrier* in verifyNIPQRContent.
  • nip_proposal.md: added "Kind allocation is load-bearing" subsection (L-4).
  • nip_proposal.md: marked Falcon-512 as the most provisional link (M-2).
  • README.md: added reproducible-build verification roadmap note (M-5).

Phase F-1 — Canonical OTS digest (H-1, the spec-breaking fix)

  • Added canonicalEventDigest(): sha256(JSON.stringify([0, pubkey, created_at, kind, tags, content, id, sig])) — cross-implementation-reproducible, reuses the canonical NIP-01 array form.
  • Added CANONICAL_DIGEST_VERSION = 1 and a digest_version tag to proof carriers; verifiers fail closed on unknown/missing versions.
  • Legacy v0 events (no digest_version, key-order-dependent hashFullEvent) remain inspectable but are NOT validForMigration.
  • Wired buildProofCarrier, verifyNIPQRContent, selectCanonicalProofCarrier, and the app's timestamping path to use the canonical digest.
  • nip_proposal.md: added normative "Canonical digest" section with worked example + versioning language.
  • hashFullEvent() retained as a deprecated legacy alias.
  • 11 new tests: determinism, key-reorder invariance (the H-1 regression test), re-serialization, buildProofCarrier emits digest_version, v1 carrier valid, legacy v0 not valid for migration, unknown version fails closed, missing version → legacy, duplicate version fails, v1-with-wrong-hash fails, field validation.

Phase F-2 — Anchor availability and deletion resistance (H-2)

  • nip_proposal.md: added "Data availability (normative)" subsection — out-of-band proof acceptance, offline archival, NIP-09 deletion-resistance guidance for PQ-aware relays.
  • Added buildProofArchive() / parseProofArchive(): self-contained archive (kind 1 event + proof carrier + OTS proof + digest version) for offline verification.
  • www/index.html: added "Download Proof Archive" button; wired in index-app.mjs.
  • www/verify.html: added "Import archive" file input; wired in verify-app.mjs to parse + verify the extracted proof carrier end-to-end without any relay.
  • 6 new tests: archive round-trip, re-verification without relay, wrong type/version rejected, malformed input rejected, invalid inputs rejected.

Phase F-3 — Algorithm policy reconciliation (M-1)

  • Created www/js/nip-qr-policy.mjs: versioned POLICY_V1 with mandatory signature + KEM sets, helper functions.
  • Refactored verifyNIPQRContent to take a policy option (default POLICY_V1); unknown algorithms now report valid: null (ignored) instead of false, matching the NIP's "verify whichever subset they support" while keeping the mandatory set strict.
  • The NIP's revocation path now actually works under a future versioned policy (a custom POLICY_V2 dropping a broken scheme allows an event omitting it — tested).
  • nip_proposal.md: added "Algorithm policy (versioned)" subsection; reconciled the revocation section to reference it.
  • policyVersion added to the verification result object.
  • 4 new tests + 1 existing test updated (unknown algorithm: falsenull).

Phase F-4 — Trust-mode transparency and upgrade-helper hardening (M-3, M-4, L-2)

  • M-3 (upgrade-helper re-verification): already satisfied — the app runs verifyOtsProof on the upgraded proof before republishing; a failed verification does not republish.
  • M-4 (explorer trust labeling): already satisfied — the verify UI surfaces trustMode prominently with honest labels ("multi-explorer-checked (cross-verified)", "single-explorer-checked (trusted API)", "block header is trusted from explorer API(s), not independently verified against PoW").
  • L-2 (pending-only selection): selectCanonicalProofCarrier now returns canonicalTrustMode: 'pending-only' for pending-only selections and 'none' when no candidates; verify-app.mjs displays a prominent warning that a pending-only selection is NOT a trust decision.
  • 2 new tests: pending-only labeled, no-candidates labeled.

Phase F-5 — Input decoding strictness (L-3)

  • Replaced base64ToBytes() (lenient atob) with strict @scure/base base64.decode — rejects whitespace, missing padding, and non-base64 characters uniformly, removing the interop ambiguity.
  • 7 new tests: valid round-trip, empty throws, missing padding throws, whitespace throws, non-base64 chars throw, non-string throws, tampered ots tag rejected by base64ToBytes and verifyOtsProof.

Phase F-6 — Cross-implementation conformance vectors

  • Created test/vectors/ with a pinned, versioned seed-to-pubkeys.v1.json: fixed mnemonic → all five derived PQ pubkeys + secp256k1 pubkey + BIP39 seed, with normative notes on the truncation rule.
  • Created test/vectors/generate-vectors.mjs: a script that regenerates the vector file from the reference implementation (so a second implementation can be conformance-tested without reading JS source).
  • 2 new tests: vector file is loadable and well-formed; implementation reproduces the pinned pubkeys exactly.

Files changed

File Change
README.md 12→24 word fixes, reproducible-build note
why_what_how.md 12→24 word fix
nip_proposal.md Canonical digest (normative), digest_version tag, data availability, kind allocation, algorithm policy (versioned), Falcon provisional, revocation reconciliation, example proof carrier updated
www/js/pq-crypto.mjs canonicalEventDigest(), CANONICAL_DIGEST_VERSION, digest_version tag in buildProofCarrier, verifier enforcement, selectCanonicalProofCarrier v1/legacy split + canonicalTrustMode, buildProofArchive()/parseProofArchive(), strict base64ToBytes, policy import, attestation-tag comment fix, param renames
www/js/nip-qr-policy.mjs New — versioned POLICY_V1 + helpers
www/js/index-app.mjs canonicalEventDigest import + timestamp path, buildProofArchive import + download button
www/js/verify-app.mjs parseProofArchive import + archive import handler, pending-only canonicalTrustMode warning
www/index.html "Download Proof Archive" button
www/verify.html "Import archive" file input
www/pq-crypto.bundle.js Rebuilt (includes all new exports)
test/pq-crypto.test.mjs 32 new tests (F-D1…F-D6) + 1 updated test
test/vectors/seed-to-pubkeys.v1.json New — pinned conformance vector
test/vectors/generate-vectors.mjs New — vector generator script

Findings status

ID Severity Title Status
H-1 High Non-canonical JSON hashing makes the OTS digest fragile Fixed (F-D1)
H-2 High NIP-09 deletion + relay churn attack on the genuine anchor Fixed (F-D2)
M-1 Medium NIP vs implementation contradiction on unknown/missing algorithms Fixed (F-D3)
M-2 Medium Falcon-512 not covered by @noble/post-quantum audit Documented (F-0) + conformance vector (F-D6)
M-3 Medium Server-assisted OTS upgrade is a soft centralization point Verified already-safe (F-4)
M-4 Medium Explorer-API trust for block headers (acknowledged) Verified already-labeled (F-4)
M-5 Medium Browser as the trust boundary (acknowledged, unavoidable for now) Documented (F-0)
L-1 Low Documentation drift Fixed (F-0)
L-2 Low Pending-candidate fallback ranks by forgeable created_at Fixed (F-D4)
L-3 Low base64ToBytes uses atob (lenient) Fixed (F-D5)
L-4 Low Kind 9999 allocation is provisional Documented (F-0)