Skip to main content

actaseal-verify

Verify an ActaSeal dispute evidence packet yourself, in about 60 seconds, without installing ActaSeal, without a network call, and without trusting us. This repo is one script (verify.py) plus a spec.

Who this is for: an underwriter, dispute analyst, or auditor who has been handed a packet (a .zip) and wants to check, independently, that:

  • the evidence chain hasn't been tampered with (every event's hash recomputes from its own recorded content, and the chain is unbroken);
  • the receipt's signature is genuinely valid over that chain;
  • the chain-of-custody and acquisition documents (the FRE 901/902 basis for admissibility) are present and internally consistent;
  • if the packet claims a payment-rail settlement anchor, that anchor's hash actually recomputes from its declared content.

Verify a packet in 60 seconds

pip install cryptography     # the only third-party dependency
unzip your-packet.zip -d packet
python verify.py packet

(Or pip install . from this repo, then python -m verify packet.)

Exit code 0 and VERIFIED: ... means every check above passed. Exit code 1 prints exactly which check failed and why -- nothing is silently waved through. Exit code 2 means cryptography isn't installed (no partial/soft-pass is possible).

Try it right now against the demo packets checked into this repo:

python verify.py demo/demo-packet-unanchored
python verify.py demo/demo-packet-anchored   # includes a rail settlement anchor

Or try it entirely in your browser, no install: tamper_demo/ loads a real signed packet, lets you edit any field, and re-verifies live as you type -- break the hash chain or the signature and watch it turn red immediately. Nothing is sent anywhere; it's a JS port of the same checks (tamper_demo/verify_min.js), using the browser's native WebCrypto Ed25519 support.

Use it in CI

action.yml in this repository is a reusable GitHub Action:

- uses: actaseal/actaseal-verify@main
  with:
    packet-dir: ./extracted-packet
    # anchors: ./anchors.jsonl   # optional

Fails the step (non-zero exit) if the packet doesn't verify.

Use it from Node / a browser

@actaseal/verify (tamper_demo/, npm install @actaseal/verify) is a minimal, zero-dependency WebCrypto port covering chain integrity and Ed25519 receipt signatures only -- see tamper_demo/README.npm.md for its exact (reduced) scope before relying on it for anything beyond a quick client-side check. The real verify.py above is the canonical, full-scope verifier.

Telemetry: opt-in, count-only, off by default, NOT in verify.py itself

verify.py makes zero network calls, period -- that's a real, tested guarantee (tests/test_verify_demo_packet.py:: test_verify_has_no_external_imports_beyond_cryptography), not a default that can be toggled. Telemetry lives entirely in a separate, optional wrapper instead: verify_with_telemetry.py calls verify.py's own main() completely unchanged, and only after that call returns does it (optionally) send a ping -- verify.py itself never imports urllib or knows telemetry exists.

python verify_with_telemetry.py --telemetry <packet_dir>

Even with --telemetry passed, the wrapper ships with TELEMETRY_ENDPOINT set to an empty string, so it's currently an inert no-op regardless -- nothing is transmitted until a maintainer deliberately sets a real endpoint in a future release. When/if that happens, a ping sends exactly three fields: the wrapper's own version string, the verification outcome (PASS/FAIL), and a failure count (derived by counting verify.py's own printed failure lines, never by inspecting packet content) -- never a filename, never anything about the specific receipt or ledger being verified. Best-effort only: any network failure is silently swallowed and never affects the exit code or printed result, which are always exactly what plain python verify.py would have produced.

Why "zero-import" is the whole point

verify.py has no dependency on ActaSeal's code, config, database, or network access -- it is standalone by construction (see the assertion in tests/test_verify_demo_packet.py::test_verify_has_no_external_imports_beyond_cryptography). The only third-party package it needs is cryptography, for Ed25519/ECDSA signature checks. That means:

  • you can read the whole verifier in one sitting (~360 lines, no framework, no magic) and know exactly what it checks;
  • you never run vendor code against your own systems to check evidence someone handed you -- it only reads files from the packet directory;
  • the packet embeds its own trust root (manifest.json's receipt_public_key_hex) -- verification needs no external key file, registry, or live service lookup. You separately compare that key against the operator's published key out of band; the packet cannot forge that comparison, only be internally consistent or not.

This is the file that ships inside every real ActaSeal dispute packet as verify.py -- what you're running here is not a simplified demo version, it's the literal artifact.

What "VERIFIED" actually means

Passing does not mean "ActaSeal says this is fine." It means every one of these independently re-derives from data already inside the packet:

  1. Ledger chain integrity -- each event's payload_hash, event_id, and event_hash recompute from that event's own fields; the slice is an unbroken hash chain from the manifest's declared starting point.
  2. Receipt signature -- the receipt's Ed25519/ECDSA signature verifies against manifest.json's embedded public key, over the canonical JSON of the receipt (minus the signature field itself).
  3. Receipt-to-chain binding -- the receipt's ledger_entry_hash names a real event in the slice, for the same action, and that event's recorded action hash matches the receipt's.
  4. Chain-of-custody / acquisition basis (FRE 901(b)(9), 902(13)-(14)) -- the acquisition report names its tool, clock source, and custodian; custody events are time-ordered and each bound to a real ledger event hash; the authentication statement's key id, pubkey hash, and mandate hash match the receipt.
  5. Scope conformance -- if the receipt claims an in-scope or breach verdict, that verdict traces back to a ScopeEvaluated ledger event whose recorded scope/action hashes recompute correctly.
  6. Settlement anchor -- if the packet claims a payment-rail settlement anchor, its anchor_hash recomputes from the anchor content; an unanchored packet says so explicitly (never silent).

See SPEC.md for the full packet format.

What this does not verify

  • That the underlying transaction/refund/decision was correct -- only that the recorded evidence is internally consistent and unaltered.
  • That receipt_public_key_hex belongs to the operator you think it does -- that binding is on you, out of band (e.g. compare against a key the operator has published elsewhere).
  • Anything about a deployment you haven't been handed a packet from.

Repo layout

verify.py                    the verifier -- the only file that matters at verify time
generate_demo_packet.py      builds the two demo packets below, from scratch, standalone
demo/demo-packet-unanchored/ a valid packet with no settlement anchor
demo/demo-packet-anchored/   a valid packet with a payment-rail settlement anchor
tamper_demo/                 in-browser, editable, live-verifying demo (see above)
SPEC.md                      packet format reference
tests/                       pytest suite: valid packets pass, tampered ones fail loudly

License

Apache-2.0. See LICENSE.

Security

See SECURITY.md for how to report a vulnerability.

Metadata

Release files for actaseal 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for actaseal 1.0.0
File Size Uploaded
actaseal-1.0.0.tar.gz 23.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for actaseal 1.0.0
File Interpreter ABI Platform
actaseal-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.3 kB

Release files / actaseal-1.0.0.tar.gz

Download URL actaseal-1.0.0.tar.gz
Size 23.1 kB
Tags Source
SHA-256 checksum
How to use checksums
9ac5f5005ce19320a8b2b8aa041fcfdb91294634a1853a74addd5f864a2c60ce
BLAKE2b-256 checksum
How to use checksums
ef00af7eba4d6b1428daa55680ac9969db9c37584305d80ac0ca6f3e2cc98136
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release files / actaseal-1.0.0-py3-none-any.whl

Download URL actaseal-1.0.0-py3-none-any.whl
Size 17.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
33110ee0739c3ef7efde0fccb24641517804c86069c1e6b92bbc7b5d64eb486d
BLAKE2b-256 checksum
How to use checksums
92b5989dd3eafcd80c97a4bdccd199053f0d624383f4d33dfb609e7b0e8a039e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page