EU AI Act Article 12 evidence pack¶
A practical guide for the operator who has to assemble Article 12-conformant record-keeping evidence for a conformity assessment. Covers what the bundle contains, how to build it, how the retention pin works, and how an auditor verifies it offline.
For the underlying chain integrity story, see audit log. For the third-party-verifiable wrapper around the bundle, see DSSE / in-toto envelope.
What Article 12 wants¶
Regulation (EU) 2024/1689 Article 12 ("Record-keeping") and Article 19(1) ("Automatically generated logs") together require that high-risk AI systems keep automatic event-logs over the lifetime of the system, retain them at least 6 months (10 years for high-risk classifications), and produce them on request for a conformity assessment.
The Article 12 bundle is the smallest viable artefact that satisfies those requirements. Source: src/bernstein/core/security/article12_bundle.py.
What the bundle contains¶
The bundle is a single deterministic zip with four entries. Same input window + same risk class → byte-identical archive.
| Entry | Purpose | Article 12 anchor |
|---|---|---|
manifest.json | Schema version, bundle id, window bounds, risk class, event count, chain anchor (head HMAC), per-artefact SHA-256, retention block. | 12(2)(c) - chain anchor; 12(3) - retention pin. |
events.jsonl | The HMAC-chained audit slice for the window, sorted, byte-identical to the on-disk source records. | 12(1) - automatic recording. |
data_catalog.json | Per-resource activity counts (input/output catalog). | 12(2)(b) - post-market monitoring; Annex IV §2(d) - data governance. |
clause_map.json | Maps each artefact above to its Article 12 sub-clause. | Auditor navigation. |
Determinism rules applied to the zip:
- Fixed file order (alphabetical).
- Fixed mtime (1980-01-01 - the floor zip can encode).
- Stored mode
0644. - Canonical JSON (sorted keys, no whitespace) for every JSON entry.
Same input bundle bytes hash to the same SHA-256 every run. The auditor re-runs the build a year later and compares against the published manifest.json:archive_sha256.
Retention pin¶
manifest.json:retention carries:
| Field | Meaning |
|---|---|
risk_class | high, limited, or minimal. |
retention_days | Computed retention horizon. 10 years (3653 days) for high; 183 days minimum otherwise. |
retention_until | ISO-8601 date by which deletion is forbidden. |
last_event_ts | ISO-8601 timestamp of the latest covered event (the anchor for the retention horizon). |
The pin is metadata, not an immutable storage backend. Pair the bundle with S3 Object Lock, WORM Postgres, or chattr +i to make the retention enforceable at the storage layer; the pin documents the obligation, the backend enforces it.
validate_retention() re-checks the pin against the current clock during verify so an auditor can assert "still within the Article 12(3) horizon" without parsing dates by hand.
Building a bundle¶
CLI¶
bernstein audit export --article-12 \
--since 2026-04-01T00:00:00+00:00 \
--until 2026-05-01T00:00:00+00:00 \
--risk-class high \
--output .sdd/evidence/
Defaults: risk-class=limited, output=<audit_dir>.parent/evidence/. The archive lands at .sdd/evidence/article12_<bundle_id>.zip.
Python API¶
from datetime import UTC, datetime
from pathlib import Path
from bernstein.core.security.article12_bundle import build_article12_bundle
bundle = build_article12_bundle(
audit_dir=Path(".sdd/audit"),
since="2026-04-01T00:00:00+00:00",
until="2026-05-01T00:00:00+00:00",
risk_class="high",
output_dir=Path(".sdd/evidence/"),
)
print(bundle.bundle_id, bundle.event_count, bundle.archive_path, bundle.sha256)
Pass write=False to build the bundle in-memory only - useful for spot-checking a window before shipping.
Per-run anchor (alternative)¶
The default path slices the calendar-rotated <sdd>/audit/*.jsonl by wall-clock window. For run-scoped evidence, assemble_from_run reads the per-run audit slice at <sdd>/runtime/audit/<run_id>.audit.jsonl and anchors the bundle to one orchestrator run rather than a wall-clock window. Use this when the auditor's question is "show me everything that happened during run X", not "show me everything in May".
Verifying a bundle¶
Narrow verifier (manifest hashes + retention)¶
from pathlib import Path
from bernstein.core.security.article12_bundle import verify_bundle
result = verify_bundle(Path(".sdd/evidence/article12_<bundle_id>.zip"))
if not result.ok:
for err in result.errors:
print("FAIL:", err)
raise SystemExit(1)
verify_bundle confirms two properties without needing the operator's HMAC key:
- Every artefact's SHA-256 in
manifest.json:artefactsmatches the on-disk bytes. - The retention pin still satisfies Article 12(3) at the current clock.
A bad zip, a mismatched hash, or an expired retention horizon flips result.ok=False with a per-check error.
HMAC chain verification (operator path)¶
To re-walk the embedded events.jsonl HMAC chain, the verifier needs the operator's HMAC key:
from bernstein.core.security.audit import AuditLog
# Extract events.jsonl from the bundle, then:
log = AuditLog(audit_dir=extracted_dir) # hmac_key resolved per the audit-log doc
result = log.verify()
See audit log for key resolution semantics.
Third-party-verifiable path¶
The narrow verifier above does not give an external auditor a verifiable-without-the-HMAC-key signal. The DSSE / in-toto envelope wraps the bundle in an Ed25519-signed payload that any party with the public key can verify offline:
python tools/verify_audit_dsse.py \
--envelope path/to/envelope.json \
--bundle path/to/article12_<bundle_id>.zip \
--public-key path/to/operator-pubkey.pem
The standalone verifier depends only on the Python standard library plus cryptography and refuses to import anything from the bernstein.* package. See DSSE / in-toto envelope for the wire format and the four-check verification model.
Risk-class selection¶
The operator picks the risk class at build time. Bernstein does not auto-assign; the EU AI Act risk classifier in core/security/eu_ai_act.py runs at task time, and the operator decides which class to pin into the evidence bundle based on the system's deployment context.
| Class | Retention | When to use |
|---|---|---|
high | 10 years (3653 days) | Annex III high-risk domains: biometrics, critical infrastructure, education, employment, essential services, law enforcement, migration, justice. |
limited | 183 days minimum | Article 50 transparency-only systems. |
minimal | 183 days minimum | Article 5-allowed minimal-risk systems. |
A pin lower than the system's actual risk class will fail at audit time; a pin higher just keeps logs longer. When in doubt, pin high.
Bundle index for multi-bundle deployments¶
Article 19(1) wants logs retained ≥6 months across the lifetime of the system. For long-lived deployments that produce one bundle per assessment window, the operator typically maintains a bundle_index.json alongside the bundles:
{
"system_id": "bernstein-orch-prod-eu-1",
"bundles": [
{
"bundle_id": "<sha256-prefix>",
"since": "2026-04-01T00:00:00+00:00",
"until": "2026-05-01T00:00:00+00:00",
"risk_class": "high",
"archive_sha256": "<hex>",
"envelope_sha256": "<hex>"
},
...
]
}
The index is operator-maintained - bernstein does not generate it. It is the artefact the auditor asks for first ("show me every bundle for this system over the last six months") and the only thing that demonstrates continuity at the lifetime granularity.
Compliance mapping¶
| Article 12 clause | Bundle artefact |
|---|---|
| 12(1) - automatic recording over lifetime | events.jsonl. |
| 12(2)(a) - Art. 79(1) risk situations + substantial modifications | Operator-side: surface the assessment via the eu_ai_act.py:assess_task() per-task store. The bundle does not auto-flag substantial modifications. |
| 12(2)(b) - post-market monitoring | data_catalog.json aggregates per-resource activity counts. |
| 12(2)(c) - third-party-verifiable monitoring of high-risk systems (Art. 26(5)) | chain_anchor + DSSE envelope (see DSSE). |
| 12(3) - retention horizon | manifest.json:retention; pair with an immutable backend for storage-side enforcement. |
| 19(1) - logs ≥6 months | Same as 12(3) plus operator-maintained bundle index. |
For the AIGF control map, see FINOS AIGF mapping. For the regulator-class lineage trail that pairs with the bundle, see regulatory lineage.
Limitations¶
- Substantial-modification detection is operator-side only. The bundle does not flag tasks that change the agent fleet's effective capabilities.
- The retention pin is metadata, not enforcement. Without an immutable backend, an attacker with
rmcan delete the bundle and the pin disappears with it. - Article 43 paperwork (full conformity assessment) is not generated by the bundle. Use
bernstein compliance assessfor the Annex IV technical document and the Article 43 conformity-assessment record. - Bundle index is not auto-generated; the operator maintains
bundle_index.jsonthemselves. - Wall-clock window vs run scope are different code paths. The default builder reads the calendar-rotated daily logs;
assemble_from_runreads the per-run slice. Pick one and document it in the operator runbook so the auditor sees consistent shape across bundles.
Related¶
- Audit log - HMAC chain layout, key management, verify procedure.
- DSSE / in-toto envelope - third-party-verifiable wrapper around the bundle.
- Multi-tenant audit-chain export - per-tenant slice of the same chain with optional RFC 3161 timestamping.
- Regulatory lineage - per-artefact lineage trail with customer-key signatures.
- Compliance CLI -
bernstein compliance assessfor the Annex IV / Article 43 paperwork. - Source:
src/bernstein/core/security/article12_bundle.py,src/bernstein/core/security/audit_dsse.py,tools/verify_audit_dsse.py.