Independent verifier for GetProofAnchor evidence bundles
Project description
gpa-verify
Independent verifier for GetProofAnchor evidence bundles.
Reads ONLY the bundled ZIP — no network, no GetProofAnchor servers required. Same input → same verdict on every machine.
Why does this exist?
A GetProofAnchor evidence bundle is a .zip containing a screenshot,
HTML, HAR, video, TLS chain, eIDAS qualified timestamp, OpenTimestamps
Bitcoin anchor, and an append-only hash chain. The bundle claims:
This URL existed in this exact form at this exact time.
For court use, this claim must be independently verifiable — a forensic expert appointed by a court should be able to confirm or deny it without trusting GetProofAnchor.
gpa-verify is the reference implementation of that verification
protocol. It runs offline, has no GetProofAnchor dependencies at
runtime, and produces a deterministic verdict.
Install
pip install gpa-verify
Requires Python 3.9+. Two pure-Python wheels are installed automatically:
asn1crypto (RFC3161 / CMS / X.509 parsing) and cryptography (RSA /
ECDSA signature verification).
Use
gpa-verify path/to/GetProofAnchor_Evidence_*.zip
GetProofAnchor Evidence Verifier
────────────────────────────────────────────────────────────────
Proof ID: aeb2b103-d7c5-441c-b540-c0df600a34cf
Bundle format: getproofanchor-evidence-2
Generated at: 2026-05-07T08:30:25Z
[PASS] bundle_integrity
31/31 files OK
[PASS] cross_references
5/5 cross-checks OK
[PASS] chain_integrity
1 entries, all linked
[PASS] eidas_signature
valid RFC3161 token, signed by CN=SK TIMESTAMPING UNIT 2026R,
gen_time=2026-05-07T08:30:24+00:00
[PASS] anchor_canonical_hash
canonical SHA matches manifest (968f0c691384c87c...)
[PASS] ots_receipt
valid OTS proof for anchor SHA 968f0c69..., bitcoin status=pending
[PASS] tls_evidence
leaf cert SHA-256 matches tls.json (e7668f38708d0411...)
────────────────────────────────────────────────────────────────
✓ VERIFIED (7 passed, 0 failed, 0 skipped)
JSON output for automation
gpa-verify --json bundle.zip > report.json
Exit codes
| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 1 | At least one check failed |
| 2 | Invalid arguments / cannot read ZIP |
What it actually checks
Verification runs in seven layers, each tightening the forensic claim.
All layers must pass for a VERIFIED verdict.
Layer 1 — Bundle integrity
Every file declared in manifest.json exists and its SHA-256 matches.
Catches any byte-level tampering with the bundle.
Layer 2 — Cross-references
proof.json's SHA-256 claims for screenshot.png, page.html,
content.txt, and capture/capture_meta.json agree with actual file
contents. Sidecar *.sha256 files agree. Catches selective tamper that
fixes one file but forgets another.
Layer 3 — Chain integrity
chain/proof_chain.jsonl entries form a valid hash chain:
entry_hash == SHA256(prev_hash | event_type | proof_id | data_hash),
recursively verified. chain/chain_head.json matches the last entry.
Catches tampering with the append-only event log.
Layer 4 — eIDAS signature
This is the cryptographic heart of the verification.
The RFC3161 token in timestamp/eidas.tsr is verified for:
- Status — token grant status is
grantedorgranted_with_mods. - Imprint — the message digest the TSA signed equals
SHA256(timestamp/eidas_payload.json). Binds the timestamp to this evidence bundle, not someone else's. - CMS signature — the
SignedDatablob is verified against the signer certificate's public key, with proper signed-attributes re-encoding ([0]→SET OFper RFC 5652). - timeStamping EKU — the signer certificate has the
id-kp-timeStampingExtended Key Usage. Defends against substitution from a non-timestamping certificate.
Together these prove the token can ONLY have been issued by the named TSA, and ONLY for our exact payload.
Layer 5 — Anchor canonical hash
manifest.anchor.payload_sha256 equals SHA-256 of the canonical JSON
of anchor/anchor_payload.json (sorted keys, no whitespace, UTF-8).
Catches anchor receipt pointing to a different chain than ours.
Layer 6 — OTS receipt structure
anchor/anchor_receipt.ots is a valid OpenTimestamps proof and its
file-hash field equals the anchor canonical SHA. Catches substituted
Bitcoin anchors. (Full Bitcoin block confirmation requires a Bitcoin
node and is out of scope; use the ots
client for that.)
Layer 7 — TLS evidence
tls/leaf_cert.pem SHA-256 fingerprint matches the network/tls.json
claim. Confirms the TLS leaf certificate stored in the bundle is the
same one observed at capture time.
Threat model
gpa-verify defends against:
| Attack | Caught by |
|---|---|
| Modify any file (image, HTML, HAR, etc.) | Layer 1 |
| Modify file + manifest SHA | Layer 2 (proof.json) |
| Modify chain entry | Layer 3 |
| Forge eIDAS payload + update all SHAs | Layer 4 (imprint mismatch) |
| Substitute eIDAS token from non-TSA cert | Layer 4 (EKU check) |
| Substitute anchor for a different chain | Layer 5 |
| Substitute OTS receipt | Layer 6 |
| Substitute leaf TLS cert | Layer 7 |
It does NOT verify:
- Whether the OpenTimestamps receipt has been confirmed in a Bitcoin
block (use the
otsclient). - Whether the TSA's certificate chain validates against the EU Trusted List in real time (the bundle includes a snapshot of the EU Trusted List as supporting context, but online validation is out of scope).
- Whether the captured page actually showed what the screenshot shows
(this is a content question, not a cryptographic one — open the
bundled
capture.webmvideo for visual confirmation).
API
from gpa_verify import verify_evidence_zip
with open("evidence.zip", "rb") as f:
report = verify_evidence_zip(f.read())
if report.all_passed:
print(f"VERIFIED proof {report.proof_id}")
else:
for c in report.checks:
if not c.passed and not c.skipped:
print(f"FAIL {c.name}: {c.detail}")
Building from source
git clone https://github.com/getproofanchor/gpa-verify.git
cd gpa-verify
pip install -e .[dev]
pytest
License
MIT — see LICENSE.
This tool is published as open source so any forensic expert, court-appointed examiner, or independent journalist can audit the verification logic line-by-line. The cryptographic algorithms used (RFC 3161, CMS / RFC 5652, X.509, SHA-256, RSA, ECDSA, OpenTimestamps) are open standards.
Standards & references
- eIDAS Regulation (EU) 910/2014, Art. 41 — qualified electronic timestamps
- RFC 3161 — Time-Stamp Protocol
- RFC 5652 — Cryptographic Message Syntax
- RFC 5280 — X.509 certificates
- ISO/IEC 27037:2012 — digital evidence guidelines
- OpenTimestamps — Bitcoin-anchored timestamps
- EU Trusted List — qualified TSPs
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gpa_verify-1.0.0.tar.gz.
File metadata
- Download URL: gpa_verify-1.0.0.tar.gz
- Upload date:
- Size: 30.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4511ab41feb6cdaa665fa62be6fc1fd20365650595cd1329fc30dba8f888e6c7
|
|
| MD5 |
ab78ca953c56f95cd7c271743db2f34e
|
|
| BLAKE2b-256 |
e9cd7306bf73b7c23bf799dece59281ba0c3770f7504a88bcc526e6071c4b758
|
Provenance
The following attestation bundles were made for gpa_verify-1.0.0.tar.gz:
Publisher:
publish.yml on Getproofanchor/gpa-verify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gpa_verify-1.0.0.tar.gz -
Subject digest:
4511ab41feb6cdaa665fa62be6fc1fd20365650595cd1329fc30dba8f888e6c7 - Sigstore transparency entry: 1460041997
- Sigstore integration time:
-
Permalink:
Getproofanchor/gpa-verify@b740e2faa540c9beca5f0bbaa0d595f1571d4d2d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Getproofanchor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b740e2faa540c9beca5f0bbaa0d595f1571d4d2d -
Trigger Event:
release
-
Statement type:
File details
Details for the file gpa_verify-1.0.0-py3-none-any.whl.
File metadata
- Download URL: gpa_verify-1.0.0-py3-none-any.whl
- Upload date:
- Size: 17.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04e1e7e31520f0d67a648e0cc4423b859e250a56da62ccd6318048a4dde969e5
|
|
| MD5 |
6562570c6847dfab3945aa47f0e8a90f
|
|
| BLAKE2b-256 |
310a0af71712aca5855979de735b99f86028be3c046b9834a0845aeb2224a575
|
Provenance
The following attestation bundles were made for gpa_verify-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on Getproofanchor/gpa-verify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gpa_verify-1.0.0-py3-none-any.whl -
Subject digest:
04e1e7e31520f0d67a648e0cc4423b859e250a56da62ccd6318048a4dde969e5 - Sigstore transparency entry: 1460042157
- Sigstore integration time:
-
Permalink:
Getproofanchor/gpa-verify@b740e2faa540c9beca5f0bbaa0d595f1571d4d2d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Getproofanchor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b740e2faa540c9beca5f0bbaa0d595f1571d4d2d -
Trigger Event:
release
-
Statement type: