Skip to content

luckyPipewrench/pipelock-verify-python

Repository files navigation

pipelock-verify

PyPI Python CI CodeQL OpenSSF Scorecard License: Apache-2.0

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

Install

pip install pipelock-verify

Only one runtime dependency: cryptography for the Ed25519 primitives.

Usage

Single receipt (auto-detects v1 vs v2)

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_type or "action_receipt_v1" routes to ActionReceipt v1.
  • "evidence_receipt_v2" routes to EvidenceReceipt v2.
  • Unknown record_type is rejected with a clear error.

Pin a signing key via the well-known directory

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)

Receipt chain

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.

CLI

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

EvidenceReceipt v2 is the contract-aware receipt envelope introduced in Pipelock v2.4. It sits alongside ActionReceipt v1 (which remains unchanged for backward compatibility).

Direct v2 verification

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}")

13 payload kinds

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.

v2 canonicalization

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.

Well-known directory

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}")

What gets verified

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_hex argument).
  • Ed25519 signature over SHA-256(canonical action record).

On a single EvidenceReceipt v2:

  • Envelope record_type and receipt_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_seq starting at 0.
  • chain_prev_hash linkage via SHA-256 of canonical envelopes.
  • First receipt's chain_prev_hash equals "genesis".

Input formats

verify_chain() accepts JSONL in two shapes:

  1. Flight-recorder entries -- the format Pipelock actually writes to disk. Each line is a recorder.Entry object with type == "action_receipt" and the receipt nested in detail. Non-receipt entries (checkpoints etc.) are skipped, not rejected.
  2. 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).

Relationship to the Go reference

Both implementations verify the same sdk/conformance/testdata/ golden files and compute identical root hashes.

Development

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]"
pytest

Maintainers: 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/
pytest

License

Apache 2.0. See LICENSE.

Packages

 
 
 

Contributors

Languages