gpa-verify 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpa_verify-1.0.0/.gitignore +19 -0
- gpa_verify-1.0.0/CHANGELOG.md +21 -0
- gpa_verify-1.0.0/LICENSE +21 -0
- gpa_verify-1.0.0/PKG-INFO +249 -0
- gpa_verify-1.0.0/README.md +219 -0
- gpa_verify-1.0.0/pyproject.toml +55 -0
- gpa_verify-1.0.0/src/gpa_verify/__init__.py +23 -0
- gpa_verify-1.0.0/src/gpa_verify/cli.py +164 -0
- gpa_verify-1.0.0/src/gpa_verify/core.py +971 -0
- gpa_verify-1.0.0/tests/fixtures/GetProofAnchor_Evidence_4d6ea8d6-10b4-414a-b336-c6f4b77732f7.zip +0 -0
- gpa_verify-1.0.0/tests/fixtures/GetProofAnchor_Evidence_aeb2b103-d7c5-441c-b540-c0df600a34cf.zip +0 -0
- gpa_verify-1.0.0/tests/test_verify.py +210 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `gpa-verify` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] — 2026-05-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
* Initial release.
|
|
12
|
+
* Seven-layer offline verification:
|
|
13
|
+
bundle integrity, cross-references, chain integrity, eIDAS RFC3161
|
|
14
|
+
signature (with timeStamping EKU enforcement), anchor canonical hash,
|
|
15
|
+
OpenTimestamps receipt structure, TLS evidence consistency.
|
|
16
|
+
* Pretty CLI output with colour and JSON output for automation.
|
|
17
|
+
* Support for evidence bundle formats `getproofanchor-evidence-1` and
|
|
18
|
+
`getproofanchor-evidence-2`.
|
|
19
|
+
* Pure-Python — no native dependencies, no GetProofAnchor servers.
|
|
20
|
+
|
|
21
|
+
[1.0.0]: https://github.com/getproofanchor/gpa-verify/releases/tag/v1.0.0
|
gpa_verify-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GetProofAnchor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpa-verify
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Independent verifier for GetProofAnchor evidence bundles
|
|
5
|
+
Project-URL: Homepage, https://getproofanchor.com
|
|
6
|
+
Project-URL: Repository, https://github.com/getproofanchor/gpa-verify
|
|
7
|
+
Project-URL: Issues, https://github.com/getproofanchor/gpa-verify/issues
|
|
8
|
+
Author-email: GetProofAnchor <support@getproofanchor.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: eidas,evidence,forensic,opentimestamps,rfc3161,verification
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: Intended Audience :: Legal Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security :: Cryptography
|
|
25
|
+
Classifier: Topic :: System :: Archiving
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: asn1crypto>=1.5
|
|
28
|
+
Requires-Dist: cryptography>=41.0
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# gpa-verify
|
|
32
|
+
|
|
33
|
+
**Independent verifier for GetProofAnchor evidence bundles.**
|
|
34
|
+
|
|
35
|
+
Reads ONLY the bundled ZIP — no network, no GetProofAnchor servers
|
|
36
|
+
required. Same input → same verdict on every machine.
|
|
37
|
+
|
|
38
|
+
[](https://github.com/getproofanchor/gpa-verify/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/gpa-verify/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Why does this exist?
|
|
45
|
+
|
|
46
|
+
A GetProofAnchor evidence bundle is a `.zip` containing a screenshot,
|
|
47
|
+
HTML, HAR, video, TLS chain, eIDAS qualified timestamp, OpenTimestamps
|
|
48
|
+
Bitcoin anchor, and an append-only hash chain. The bundle claims:
|
|
49
|
+
|
|
50
|
+
> *This URL existed in this exact form at this exact time.*
|
|
51
|
+
|
|
52
|
+
For court use, this claim must be **independently verifiable** — a
|
|
53
|
+
forensic expert appointed by a court should be able to confirm or deny
|
|
54
|
+
it without trusting GetProofAnchor.
|
|
55
|
+
|
|
56
|
+
`gpa-verify` is the reference implementation of that verification
|
|
57
|
+
protocol. It runs offline, has no GetProofAnchor dependencies at
|
|
58
|
+
runtime, and produces a deterministic verdict.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install gpa-verify
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires Python 3.9+. Two pure-Python wheels are installed automatically:
|
|
67
|
+
`asn1crypto` (RFC3161 / CMS / X.509 parsing) and `cryptography` (RSA /
|
|
68
|
+
ECDSA signature verification).
|
|
69
|
+
|
|
70
|
+
## Use
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gpa-verify path/to/GetProofAnchor_Evidence_*.zip
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
GetProofAnchor Evidence Verifier
|
|
78
|
+
────────────────────────────────────────────────────────────────
|
|
79
|
+
Proof ID: aeb2b103-d7c5-441c-b540-c0df600a34cf
|
|
80
|
+
Bundle format: getproofanchor-evidence-2
|
|
81
|
+
Generated at: 2026-05-07T08:30:25Z
|
|
82
|
+
|
|
83
|
+
[PASS] bundle_integrity
|
|
84
|
+
31/31 files OK
|
|
85
|
+
|
|
86
|
+
[PASS] cross_references
|
|
87
|
+
5/5 cross-checks OK
|
|
88
|
+
|
|
89
|
+
[PASS] chain_integrity
|
|
90
|
+
1 entries, all linked
|
|
91
|
+
|
|
92
|
+
[PASS] eidas_signature
|
|
93
|
+
valid RFC3161 token, signed by CN=SK TIMESTAMPING UNIT 2026R,
|
|
94
|
+
gen_time=2026-05-07T08:30:24+00:00
|
|
95
|
+
|
|
96
|
+
[PASS] anchor_canonical_hash
|
|
97
|
+
canonical SHA matches manifest (968f0c691384c87c...)
|
|
98
|
+
|
|
99
|
+
[PASS] ots_receipt
|
|
100
|
+
valid OTS proof for anchor SHA 968f0c69..., bitcoin status=pending
|
|
101
|
+
|
|
102
|
+
[PASS] tls_evidence
|
|
103
|
+
leaf cert SHA-256 matches tls.json (e7668f38708d0411...)
|
|
104
|
+
|
|
105
|
+
────────────────────────────────────────────────────────────────
|
|
106
|
+
✓ VERIFIED (7 passed, 0 failed, 0 skipped)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### JSON output for automation
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
gpa-verify --json bundle.zip > report.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Exit codes
|
|
116
|
+
|
|
117
|
+
| Code | Meaning |
|
|
118
|
+
|------|---------|
|
|
119
|
+
| 0 | All checks passed |
|
|
120
|
+
| 1 | At least one check failed |
|
|
121
|
+
| 2 | Invalid arguments / cannot read ZIP |
|
|
122
|
+
|
|
123
|
+
## What it actually checks
|
|
124
|
+
|
|
125
|
+
Verification runs in seven layers, each tightening the forensic claim.
|
|
126
|
+
**All layers must pass for a `VERIFIED` verdict.**
|
|
127
|
+
|
|
128
|
+
### Layer 1 — Bundle integrity
|
|
129
|
+
Every file declared in `manifest.json` exists and its SHA-256 matches.
|
|
130
|
+
Catches any byte-level tampering with the bundle.
|
|
131
|
+
|
|
132
|
+
### Layer 2 — Cross-references
|
|
133
|
+
`proof.json`'s SHA-256 claims for `screenshot.png`, `page.html`,
|
|
134
|
+
`content.txt`, and `capture/capture_meta.json` agree with actual file
|
|
135
|
+
contents. Sidecar `*.sha256` files agree. Catches selective tamper that
|
|
136
|
+
fixes one file but forgets another.
|
|
137
|
+
|
|
138
|
+
### Layer 3 — Chain integrity
|
|
139
|
+
`chain/proof_chain.jsonl` entries form a valid hash chain:
|
|
140
|
+
`entry_hash == SHA256(prev_hash | event_type | proof_id | data_hash)`,
|
|
141
|
+
recursively verified. `chain/chain_head.json` matches the last entry.
|
|
142
|
+
Catches tampering with the append-only event log.
|
|
143
|
+
|
|
144
|
+
### Layer 4 — eIDAS signature
|
|
145
|
+
**This is the cryptographic heart of the verification.**
|
|
146
|
+
The RFC3161 token in `timestamp/eidas.tsr` is verified for:
|
|
147
|
+
|
|
148
|
+
1. **Status** — token grant status is `granted` or `granted_with_mods`.
|
|
149
|
+
2. **Imprint** — the message digest the TSA signed equals
|
|
150
|
+
`SHA256(timestamp/eidas_payload.json)`. Binds the timestamp to *this*
|
|
151
|
+
evidence bundle, not someone else's.
|
|
152
|
+
3. **CMS signature** — the `SignedData` blob is verified against the
|
|
153
|
+
signer certificate's public key, with proper signed-attributes
|
|
154
|
+
re-encoding (`[0]` → `SET OF` per RFC 5652).
|
|
155
|
+
4. **timeStamping EKU** — the signer certificate has the
|
|
156
|
+
`id-kp-timeStamping` Extended Key Usage. Defends against substitution
|
|
157
|
+
from a non-timestamping certificate.
|
|
158
|
+
|
|
159
|
+
Together these prove the token can ONLY have been issued by the named
|
|
160
|
+
TSA, and ONLY for our exact payload.
|
|
161
|
+
|
|
162
|
+
### Layer 5 — Anchor canonical hash
|
|
163
|
+
`manifest.anchor.payload_sha256` equals SHA-256 of the canonical JSON
|
|
164
|
+
of `anchor/anchor_payload.json` (sorted keys, no whitespace, UTF-8).
|
|
165
|
+
Catches anchor receipt pointing to a different chain than ours.
|
|
166
|
+
|
|
167
|
+
### Layer 6 — OTS receipt structure
|
|
168
|
+
`anchor/anchor_receipt.ots` is a valid OpenTimestamps proof and its
|
|
169
|
+
file-hash field equals the anchor canonical SHA. Catches substituted
|
|
170
|
+
Bitcoin anchors. (Full Bitcoin block confirmation requires a Bitcoin
|
|
171
|
+
node and is out of scope; use the [`ots`
|
|
172
|
+
client](https://opentimestamps.org/) for that.)
|
|
173
|
+
|
|
174
|
+
### Layer 7 — TLS evidence
|
|
175
|
+
`tls/leaf_cert.pem` SHA-256 fingerprint matches the `network/tls.json`
|
|
176
|
+
claim. Confirms the TLS leaf certificate stored in the bundle is the
|
|
177
|
+
same one observed at capture time.
|
|
178
|
+
|
|
179
|
+
## Threat model
|
|
180
|
+
|
|
181
|
+
`gpa-verify` defends against:
|
|
182
|
+
|
|
183
|
+
| Attack | Caught by |
|
|
184
|
+
|--------|-----------|
|
|
185
|
+
| Modify any file (image, HTML, HAR, etc.) | Layer 1 |
|
|
186
|
+
| Modify file + manifest SHA | Layer 2 (proof.json) |
|
|
187
|
+
| Modify chain entry | Layer 3 |
|
|
188
|
+
| Forge eIDAS payload + update all SHAs | Layer 4 (imprint mismatch) |
|
|
189
|
+
| Substitute eIDAS token from non-TSA cert | Layer 4 (EKU check) |
|
|
190
|
+
| Substitute anchor for a different chain | Layer 5 |
|
|
191
|
+
| Substitute OTS receipt | Layer 6 |
|
|
192
|
+
| Substitute leaf TLS cert | Layer 7 |
|
|
193
|
+
|
|
194
|
+
It does NOT verify:
|
|
195
|
+
|
|
196
|
+
* Whether the OpenTimestamps receipt has been confirmed in a Bitcoin
|
|
197
|
+
block (use the `ots` client).
|
|
198
|
+
* Whether the TSA's certificate chain validates against the EU Trusted
|
|
199
|
+
List in real time (the bundle includes a snapshot of the EU Trusted
|
|
200
|
+
List as supporting context, but online validation is out of scope).
|
|
201
|
+
* Whether the captured page actually showed what the screenshot shows
|
|
202
|
+
(this is a content question, not a cryptographic one — open the
|
|
203
|
+
bundled `capture.webm` video for visual confirmation).
|
|
204
|
+
|
|
205
|
+
## API
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from gpa_verify import verify_evidence_zip
|
|
209
|
+
|
|
210
|
+
with open("evidence.zip", "rb") as f:
|
|
211
|
+
report = verify_evidence_zip(f.read())
|
|
212
|
+
|
|
213
|
+
if report.all_passed:
|
|
214
|
+
print(f"VERIFIED proof {report.proof_id}")
|
|
215
|
+
else:
|
|
216
|
+
for c in report.checks:
|
|
217
|
+
if not c.passed and not c.skipped:
|
|
218
|
+
print(f"FAIL {c.name}: {c.detail}")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Building from source
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
git clone https://github.com/getproofanchor/gpa-verify.git
|
|
225
|
+
cd gpa-verify
|
|
226
|
+
pip install -e .[dev]
|
|
227
|
+
pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT — see [LICENSE](LICENSE).
|
|
233
|
+
|
|
234
|
+
This tool is published as open source so any forensic expert,
|
|
235
|
+
court-appointed examiner, or independent journalist can audit the
|
|
236
|
+
verification logic line-by-line. The cryptographic algorithms used
|
|
237
|
+
(RFC 3161, CMS / RFC 5652, X.509, SHA-256, RSA, ECDSA, OpenTimestamps)
|
|
238
|
+
are open standards.
|
|
239
|
+
|
|
240
|
+
## Standards & references
|
|
241
|
+
|
|
242
|
+
* [eIDAS Regulation (EU) 910/2014](https://eur-lex.europa.eu/eli/reg/2014/910/oj),
|
|
243
|
+
Art. 41 — qualified electronic timestamps
|
|
244
|
+
* [RFC 3161](https://datatracker.ietf.org/doc/html/rfc3161) — Time-Stamp Protocol
|
|
245
|
+
* [RFC 5652](https://datatracker.ietf.org/doc/html/rfc5652) — Cryptographic Message Syntax
|
|
246
|
+
* [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280) — X.509 certificates
|
|
247
|
+
* [ISO/IEC 27037:2012](https://www.iso.org/standard/44381.html) — digital evidence guidelines
|
|
248
|
+
* [OpenTimestamps](https://opentimestamps.org/) — Bitcoin-anchored timestamps
|
|
249
|
+
* [EU Trusted List](https://ec.europa.eu/tools/lotl/eu-lotl.xml) — qualified TSPs
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# gpa-verify
|
|
2
|
+
|
|
3
|
+
**Independent verifier for GetProofAnchor evidence bundles.**
|
|
4
|
+
|
|
5
|
+
Reads ONLY the bundled ZIP — no network, no GetProofAnchor servers
|
|
6
|
+
required. Same input → same verdict on every machine.
|
|
7
|
+
|
|
8
|
+
[](https://github.com/getproofanchor/gpa-verify/actions/workflows/ci.yml)
|
|
9
|
+
[](https://pypi.org/project/gpa-verify/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why does this exist?
|
|
15
|
+
|
|
16
|
+
A GetProofAnchor evidence bundle is a `.zip` containing a screenshot,
|
|
17
|
+
HTML, HAR, video, TLS chain, eIDAS qualified timestamp, OpenTimestamps
|
|
18
|
+
Bitcoin anchor, and an append-only hash chain. The bundle claims:
|
|
19
|
+
|
|
20
|
+
> *This URL existed in this exact form at this exact time.*
|
|
21
|
+
|
|
22
|
+
For court use, this claim must be **independently verifiable** — a
|
|
23
|
+
forensic expert appointed by a court should be able to confirm or deny
|
|
24
|
+
it without trusting GetProofAnchor.
|
|
25
|
+
|
|
26
|
+
`gpa-verify` is the reference implementation of that verification
|
|
27
|
+
protocol. It runs offline, has no GetProofAnchor dependencies at
|
|
28
|
+
runtime, and produces a deterministic verdict.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install gpa-verify
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.9+. Two pure-Python wheels are installed automatically:
|
|
37
|
+
`asn1crypto` (RFC3161 / CMS / X.509 parsing) and `cryptography` (RSA /
|
|
38
|
+
ECDSA signature verification).
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gpa-verify path/to/GetProofAnchor_Evidence_*.zip
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
GetProofAnchor Evidence Verifier
|
|
48
|
+
────────────────────────────────────────────────────────────────
|
|
49
|
+
Proof ID: aeb2b103-d7c5-441c-b540-c0df600a34cf
|
|
50
|
+
Bundle format: getproofanchor-evidence-2
|
|
51
|
+
Generated at: 2026-05-07T08:30:25Z
|
|
52
|
+
|
|
53
|
+
[PASS] bundle_integrity
|
|
54
|
+
31/31 files OK
|
|
55
|
+
|
|
56
|
+
[PASS] cross_references
|
|
57
|
+
5/5 cross-checks OK
|
|
58
|
+
|
|
59
|
+
[PASS] chain_integrity
|
|
60
|
+
1 entries, all linked
|
|
61
|
+
|
|
62
|
+
[PASS] eidas_signature
|
|
63
|
+
valid RFC3161 token, signed by CN=SK TIMESTAMPING UNIT 2026R,
|
|
64
|
+
gen_time=2026-05-07T08:30:24+00:00
|
|
65
|
+
|
|
66
|
+
[PASS] anchor_canonical_hash
|
|
67
|
+
canonical SHA matches manifest (968f0c691384c87c...)
|
|
68
|
+
|
|
69
|
+
[PASS] ots_receipt
|
|
70
|
+
valid OTS proof for anchor SHA 968f0c69..., bitcoin status=pending
|
|
71
|
+
|
|
72
|
+
[PASS] tls_evidence
|
|
73
|
+
leaf cert SHA-256 matches tls.json (e7668f38708d0411...)
|
|
74
|
+
|
|
75
|
+
────────────────────────────────────────────────────────────────
|
|
76
|
+
✓ VERIFIED (7 passed, 0 failed, 0 skipped)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### JSON output for automation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
gpa-verify --json bundle.zip > report.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Exit codes
|
|
86
|
+
|
|
87
|
+
| Code | Meaning |
|
|
88
|
+
|------|---------|
|
|
89
|
+
| 0 | All checks passed |
|
|
90
|
+
| 1 | At least one check failed |
|
|
91
|
+
| 2 | Invalid arguments / cannot read ZIP |
|
|
92
|
+
|
|
93
|
+
## What it actually checks
|
|
94
|
+
|
|
95
|
+
Verification runs in seven layers, each tightening the forensic claim.
|
|
96
|
+
**All layers must pass for a `VERIFIED` verdict.**
|
|
97
|
+
|
|
98
|
+
### Layer 1 — Bundle integrity
|
|
99
|
+
Every file declared in `manifest.json` exists and its SHA-256 matches.
|
|
100
|
+
Catches any byte-level tampering with the bundle.
|
|
101
|
+
|
|
102
|
+
### Layer 2 — Cross-references
|
|
103
|
+
`proof.json`'s SHA-256 claims for `screenshot.png`, `page.html`,
|
|
104
|
+
`content.txt`, and `capture/capture_meta.json` agree with actual file
|
|
105
|
+
contents. Sidecar `*.sha256` files agree. Catches selective tamper that
|
|
106
|
+
fixes one file but forgets another.
|
|
107
|
+
|
|
108
|
+
### Layer 3 — Chain integrity
|
|
109
|
+
`chain/proof_chain.jsonl` entries form a valid hash chain:
|
|
110
|
+
`entry_hash == SHA256(prev_hash | event_type | proof_id | data_hash)`,
|
|
111
|
+
recursively verified. `chain/chain_head.json` matches the last entry.
|
|
112
|
+
Catches tampering with the append-only event log.
|
|
113
|
+
|
|
114
|
+
### Layer 4 — eIDAS signature
|
|
115
|
+
**This is the cryptographic heart of the verification.**
|
|
116
|
+
The RFC3161 token in `timestamp/eidas.tsr` is verified for:
|
|
117
|
+
|
|
118
|
+
1. **Status** — token grant status is `granted` or `granted_with_mods`.
|
|
119
|
+
2. **Imprint** — the message digest the TSA signed equals
|
|
120
|
+
`SHA256(timestamp/eidas_payload.json)`. Binds the timestamp to *this*
|
|
121
|
+
evidence bundle, not someone else's.
|
|
122
|
+
3. **CMS signature** — the `SignedData` blob is verified against the
|
|
123
|
+
signer certificate's public key, with proper signed-attributes
|
|
124
|
+
re-encoding (`[0]` → `SET OF` per RFC 5652).
|
|
125
|
+
4. **timeStamping EKU** — the signer certificate has the
|
|
126
|
+
`id-kp-timeStamping` Extended Key Usage. Defends against substitution
|
|
127
|
+
from a non-timestamping certificate.
|
|
128
|
+
|
|
129
|
+
Together these prove the token can ONLY have been issued by the named
|
|
130
|
+
TSA, and ONLY for our exact payload.
|
|
131
|
+
|
|
132
|
+
### Layer 5 — Anchor canonical hash
|
|
133
|
+
`manifest.anchor.payload_sha256` equals SHA-256 of the canonical JSON
|
|
134
|
+
of `anchor/anchor_payload.json` (sorted keys, no whitespace, UTF-8).
|
|
135
|
+
Catches anchor receipt pointing to a different chain than ours.
|
|
136
|
+
|
|
137
|
+
### Layer 6 — OTS receipt structure
|
|
138
|
+
`anchor/anchor_receipt.ots` is a valid OpenTimestamps proof and its
|
|
139
|
+
file-hash field equals the anchor canonical SHA. Catches substituted
|
|
140
|
+
Bitcoin anchors. (Full Bitcoin block confirmation requires a Bitcoin
|
|
141
|
+
node and is out of scope; use the [`ots`
|
|
142
|
+
client](https://opentimestamps.org/) for that.)
|
|
143
|
+
|
|
144
|
+
### Layer 7 — TLS evidence
|
|
145
|
+
`tls/leaf_cert.pem` SHA-256 fingerprint matches the `network/tls.json`
|
|
146
|
+
claim. Confirms the TLS leaf certificate stored in the bundle is the
|
|
147
|
+
same one observed at capture time.
|
|
148
|
+
|
|
149
|
+
## Threat model
|
|
150
|
+
|
|
151
|
+
`gpa-verify` defends against:
|
|
152
|
+
|
|
153
|
+
| Attack | Caught by |
|
|
154
|
+
|--------|-----------|
|
|
155
|
+
| Modify any file (image, HTML, HAR, etc.) | Layer 1 |
|
|
156
|
+
| Modify file + manifest SHA | Layer 2 (proof.json) |
|
|
157
|
+
| Modify chain entry | Layer 3 |
|
|
158
|
+
| Forge eIDAS payload + update all SHAs | Layer 4 (imprint mismatch) |
|
|
159
|
+
| Substitute eIDAS token from non-TSA cert | Layer 4 (EKU check) |
|
|
160
|
+
| Substitute anchor for a different chain | Layer 5 |
|
|
161
|
+
| Substitute OTS receipt | Layer 6 |
|
|
162
|
+
| Substitute leaf TLS cert | Layer 7 |
|
|
163
|
+
|
|
164
|
+
It does NOT verify:
|
|
165
|
+
|
|
166
|
+
* Whether the OpenTimestamps receipt has been confirmed in a Bitcoin
|
|
167
|
+
block (use the `ots` client).
|
|
168
|
+
* Whether the TSA's certificate chain validates against the EU Trusted
|
|
169
|
+
List in real time (the bundle includes a snapshot of the EU Trusted
|
|
170
|
+
List as supporting context, but online validation is out of scope).
|
|
171
|
+
* Whether the captured page actually showed what the screenshot shows
|
|
172
|
+
(this is a content question, not a cryptographic one — open the
|
|
173
|
+
bundled `capture.webm` video for visual confirmation).
|
|
174
|
+
|
|
175
|
+
## API
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from gpa_verify import verify_evidence_zip
|
|
179
|
+
|
|
180
|
+
with open("evidence.zip", "rb") as f:
|
|
181
|
+
report = verify_evidence_zip(f.read())
|
|
182
|
+
|
|
183
|
+
if report.all_passed:
|
|
184
|
+
print(f"VERIFIED proof {report.proof_id}")
|
|
185
|
+
else:
|
|
186
|
+
for c in report.checks:
|
|
187
|
+
if not c.passed and not c.skipped:
|
|
188
|
+
print(f"FAIL {c.name}: {c.detail}")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Building from source
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/getproofanchor/gpa-verify.git
|
|
195
|
+
cd gpa-verify
|
|
196
|
+
pip install -e .[dev]
|
|
197
|
+
pytest
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT — see [LICENSE](LICENSE).
|
|
203
|
+
|
|
204
|
+
This tool is published as open source so any forensic expert,
|
|
205
|
+
court-appointed examiner, or independent journalist can audit the
|
|
206
|
+
verification logic line-by-line. The cryptographic algorithms used
|
|
207
|
+
(RFC 3161, CMS / RFC 5652, X.509, SHA-256, RSA, ECDSA, OpenTimestamps)
|
|
208
|
+
are open standards.
|
|
209
|
+
|
|
210
|
+
## Standards & references
|
|
211
|
+
|
|
212
|
+
* [eIDAS Regulation (EU) 910/2014](https://eur-lex.europa.eu/eli/reg/2014/910/oj),
|
|
213
|
+
Art. 41 — qualified electronic timestamps
|
|
214
|
+
* [RFC 3161](https://datatracker.ietf.org/doc/html/rfc3161) — Time-Stamp Protocol
|
|
215
|
+
* [RFC 5652](https://datatracker.ietf.org/doc/html/rfc5652) — Cryptographic Message Syntax
|
|
216
|
+
* [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280) — X.509 certificates
|
|
217
|
+
* [ISO/IEC 27037:2012](https://www.iso.org/standard/44381.html) — digital evidence guidelines
|
|
218
|
+
* [OpenTimestamps](https://opentimestamps.org/) — Bitcoin-anchored timestamps
|
|
219
|
+
* [EU Trusted List](https://ec.europa.eu/tools/lotl/eu-lotl.xml) — qualified TSPs
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gpa-verify"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Independent verifier for GetProofAnchor evidence bundles"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "GetProofAnchor", email = "support@getproofanchor.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["forensic", "evidence", "rfc3161", "eidas", "opentimestamps", "verification"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Legal Industry",
|
|
19
|
+
"Intended Audience :: Information Technology",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Security :: Cryptography",
|
|
30
|
+
"Topic :: System :: Archiving",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"asn1crypto>=1.5",
|
|
34
|
+
"cryptography>=41.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://getproofanchor.com"
|
|
39
|
+
Repository = "https://github.com/getproofanchor/gpa-verify"
|
|
40
|
+
Issues = "https://github.com/getproofanchor/gpa-verify/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
gpa-verify = "gpa_verify.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/gpa_verify"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = [
|
|
50
|
+
"/src",
|
|
51
|
+
"/tests",
|
|
52
|
+
"/README.md",
|
|
53
|
+
"/LICENSE",
|
|
54
|
+
"/CHANGELOG.md",
|
|
55
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gpa-verify — independent verifier for GetProofAnchor evidence bundles.
|
|
3
|
+
|
|
4
|
+
This package is the reference implementation of the verification protocol
|
|
5
|
+
documented at https://getproofanchor.com/verify. It does NOT depend on
|
|
6
|
+
any GetProofAnchor server — given an evidence ZIP and the bundled
|
|
7
|
+
artifacts, it produces a deterministic verdict.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
|
|
12
|
+
from .core import (
|
|
13
|
+
CheckResult,
|
|
14
|
+
VerifyReport,
|
|
15
|
+
verify_evidence_zip,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CheckResult",
|
|
20
|
+
"VerifyReport",
|
|
21
|
+
"verify_evidence_zip",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|