106 lines
9.7 KiB
Markdown
106 lines
9.7 KiB
Markdown
# Fable5 Audit Remediation Progress
|
|
|
|
**Date:** 2026-07-25
|
|
**Companion to:** [`audit.md`](audit.md:1) and [`mitigation_plan.md`](mitigation_plan.md:1)
|
|
**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`](../../README.md:706): fixed 12-word → 24-word drift in the migration flow mermaid + user-experience steps + implementation-status table.
|
|
- [`why_what_how.md`](../../why_what_how.md:180): fixed 12-word → 24-word.
|
|
- [`www/js/pq-crypto.mjs`](../../www/js/pq-crypto.mjs:1618): fixed the wrong pending-attestation-tag comment (`83df830d1c9d4a51` → `83dfe30d2ef90c8e`); the code constant was already correct.
|
|
- Renamed `kind11112*` internal parameter names to `proofCarrier*` in `verifyNIPQRContent`.
|
|
- [`nip_proposal.md`](../../nip_proposal.md:177): added "Kind allocation is load-bearing" subsection (L-4).
|
|
- [`nip_proposal.md`](../../nip_proposal.md:284): marked Falcon-512 as the most provisional link (M-2).
|
|
- [`README.md`](../../README.md:833): added reproducible-build verification roadmap note (M-5).
|
|
|
|
### Phase F-1 — Canonical OTS digest (H-1, the spec-breaking fix)
|
|
- Added [`canonicalEventDigest()`](../../www/js/pq-crypto.mjs:691): `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`](../../nip_proposal.md:209): 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`](../../nip_proposal.md:269): added "Data availability (normative)" subsection — out-of-band proof acceptance, offline archival, NIP-09 deletion-resistance guidance for PQ-aware relays.
|
|
- Added [`buildProofArchive()`](../../www/js/pq-crypto.mjs:2334) / [`parseProofArchive()`](../../www/js/pq-crypto.mjs:2373): self-contained archive (kind 1 event + proof carrier + OTS proof + digest version) for offline verification.
|
|
- [`www/index.html`](../../www/index.html:535): added "Download Proof Archive" button; wired in [`index-app.mjs`](../../www/js/index-app.mjs:120).
|
|
- [`www/verify.html`](../../www/verify.html:315): added "Import archive" file input; wired in [`verify-app.mjs`](../../www/js/verify-app.mjs:455) 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`](../../www/js/nip-qr-policy.mjs:1): 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`](../../nip_proposal.md:150): 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: `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 `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`](../../www/js/verify-app.mjs:403) 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()`](../../www/js/pq-crypto.mjs:361) (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/`](../../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`](../../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`](../../README.md:1) | 12→24 word fixes, reproducible-build note |
|
|
| [`why_what_how.md`](../../why_what_how.md:1) | 12→24 word fix |
|
|
| [`nip_proposal.md`](../../nip_proposal.md:1) | 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`](../../www/js/pq-crypto.mjs:1) | `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`](../../www/js/nip-qr-policy.mjs:1) | New — versioned `POLICY_V1` + helpers |
|
|
| [`www/js/index-app.mjs`](../../www/js/index-app.mjs:1) | `canonicalEventDigest` import + timestamp path, `buildProofArchive` import + download button |
|
|
| [`www/js/verify-app.mjs`](../../www/js/verify-app.mjs:1) | `parseProofArchive` import + archive import handler, pending-only `canonicalTrustMode` warning |
|
|
| [`www/index.html`](../../www/index.html:1) | "Download Proof Archive" button |
|
|
| [`www/verify.html`](../../www/verify.html:1) | "Import archive" file input |
|
|
| [`www/pq-crypto.bundle.js`](../../www/pq-crypto.bundle.js:1) | Rebuilt (includes all new exports) |
|
|
| [`test/pq-crypto.test.mjs`](../../test/pq-crypto.test.mjs:1) | 32 new tests (F-D1…F-D6) + 1 updated test |
|
|
| [`test/vectors/seed-to-pubkeys.v1.json`](../../test/vectors/seed-to-pubkeys.v1.json:1) | New — pinned conformance vector |
|
|
| [`test/vectors/generate-vectors.mjs`](../../test/vectors/generate-vectors.mjs:1) | 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) |
|