Independent verifier for GetProofAnchor evidence bundles
Project description
gpa-verify
Independent verifier for GetProofAnchor evidence bundles.
Reads only the bundled ZIP — no network requests, no GetProofAnchor servers contacted at any point during verification. 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, the cryptographic part of that claim must be independently verifiable — a forensic expert appointed by a court should be able to confirm or deny it without relying on the GetProofAnchor service.
gpa-verify is the reference implementation of that verification
protocol. It runs offline, contacts no external services at runtime,
and produces a deterministic verdict.
Scope of verification
This tool verifies the cryptographic seals on an evidence bundle. A successful verdict establishes that the bundle is internally consistent, cryptographically intact since sealing, and bound to a qualified electronic timestamp from an EU-accredited Trust Service Provider.
It does not, on its own, establish that the screenshot or HTML faithfully represent what an ordinary visitor would have seen at the captured URL — that question depends on the trustworthiness of the capture process and is supported (but not proved) by the bundled video recording, HAR network log, and TLS evidence. A complete forensic opinion combines this cryptographic verification with the visual and network record, and any contextual evidence available to the examiner.
See the Threat model section below for a precise statement of what each layer catches and what is out of scope.
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).
Note on Python command name: On Linux and macOS the command is
python3andpip3; on Windows it ispythonandpip.
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.
Supported signature algorithms: RSA-PKCS1v15, ECDSA, RSA-PSS (across SHA-256, SHA-384, SHA-512 digests).
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 receipts 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 therefore 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 the following attacks:
| 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. This requires an online Bitcoin node — use the
otsclient. - Whether the TSA's certificate is currently listed on the EU Trusted List. The bundle includes a snapshot of the EU Trusted List as supporting context, but real-time validation against the live list is out of scope for an offline tool.
- Whether the captured page actually showed what the screenshot
shows. This is a content question, not a cryptographic one. The
bundled
capture.webmvideo, HAR network log, and TLS evidence exist to support that forensic judgement, but interpreting them is the job of a qualified examiner.
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, Article 41 — qualified electronic timestamps and their legal effect across EU Member States
- ETSI EN 319 422 — Time-stamping protocol and electronic time-stamp profiles
- ETSI EN 319 421 — Policy and security requirements for trust service providers issuing time-stamps
- ETSI EN 319 102-1 — Procedures for creation and validation of AdES digital signatures
- ISO/IEC 27037:2012 — Guidelines for identification, collection, acquisition and preservation of digital evidence
- RFC 3161 — Time-Stamp Protocol (TSP)
- RFC 5652 — Cryptographic Message Syntax (CMS)
- RFC 5280 — X.509 certificates
- 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.1.0.tar.gz.
File metadata
- Download URL: gpa_verify-1.1.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 |
b012490500b4023ee0d7641f2b32bc25d21a997d7bd2afaa9357e8b99e5c12af
|
|
| MD5 |
e84ed7b8b775d2e8f2c37c0fbc65c4ca
|
|
| BLAKE2b-256 |
717af411c62053b4bd2a6df867d5cfcaefd8e5191c9bf2fa29e5e1905586900b
|
Provenance
The following attestation bundles were made for gpa_verify-1.1.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.1.0.tar.gz -
Subject digest:
b012490500b4023ee0d7641f2b32bc25d21a997d7bd2afaa9357e8b99e5c12af - Sigstore transparency entry: 1470398172
- Sigstore integration time:
-
Permalink:
Getproofanchor/gpa-verify@a0bd13f337d9760697f39afdfd278bc28e6f6958 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Getproofanchor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a0bd13f337d9760697f39afdfd278bc28e6f6958 -
Trigger Event:
release
-
Statement type:
File details
Details for the file gpa_verify-1.1.0-py3-none-any.whl.
File metadata
- Download URL: gpa_verify-1.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 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 |
e59dc2a24edcd89d49716bef67b525e41188d4fd0f1383e514f30b6dad0b0d15
|
|
| MD5 |
e2e7ebc58c15becc6a2c5e330b292a49
|
|
| BLAKE2b-256 |
18ccb9cd68575b3b71576f92d9c8749186771a1e635e88811de183afd506f669
|
Provenance
The following attestation bundles were made for gpa_verify-1.1.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.1.0-py3-none-any.whl -
Subject digest:
e59dc2a24edcd89d49716bef67b525e41188d4fd0f1383e514f30b6dad0b0d15 - Sigstore transparency entry: 1470398345
- Sigstore integration time:
-
Permalink:
Getproofanchor/gpa-verify@a0bd13f337d9760697f39afdfd278bc28e6f6958 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Getproofanchor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a0bd13f337d9760697f39afdfd278bc28e6f6958 -
Trigger Event:
release
-
Statement type: