Python verifier

June 24, 2026 · View on GitHub

A second reference implementation of the agent-attestation/v1 verifier, written in Python and depending only on the stdlib + optional sigstore-python. Lives alongside audit_walker.py (which it reuses for the chain step).

Why this exists

A predicate type whose verification is only feasible in the language that produced it is not a standard — it is one vendor's artifact. The Python verifier proves that Occasio attestations are language-independent and can be re-verified by any auditor in their environment of choice.

This is the proof artifact for the OpenSSF / in-toto Attestation Registry submission. The same predicate JSON + Sigstore bundle is verified pass/fail by:

  • occasio verify (Node, src/bundle/verify.js)
  • python docs/verify_bundle.py (Python)
  • The browser viewer at integrations/attest-view/ (in-browser, partial — Sigstore crypto is deferred to one of the two above)

The Node test suite asserts that Node and Python agree byte-for-byte on the same single-file evidence bundle (test-bundle-xlang.js) — honest bundles pass in both, each single-point tamper (chain / policy / git / manifest bytes) fails the same check in both, and the strict requirements fail closed identically. (The per-row canonical-serialization guarantee they both depend on has its own Node↔Python pin in test-audit-xlang.js.)

Files

FilePurpose
canonicalize.pyRFC 8785 subset, mirror of src/attest/canonicalize.js (predicate + git_state comparison)
audit_walker.pySHA-256 chain walker + V8 JSON.stringify reproduction (_v8_json), reused for the manifest hashes and per-row chain hash
verify_bundle.pyBundle verifier for run.occasio.json, mirror of src/bundle/verify.js (the launch verifier)
attest_verify.pyLegacy verifier for the pre-cutover multi-file form (.json + .sigstore.json + chain)

The Python canonicalize and the JS canonicalize must stay in lockstep. The two files exist in parallel deliberately — bundling them into one cross-compiled artifact would defeat the point of cross-language verifiability.

Usage

# Verify a single-file evidence bundle (lenient — quick local check)
python docs/verify_bundle.py path/to/run.occasio.json

# Audit-grade: require signature + policy binding + git state
python docs/verify_bundle.py path/to/run.occasio.json --strict

# Or granular requirements
python docs/verify_bundle.py path/to/run.occasio.json --require-policy-binding --require-git-state

# Machine-readable output
python docs/verify_bundle.py --json path/to/run.occasio.json

Exit code 0 when every check passes, 1 otherwise. (The legacy attest_verify.py keeps the same CLI shape for the old multi-file form.)

What the six checks prove

Identical to occasio verify, in the same order: schema, manifest integrity (embedded artifacts hash to the manifest, via the V8 JSON.stringify reproduction in audit_walker._v8_json), chain slice integrity (per-row hash recomputes + links, anchored to the attestation's first_hash/last_hash), policy binding (sha256(policy_snapshot) == attestation.policy.file_hash), git state (deriveGitState(slice) byte-matches subject.git_state), and signature (Sigstore valid + DSSE predicate matches; needs pip install sigstore). Under --strict the last-three skips become hard failures.

Round-trip claim

For a run.occasio.json produced by occasio bundle --sign, the Python verifier produces the same pass/fail result as occasio verify on (proven by test-bundle-xlang.js):

  • the unmodified bundle (both pass)
  • a tampered chain row (both fail at chain slice integrity)
  • a swapped policy snapshot (both fail at policy binding)
  • a forged subject.git_state (both fail at git state matches chain)
  • edited manifest bytes (both fail at manifest integrity — proves the _v8_json byte-equality)
  • an unsigned bundle under --strict (both fail at signature)

The test suite covers cases 1, 2, and 3 deterministically with the Sigstore step mocked. The cross-language byte-equivalence on the predicate-canonicalization step is asserted via Python-spawn from the Node test runner (xlang: and xlang-float: test blocks); both implementations reject non-integer numbers so the equivalence cannot be silently broken by adding a float field to a future schema. Case 4 (real Sigstore tamper detection) requires GitHub Actions OIDC infrastructure and is exercised by the live Action's self-verify step in CI, not by the in-process test suite.

Install hint for Sigstore step

pip install sigstore        # adds the Fulcio + Rekor verification step

Versions tracked: sigstore-python >= 3.0. The verifier degrades gracefully if a different major version is installed (the Verifier API is the stable surface used here).

Limitations

  • Identity pinning is not enforced by the reference verifier. The default policy.UnsafeNoOp() accepts any Fulcio cert. An auditor whose compliance regime requires a specific workflow-ref identity (e.g. repo:org/repo:ref:refs/heads/main) should adapt the call to policy.Identity(...). Pattern intentionally exposed: this is a policy decision, not a verifier decision.
  • The Python canonicalize is a JCS subset, not full RFC 8785. The deviations are documented inline in canonicalize.py. Non-integer numbers are explicitly rejected on both sides as a load-bearing cross-language invariant — see canonicalize.py and src/attest/canonicalize.js for the rationale.