Python verifier for Pipelock receipts. Supports both ActionReceipt v1 (legacy proxy decisions) and EvidenceReceipt v2 (contract-aware lifecycle events). Verifies Ed25519 signatures, chain linkage, payload schemas, key-purpose authority, and flight-recorder wrapping.
Mirrors the Go reference implementation byte-for-byte. The conformance golden files in tests/conformance/ are generated by Pipelock's Go code and verified identically by both sides.
Install · Usage · EvidenceReceipt v2 · Well-known directory · What gets verified · Spec · Go reference
pip install pipelock-verifyOnly one runtime dependency: cryptography for the Ed25519 primitives.
import pipelock_verify
with open("receipt.json", "rb") as f:
result = pipelock_verify.verify(f.read())
if not result.valid:
raise SystemExit(f"bad receipt: {result.error}")
print(f"OK: {result.action_id} {result.verdict} {result.target}")The verify() function automatically routes to the correct verification
path based on the record_type field:
- No
record_typeor"action_receipt_v1"routes to ActionReceipt v1. "evidence_receipt_v2"routes to EvidenceReceipt v2.- Unknown
record_typeis rejected with a clear error.
import pipelock_verify
# Fetch the signing keyset from the Pipelock instance.
directory = pipelock_verify.fetch_directory("pipelab.org")
key_hex = directory.public_key_hex()
result = pipelock_verify.verify(receipt_bytes, public_key_hex=key_hex)Pass a flight-recorder JSONL path:
chain = pipelock_verify.verify_chain("evidence-proxy-0.jsonl")
if not chain.valid:
raise SystemExit(
f"chain broken at seq {chain.broken_at_seq}: {chain.error}"
)
print(f"CHAIN VALID: {chain.receipt_count} receipts, root {chain.root_hash}")When no trust anchor is supplied, the first receipt's signer_key becomes
the expected key for the rest of the chain. This matches the signer-
consistency check in Go's receipt.VerifyChain.
python -m pipelock_verify receipt.json
python -m pipelock_verify evidence.jsonl
python -m pipelock_verify evidence.jsonl --key 70b991eb77816fc4...Exit codes match pipelock verify-receipt: 0 on success, 1 on failure.
EvidenceReceipt v2 is the contract-aware receipt envelope introduced in Pipelock v2.4. It sits alongside ActionReceipt v1 (which remains unchanged for backward compatibility).
For fine-grained control over v2-specific checks (key purpose enforcement, signer key ID pinning):
from pipelock_verify import verify_evidence
result = verify_evidence(
receipt_dict,
public_key_hex="...",
expected_signer_key_id="receipt-key-prod",
expected_key_purpose="receipt-signing",
)
if not result.valid:
raise SystemExit(f"v2 receipt failed: {result.error}")
print(f"Event: {result.event_id}, Kind: {result.payload_kind}")| Payload kind | Signing purpose |
|---|---|
proxy_decision |
receipt-signing |
contract_ratified |
receipt-signing |
contract_promote_intent |
contract-activation-signing |
contract_promote_committed |
receipt-signing |
contract_rollback_authorized |
contract-activation-signing |
contract_rollback_committed |
receipt-signing |
contract_demoted |
receipt-signing |
contract_expired |
receipt-signing |
contract_drift |
receipt-signing |
shadow_delta |
receipt-signing |
opportunity_missing |
receipt-signing |
key_rotation |
contract-activation-signing |
contract_redaction_request |
contract-activation-signing |
The authority matrix is enforced automatically. A valid signature from the wrong key purpose is rejected.
EvidenceReceipt v2 uses RFC 8785 JSON Canonicalization Scheme (JCS) for
signable preimages, not Go's encoding/json byte order (which is what
ActionReceipt v1 uses). JCS rules:
- Object keys sorted lexicographically by Unicode codepoint.
- Strings NFC-normalized.
- Floats rejected (use decimal strings).
- No whitespace between tokens.
The signature field is zeroed before computing the preimage.
Pipelock instances serve their signing keys at
/.well-known/http-message-signatures-directory (RFC 9421). Use the
built-in fetch helper to retrieve and parse the keyset:
from pipelock_verify import fetch_directory, parse_directory
# Fetch from a live instance.
directory = fetch_directory("pipelab.org")
# Or parse from a pre-fetched JSON blob.
directory = parse_directory(json_bytes)
# Look up a specific key.
key = directory.get_key("pipelock-mediation-prod")
if key:
print(f"Key: {key.public_key}, Use: {key.use}")On a single ActionReceipt v1:
- Envelope version (rejects anything other than v1).
- Action record version (rejects anything other than v1).
- Required action record fields (
action_id,action_type,timestamp,target,verdict,transport). - Signature format (
ed25519:<hex>prefix, 64-byte length). - Signer key format (32-byte hex).
- Optional trust anchor match (
public_key_hexargument). - Ed25519 signature over
SHA-256(canonical action record).
On a single EvidenceReceipt v2:
- Envelope
record_typeandreceipt_version. - Strict unknown-field rejection (envelope, signature proof, and payload).
- Required envelope fields (
event_id,timestamp,payload_kind). - Payload schema validation for all 13 payload kinds.
- Key purpose authority matrix enforcement.
- Signature proof structure (
signer_key_id,key_purpose,algorithm). - Ed25519 PureEdDSA signature over
JCS(receipt_without_signature). - Optional trust anchors:
public_key_hex,expected_signer_key_id,expected_key_purpose.
On a chain:
- Every individual receipt above (v1 or v2).
- Signer consistency across the chain.
- Monotonic
chain_seqstarting at 0. chain_prev_hashlinkage via SHA-256 of canonical envelopes.- First receipt's
chain_prev_hashequals"genesis".
verify_chain() accepts JSONL in two shapes:
- Flight-recorder entries -- the format Pipelock actually writes to
disk. Each line is a
recorder.Entryobject withtype == "action_receipt"and the receipt nested indetail. Non-receipt entries (checkpoints etc.) are skipped, not rejected. - Bare receipts -- one receipt object per line, no wrapping. Used by the conformance suite and handy for ad-hoc testing. Both v1 and v2 bare receipts are recognized.
verify() accepts:
- A JSON string or UTF-8 bytes.
- A pre-parsed
dict(for callers that already have the receipt loaded). - A flight-recorder entry dict (transparently unwrapped).
- Go reference: https://github.com/luckyPipewrench/pipelock/tree/main/internal/receipt (v1), https://github.com/luckyPipewrench/pipelock/tree/main/internal/contract/receipt (v2)
- Conformance suite: https://github.com/luckyPipewrench/pipelock/tree/main/sdk/conformance
- Spec page: https://pipelab.org/learn/action-receipt-spec/
Both implementations verify the same sdk/conformance/testdata/ golden
files and compute identical root hashes.
git clone https://github.com/luckyPipewrench/pipelock-verify-python
cd pipelock-verify-python
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytestMaintainers: see RELEASING.md for the OIDC-based publish flow.
To refresh the conformance fixtures from a local Pipelock checkout:
cd /path/to/pipelock
go test ./sdk/conformance/ -run TestGenerateGoldenFiles -update
cp sdk/conformance/testdata/*.{json,jsonl} \
/path/to/pipelock-verify-python/tests/conformance/
pytestApache 2.0. See LICENSE.