Metadata-Version: 2.4
Name: pipelock-verify
Version: 0.1.1
Summary: Verify Pipelock action receipts (Ed25519-signed, chain-linked).
Author-email: PipeLab <luckypipe@pipelab.org>
License: 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: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Security
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Provides-Extra: release
Requires-Dist: build>=1.0; extra == "release"
Requires-Dist: twine>=6.0; extra == "release"
Requires-Dist: setuptools>=64; 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) action receipts.** Verifies the Ed25519 signature, chain linkage, and flight-recorder wrapping of receipts emitted by the Pipelock mediator.

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) · [What gets verified](#what-gets-verified) · [Canonicalization](#canonicalization-rules) · [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

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

Pin a specific signing key to reject receipts from any other signer:

```python
PROD_KEY = "70b991eb77816fc4ef0ae6a54d8a4119ddc5a16c9711c332c39e743079f6c63e"
result = pipelock_verify.verify(receipt_bytes, public_key_hex=PROD_KEY)
```

### Receipt 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`.

### CLI

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

## What gets verified

On a single receipt:

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

- Every individual receipt above.
- Signer consistency (every receipt uses the same `signer_key`, or the
  pinned trust anchor if one was supplied).
- Monotonic `chain_seq` starting at 0.
- `chain_prev_hash` linkage: each receipt's `chain_prev_hash` equals
  `SHA-256` of the previous receipt's canonical envelope, in hex.
- First receipt's `chain_prev_hash` equals the literal string `"genesis"`.

Failing receipts return the first break point (`broken_at_seq`) and a
descriptive `error`, the same shape the Go CLI prints.

## 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.

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

## Canonicalization rules

The signing input is the SHA-256 of the Go `json.Marshal` output of the
`ActionRecord` struct. "Canonical" means matching that exactly:

- Fields emitted in Go struct declaration order (not alphabetical).
- `omitempty` fields dropped when the value is the Go zero value
  (`""`, empty slice, `0`, `false`, `nil`).
- Compact JSON (no whitespace between tokens).
- HTML-safe escapes: `<`, `>`, `&`, U+2028, U+2029 encoded as Unicode
  escapes, matching Go's default `encoding/json` behavior.
- Fields unknown to the v1 schema are dropped (matches Go
  `json.Unmarshal` round-trip behavior).

Any deviation produces different bytes, a different hash, and a failed
signature. See `pipelock_verify/_canonical.py` for the full rule set.

## Relationship to the Go reference

* Go reference: https://github.com/luckyPipewrench/pipelock/tree/main/internal/receipt
* 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.

## 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).
