Files
Paul HittandClaude Opus 5 4e18eae131
ci / secret-sweep (pull_request) Successful in 5s
ci / ratchet (pull_request) Successful in 5s
ci / vault (pull_request) Successful in 6s
ci / conformance (pull_request) Successful in 9s
ci / doc-references (pull_request) Successful in 3s
ci / complete (pull_request) Successful in 2s
ci / spec-counts (pull_request) Successful in 26s
ci / client (pull_request) Successful in 33s
ci / analysis (pull_request) Successful in 1m30s
Static analysis and the fuzzer in CI, and what the rules caught
docs/BEST-PRACTICES.md declared eight criteria unmet for want of static
and dynamic analysis, and the letter to an auditor points at that
document. This closes them.

scripts/check-static.py is six rules aimed at what this engine can
actually have. The one worth having alone is a WebCrypto Promise used as
a value: `if (SUBTLE.verify(...))` without `await` is truthy for a forged
signature as readily as for a good one. The others are Math.random
anywhere in src/, eval or new Function, loose equality on a
secret-shaped name, an empty catch outside browser storage, and
JSON.parse returned with no shape check.

🔴 The first version flagged nine things and seven were its own fault,
which is how a rule teaches people to ignore it. Six were
`return SUBTLE.importKey(...)` from an async function, which is correct
and idiomatic. Two were empty catches around `localStorage.setItem`,
which throws on a quota or in a private window and should be swallowed.
Both rules are narrowed and both narrowings are explained where someone
will read them.

The ninth was real. `openBackup` returned `JSON.parse` of the decrypted
plaintext with no shape check, so a backup whose payload was `null` or a
bare string came back to a caller that reads fields off it. AES-GCM has
authenticated those bytes under a passphrase-derived key, so it is a
smaller hazard than the payload parser that test/fuzz.mjs found, and it
is the same bug. It now throws `backupPayloadNotAnObject`.

Every rule was checked by planting the bug it looks for and confirming
it fires, then removing it and confirming the tree is clean.

test/fuzz.mjs joins the same `analysis` job. A check nobody runs stops
finding things, and this one found a real defect on the day it was
written. The CI comment states the measured runtime, 45 seconds, rather
than the 11 I guessed first.

Gate: static analysis clean across 10 files, 8,001 fuzz cases with 0
problems, conformance 304 passed with 2 skipped, client 289, ratchet 35,
vault 26, all 0 failures. Secret sweep clean across 91 files and
doc-references resolves all 153 sections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 06:59:25 -04:00
..