Evidence Bundle v1 Contract (2025-11-19)
Scope
Frozen contract for Evidence Bundle v1 covering AdvisoryAI/Concelier/Excititor evidence exports used by air-gap and attestation flows.
Artefact layout
- Tarball name:
evidence-bundle-<id>.tar.gz - Manifest (required):
manifest.json - Payloads (required):
observations.ndjson,linksets.ndjson - Optional:
timeline.ndjson(time anchors),transparency.json(Rekor UUID or skip_reason) - Hash list:
hashes.sha256(sha256 of each file)
manifest.json fields
{
"bundle_id": "evidence-bundle-m0",
"version": "1.0.0",
"created": "2025-11-19T00:00:00Z",
"tenant": "demo",
"scope": "vex",
"inputs": ["sha256:payload-obs", "sha256:payload-linksets"],
"aoc": {"guardrails": true, "details": ["schema:frozen:1.0", "limits:chunk:max=2000"]}
}
observations.ndjson (sample record)
{"observationId":"obs-ossl-001","advisoryId":"CVE-2024-1234","component":"pkg:deb/openssl@1.1.1w","source":"nvd","fetchedAt":"2025-11-18T12:00:00Z"}
linksets.ndjson (sample record)
{"linksetId":"lnm-ossl-001","advisoryId":"CVE-2024-1234","components":["pkg:deb/openssl@1.1.1w"],"normalized":true,"createdAt":"2025-11-18T12:05:00Z"}
transparency.json (optional)
{"rekor_uuid": null, "skip_reason": "offline"}
Determinism rules
- All timestamps must be UTC RFC3339.
- Ordering: sort NDJSON by
advisoryId, thencomponent, ascending. - Hashes: compute sha256 on raw file bytes; record in
hashes.sha256and in manifestinputs. - Tenant must be lowercase; include in manifest and any attestation subject claims.
Example bundle (sample)
- Path:
docs/modules/evidence-locker/samples/evidence-bundle-m0.tar.gz - SHA256:
$(cat docs/modules/evidence-locker/samples/evidence-bundle-m0.tar.gz.sha256 | awk '{print $1}') - Contains sample manifest/observations/linksets/transparency per above.
Attestation linkage
- See
attestation-scope-note.mdfor required claims. - DSSE subject uses the Merkle root derived from
checksums.txt(sha256 of sorted entry hashes). Record the OCI tarball digest as metadata, not the subject.
Append-only persistence (SPRINT_20260501_050)
The on-disk bundle metadata schema gained two fields and lost the Pending/Assembling/Failed persisted statuses:
bundle_id— content-addressed at the application layer (deterministic derivation from the canonicalroot_hash); deterministic re-build of the same payload yields the samebundle_id, so the supersede chain only forms when the payload actually changed.supersedes(UUID, nullable) — when this row is a re-seal, references the predecessorbundle_id. NULL on the first seal of a chain.superseded_by(UUID, nullable) — when this row has been re-sealed by a successor, references the successorbundle_id. The only field allowed to mutate post-insert (NULL → set), and only via the SECURITY DEFINER SQL functionevidence_locker.evidence_bundle_set_superseded_by. Direct UPDATE/DELETE on any other column raisesEVIDENCE_BUNDLE_IMMUTABLE(SQLSTATEEL010).
Persisted status is restricted to Sealed (3) or Archived (5). Pending, Assembling, and Failed exist purely as in-memory states inside EvidenceSnapshotService while the manifest is being built; a failed seal writes nothing to the database.
Retention: expires_at is set at seal time and is immutable. Holds extend retention via the evidence_holds table; effective retention is MAX(bundle.expires_at, MAX(active hold.expires_at)).
Change log
- 2026-04-30: SPRINT_20260501_050 — append-only on-disk schema; added
supersedes/superseded_by; constrained persistedstatustoSealed/Archived; documented chain-on-re-seal. - 2025-12-04: Updated subject to Merkle root and aligned with EB1–EB10 docs/fixtures.
- 2025-11-19: v1 frozen (initial publication). Add real sample tarball + hashes once produced.
