The .cgfevidence bundle format
A .cgfevidence file is a single, self-contained, signed compliance
artifact. It is a zstd-compressed tar archive with a fixed internal
layout and Ed25519 + RFC 3161 cryptography. Any CGF reader can verify it
offline, with nothing but the bundle itself and a trust store.
On-disk layout
dossier.cgfevidence ← tar + zstd
├── manifest.json ← format / version / hashes
├── graph.json ← deterministic, content-addressed graph
├── claims.json ← claim-pack results, structured
├── narrative.md ← rendered from graph + claims
├── signatures/
│ ├── ed25519-ops@acme.sig ← detached signature over signed-payload.bin
│ └── ed25519-cto@acme.sig
├── signed-payload.bin ← canonical bytes the signatures cover
└── timestamp/
└── freetsa.tsr ← RFC 3161 TimeStampToken (optional)
Everything except signatures/ and timestamp/ is hashed into
manifest.rootSha256. The signatures cover signed-payload.bin, which is
the canonical (sorted-key, LF-terminated) JSON encoding of manifest.json.
That gives you a single hash to sign and a single hash to verify.
manifest.json
{
"format": "cgf",
"version": "1.0",
"rootSha256": "9f3a…d27e",
"createdAt": "2026-04-12T09:14:22Z",
"ingestor": {
"name": "@daitai/cgf",
"version": "1.0.0-rc.1"
},
"files": [
{ "path": "graph.json", "sha256": "1e88…5a02", "bytes": 184211 },
{ "path": "claims.json", "sha256": "73c0…b119", "bytes": 41098 },
{ "path": "narrative.md", "sha256": "ab4f…0c7d", "bytes": 22440 }
],
"claimPacks": [
{ "id": "eu-ai-act-high-risk", "version": "1.0.0", "result": "fail" },
{ "id": "gdpr-art-30", "version": "1.0.0", "result": "pass" },
{ "id": "soc2-cc", "version": "1.0.0", "result": "pass" }
]
}
rootSha256 is sha256(canonicalize({ files, claimPacks, ingestor, createdAt })).
If any byte in any file changes, the rootSha256 changes, and every
signature fails.
Signature schema
Signatures are Ed25519 over signed-payload.bin. The filename is
informational — the binding is the embedded keyId.
// signatures/ed25519-ops@acme.sig.json
{
"alg": "Ed25519",
"keyId": "ops@acme",
"pub": "9c4d…ef10", // 32-byte raw public key, hex
"sig": "2c7b…f441", // 64-byte signature, hex
"over": "signed-payload.bin",
"ts": "2026-04-12T09:14:22Z"
}
Multi-signer bundles simply contain multiple *.sig.json files. A trust
policy decides how many are required and whether each must be in the
trust store.
Timestamp (RFC 3161)
If the bundle was built with --tsa <url>, the response from the Time
Stamping Authority is stored verbatim as a DER-encoded TimeStampToken:
timestamp/freetsa.tsr ← raw RFC 3161 TimeStampToken (DER)
timestamp/freetsa.json ← parsed convenience copy
The parsed copy:
{
"alg": "RFC3161",
"tsa": "https://freetsa.org/tsr",
"time": "2026-04-12T09:14:25Z",
"hash": { "alg": "sha256", "value": "9f3a…d27e" }
}
hash.value must equal manifest.rootSha256. That binds the TSA's "this
existed before time T" attestation to exactly this bundle.
Verification algorithm
A conforming reader MUST:
- Untar + decompress the bundle into memory.
- Recompute
sha256for every entry inmanifest.filesand compare. - Recompute
rootSha256and compare tomanifest.rootSha256. - For each signature, verify Ed25519 with the embedded
puboversigned-payload.bin. - If a
timestamp/*.tsrexists, parse it, verify the TSA's CMS signature, and check thatmessageImprintmatchesrootSha256. - Apply the active
TrustPolicy(see Trust store & policy presets) and emitpassor a list of structured violations.
Steps 1–5 are cryptographic and the same on every machine. Step 6 is policy and reflects the verifying organisation's risk appetite.
Example: minimal valid bundle
demo.cgfevidence
├── manifest.json
├── graph.json
├── claims.json
├── narrative.md
├── signed-payload.bin
└── signatures/
└── ed25519-alice.sig.json
That's it — no TSA, single signer. Verifies as
signed, untimestamped, 1/1 signers trusted against any policy that does
not require a timestamp.