v2.22.1 (Audit chain: a false tamper verdict, an unrotated file, a verifier that read everything)
v2.22.1 (Audit chain: a false tamper verdict, an unrotated file, a verifier that read everything)
Three defects in the audit trail, plus a startup hardening that came with one of them. All of it was found by taking #437 — "rotating the tamper-evident chain needs its own design" — seriously enough to read the code before writing any. The issue turned out to name the wrong file, and the design question turned out to rest on a primitive that was missing and already broken in production. Full analysis on #437; the remaining half, retention, is #445.
Nothing here is breaking. Existing chains verify unchanged, and a full export bundle is byte-identical to what previous versions produced.
Audit
- An incremental export slice verifies instead of accusing you (#441).
GET /api/audit/export?from_seq=Nwas documented as producing a "self-verifying" slice and produced a bundle thatpython -m modules.core.audit_verify— the verifier CertMate hands to auditors — rejected withbroken link at seq N ... a deletion or reorder. Every documented incremental-export workflow was broken, and it failed in the worst available wording: a tamper verdict on a perfectly intact record. The cause was that verification always started from the genesis, which is load-bearing (it is why removing entries from the head of a chain is detected) and is kept. Instead a fragment now declares, inside the signed manifest, the predecessor hash it continues from: an anchored slice isformat_version: 2carryinganchor_prev_hash/anchor_seq, checked against the entries rather than merely recorded. Neither the API result nor the CLI will call such a slice "intact" — both report a partial slice and name the anchor, because it proves the entries from the anchor forward and nothing at all about the prefix. Full exports remainformat_version: 1and byte-compatible; a verifier that predates this release meeting an anchored slice says "unsupported format version" rather than claiming the chain is broken. - The human-readable audit log is rotated (#443).
logs/audit/certificate_audit.logwas written through a plainFileHandlerand grew without bound. The v2.22.0 rotation fix bounded the application log; this handler is built independently and was never reached by it, so the file was still unbounded after that release shipped. It is now a rotating handler (CERTMATE_AUDIT_LOG_MAX_BYTES/CERTMATE_AUDIT_LOG_BACKUP_COUNT, 10 MB × 5 by default,0to disable). Safe to rotate precisely because it carries no integrity property: it is text, not hashed, not chained, and excluded from backups. The tamper-evident chain underdata/audit/is emphatically not rotated, and a test pins that rolling the log 200 times leaves it a single intact file. The Activity page tails only the active file, so it shows fewer entries immediately after a roll. - An unwritable logs directory no longer stops the application (#443). Building the audit handler raised out of a constructor the app factory calls unguarded, so a certificate manager refused to start over a log file. It now logs the failure, drops the handler, and keeps writing the hash chain — the record that actually matters — which continues to verify with no log file at all.
- Verifying the chain no longer costs as much memory as the chain is big (#444).
verify_chain()read the whole file,load_records()read it again, and the checkpoint cross-check read it a third time. The chain is append-only and never truncated, so this put a ceiling on how much history an instance could keep — and made rotation look like the answer to a problem that was really about how the file is read. Verification now streams one record at a time: on a 5 MB chain, peak allocation went from 8.4 MB to 25 KB. The file verifier and the bundle verifier run on the same incremental engine, so the two cannot drift apart. One deliberate behaviour change: when a file contains more than one fault, the reported one is now the earliest in the file rather than whichever the parse pass met first.
Notes for operators
GET /api/audit/verifyand the standalone verifier are unchanged in output for an intact chain. If you script against them, the new fields areanchored/anchor_seqon bundle verification.- The two new environment variables are documented in the README table. Neither needs setting; the defaults bound the file.