Evidence Pack Specification
A standard format for packaging, signing, and distributing assurance evidence as portable, verifiable ZIP archives.
Evidence Packs solve a fundamental problem in compliance: evidence is scattered across vendors, formats, and portals with no standard way to verify authenticity or track provenance. This spec defines how to bundle evidence into a single archive that anyone can verify without vendor-specific tools.
Conformance Levels
Not every tool needs to implement the full spec. A simple validator only needs Level 1 (pack format). A signing tool adds Level 2 (attestations). This layered approach lets you implement exactly what you need.
| Level | Description | Requirements |
|---|---|---|
| Level 1 | Pack format only - create and verify pack structure and integrity | Parse packs, validate paths, verify digests, reject unsafe archives |
| Level 2 | Add attestation support - sign and verify Sigstore attestations | + JCS canonicalization (RFC 8785), Sigstore bundle verification, identity verification |
Pack Format
An Evidence Pack is just a ZIP file with a specific structure. We chose ZIP because it's universally supported, streamable, and already used everywhere. The format is designed so you can verify a pack's integrity without extracting it to disk.
unzip -l let you inspect contents without code. The tradeoff is that ZIP has legacy quirks (timestamps, compression ratios, platform-specific attributes) that we explicitly ignore for integrity purposes.
.epack extension
Directory Structure
Every pack has the same basic structure: a manifest at the root, artifacts in a folder, and optional attestations. This predictability means any tool can find what it needs without parsing the manifest first.
pack.epack +-- manifest.json # REQUIRED - describes the pack +-- artifacts/ # REQUIRED - the actual evidence files | +-- {artifact-files} # May use subdirectories +-- attestations/ # OPTIONAL - cryptographic signatures +-- *.sigstore.json # Direct children only, no subdirs
manifest.json MUST exist at the archive root
artifacts/ directory MUST exist (may be empty)
attestations/ (no subdirectories)
.sigstore.json suffix
manifest.json, artifacts/, or attestations/
Path Requirements
Paths in a pack must work identically on Windows, macOS, and Linux. This means being strict about character encoding, normalization, and avoiding platform-specific gotchas like Windows device names or path traversal attacks.
artifacts[].path MUST match ZIP entry name exactly (codepoint-for-codepoint)
NFC(path) != path
/) only, no backslashes
.. or . segments, or empty segments (//)
/, Windows drive letters, or UNC paths
con, prn, aux, nul, com1-9, lpt1-9
con.json would fail to extract on Windows. By rejecting these names on all platforms, we ensure packs are portable.
.) or a space ( )
report. → report, file → file. This creates integrity ambiguity during extraction and can be exploited for path confusion attacks.
Manifest
The manifest is the table of contents for your pack. It lists every artifact with its digest, declares the overall pack digest, and optionally tracks where the evidence came from. Think of it as the pack's "bill of materials."
- Duplicate keys: MUST reject at any nesting level. Detection MUST occur during or before parsing.
- Numbers: MUST be finite (no
NaN,Infinity). Integer fields MUST be in range 0..253-1. - Unknown fields: MUST reject manifests containing fields not defined in this specification. New fields are introduced through spec version bumps, not silently ignored.
manifest.json MUST be valid UTF-8
NaN, Infinity, -Infinity)
{
"spec_version": "1.0",
"stream": "published/acme/prod",
"generated_at": "2026-01-20T15:30:00Z",
"pack_digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"sources": [
{
"name": "github",
"version": "1.0.0",
"source": "github.com/locktivity/epack-collector-github",
"commit": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"binary_digest": "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"artifacts": 5
}
],
"artifacts": []
}
Manifest Fields
| Field | Type | Required | Description |
|---|---|---|---|
spec_version |
String | Yes | MUST be "1.0" |
stream |
String | Yes | Unique identifier for this evidence stream (opaque) |
generated_at |
String | Yes | Exact format: YYYY-MM-DDTHH:MM:SSZ (no offsets, no fractions) |
pack_digest |
String | Yes | sha256: + 64 lowercase hex chars |
sources |
Array | Yes | List of collectors that contributed artifacts (MAY be empty; informational only) |
artifacts |
Array | Yes | Index of all artifacts (MAY be empty) |
provenance |
Object | No | Origin and attestation chain for merged packs |
profile |
String | No | Reserved for semantic validation. See Semantic Extensibility. |
overlays |
Array | No | Reserved for semantic validation. See Semantic Extensibility. |
profile_lock |
Array | No | Reserved for semantic validation. See Semantic Extensibility. |
spec_version MUST be exactly "1.0"
stream MUST be a non-empty string
generated_at MUST match format YYYY-MM-DDTHH:MM:SSZ (no offsets, no fractions)
pack_digest MUST be sha256: + 64 lowercase hex characters
Source Element
Each element in the sources array describes a collector that contributed artifacts. Sources are informational and do not affect verification or pack_digest computation.
| Field | Type | Required | Description |
|---|---|---|---|
name |
String | Yes | Collector identifier (e.g., github, aws) |
version |
String | No | Version of the collector that generated the artifacts |
source |
String | No | Repository path where the collector source code is hosted (e.g., github.com/locktivity/epack-collector-aws) |
commit |
String | No | Git commit SHA that built the collector binary |
binary_digest |
String | No | SHA256 digest of the collector binary (sha256: + 64 hex chars) |
artifacts |
Integer | No | Number of artifacts contributed by this collector |
source, commit, and binary_digest fields enable cryptographic verification of which exact collector binary produced the artifacts. The source field specifies the repository where the source code is hosted, allowing verifiers to locate and inspect the code. When combined with SLSA Level 3 attestations on collector binaries, verifiers can establish a complete chain from source code to evidence output.
Provenance (Merged Packs)
When a pack is created by merging other packs (for example, combining evidence from multiple vendors), the provenance field tracks where everything came from.
The provenance.source_packs array contains Source Pack objects with the following structure:
| Field | Type | Required | Description |
|---|---|---|---|
stream |
String | Yes | Stream identifier of the source pack |
pack_digest |
String | Yes | Pack digest of the source pack (sha256: + 64 hex) |
manifest_digest |
String | Yes | JCS-canonicalized SHA-256 of source manifest (64 hex, no prefix) |
artifacts |
Integer | Yes | Number of artifacts from this source |
embedded_attestations |
Array | No | Array of Sigstore bundles from the source pack (if signed) |
Each element of the embedded_attestations array is a complete Sigstore bundle from the source pack, enabling verifiers to validate signatures without fetching the original pack. To verify source pack integrity, verifiers MUST verify each embedded attestation using the Sigstore trusted root and expected signer identities.
Semantic Extensibility (Reserved Fields)
The manifest reserves three fields for future semantic validation: profile, overlays, and profile_lock. These enable a companion specification to define what artifacts a pack should contain and how they should be validated.
{
"spec_version": "1.0",
"stream": "published/acme/prod",
"profile": "evidencepack/soc2-basic@v1", // Reserved - identifier string
"overlays": ["evidencepack/hipaa-overlay@v1"], // Reserved - array of identifier strings
"profile_lock": [ // Reserved - pinned digests for reproducibility
{ "id": "evidencepack/soc2-basic@v1", "digest": "sha256:abc123..." },
{ "id": "evidencepack/hipaa-overlay@v1", "digest": "sha256:def456..." },
{ "id": "evidencepack/soc2-report@v1", "digest": "sha256:789def..." }
],
...
}
profile, overlays, and profile_lock fields unless implementing a companion semantic specification
profile is present, it MUST be a non-empty string
overlays is present, it MUST be an array of non-empty strings
profile_lock is present, it MUST be an array of objects with id (string) and digest (string in sha256:<hex> format)
Identifier Shape
Identifiers in profile, overlays, and profile_lock[].id are opaque strings to v1.0 validators. However, to prevent future migration pain, we recommend the following shape:
- Format:
namespace/name@vN(e.g.,evidencepack/soc2-report@v1) - Lowercase ASCII alphanumeric, hyphens, underscores; 1-63 characters per segment
- Version: literal
vfollowed by a positive integer with no leading zeros
profile, overlays, or profile_lock, tools MUST NOT silently substitute or upgrade that identifier
profile: "foo" is structurally valid even if no tool understands what "foo" means.
Extension Available
The Profiles & Semantic Validation draft defines how to use these fields for declaring and validating pack requirements.
Artifacts
Artifacts are the actual evidence files in your pack. They come in two flavors: embedded (the bytes are in the ZIP) and referenced (a pointer to an external URL). Most artifacts are embedded, but references are useful for large files or gated content like SOC 2 reports behind NDA portals.
Embedded Artifact
{
"type": "embedded",
"path": "artifacts/aws/iam-policies.json",
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 48213,
"content_type": "application/json",
"collected_at": "2026-01-20T14:00:00Z",
"semantic_type": "evidencepack/iam-policies@v1", // For profile validation (see Profiles spec)
"metadata": { "region": "us-east-1" }, // Validated against type's metadata_schema
"controls": ["AC-2", "AC-6"]
}
type MUST be "embedded"
path, digest, and size are REQUIRED
path values MUST be unique across the manifest (using Windows-canonical comparison)
SHA256(bytes) MUST match artifact.digest
artifact.size
Referenced Artifact
Referenced artifacts point to external bytes (gated portals, VDRs). They are informational pointers excluded from pack_digest because the bytes aren't in the pack.
{
"type": "reference",
"name": "soc2-type-ii-report",
"uri": "https://trust.vendor.com/portal/soc2",
"access": { "policy": "nda_required" },
"digest": "sha256:..." // Optional - if present and bytes fetched, MUST validate
}
https
Integrity
The pack digest is a fingerprint of all embedded artifacts. If any artifact changes, the digest changes. This lets you verify a pack's contents with a single hash comparison and enables content-addressed storage (two packs with identical content have identical digests).
Pack Digest Computation
The algorithm is designed to be deterministic across implementations:
- List all embedded artifacts from the manifest
- Verify each artifact's
SHA256(bytes)matchesartifact.digest - Build canonical list:
{path}\t{digest}\nper artifact (tab = 0x09, newline = 0x0A) - Sort lines by byte-wise lexicographic ordering on raw UTF-8 bytes (like
memcmp) - Concatenate sorted lines. Each line ends with exactly one
\n. No trailing newline after final entry. - Empty artifact list = empty byte string (zero bytes)
- Compute
SHA256of concatenated result - Format as
sha256:{hex}(lowercase)
{path}\t{digest}\n (tab = 0x09, newline = 0x0A)
manifest.json (contains the digest itself), all files under attestations/ (added after pack generation), and referenced artifacts (bytes are external).
Verification Steps
manifest.json
artifacts/ not listed as embedded artifacts
manifest.pack_digest
Two-Digest Model
Evidence Packs use two digests for different purposes:
| Digest | Computed From | Purpose |
|---|---|---|
pack_digest |
Canonical list of embedded artifacts | Content-addressed identifier; enables deduplication and integrity checks |
manifest_digest |
JCS-canonicalized manifest.json |
Stable signing target; allows attestations without changing signed content |
Format conventions: pack_digest uses the prefixed format (sha256:hex) as a self-describing content identifier. manifest_digest uses raw hex (no prefix) to match in-toto's digest map structure where the algorithm is the key.
pack_digest covers artifact content but not metadata. The manifest_digest covers the entire manifest including metadata. Attestations sign the manifest digest because it's stable: you can add new attestations to a pack without invalidating existing signatures. If attestations signed the pack digest, adding a signature would change the thing being signed.
Security Considerations
ZIP files are a common attack vector. Malicious archives can contain path traversal attacks, symlinks that escape extraction directories, or zip bombs that expand to enormous sizes. These requirements ensure packs can be safely extracted and verified.
Size Limits
Implementations MUST enforce limits. Defaults are recommended; minimums are mandatory floors.
| Limit | Default | Minimum | Rationale |
|---|---|---|---|
| Max artifact size | 100 MB | 1 MB | Prevents memory exhaustion |
| Max pack size | 2 GB | 10 MB | Reasonable upper bound |
| Max artifact count | 10,000 | 100 | Prevents file handle exhaustion |
| Max compression ratio | 100:1 | N/A | Mitigates zip bomb attacks |
__MACOSX/, ._ prefixed files)
O_NOFOLLOW when writing extracted content
Attestation Format
Attestations are cryptographic signatures that prove a pack came from a specific source. Evidence Packs use Sigstore for keyless signing, where the signer authenticates via OIDC (GitHub, Google, Microsoft) and receives a short-lived certificate from Fulcio. All signatures are recorded in Rekor, a public transparency log.
Sigstore Bundle Structure
Attestation files use the Sigstore bundle format with media type application/vnd.dev.sigstore.bundle.v0.3+json:
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"x509CertificateChain": {
"certificates": [{ "rawBytes": "<base64-encoded-certificate>" }]
},
"tlogEntries": [{
"logIndex": "123456",
"logId": { "keyId": "<base64>" },
"integratedTime": "1234567890",
"inclusionProof": {
"logIndex": "123456",
"rootHash": "<base64>",
"treeSize": "1000000",
"hashes": ["<base64>", "..."]
}
}]
},
"dsseEnvelope": {
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64(in-toto statement)>",
"signatures": [{ "keyid": "", "sig": "<base64-signature>" }]
}
}
in-toto Statement (Decoded Payload)
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "manifest.json",
"digest": {"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
}],
"predicateType": "https://evidencepack.org/attestation/v1",
"predicate": {}
}
mediaType MUST be "application/vnd.dev.sigstore.bundle.v0.3+json" or a later compatible Sigstore bundle media type
verificationMaterial MUST contain certificate chain and transparency log proof
dsseEnvelope.payloadType MUST be "application/vnd.in-toto+json"
dsseEnvelope.signatures array MUST contain at least one signature
- and _) MUST NOT be used
_type MUST be "https://in-toto.io/Statement/v1"
subject array MUST contain exactly one subject
subject[0].name MUST be "manifest.json"
subject[0].digest.sha256 MUST be 64 lowercase hex characters (manifest_digest without "sha256:" prefix)
predicateType MUST be "https://evidencepack.org/attestation/v1"
predicate MUST be an empty object {}
Attestation File Naming
Attestation files MUST be named {identity-hash}.sigstore.json where identity-hash is the first 8 characters of the SHA-256 hash of the certificate's subject identity.
attestations/ ├── a1b2c3d4.sigstore.json # security@acme.com ├── e5f6g7h8.sigstore.json # auditor@thirdparty.com └── i9j0k1l2.sigstore.json # github.com/org/repo workflow
Signing Process
Signing uses Sigstore for keyless, identity-based signatures. The signer authenticates via OIDC, receives a short-lived certificate from Fulcio, and the signature is recorded in Rekor's transparency log.
Manifest Digest Computation
The manifest digest (used as the attestation subject) is computed using JCS (RFC 8785):
NaN, Infinity)
Signing with Sigstore
Using cosign to sign the manifest:
# Extract manifest from pack unzip -p pack.epack manifest.json > manifest.json # Sign with Sigstore (opens browser for OIDC authentication) cosign sign-blob manifest.json \ --bundle attestations/signer.sigstore.json \ --yes # Add attestation to pack zip pack.epack attestations/signer.sigstore.json
Verification Process
Verification uses the Sigstore trusted root (Fulcio CA, Rekor public key) to validate the bundle. The bundle is self-contained with stapled proofs, enabling offline verification without network access.
manifest.json using JCS and compare to subject[0].digest.sha256
pack_digest per pack specification
Verification Failures
subject[0].digest.sha256
payloadType is not "application/vnd.in-toto+json"
_type is not "https://in-toto.io/Statement/v1"
predicateType is not "https://evidencepack.org/attestation/v1"
subject[0].name is not "manifest.json"
predicate is not an empty object {}
Identity Verification
Sigstore certificates contain the signer's identity from their OIDC provider. Verifiers specify which identities they trust, such as specific email addresses or CI/CD workflow identities.
Common Identity Types
| OIDC Provider | Identity Example |
|---|---|
| GitHub Actions | https://github.com/org/repo/.github/workflows/release.yaml@refs/heads/main |
user@example.com |
|
| Microsoft | user@example.com |
| GitHub (personal) | user@users.noreply.github.com |
Test Vectors
Test vectors let you verify your implementation is conformant. Each vector includes a pack (valid or invalid) and the expected result. Run them against your code to catch edge cases before they become production bugs.
| Conformance Level | Test Vector Categories | Key Requirements |
|---|---|---|
| Level 1 | Pack format, paths, manifest, integrity | R-001 - R-062 |
| Level 2 | Attestation format, signing, verification | R-062a, R-063a, R-064a, R-067 - R-091 |
Level 1: Pack Format
Level 2: Attestation
References
Sigstore & Signing
- Sigstore - Keyless signing for the software supply chain
- Fulcio - Certificate authority for code signing
- Rekor - Transparency log for signatures
- DSSE Specification - Dead Simple Signing Envelope
- in-toto Attestation Framework - in-toto Statement format
RFCs
Evidence Pack Specification v1.0 - Draft