Metadata-Version: 2.4
Name: pipelock-verify
Version: 0.4.0
Summary: Verify Pipelock receipts: ActionReceipt v1 chains and individual EvidenceReceipt v2 envelopes.
Author-email: PipeLab <luckypipe@pipelab.org>
License-Expression: Apache-2.0
Project-URL: Homepage, https://pipelab.org
Project-URL: Repository, https://github.com/luckyPipewrench/pipelock-verify-python
Project-URL: Pipelock (Go reference), https://github.com/luckyPipewrench/pipelock
Project-URL: Receipt Spec, https://pipelab.org/learn/action-receipt-spec/
Keywords: pipelock,agent-security,receipt,ed25519,verifier,ai-safety
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=48.0.1
Provides-Extra: dev
Requires-Dist: pytest>=9.0.3; extra == "dev"
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
Requires-Dist: ruff>=0.15.12; extra == "dev"
Requires-Dist: mypy>=1.19.1; extra == "dev"
Provides-Extra: release
Requires-Dist: build>=1.4.4; extra == "release"
Requires-Dist: twine>=6.2.0; extra == "release"
Requires-Dist: setuptools>=83.0.0; extra == "release"
Requires-Dist: wheel; extra == "release"
Dynamic: license-file

# pipelock-verify

[![PyPI](https://img.shields.io/pypi/v/pipelock-verify.svg?label=PyPI&logo=pypi&logoColor=white)](https://pypi.org/project/pipelock-verify/)
[![Python](https://img.shields.io/pypi/pyversions/pipelock-verify.svg?label=Python&logo=python&logoColor=white)](https://pypi.org/project/pipelock-verify/)
[![CI](https://github.com/luckyPipewrench/pipelock-verify-python/actions/workflows/ci.yml/badge.svg)](https://github.com/luckyPipewrench/pipelock-verify-python/actions/workflows/ci.yml)
[![CodeQL](https://github.com/luckyPipewrench/pipelock-verify-python/actions/workflows/codeql.yml/badge.svg)](https://github.com/luckyPipewrench/pipelock-verify-python/actions/workflows/codeql.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/luckyPipewrench/pipelock-verify-python/badge)](https://scorecard.dev/viewer/?uri=github.com/luckyPipewrench/pipelock-verify-python)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

**Python verifier for [Pipelock](https://github.com/luckyPipewrench/pipelock) receipts.** Supports **ActionReceipt v1** chains and individual **EvidenceReceipt v2** envelopes for contract-aware lifecycle events. Verifies Ed25519 signatures, v1 chain linkage, v2 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](#install) · [Usage](#usage) · [EvidenceReceipt v2](#evidencereceipt-v2) · [Well-known directory](#well-known-directory) · [What gets verified](#what-gets-verified) · [Spec](https://pipelab.org/learn/action-receipt-spec/) · [Go reference](https://github.com/luckyPipewrench/pipelock)

## Install

```bash
pip install pipelock-verify
```

Only one runtime dependency: [`cryptography`](https://cryptography.io) for the Ed25519 primitives.

## Usage

### Single receipt (auto-detects v1 vs v2)

```python
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

```python
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)
```

### ActionReceipt v1 chain

Pass a flight-recorder JSONL path:

```python
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`.

`verify_chain()` intentionally fails closed when the chain contains
`EvidenceReceipt v2` entries. v0.2.0 verifies v2 receipts one at a time with
`verify()` or `verify_evidence()`; v2 chain verification is reserved for a
follow-up release once the cross-version chain-linking rules are specified.

For an ActionReceipt v1 chain that rotated signing keys, pin the original root
and supply one old-key-signed endorsement for each rotation boundary:

```python
import pipelock_verify

endorsement = pipelock_verify.load_rotation_endorsement(
    "receipt-rotation-2026-07-30.json"
)
chain = pipelock_verify.verify_chain(
    "evidence-proxy.jsonl",
    public_key_hex="4655a7e605c12ebb00a46037881c33c5bca5eb74b45a02e8e7261a7ff5a21678",
    session_id="proxy",
    rotation_endorsements=[endorsement],
)
```

The endorsement is verified under the retiring key and bound to the exact
prior sequence, tail hash, recorder session, and successor key. Missing,
altered, duplicate, replayed, cross-session, and unused endorsements fail
closed.

`session_id` must match `recorder_session` in every signed `session_open`, and
the root segment must contain one. A chain without that signed session binding
cannot be verified with endorsements.

### CLI

```bash
python -m pipelock_verify receipt.json
python -m pipelock_verify evidence.jsonl
python -m pipelock_verify evidence.jsonl --key 70b991eb77816fc4...
python -m pipelock_verify evidence.jsonl \
  --key 4655a7e605c12ebb00a46037881c33c5bca5eb74b45a02e8e7261a7ff5a21678 \
  --session-id proxy \
  --rotation-endorsement receipt-rotation-2026-07-30.json
```

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):

```python
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:

```python
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 an **ActionReceipt v1 chain**:

- Every individual ActionReceipt v1 above.
- 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"`.

EvidenceReceipt v2 entries are rejected in chain mode with an explicit
unsupported-v2-chain error. Verify them individually in v0.2.0.

## 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. ActionReceipt v1 bare
   receipts are verified as a chain. EvidenceReceipt v2 bare receipts are
   rejected in chain mode and should be verified individually.

`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

* 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 single-receipt v2 golden files. For
chain root hashes, v0.2.0 parity is limited to ActionReceipt v1 chains.

## Development

```bash
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](RELEASING.md) for the OIDC-based publish flow.

To refresh the conformance fixtures from a local Pipelock checkout:

```bash
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](LICENSE).
