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'sreceipt_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:
- Ledger chain integrity -- each event's
payload_hash,event_id, andevent_hashrecompute from that event's own fields; the slice is an unbroken hash chain from the manifest's declared starting point. - 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). - Receipt-to-chain binding -- the receipt's
ledger_entry_hashnames a real event in the slice, for the same action, and that event's recorded action hash matches the receipt's. - 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.
- Scope conformance -- if the receipt claims an in-scope or breach
verdict, that verdict traces back to a
ScopeEvaluatedledger event whose recorded scope/action hashes recompute correctly. - Settlement anchor -- if the packet claims a payment-rail
settlement anchor, its
anchor_hashrecomputes 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_hexbelongs 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)
| File | Size | Uploaded | |
|---|---|---|---|
| actaseal-1.0.0.tar.gz | 23.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|