sengol-verify
A standalone, offline verifier for Sengol signed
evidence bundles — the file sengol audit export produces.
What it verifies
Given a bundle (records, Ed25519 countersignatures, Merkle anchor receipts, and public key material), it recomputes these checks entirely from the bytes in the file. Step 0 and step 7 are named for the checks of the same name in sengol's own verifier; the rest are numbered as before:
- Canonical payload — every record has a canonical payload at all. A
value with no RFC 8785 form (NaN, infinity, an integer beyond 2^53), a
payload version the family does not register, or a
record_typethis tool does not register FAILs here, naming the record. - Payload hash integrity — each record's canonical payload still hashes to the value its countersignature committed to.
- HMAC verification — if the bundle embeds HMAC key material (rare; normally withheld for security), the symmetric signature is checked too.
- Hash-chain continuity —
sequence_numberis monotone per(tenant_id, agent_id)partition and everyprev_hashlinks to the previous record's canonical-payload hash. - Ed25519 countersignature verification — each countersignature is checked against its record's payload hash. The key it is checked against decides what a pass means; see "Trusting the countersignature key" below.
- Merkle anchor coverage — each anchor receipt's Merkle root is recomputed from its leaf hashes and compared.
- Field coverage — a report, not a check: it never FAILs. Every set
field is signed except the signature fields, the family's unsigned fields
and a few nested ones (an evaluator score's
reasonandreason_status, a regression case binding'sconfigandreference; all listed under "How"), so steps 1-5 already cover everything else. This step names which of those by-design unsigned fields the bundle's records actually carry (for examplehmac_signature,eval_result.scores.reason), so you know which values no check vouches for. Its row always reads PASS; it does not detect an edit to an unsigned field, and a record with an unregisteredrecord_typeis not assessed (the detail says how many). - Signed-field presence — the keys this tool would otherwise read with a
default (
record_id,tenant_id,agent_id,sequence_number,prev_hash,key_id,payload_version) are present on every record. Nothing is refilled from a default here, so deleting any other signed key changes the recomputed bytes and fails step 1 or 2; a record with norecord_idmatches no countersignature and would otherwise read as merely "not countersigned yet".
If the bundle has an optional top-level regression_evidence section
({"format": "sengol-regression-evidence/v1", "agent_id": ..., "records": [{"family": ..., "record": {...}}]}), a seventh step, regression_lineage,
is added. Any other format FAILs, and every record's signed agent_id must
equal the section's agent_id. Every family is one of the six regression
families (failure, case, run, certification, and the unsigned retirement and
waiver; unknown = FAIL), each record's canonical payload / HMAC (UNVERIFIABLE without
public_keys["hmac_material"]) checks out, and every link
resolves (case -> failure, retirement -> case, waiver -> failure, run
case_results -> case, certification run_id -> a run in the section, and
its run_payload_sha256 -> that run's canonical-payload hash, with the same
agent_version as the run). A certification with no run_payload_sha256
FAILs, because it binds no run (sengol's deploy gate refuses it the same way).
Dangling ids and hash mismatches FAIL and are named. The signed records are not chained, so a deleted
record is not detectable. Retirements and waivers are exported unsigned (sengol
ADR-0024, families RegressionCaseRetirement and ProductionFailureWaiver),
so they have no HMAC to check; only their agent_id and links are verified.
The old signed families RegressionCaseRetirementRecord and
ProductionFailureWaiverRecord are not registered and FAIL as unknown.
Bundles without the section, or with it set to null, verify exactly as
before.
The verdict is PASS, FAIL (tampering or a broken chain), or UNVERIFIABLE (the bundle honestly doesn't carry enough — e.g. no countersignatures, no anchor receipts, or no trusted key). The exit code does not follow the verdict; see "Exit codes".
How
Nothing here is normalized, coerced, or "cleaned up" before verification:
this tool recomputes over the bytes exactly as stored, which is what makes
offline verification meaningful months after the evidence was written. The
canonical payload is built by the same rule sengol signs with (RFC 8785 over
{"record": <set fields>, "type": <family>}, minus each family's unsigned
fields, with unordered lists sorted), using signing tables vendored from
sengol's payload_registry.py and types.py. They are not imported from
sengol, so they can drift. Two things keep them honest: scripts/crosscheck.py
compares them with a sengol checkout's own classes and bytes (every registered
family at every payload version, a hash-chained partition of every audit
family, a judge evidence pack), and the committed fixtures, which sengol itself
signed, are replayed by the tests; tests/test_sengol_tables.py fails when a
vendored table differs from the snapshot of sengol's. A sengol release newer
than the snapshot is not covered until the fixtures are regenerated
(scripts/generate_fixtures.py).
Some fields of an exported record are outside the signature by design
(sengol's UNSIGNED_FIELDS), so no check here covers them; the
field_coverage row lists the ones a bundle's records carry:
- an evaluator score's
reasontext andreason_status(sengol ADR-0078). The signedreason_digestis a keyed HMAC under an install key this tool never holds, so the text can be edited or erased without failing a check, and this tool does not verify it. The digest itself is signed; CertificationRecord.evidence_pack_idand.anchor_record_id,SaturationEvent.remediated_at, the rawconfigandreferenceof a regression case binding (their hashes are signed), and theeval_result,mcp_server_version,model, token and cost fields of anAuthorizationDecisionRecord;call_signature_manifests, a transport-only field sengol removes before storing a record.
pip install sengol-verify
sengol-verify bundle.json --trusted-key appliance-key-1=appliance.pub.pem
sengol-verify path/to/bundle-dir/ --trusted-keys-file trusted.json
sengol-verify bundle.zip --trusted-key appliance-key-1=appliance.pub.pem
sengol-verify bundle.json --trusted-key ... --json # machine-readable
Trusting the countersignature key
A bundle's own public_keys is part of the file being checked: someone who
rewrites every record can mint a new Ed25519 key, countersign with it, and embed
its public half, and every check above still passes against that key. A
countersignature that verifies against a key from the bundle proves the file is
internally consistent, not which appliance produced it. Give the verifier the
appliance's public key from outside the bundle (the operator, or the key you
recorded at commissioning):
--trusted-key KEY_ID=PEM_PATH, repeatable: the Ed25519 public key (PEM file) for countersignatures whosekey_idisKEY_ID.--trusted-keys-file PATH: a JSON object{"key_id": "<pem>"}. Applied first, so a--trusted-keyoverrides one of its entries.
With no trusted key, the ed25519_countersig step still checks each signature
against the embedded key as a tamper signal but reports at most UNVERIFIABLE,
and the command exits 3 with a message saying no trusted key was supplied. With
any trusted key supplied (even an empty file, meaning "trust nothing"), only
those keys are used: a countersignature whose key_id is not among them, or
that does not verify against it, FAILs. The embedded key is then ignored.
A bundle with no countersignatures is not accepted, key or not. If the
countersignatures were stripped, or countersigning was never enabled, the step
is UNVERIFIABLE even with a trusted key (nothing remains to check against it)
and the command exits 3 with a message saying so. Re-export the bundle from the
appliance with countersigning enabled, after its countersign outbox has drained
(sengol audit export --wait-for-countersign), and verify that file. A bundle
with no records and no countersignatures reads PASS on this step, in sengol and
here, and exits 0.
Exit codes
The codes are those of sengol audit verify --offline and follow the
ed25519_countersig step result, not the overall verdict:
| Code | Meaning |
|---|---|
0 |
No step FAILed and the ed25519_countersig step is PASS (countersignatures verified against a trusted key). An overall UNVERIFIABLE from hmac_verify (always UNVERIFIABLE offline: bundles carry no HMAC key) or anchor_coverage does not change this. |
1 |
At least one step FAILed. Wins over every other outcome. |
2 |
Usage error, unreadable bundle or keys file, or a malformed --trusted-key. |
3 |
Nothing FAILed, but countersignature trust is not established: no trusted key was supplied, or a key was supplied and the bundle has no countersignatures. The message names which. Treat the bundle as not verified. |
--json reports trusted_key_supplied and countersignature_trust_established
(true exactly when the countersignature step is PASS).
What it never does
sengol-verify never contacts Sengol, any Sengol-operated service, or any
network endpoint at all. It reads one local file and prints a verdict. It
requires no license, no API key, and no account — verification is a
property of the bytes, not a service call.
It also has zero runtime dependency on the sengol package. Its only
third-party dependencies are cryptography (for
Ed25519) and rfc8785 (for the canonical
JSON the signatures are computed over). This is enforced by a test that imports sengol_verify with
sengol absent from sys.modules and asserts nothing named sengol or
sengol.* gets imported as a side effect.
Scope
This tool implements the offline bundle format as of payload_registry.py
in sengol at the time it was vendored, including the call-start,
accepted-set-lease, guard-decision (sengol.guard.decision) and dataset-rows-reveal
(sengol.audit.dataset_rows_reveal) families. It verifies the signed commitment values of a
guard decision as part of the record; it does not recompute a text or context commitment, because
a bundle carries neither the text nor its nonce. It registers only families sengol signs: the
families sengol has stopped signing (agent registration and secret records,
retention and legal-hold records, signed regression retirements and waivers,
and the like) are not registered. A bundle record whose record_type is not
registered, whether one of those or a type this tool has never heard of, has
no canonical payload here: payload_hash_integrity FAILs and names the
record and its record_type, and the other steps that need its payload fail
with it. It is never verified as a generic record. A regression_evidence
entry naming a family that is not one of the six regression families FAILs as
an unknown family. An export of such a record from an older sengol therefore cannot be
verified with this version. It does not verify anything Sengol didn't sign —
trace IDs, span IDs, and other fields documented as outside every canonical
payload are not assessed by design.
License
Apache-2.0. See LICENSE.
Metadata
Release files for sengol-verify 2.1.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 | |
|---|---|---|---|
| sengol_verify-2.1.0.tar.gz | 117.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sengol_verify-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 145.7 kB
Release files / sengol_verify-2.1.0.tar.gz
| Download URL | sengol_verify-2.1.0.tar.gz |
|---|---|
| Size | 117.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b8bc609046a3c8dc32ad471643c77366e30e617ce574676267156c42efb4e064
|
|
BLAKE2b-256 checksum How to use checksums |
d3f77c2c257036b20bc852f4db8010f5c3110c6fa3f4d351aab84a9aa9a526c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / sengol_verify-2.1.0-py3-none-any.whl
| Download URL | sengol_verify-2.1.0-py3-none-any.whl |
|---|---|
| Size | 28.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
926062de7715a7a26cf52ecf0d1a05aa60df2bc94c5ae7b51a5e316b2531afb2
|
|
BLAKE2b-256 checksum How to use checksums |
28c5a4dac3e25dc44b4b0faf504d2b1d111d623d1a371bd674371700ae4745df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|