SEE MATPROOF ON YOUR STACK — BOOK A 30-MINUTE DEMO

Open specification · Version 1 · CC BY 4.0

Sealed Evidence Bundle

An open format for compliance evidence that anyone can verify without trusting the vendor that produced it. It is a plain ZIP file. There is nothing to install and nothing to sign up for.

Format ID sealed-evidence-bundle/v1Media type application/vnd.sealed-evidence-bundle+zip

1The problem

Compliance evidence moves as email attachments, portal downloads, screenshots and ZIP files. Once it has moved, the person receiving it cannot answer two basic questions:

  1. Has anybody changed these files since they were produced?
  2. Did the system that claims to have produced them really do so?

A ZIP full of PDFs answers neither. Any file in it can be edited, replaced, or removed, and nothing in the package records that it happened. An auditor comparing this year to last year has no way to tell which controls actually changed and which were simply re-exported.

This specification fixes that with two additions and no new software: a list of hashes, and a signature over that list.

2Bundle structure

A bundle MUST be a ZIP archive. Paths inside it MUST be relative, MUST use forward slashes, and MUST NOT contain ., .., a drive letter, or a NUL byte. Two entries MUST NOT share a path.

bundle.zip
├── manifest.json     required   file list, hashes, scope, digest
├── SHA256SUMS        required   the same list, sha256sum format
├── signature.json    optional   issuer signature over the digest
├── verify.sh         optional   offline verifier
├── README.txt        optional   plain-language explanation
├── artifacts/        evidence files as originally uploaded
├── reports/          documents generated from stored records
└── data/             the same records as JSON

manifest.json, SHA256SUMS and signature.json are reserved paths. They MUST NOT appear in the manifest’s own file list, because a list cannot contain its own hash. Every other file in the archive MUST appear exactly once.

3The manifest

{
  "format": "sealed-evidence-bundle",
  "formatVersion": 1,
  "spec": "https://matproof.com/spec/sealed-evidence-bundle",
  "bundleId": "eb_9f2c...",
  "createdAt": "2026-08-13T09:00:00.000Z",
  "generator": { "product": "Matproof", "version": "01fd8eab" },
  "organization": { "id": "org_...", "name": "Acme GmbH" },
  "scope": {
    "kind": "organization",
    "taskCount": 75,
    "automationCount": 12,
    "fileCount": 168,
    "totalBytes": 48293011,
    "redacted": true,
    "heldRuns": 3,
    "integrityWarnings": []
  },
  "files": [
    {
      "path": "artifacts/access-review-q2.pdf",
      "sha256": "9f2c...",
      "bytes": 184320,
      "kind": "artifact",
      "source": { "type": "attachment", "id": "att_..." },
      "contentSha256": "9f2c...",
      "integrity": "verified"
    }
  ],
  "bundleDigest": "sha256:1a4e..."
}

sha256 covers the bytes stored in the archive. It proves nothing changed after the export.

contentSha256 covers the underlying evidence and stays stable across re-exports. The two differ for generated documents, because a rendered report usually carries its own export timestamp. A tool comparing two bundles SHOULD compare contentSha256, not sha256.

integrity reports an artifact against the hash recorded when it was first stored: verified, unhashed, or mismatch. A file whose hash does not match MUST remain in the bundle and MUST be listed in scope.integrityWarnings. Removing it would hide the very problem the reader needs to see.

scope.heldRuns counts automated checks that returned no usable result and were therefore excluded. A gap in the evidence is not a pass, so the count MUST be reported rather than silently dropped.

4The bundle digest

The digest identifies the bundle. It is computed from the file list, not from the ZIP container, so it is unaffected by compression settings or archive metadata.

  1. Sort the entries by path, comparing raw UTF-8 bytes. Locale collation MUST NOT be used, so the result does not depend on the machine.
  2. Render one line per entry: <sha256><SP><SP><path><LF>
  3. Concatenate the lines and take the SHA-256 of the result. Prefix it with sha256:.

That line format is exactly what sha256sum prints. A bundle MUST therefore also ship the same content as SHA256SUMS, which gives the format its useful property:

sha256sum -c SHA256SUMS   # every file still matches its hash
sha256sum SHA256SUMS      # this value is the bundle digest

Verification needs no JSON parser, no library, and no code from the issuer. Serialising manifest.json SHOULD use sorted keys so that identical input produces identical bytes.

5The signature

{
  "alg": "ed25519",
  "keyId": "mp-2026-08",
  "bundleDigest": "sha256:1a4e...",
  "signature": "<base64 raw Ed25519 signature>",
  "signedAt": "2026-08-13T09:00:00.000Z",
  "publicKeyUrl": "https://api.matproof.com/.well-known/matproof-evidence-keys.json"
}

The signature covers the ASCII of bundleDigest, which in turn covers every file. Signing one short string keeps verification cheap and keeps the signed input unambiguous.

An issuer MUST publish its public keys at a stable URL. That list MUST be append-only: expired and revoked keys stay published forever, because removing one would make every bundle it ever signed unverifiable. Each entry carries validFrom, validUntil and revokedAt.

No public transparency log. This format deliberately does not use keyless signing backed by a public log. Publishing a record of every signature to third-party infrastructure leaks the timing and volume of a customer’s audit activity, and moves that record outside the jurisdiction the customer chose. An issuer holds its own key.

Signing is optional. An unsigned bundle is still fully verifiable against SHA256SUMS; it simply makes no claim about who produced it.

6Verifying a bundle

In order of how much you have to trust. The first step needs nothing but coreutils and is sufficient to detect any change to any file.

# 1. Integrity — offline, no network, no dependencies
unzip bundle.zip -d bundle && cd bundle
sha256sum -c SHA256SUMS

# 2. Identity — fetch the issuer's public key list once
curl -s "$(jq -r .publicKeyUrl signature.json)" > keys.json
#    check keyId, validFrom/validUntil and revokedAt, then verify
#    the base64 signature over the ASCII of bundleDigest

# 3. Issuance — optional, confirms the issuer recognises this bundle
curl -s -X POST https://api.matproof.com/v1/evidence-bundles/verify \
  -H 'Content-Type: application/json' \
  -d "{\"bundleDigest\":\"$(sha256sum SHA256SUMS | cut -d' ' -f1 | sed 's/^/sha256:/')\"}"

Step 3 sends a hash and nothing else. The evidence never leaves the verifier’s machine. An issuer SHOULD NOT return the subject organisation’s name unless that organisation has already chosen to publish it, otherwise the endpoint becomes a way to enumerate customers.

7What a bundle does not claim

A signature proves that a named issuer produced a set of files at a stated time and that the files have not changed since. That is all it proves.

  • It is not a qualified electronic signature or a qualified timestamp under eIDAS, unless the issuer is separately a qualified trust service provider and says so.
  • It says nothing about whether the evidence is sufficient, whether a control actually works, or whether an organisation is compliant.
  • It does not certify the issuer’s own honesty. A signature binds a claim to an identity so that the claim can be held against someone. It does not make the claim true.

Implementations MUST NOT describe a bundle as legally binding, notarised, or court-proof.

8Licence and versioning

This specification is published under CC BY 4.0. Anyone may implement it, including competitors. A standard that only one vendor can use is not a standard.

Version 1 is frozen. Fields may be added in a later version; the meaning of an existing field will not change. A verifier encountering an unknown field SHOULD ignore it, and one encountering an unknown formatVersion SHOULD refuse rather than guess.

The machine-readable schema lives at manifest.schema.json.

Matproof authored this specification and implements it. The format is not tied to Matproof: an implementation needs a ZIP writer, SHA-256, and optionally Ed25519.

Found a problem with the spec? Tell us.