Immutable filesystem-level audit log for AI inference events. SHA3-256 Merkle segments, ML-DSA signed headers, cross-segment hash chain, O(log n) inclusion proofs. Built for EU AI Act / legal discovery.
Project description
PQC Immutable AI Audit Log (Filesystem)
Tamper-evident audit log for AI inference events, designed for legal discovery. When an AI system denies a loan, flags a medical claim, moderates content, or calls a tool, that decision needs to survive 15+ years of potential litigation. This library writes each inference to an append-only segmented file, builds an SHA3-256 Merkle tree per segment, signs the segment header with ML-DSA (FIPS 204), and chains every segment to the previous one by root hash. Any single bit flipped anywhere in the log is detected on verification.
The Problem
Regulators and plaintiff's lawyers are converging on the same demand: show us the inference log. The EU AI Act (Article 12) requires high-risk AI systems to keep automatically-generated logs for the lifetime of the system. US class-action litigation against AI lenders, insurers, and content platforms routinely subpoenas inference histories. Existing solutions fall short:
- Application-DB logs are mutable — a DBA or a compromised service can edit them without trace.
- Cloud log services are opaque to the model operator; if the provider loses them you have no recourse.
- RSA/ECDSA-signed archives decay the moment a cryptographically relevant quantum computer exists — signatures made today must still verify in 2040.
The Solution
A pure-Python library with an append-only on-disk layout:
- Each
InferenceEventstores SHA3-256 hashes of the input/output (not the raw content — privacy-preserving) plus model DID, actor DID, decision label, timestamp. LogAppenderwrites events as JSON-Lines intosegment-NNNNN.log. When the rotation policy fires (events, bytes, or age), aSegmentHeaderis built: Merkle root over every leaf hash, plusprevious_segment_rootchaining to the prior segment. The header is signed with ML-DSA and written tosegment-NNNNN.sig.json.LogReader.verify_chain()walks every segment, recomputes each Merkle root, verifies each ML-DSA signature, and confirms every chain link. One mutation anywhere fails verification.InclusionProverproducesO(log n)proofs that a specific event was in a specific segment — useful when you must surrender a single decision to a court without leaking the surrounding log.
Installation
pip install pqc-audit-log-fs
Development:
pip install -e ".[dev]"
Quick Start
from quantumshield.identity.agent import AgentIdentity
from pqc_audit_log_fs import (
InferenceEvent, LogAppender, LogReader, RotationPolicy,
)
signer = AgentIdentity.create(name="audit-signer")
with LogAppender(
"./audit-log",
signer,
rotation=RotationPolicy(max_events_per_segment=10_000),
) as appender:
for decision in your_decisions:
appender.append(InferenceEvent.create(
model_did="did:pqaid:credit-model-v3",
model_version="3.2.1",
input_bytes=decision.input_blob,
output_bytes=decision.output_blob,
decision_type="classification",
decision_label=decision.label, # 'approve' | 'deny'
actor_did=decision.user_did,
session_id=decision.session_id,
))
reader = LogReader("./audit-log")
ok, errors = reader.verify_chain()
assert ok, errors
Architecture
Inference Service Verifier / Court / Auditor
---------------- ---------------------------
event -> LogAppender.append()
|
| jsonl line
v
segment-NNNNN.log (append-only)
|
| rotation trigger
v
[merkle root over leaf hashes]
|
| ML-DSA sign
v
segment-NNNNN.sig.json <--------------> LogReader.verify_chain()
| |
| previous_segment_root | recompute roots
v | verify ML-DSA sigs
segment-(N+1)*****.log -> .sig.json | check chain links
| v
| ok / errors
v
(optional) MerkleAnchor
-> external transparency log
Cryptography
| Primitive | Algorithm | Purpose |
|---|---|---|
| Leaf hash | SHA3-256 | SHA3-256(0x00 ‖ canonical(event)) |
| Internal Merkle node | SHA3-256 | SHA3-256(0x01 ‖ left ‖ right) |
| Segment signature | ML-DSA-65 | over SHA3-256(canonical(header)) |
| Cross-segment chaining | SHA3-256 | header.previous_segment_root |
Leaves and internal nodes use domain-separation prefixes to prevent second-preimage attacks. Segments chain like a blockchain: rewriting segment k forces every subsequent previous_segment_root to also be rewritten, and every subsequent ML-DSA signature to be forged.
Segment File Layout
audit-log/
segment-00001.log JSON-Lines, one InferenceEvent per line
segment-00001.sig.json signed SegmentHeader
segment-00002.log
segment-00002.sig.json
...
A segment-NNNNN.sig.json looks like:
{
"segment_id": "segment-00001",
"segment_number": 1,
"created_at": "2026-04-20T12:00:00+00:00",
"sealed_at": "2026-04-20T13:00:00+00:00",
"event_count": 10000,
"merkle_root": "a1b2c3...",
"previous_segment_root": "",
"log_id": "urn:pqc-audit-log:...",
"signer_did": "did:pqaid:...",
"algorithm": "ML-DSA-65",
"signature": "ff...",
"public_key": "aa..."
}
Rotation Policy
RotationPolicy triggers a seal when any threshold is crossed:
| Field | Default | Meaning |
|---|---|---|
max_events_per_segment |
10,000 | Seal after N events |
max_bytes_per_segment |
10 MB | Seal after N bytes of JSONL |
max_segment_age_seconds |
3600 (1h) | Seal after time elapsed |
Threat Model
| Attack | How we detect it |
|---|---|
Flip a byte in a sealed .log file |
Merkle root mismatch in verify_segment |
| Delete an event line from a sealed segment | Merkle root mismatch |
| Swap a whole segment for a forged one | Chain break: previous_segment_root of next segment mismatches |
| Forge a signature today | ML-DSA-65 — no known classical or quantum break |
| Re-sign after tamper using the signer's key | Requires private key exfiltration; out of scope |
| Delete trailing segments | Detectable if segment roots are anchored externally (MerkleAnchor) |
The log is designed to be post-quantum hard: ML-DSA-65 targets NIST security category 3, equivalent to AES-192 classical / post-quantum. Signatures made today remain verifiable after cryptographically relevant quantum computers arrive.
EU AI Act Mapping
| Requirement (Article 12, "Record-keeping") | This library |
|---|---|
| Automatic generation of logs | LogAppender.append() |
| Logs appropriate to intended purpose | InferenceEvent.metadata is free-form |
| Logs kept for the lifetime of the system | Append-only segments; no size cap |
| Logs traceable to a specific system version | model_did + model_version per event |
| Logs enabling post-market monitoring | LogReader.verify_chain() for spot audits |
| Integrity protection | SHA3-256 + ML-DSA-65 + cross-segment chain |
Combine with MerkleAnchor to publish segment roots to a public transparency log (blockchain, Rekor, etc.) for externally-anchored non-repudiation.
CLI Reference
pqc-audit verify <log_dir>
pqc-audit prove <log_dir> <segment_number> <event_id>
pqc-audit info <log_dir>
Example:
$ pqc-audit info ./audit-log
log_dir: ./audit-log
segments: 3
segment 00001 events=10000 root=a1b2c3d4e5f6a7b8... prev=<genesis> sealed_at=2026-04-20T13:00:00+00:00
segment 00002 events=10000 root=b2c3d4e5f6a7b8c9... prev=a1b2c3d4e5f6... sealed_at=2026-04-20T14:00:00+00:00
segment 00003 events= 4231 root=c3d4e5f6a7b8c9d0... prev=b2c3d4e5f6a7... sealed_at=2026-04-20T15:00:00+00:00
$ pqc-audit verify ./audit-log
[OK] all 3 segments verify
API Reference
InferenceEvent
| Field | Type | Description |
|---|---|---|
event_id |
str | urn:pqc-audit-evt:<hex> |
timestamp |
str (ISO) | UTC wall-clock |
model_did |
str | did:pqaid:... identifying the model |
model_version |
str | Semver or hash of model binary |
input_hash |
str | SHA3-256 hex of canonical input |
output_hash |
str | SHA3-256 hex of canonical output |
reasoning_chain_hash |
str | SHA3-256 hex over chain-of-thought |
decision_type |
str | e.g. classification, generation |
decision_label |
str | e.g. approve, deny |
actor_did |
str | DID of the user/agent that invoked the model |
session_id |
str | Free-form session identifier |
metadata |
dict | Free-form metadata |
LogAppender
append(event)— append one event; may trigger a seal.seal_current_segment()— force-seal now.close()— seal and flush; also invoked by__exit__.
LogReader
list_segments() -> list[int]read_header(n) -> SegmentHeaderread_segment(n) -> AuditSegmentverify_segment(n) -> boolverify_chain() -> (ok, errors)
InclusionProver
prove_event(segment_number, event_id) -> InclusionProofverify_proof(event, proof) -> bool
MerkleAnchor / AnchorSink
- Pluggable sink interface for publishing segment roots to an external transparency log (blockchain, Rekor-style log, internal KMS, etc.).
FilesystemGuard
- Best-effort OS-level enforcement:
chmodon all platforms;chattr +a/+ion Linux;chflags uchgon macOS.
Why PQC for Audit Logs
AI liability litigation runs on timescales of a decade or more:
- A 2026 loan denial may surface in a 2035 class-action settlement.
- A 2027 medical model may face a 2040 product-liability suit.
- EU AI Act retention is tied to the lifetime of the system — potentially 20+ years.
Classical signatures made in 2026 will not survive a cryptographically relevant quantum computer ("Q-day") if one arrives mid-retention-window. ML-DSA-65 is the right default: NIST-standardized, FIPS 204, security category 3.
Examples
examples/basic_log.py— Write 30 events with rotation every 10, verify chain.examples/prove_inclusion.py— Build and verify an inclusion proof for event #25.examples/tamper_detection.py— Mutate a JSONL line; showverify_chain()flagging the specific segment.
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_audit_log_fs-0.1.0.tar.gz.
File metadata
- Download URL: pqc_audit_log_fs-0.1.0.tar.gz
- Upload date:
- Size: 23.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b772b60cb54d5a3292f4ac8ee003befd2c6fd4008b74ddc851dc77cdb400d0c3
|
|
| MD5 |
3b6aa66871088f22c5b688a1dcf7b3bf
|
|
| BLAKE2b-256 |
4f7c010b37b0761b36fdc956aa85b6d77f394ff65692465795d2ae2e9e3c4ea1
|
File details
Details for the file pqc_audit_log_fs-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pqc_audit_log_fs-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cdcc6dcef671ff1d7a2a42148b150c9c7b72ec3d53aa1e5686a40d2083b934b
|
|
| MD5 |
3d691bd17ed841cecf18cf642719c8d6
|
|
| BLAKE2b-256 |
2d9133735d8e28d89a12af339025bb0d897c8e666d002c2c154e09f858b5c45f
|