Observed since 20 Oct, 18:29 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Standalone offline verifier for ActaSeal dispute evidence packets
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:
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.
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.
@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.
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.
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:
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.
Passing does not mean "ActaSeal says this is fine." It means every one of these independently re-derives from data already inside the packet:
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.manifest.json's embedded public key, over the canonical JSON
of the receipt (minus the signature field itself).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.ScopeEvaluated ledger event
whose recorded scope/action hashes recompute correctly.anchor_hash recomputes from the anchor
content; an unanchored packet says so explicitly (never silent).See SPEC.md for the full packet format.
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).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
Apache-2.0. See LICENSE.
See SECURITY.md for how to report a vulnerability.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 1.0.0 | 20 Jul 2026 | 1 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 18:29 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:1deca6f39a73a61fd6769ec4e1d1ab8257f8dccccbb44bd0485217f8ba642a3a