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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

actaseal-1.0.0.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

actaseal-1.0.0-py3-none-any.whl (17.2 kB view details)

Uploaded Python 3

File details

Details for the file actaseal-1.0.0.tar.gz.

File metadata

  • Download URL: actaseal-1.0.0.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for actaseal-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9ac5f5005ce19320a8b2b8aa041fcfdb91294634a1853a74addd5f864a2c60ce
MD5 fc53ae8a31767ebc3c0c9989ea177036
BLAKE2b-256 ef00af7eba4d6b1428daa55680ac9969db9c37584305d80ac0ca6f3e2cc98136

See more details on using hashes here.

File details

Details for the file actaseal-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: actaseal-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 17.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for actaseal-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 33110ee0739c3ef7efde0fccb24641517804c86069c1e6b92bbc7b5d64eb486d
MD5 15fd5fa5106a7bee3be06fc6b4f7e75d
BLAKE2b-256 92b5989dd3eafcd80c97a4bdccd199053f0d624383f4d33dfb609e7b0e8a039e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page