9.7 KiB
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 (83df830d1c9d4a51→83dfe30d2ef90c8e); the code constant was already correct.- Renamed
kind11112*internal parameter names toproofCarrier*inverifyNIPQRContent. 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 = 1and adigest_versiontag to proof carriers; verifiers fail closed on unknown/missing versions. - Legacy v0 events (no
digest_version, key-order-dependenthashFullEvent) remain inspectable but are NOTvalidForMigration. - 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,
buildProofCarrieremitsdigest_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 inindex-app.mjs.www/verify.html: added "Import archive" file input; wired inverify-app.mjsto 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: versionedPOLICY_V1with mandatory signature + KEM sets, helper functions. - Refactored
verifyNIPQRContentto take apolicyoption (defaultPOLICY_V1); unknown algorithms now reportvalid: null(ignored) instead offalse, 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_V2dropping a broken scheme allows an event omitting it — tested). nip_proposal.md: added "Algorithm policy (versioned)" subsection; reconciled the revocation section to reference it.policyVersionadded to the verification result object.- 4 new tests + 1 existing test updated (unknown algorithm:
false→null).
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
verifyOtsProofon the upgraded proof before republishing; a failed verification does not republish. - M-4 (explorer trust labeling): already satisfied — the verify UI surfaces
trustModeprominently 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):
selectCanonicalProofCarriernow returnscanonicalTrustMode: 'pending-only'for pending-only selections and'none'when no candidates;verify-app.mjsdisplays 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()(lenientatob) with strict@scure/basebase64.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
base64ToBytesandverifyOtsProof.
Phase F-6 — Cross-implementation conformance vectors
- Created
test/vectors/with a pinned, versionedseed-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) |