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.
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.
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.
pip install gpa-verifyRequires 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.
gpa-verify path/to/GetProofAnchor_Evidence_*.zipGetProofAnchor 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)
gpa-verify --json bundle.zip > report.json| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 1 | At least one check failed |
| 2 | Invalid arguments / cannot read ZIP |
Verification runs in seven layers, each tightening the forensic claim.
All layers must pass for a VERIFIED verdict.
Every file declared in manifest.json exists and its SHA-256 matches.
Catches any byte-level tampering with the bundle.
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.
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.
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).
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.
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.)
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.
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.
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}")git clone https://github.com/Getproofanchor/gpa-verify.git
cd gpa-verify
pip install -e .[dev]
pytestMIT — 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.
- 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