Skip to content

Signed, hash-chained audit log for Warden decisions (v1.3.0)#25

Open
Ar9av wants to merge 1 commit into
mainfrom
feat/audit-log-signing
Open

Signed, hash-chained audit log for Warden decisions (v1.3.0)#25
Ar9av wants to merge 1 commit into
mainfrom
feat/audit-log-signing

Conversation

@Ar9av
Copy link
Copy Markdown
Contributor

@Ar9av Ar9av commented May 2, 2026

Summary

Adds a tamper-evident, optionally Ed25519-signed audit log for every decision Warden makes (allow / observe / block). This is the answer to the governance question raised externally — "are you generating a signed, replayable log of what gets blocked and why?" — with verification that doesn't require trusting the host.

  • warden/audit_log.py — append-only NDJSON log at .prismor-warden/audit/<session>.ndjson, hash-chained via SHA-256 of canonical bytes, with pinned policy_hash + feed_hash snapshots for replay.
  • warden/signing.py — Ed25519 keygen / sign / verify; uses cryptography if installed, falls back to openssl pkeyutl (same approach as pipeline/sign_feed.sh).
  • warden audit-log CLIkeygen, pubkey, list, show, verify, seal, register-pubkey, replay. verify exits non-zero on tampering.
  • Privacy by default — sensitive event fields (command, path, url, prompt, content, response) and finding evidence are stored as SHA-256 + length, not plaintext. audit.include_raw: true in policy.yaml opts in to plaintext retention.
  • Zero-setup chain, opt-in signatures — hash chain is always on; signing kicks in automatically after warden audit-log keygen (or via WARDEN_AUDIT_SIGNING_KEY env).

What an audit record looks like

```json
{
"v": 1, "alg": "sha256", "seq": 0,
"ts": "2026-05-03T19:03:24.139Z",
"session_id": "...", "agent": "claude", "mode": "enforce",
"event": { "type": "shell", "command_hash": "1de700c2...", "command_len": 6 },
"decision": "allow",
"findings": [],
"policy_hash": "a0066d3f...",
"feed_hash": "fb010dd8...",
"agent_version": "warden 1.3.0",
"prev_hash": "GENESIS",
"record_hash": "9286523ecb239dc5...",
"sig": { "alg": "ed25519", "key_id": "0d835451d9e089ff", "value": "" }
}
```

Threat model coverage

Tamper attempt Detection
Modify a field, leave record_hash Recomputed hash mismatches written hash
Modify field + recompute hash Signature over canonical bytes invalid (without private key)
Modify field + recompute hash + cascade through chain Signatures invalid for every touched record
Drop a record from the middle prev_hash mismatch and seq jump on the next record
Forge an entirely new record Signature invalid (no private key)

Tested explicitly in tests/test_audit_log.py::TestVerify::test_verify_detects_full_recompute_attempt_without_key.

Test plan

  • python3 -m unittest discover tests/ — 258 tests pass (31 new)
  • Local end-to-end: keygen → 4 hook-dispatch payloads (allow/block/block/observe) → list/show/verify clean → seal → flip a decision → verify FAILS with both chain break and bad signature, exit 2
  • Same e2e suite run on the AWS Lightsail test box (st3ve, Ubuntu 24.04, Python 3.12, cryptography 41.0.7) — all 14 checks pass
  • File modes verified: audit-signer.key is 0600, ~/.prismor/keys/ is 0700
  • Privacy: raw command is not present in records by default; only command_hash + command_len

Out of scope (follow-up)

  • Sink integration: forwarding record_hash + signature + key_id over the existing webhook/syslog/file sinks, so a SIEM can verify provenance independently. Tracked in a separate issue.
  • Export formats (in-toto attestation / CMS) — deferred until a customer asks for a specific schema.

Docs

  • New: docs/audit-log.md — record schema, privacy controls, CLI reference, off-host verification recipe.
  • README capabilities list updated.
  • CHANGELOG entry under 1.3.0.

Every decision (allow / observe / block) now lands in an append-only NDJSON
log at .prismor-warden/audit/<session_id>.ndjson. Records are linked by
SHA-256 hash chain and optionally Ed25519-signed, so a third party with the
public key can verify what the agent attempted, what was blocked, and why —
and prove the log has not been tampered with.

New modules:
  - warden/signing.py — Ed25519 keygen / sign / verify, with cryptography
    backend and openssl fallback.
  - warden/audit_log.py — canonical encoder, hash chain writer, signing,
    pinned policy/feed snapshots, chain verification, session sealing.

CLI: warden audit-log {keygen, pubkey, list, show, verify, seal,
register-pubkey, replay}. `verify` walks the chain, recomputes hashes, and
validates signatures, exiting non-zero on tampering.

Privacy: sensitive event fields (command, path, url, prompt, content,
response) and finding evidence are stored as SHA-256 digests + length, not
plaintext. Set `audit.include_raw: true` in policy.yaml to retain raw text.

Hash chaining is on by default (zero setup). Signing turns on automatically
once `warden audit-log keygen` has run, or when WARDEN_AUDIT_SIGNING_KEY is
set.

Tests cover: chain linkage, GENESIS handling, tamper detection via
record_hash and via dropped records, signature roundtrip, signature failure
on tampered records (including the "attacker recomputes hashes but lacks
the private key" case), policy/feed pinning to disk, redaction defaults,
and seal manifest creation. 31 new tests, full suite 258 pass.
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