audit-labs/evidence-seal
Tamper-evident seals and chain of custody for audit evidence.
clone: git clone https://gitbay.org/audit-labs/evidence-seal.git
v1.0.0: README.md · raw
1# evidence-seal
2
3[](LICENSE)
4[]()
5
6Tamper-evident seals and chain of custody for audit evidence packages.
7
8Every Audit Labs tool assumes the evidence it handles is authentic. `evidence-seal`
9is what makes that assumption checkable. Point it at a directory of evidence — an
10[audit-tools](https://github.com/audit-labs/audit-tools) package, a folder of
11exported screenshots, anything — and it writes a `manifest.json` that pins every
12file's SHA-256 into a single Merkle fingerprint. Later, `verify` proves the
13directory is byte-for-byte what was sealed, and names anything that changed.
14
15- **Integrity** — detect any modified, added, or removed file.
16- **Chain of custody** — link sequential seals so a series of dated packages
17 forms an append-only history; reordering or removing one is detectable.
18- **Attribution** *(optional)* — sign a manifest with an ed25519 key so a named
19 party attests "I collected this," not just "it is unchanged."
20- **Trusted time** *(optional)* — obtain an RFC 3161 timestamp from an
21 independent authority so the seal is provably *not backdated*.
22
23The core (`seal`, `verify`, `chain`) is **pure standard library** — no
24dependencies. Signing needs `cryptography` (`evidence-seal[sign]`) and
25timestamping needs `asn1crypto` (`evidence-seal[timestamp]`).
26
27## Install
28
29```bash
30# Core is pure standard library; extras add signing + timestamping.
31pip install "evidence-seal[sign,timestamp]"
32
33# Or, for the zero-dependency core, drop the extras:
34pip install evidence-seal
35```
36
37To hack on it from a clone instead, see [Development](#development).
38
39## Usage
40
41```bash
42# Seal a package (manifest written to <dir>.manifest.json alongside it)
43evidence-seal seal ./output/aws_audit_prod_2026-01-01 \
44 --meta engagement=ACME-2026 --meta collector="Christian Cleberg"
45
46# Later, prove nothing changed
47evidence-seal verify ./output/aws_audit_prod_2026-01-01
48# -> intact — 8 files match the seal (exit 0)
49# -> TAMPERED … MODIFIED iam_users.csv (exit 1)
50```
51
52### Chain of custody
53
54Seal each new package against the previous manifest to build a verifiable
55timeline:
56
57```bash
58evidence-seal seal ./pkg_jan --out seals/jan.json
59evidence-seal seal ./pkg_feb --out seals/feb.json --prev seals/jan.json
60evidence-seal seal ./pkg_mar --out seals/mar.json --prev seals/feb.json
61
62evidence-seal chain seals/jan.json seals/feb.json seals/mar.json
63# -> chain intact — 3 seals link correctly
64```
65
66Each manifest's `id` is the hash of its own canonical contents, and `previous`
67holds the prior manifest's id — so a broken, reordered, or spliced-out link is
68caught.
69
70### Signing (attribution)
71
72```bash
73evidence-seal keygen --private acme.key --public acme.pub # once
74evidence-seal seal ./pkg --sign acme.key # seal + sign
75evidence-seal verify ./pkg --pubkey acme.pub # require this signer
76```
77
78Without `--pubkey`, a present signature is still checked for validity; with it,
79the signer's key must also match, proving *identity* and not just integrity.
80
81### Timestamping (trusted time)
82
83A signature says *who*; a timestamp says *when*, attested by an independent
84Time-Stamp Authority rather than the sealer's own clock. The TSA timestamps the
85manifest `id`, so one token vouches for the whole package.
86
87```bash
88# One step: request, POST to a TSA, and bind the token in
89evidence-seal timestamp submit pkg.manifest.json --tsa https://freetsa.org/tsr
90
91# Or split it — build a request, submit it however you like, then apply
92evidence-seal timestamp request pkg.manifest.json --out pkg.tsq
93curl -sS -H 'Content-Type: application/timestamp-query' \
94 --data-binary @pkg.tsq https://freetsa.org/tsr -o pkg.tsr
95evidence-seal timestamp apply pkg.manifest.json --token pkg.tsr
96
97evidence-seal timestamp verify pkg.manifest.json
98# -> timestamp OK: timestamped at 2026-08-06T09:00:00Z
99
100# Full verification: also check the token's CMS signature against the TSA cert
101evidence-seal timestamp verify pkg.manifest.json --tsa-cert freetsa.pem
102# -> timestamp OK: timestamped at 2026-08-06T09:00:00Z; TSA signature valid (CN=…)
103evidence-seal verify ./pkg --tsa-cert freetsa.pem # same check inside a full verify
104```
105
106`apply` refuses any token whose imprint is not this manifest's `id`. Because the
107`id` moves if a single byte changes, a token can never be transplanted onto
108tampered evidence — re-sealing after a change orphans the timestamp. A present
109timestamp is checked automatically during `verify`.
110
111With `--tsa-cert`, the token's RFC 3161 CMS signature is verified against the
112supplied certificate: the certificate must carry the timeStamping extended key
113usage, identify the token's signer, be valid at `gen_time`, and its key must
114verify the signature over the timestamped content. This authenticates the token
115against a TSA certificate you trust; establishing that the certificate itself
116chains to a known root is left to you (supply a cert you already trust).
117
118## The manifest
119
120Canonical JSON, sorted keys — diff-friendly and reproducible:
121
122```json
123{
124 "algorithm": "sha256",
125 "created_at": "2026-08-06T08:06:05Z",
126 "subject": "aws_audit_prod_2026-01-01",
127 "previous": null,
128 "metadata": { "collector": "Christian Cleberg", "engagement": "ACME-2026" },
129 "root": "23fdf7eb…",
130 "file_count": 8,
131 "files": [ { "path": "iam_users.csv", "sha256": "309b0e45…", "bytes": 412 } ],
132 "id": "08b846a0…",
133 "signature": { "algorithm": "ed25519", "public_key": "1bea5f1d…", "value": "2d7c2d63…" },
134 "timestamp": { "format": "rfc3161", "gen_time": "2026-08-06T09:00:00Z", "imprint": "08b846a0…", "token": "MIIB…" }
135}
136```
137
138- **`root`** — Merkle root over all `(path, sha256)` leaves; one value that
139 changes if any file, name, or byte changes.
140- **`id`** — SHA-256 of the manifest's canonical form (excluding `id`,
141 `signature`, and `timestamp`); makes it self-verifying and chainable. Because
142 the signature and the timestamp both attest *to* the id, they sit outside it
143 and compose in any order.
144- **`ignore`** — glob patterns skipped at seal time; `verify` reuses them so it
145 never false-flags an intentionally excluded file.
146
147## Exit codes
148
149| Code | Meaning |
150| --- | --- |
151| `0` | Intact / valid. |
152| `1` | Tamper detected, chain broken, or signature invalid. |
153| `2` | Usage error (missing directory, bad `--meta`, missing optional dependency). |
154
155Fail a pipeline on `1`; treat `2` as a misconfiguration to fix.
156
157## Threat model
158
159`evidence-seal` proves a directory matches a manifest, who produced it (when
160signed), and that it existed by a given time (when timestamped). An *unsigned,
161untimestamped* manifest can be regenerated by anyone with the files, and its
162`created_at` is self-reported. For a strong "sealed at time T by party P"
163guarantee, **sign** the manifest (retain the public key out of band) and
164**timestamp** it with a trusted TSA.
165
166**What a seal does not prove.** A seal proves the *package* is unchanged since it
167was sealed — nothing more. It says nothing about whether the collection faithfully
168represented the system at collection time: whether the right scope was captured,
169whether a query was complete, or whether the evidence was gathered from the
170production system at all. That is the question an auditor actually asks, and it is
171answered by collection controls and re-performance, not by this tool. Seal the
172evidence; don't mistake an intact seal for a trustworthy collection.
173
174Further limits to be honest about:
175
176- **`timestamp verify` checks the binding; `--tsa-cert` adds signature
177 verification but not chain-of-trust.** Without a cert, verification proves the
178 stored token timestamps this manifest's `id`. With `--tsa-cert`, it also
179 verifies the token's CMS signature, the timeStamping EKU, the signer match,
180 and validity at `gen_time`. It does **not** verify that the certificate chains
181 to a trusted root — supply a TSA certificate you already trust.
182- Private keys are written **unencrypted** — store them accordingly.
183
184## Stability
185
186`evidence-seal` is stable as of **v1.0.0** and follows [semantic versioning](https://semver.org).
187The `manifest.json` schema (`manifest_version`) and the CLI exit codes are a
188committed contract — neither changes in a breaking way without a major-version bump.
189
190## Development
191
192```bash
193pip install -e ".[dev]"
194pytest
195ruff check .
196./scripts/e2e.sh # every feature end-to-end through the CLI, offline
197```
198
199## License
200
201GPL-3.0-or-later. See [LICENSE](LICENSE).