Skip to content

feat(audit): Phase 3 — Ed25519-signed, third-party-verifiable audit trail (v2.17.0)#320

Merged
fabriziosalmi merged 3 commits into
mainfrom
feat/audit-trail-phase3
Jun 15, 2026
Merged

feat(audit): Phase 3 — Ed25519-signed, third-party-verifiable audit trail (v2.17.0)#320
fabriziosalmi merged 3 commits into
mainfrom
feat/audit-trail-phase3

Conversation

@fabriziosalmi

Copy link
Copy Markdown
Owner

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

  • Ed25519 signing key (modules/core/audit_signing.py): persisted at data/.audit_signing_key like the Flask secret key — generated 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 = PEM key + a base64(sha256(raw pubkey))[:16] fingerprint.
  • Signed checkpoints: every N entries (default 100) and on demand, the chain head is signed into 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 via head_hash (no per-entry signatures).
  • Verifier upgradepython -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_chain was refactored to share a verify_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.md state this precisely.

Tests / safety

  • No new dependencies (cryptography already required).
  • 18 new tests (key lifecycle / persistence / corrupt-disable / env-override, signed checkpoints, export-bundle shape, verify intact / tamper / manifest-forgery / wrong-and-right pinning, unsigned fallback, the CLI, both endpoints). Phase 1/2 regression green. Full suite 1547 passed.
  • All audit emission stays best-effort/isolated; signing is best-effort (the unsigned chain still works if a key can't be set up).

Before merge: this touches factory.py (signer wiring) and the audit path additively — a real-cert smoke is the prudent final check, as for prior audit releases.

🤖 Generated with Claude Code

fabriziosalmi and others added 2 commits June 15, 2026 19:43
… 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

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.46965% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.67%. Comparing base (96cdfe4) to head (ed0d541).

Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
unittests 70.67% <81.46%> (+0.28%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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>
@fabriziosalmi fabriziosalmi merged commit d6f48af into main Jun 15, 2026
9 checks passed
@fabriziosalmi fabriziosalmi deleted the feat/audit-trail-phase3 branch June 15, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant