feat(audit): Phase 3 — Ed25519-signed, third-party-verifiable audit trail (v2.17.0)#320
Merged
Conversation
… bundle
Make the audit trail verifiable by a third party off the box. The tamper-evident
hash chain (Phase 2) proved authenticity and ordering; this signs it and lets an
auditor verify which instance produced the record without running or trusting
CertMate.
- Ed25519 signing key (modules/core/audit_signing.py), persisted at
data/.audit_signing_key like the Flask secret key (generate-on-first-run,
0600, off-box via AUDIT_SIGNING_KEY_FILE). A corrupt key disables signing
rather than regenerating, to avoid forking the instance identity. Public
identity = the PEM key + a base64(sha256(raw pubkey))[:16] fingerprint.
- Signed checkpoints: every N entries (default 100) and on demand, the chain
head is signed and appended to certificate_audit.checkpoints.jsonl.
- GET /api/audit/public-key (admin) exposes the signing identity; GET
/api/audit/export (admin, ?from_seq/?to_seq) returns a signed, self-verifying
bundle {manifest, entries, bundle_signature}. The manifest pins the
fingerprint, public key, seq range and head_hash; the signature over the
canonical manifest transitively commits to every entry via head_hash.
- The standalone verifier gains --bundle and --pubkey: it checks the chain
structure, manifest consistency, the Ed25519 signature, the fingerprint, and
optional out-of-band key pinning. audit_chain.verify_chain was refactored to
share a verify_records() core with the bundle verifier (no behaviour change).
- docs/api.md + docs/compliance.md updated: the signed export exists now; the
remaining operator-binding gap (external anchoring of checkpoints) is the only
deferred, opt-in follow-up.
No new dependencies (cryptography is already pinned). 18 new tests: key
lifecycle/persistence/corrupt-disable/env-override, signed checkpoints,
export-bundle shape, verify intact/tamper/manifest-forgery/pinning, unsigned
fallback, the CLI, and both endpoints. Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor: completes the agentic audit trail (Phase 3 — Ed25519-signed export bundle + verifier). Bumps version and adds the RELEASE_NOTES entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #320 +/- ##
==========================================
+ Coverage 70.39% 70.67% +0.28%
==========================================
Files 49 50 +1
Lines 11244 11548 +304
==========================================
+ Hits 7915 8162 +247
- Misses 3329 3386 +57
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
An adversarial review of the signed-export feature found two should-fix issues
(no exploitable forgery under the trust model):
- Empty signed bundle failed its own verification: build_manifest used "" as the
head_hash for an empty slice but verify_records([]) returned None, so an export
with no entries (e.g. ?from_seq past the end) verified as broken. verify_records
now reports the genesis prev_hash ("") for an empty chain, matching the manifest.
- Half-signed downgrade: a bundle carrying a signature but no public key (or vice
versa) skipped signature verification and passed as a clean "unsigned" bundle.
verify_bundle now rejects a half-present signature/key as inconsistent.
Plus two nits: verify_bundle rejects an unknown format_version/algorithm instead
of mis-handling it, and the CLI prints a "public key NOT pinned" caveat on an
unpinned (TOFU) verify so a self-asserted fingerprint isn't read as a guarantee.
3 new tests (empty signed bundle, half-signed rejection, unsupported format).
Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the agentic audit trail. v2.16.0 added attribution + the tamper-evident hash chain (Phases 1+2); this is Phase 3 — third-party verifiability: the record can now be verified by an auditor off the box, without running or trusting CertMate, and tied to the instance that produced it.
What's new
modules/core/audit_signing.py): persisted atdata/.audit_signing_keylike the Flask secret key — generated on first run,0600, off-box viaAUDIT_SIGNING_KEY_FILE. A corrupt key disables signing rather than regenerating, to avoid forking the instance identity. Public identity = PEM key + abase64(sha256(raw pubkey))[:16]fingerprint.certificate_audit.checkpoints.jsonl.GET /api/audit/public-key(admin) — the signing identity to pin out of band.GET /api/audit/export(admin,?from_seq/?to_seq) — a signed, self-verifying bundle{manifest, entries, bundle_signature}. The manifest pins fingerprint / public key / seq range /head_hash; the signature over the canonical manifest transitively commits to every entry viahead_hash(no per-entry signatures).python -m modules.core.audit_verify --bundle bundle.json [--pubkey instance.pem]checks chain structure, manifest consistency, the Ed25519 signature, the fingerprint, and optional out-of-band pinning.verify_chainwas refactored to share averify_records()core (no behaviour change to Phase 2).Honesty / scope
A local signing key detects tampering by anyone who doesn't hold it and attributes the export to an instance, but does not bind the operator (who holds the key). Fully constraining the operator needs opt-in external anchoring of the signed checkpoints to an append-only off-box sink — a deliberate, deferred follow-up (it touches SMTP/S3).
docs/api.md+docs/compliance.mdstate this precisely.Tests / safety
cryptographyalready required).🤖 Generated with Claude Code