pipelock-verify 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 PipeLab
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: pipelock-verify
3
+ Version: 0.1.0
4
+ Summary: Verify Pipelock action receipts (Ed25519-signed, chain-linked).
5
+ Author-email: PipeLab <luckypipe@pipelab.org>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://pipelab.org
8
+ Project-URL: Repository, https://github.com/luckyPipewrench/pipelock-verify-python
9
+ Project-URL: Pipelock (Go reference), https://github.com/luckyPipewrench/pipelock
10
+ Project-URL: Receipt Spec, https://pipelab.org/learn/action-receipt-spec/
11
+ Keywords: pipelock,agent-security,receipt,ed25519,verifier,ai-safety
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Security :: Cryptography
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: cryptography>=41.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.4; extra == "dev"
34
+ Requires-Dist: mypy>=1.8; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # pipelock-verify
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/pipelock-verify.svg)](https://pypi.org/project/pipelock-verify/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/pipelock-verify.svg)](https://pypi.org/project/pipelock-verify/)
41
+ [![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)
42
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
43
+
44
+ Python verifier for [Pipelock](https://github.com/luckyPipewrench/pipelock)
45
+ action receipts. Verifies the Ed25519 signature, chain linkage, and
46
+ flight-recorder wrapping of receipts emitted by the Pipelock mediator.
47
+
48
+ The library mirrors the Go reference implementation byte-for-byte. The
49
+ conformance golden files in `tests/conformance/` are generated by Pipelock's
50
+ Go code and verified identically by both sides.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install pipelock-verify
56
+ ```
57
+
58
+ Only one runtime dependency: [`cryptography`](https://cryptography.io) for
59
+ the Ed25519 primitives.
60
+
61
+ ## Usage
62
+
63
+ ### Single receipt
64
+
65
+ ```python
66
+ import pipelock_verify
67
+
68
+ with open("receipt.json", "rb") as f:
69
+ result = pipelock_verify.verify(f.read())
70
+
71
+ if not result.valid:
72
+ raise SystemExit(f"bad receipt: {result.error}")
73
+
74
+ print(f"OK: {result.action_id} {result.verdict} {result.target}")
75
+ ```
76
+
77
+ Pin a specific signing key to reject receipts from any other signer:
78
+
79
+ ```python
80
+ PROD_KEY = "70b991eb77816fc4ef0ae6a54d8a4119ddc5a16c9711c332c39e743079f6c63e"
81
+ result = pipelock_verify.verify(receipt_bytes, public_key_hex=PROD_KEY)
82
+ ```
83
+
84
+ ### Receipt chain
85
+
86
+ Pass a flight-recorder JSONL path:
87
+
88
+ ```python
89
+ chain = pipelock_verify.verify_chain("evidence-proxy-0.jsonl")
90
+
91
+ if not chain.valid:
92
+ raise SystemExit(
93
+ f"chain broken at seq {chain.broken_at_seq}: {chain.error}"
94
+ )
95
+
96
+ print(f"CHAIN VALID: {chain.receipt_count} receipts, root {chain.root_hash}")
97
+ ```
98
+
99
+ When no trust anchor is supplied, the first receipt's `signer_key` becomes
100
+ the expected key for the rest of the chain. This matches the signer-
101
+ consistency check in Go's `receipt.VerifyChain`.
102
+
103
+ ### CLI
104
+
105
+ ```bash
106
+ python -m pipelock_verify receipt.json
107
+ python -m pipelock_verify evidence.jsonl
108
+ python -m pipelock_verify evidence.jsonl --key 70b991eb77816fc4...
109
+ ```
110
+
111
+ Exit codes match `pipelock verify-receipt`: 0 on success, 1 on failure.
112
+
113
+ ## What gets verified
114
+
115
+ On a single receipt:
116
+
117
+ - Envelope version (rejects anything other than v1).
118
+ - Action record version (rejects anything other than v1).
119
+ - Required action record fields (`action_id`, `action_type`, `timestamp`,
120
+ `target`, `verdict`, `transport`).
121
+ - Signature format (`ed25519:<hex>` prefix, 64-byte length).
122
+ - Signer key format (32-byte hex).
123
+ - Optional trust anchor match (`public_key_hex` argument).
124
+ - Ed25519 signature over `SHA-256(canonical action record)`.
125
+
126
+ On a chain:
127
+
128
+ - Every individual receipt above.
129
+ - Signer consistency (every receipt uses the same `signer_key`, or the
130
+ pinned trust anchor if one was supplied).
131
+ - Monotonic `chain_seq` starting at 0.
132
+ - `chain_prev_hash` linkage: each receipt's `chain_prev_hash` equals
133
+ `SHA-256` of the previous receipt's canonical envelope, in hex.
134
+ - First receipt's `chain_prev_hash` equals the literal string `"genesis"`.
135
+
136
+ Failing receipts return the first break point (`broken_at_seq`) and a
137
+ descriptive `error`, the same shape the Go CLI prints.
138
+
139
+ ## Input formats
140
+
141
+ `verify_chain()` accepts JSONL in two shapes:
142
+
143
+ 1. **Flight-recorder entries** — the format Pipelock actually writes to
144
+ disk. Each line is a `recorder.Entry` object with `type ==
145
+ "action_receipt"` and the receipt nested in `detail`. Non-receipt
146
+ entries (checkpoints etc.) are skipped, not rejected.
147
+ 2. **Bare receipts** — one receipt object per line, no wrapping. Used by
148
+ the conformance suite and handy for ad-hoc testing.
149
+
150
+ `verify()` accepts:
151
+
152
+ - A JSON string or UTF-8 bytes.
153
+ - A pre-parsed `dict` (for callers that already have the receipt loaded).
154
+ - A flight-recorder entry dict (transparently unwrapped).
155
+
156
+ ## Canonicalization rules
157
+
158
+ The signing input is the SHA-256 of the Go `json.Marshal` output of the
159
+ `ActionRecord` struct. "Canonical" means matching that exactly:
160
+
161
+ - Fields emitted in Go struct declaration order (not alphabetical).
162
+ - `omitempty` fields dropped when the value is the Go zero value
163
+ (`""`, empty slice, `0`, `false`, `nil`).
164
+ - Compact JSON (no whitespace between tokens).
165
+ - HTML-safe escapes: `<`, `>`, `&`, U+2028, U+2029 encoded as Unicode
166
+ escapes, matching Go's default `encoding/json` behavior.
167
+ - Fields unknown to the v1 schema are dropped (matches Go
168
+ `json.Unmarshal` round-trip behavior).
169
+
170
+ Any deviation produces different bytes, a different hash, and a failed
171
+ signature. See `pipelock_verify/_canonical.py` for the full rule set.
172
+
173
+ ## Relationship to the Go reference
174
+
175
+ * Go reference: https://github.com/luckyPipewrench/pipelock/tree/main/internal/receipt
176
+ * Conformance suite: https://github.com/luckyPipewrench/pipelock/tree/main/sdk/conformance
177
+ * Spec page: https://pipelab.org/learn/action-receipt-spec/
178
+
179
+ Both implementations verify the same `sdk/conformance/testdata/` golden
180
+ files and compute identical root hashes.
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ git clone https://github.com/luckyPipewrench/pipelock-verify-python
186
+ cd pipelock-verify-python
187
+ python -m venv .venv
188
+ source .venv/bin/activate
189
+ pip install -e ".[dev]"
190
+ pytest
191
+ ```
192
+
193
+ To refresh the conformance fixtures from a local Pipelock checkout:
194
+
195
+ ```bash
196
+ cd /path/to/pipelock
197
+ go test ./sdk/conformance/ -run TestGenerateGoldenFiles -update
198
+ cp sdk/conformance/testdata/*.{json,jsonl} \
199
+ /path/to/pipelock-verify-python/tests/conformance/
200
+ pytest
201
+ ```
202
+
203
+ ## License
204
+
205
+ Apache 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,169 @@
1
+ # pipelock-verify
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/pipelock-verify.svg)](https://pypi.org/project/pipelock-verify/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/pipelock-verify.svg)](https://pypi.org/project/pipelock-verify/)
5
+ [![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)
6
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
7
+
8
+ Python verifier for [Pipelock](https://github.com/luckyPipewrench/pipelock)
9
+ action receipts. Verifies the Ed25519 signature, chain linkage, and
10
+ flight-recorder wrapping of receipts emitted by the Pipelock mediator.
11
+
12
+ The library mirrors the Go reference implementation byte-for-byte. The
13
+ conformance golden files in `tests/conformance/` are generated by Pipelock's
14
+ Go code and verified identically by both sides.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install pipelock-verify
20
+ ```
21
+
22
+ Only one runtime dependency: [`cryptography`](https://cryptography.io) for
23
+ the Ed25519 primitives.
24
+
25
+ ## Usage
26
+
27
+ ### Single receipt
28
+
29
+ ```python
30
+ import pipelock_verify
31
+
32
+ with open("receipt.json", "rb") as f:
33
+ result = pipelock_verify.verify(f.read())
34
+
35
+ if not result.valid:
36
+ raise SystemExit(f"bad receipt: {result.error}")
37
+
38
+ print(f"OK: {result.action_id} {result.verdict} {result.target}")
39
+ ```
40
+
41
+ Pin a specific signing key to reject receipts from any other signer:
42
+
43
+ ```python
44
+ PROD_KEY = "70b991eb77816fc4ef0ae6a54d8a4119ddc5a16c9711c332c39e743079f6c63e"
45
+ result = pipelock_verify.verify(receipt_bytes, public_key_hex=PROD_KEY)
46
+ ```
47
+
48
+ ### Receipt chain
49
+
50
+ Pass a flight-recorder JSONL path:
51
+
52
+ ```python
53
+ chain = pipelock_verify.verify_chain("evidence-proxy-0.jsonl")
54
+
55
+ if not chain.valid:
56
+ raise SystemExit(
57
+ f"chain broken at seq {chain.broken_at_seq}: {chain.error}"
58
+ )
59
+
60
+ print(f"CHAIN VALID: {chain.receipt_count} receipts, root {chain.root_hash}")
61
+ ```
62
+
63
+ When no trust anchor is supplied, the first receipt's `signer_key` becomes
64
+ the expected key for the rest of the chain. This matches the signer-
65
+ consistency check in Go's `receipt.VerifyChain`.
66
+
67
+ ### CLI
68
+
69
+ ```bash
70
+ python -m pipelock_verify receipt.json
71
+ python -m pipelock_verify evidence.jsonl
72
+ python -m pipelock_verify evidence.jsonl --key 70b991eb77816fc4...
73
+ ```
74
+
75
+ Exit codes match `pipelock verify-receipt`: 0 on success, 1 on failure.
76
+
77
+ ## What gets verified
78
+
79
+ On a single receipt:
80
+
81
+ - Envelope version (rejects anything other than v1).
82
+ - Action record version (rejects anything other than v1).
83
+ - Required action record fields (`action_id`, `action_type`, `timestamp`,
84
+ `target`, `verdict`, `transport`).
85
+ - Signature format (`ed25519:<hex>` prefix, 64-byte length).
86
+ - Signer key format (32-byte hex).
87
+ - Optional trust anchor match (`public_key_hex` argument).
88
+ - Ed25519 signature over `SHA-256(canonical action record)`.
89
+
90
+ On a chain:
91
+
92
+ - Every individual receipt above.
93
+ - Signer consistency (every receipt uses the same `signer_key`, or the
94
+ pinned trust anchor if one was supplied).
95
+ - Monotonic `chain_seq` starting at 0.
96
+ - `chain_prev_hash` linkage: each receipt's `chain_prev_hash` equals
97
+ `SHA-256` of the previous receipt's canonical envelope, in hex.
98
+ - First receipt's `chain_prev_hash` equals the literal string `"genesis"`.
99
+
100
+ Failing receipts return the first break point (`broken_at_seq`) and a
101
+ descriptive `error`, the same shape the Go CLI prints.
102
+
103
+ ## Input formats
104
+
105
+ `verify_chain()` accepts JSONL in two shapes:
106
+
107
+ 1. **Flight-recorder entries** — the format Pipelock actually writes to
108
+ disk. Each line is a `recorder.Entry` object with `type ==
109
+ "action_receipt"` and the receipt nested in `detail`. Non-receipt
110
+ entries (checkpoints etc.) are skipped, not rejected.
111
+ 2. **Bare receipts** — one receipt object per line, no wrapping. Used by
112
+ the conformance suite and handy for ad-hoc testing.
113
+
114
+ `verify()` accepts:
115
+
116
+ - A JSON string or UTF-8 bytes.
117
+ - A pre-parsed `dict` (for callers that already have the receipt loaded).
118
+ - A flight-recorder entry dict (transparently unwrapped).
119
+
120
+ ## Canonicalization rules
121
+
122
+ The signing input is the SHA-256 of the Go `json.Marshal` output of the
123
+ `ActionRecord` struct. "Canonical" means matching that exactly:
124
+
125
+ - Fields emitted in Go struct declaration order (not alphabetical).
126
+ - `omitempty` fields dropped when the value is the Go zero value
127
+ (`""`, empty slice, `0`, `false`, `nil`).
128
+ - Compact JSON (no whitespace between tokens).
129
+ - HTML-safe escapes: `<`, `>`, `&`, U+2028, U+2029 encoded as Unicode
130
+ escapes, matching Go's default `encoding/json` behavior.
131
+ - Fields unknown to the v1 schema are dropped (matches Go
132
+ `json.Unmarshal` round-trip behavior).
133
+
134
+ Any deviation produces different bytes, a different hash, and a failed
135
+ signature. See `pipelock_verify/_canonical.py` for the full rule set.
136
+
137
+ ## Relationship to the Go reference
138
+
139
+ * Go reference: https://github.com/luckyPipewrench/pipelock/tree/main/internal/receipt
140
+ * Conformance suite: https://github.com/luckyPipewrench/pipelock/tree/main/sdk/conformance
141
+ * Spec page: https://pipelab.org/learn/action-receipt-spec/
142
+
143
+ Both implementations verify the same `sdk/conformance/testdata/` golden
144
+ files and compute identical root hashes.
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ git clone https://github.com/luckyPipewrench/pipelock-verify-python
150
+ cd pipelock-verify-python
151
+ python -m venv .venv
152
+ source .venv/bin/activate
153
+ pip install -e ".[dev]"
154
+ pytest
155
+ ```
156
+
157
+ To refresh the conformance fixtures from a local Pipelock checkout:
158
+
159
+ ```bash
160
+ cd /path/to/pipelock
161
+ go test ./sdk/conformance/ -run TestGenerateGoldenFiles -update
162
+ cp sdk/conformance/testdata/*.{json,jsonl} \
163
+ /path/to/pipelock-verify-python/tests/conformance/
164
+ pytest
165
+ ```
166
+
167
+ ## License
168
+
169
+ Apache 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,49 @@
1
+ """Pipelock action receipt verifier.
2
+
3
+ Verifies Ed25519-signed action receipts emitted by the Pipelock mediator.
4
+ Matches the Go reference implementation in ``internal/receipt`` byte-for-byte
5
+ so that receipts generated by the Pipelock binary verify identically in Go
6
+ and Python.
7
+
8
+ Typical usage::
9
+
10
+ import pipelock_verify
11
+
12
+ # Single receipt from a JSON string, bytes, or already-parsed dict.
13
+ result = pipelock_verify.verify(receipt_json)
14
+ if not result.valid:
15
+ raise SystemExit(f"bad receipt: {result.error}")
16
+
17
+ # Receipt chain from a flight recorder JSONL file.
18
+ chain = pipelock_verify.verify_chain("evidence-proxy-0.jsonl")
19
+ if not chain.valid:
20
+ raise SystemExit(f"chain broken at seq {chain.broken_at_seq}: {chain.error}")
21
+
22
+ Trust anchors are opt-in. Pass ``public_key_hex`` to pin a specific signer,
23
+ or leave it empty to trust the key embedded in the receipt (chain mode then
24
+ enforces signer consistency across every receipt in the file).
25
+
26
+ Wire format: see https://pipelab.org/learn/action-receipt-spec/ for field
27
+ layout, canonicalization rules, and the exact signing input. The format is
28
+ the Go ``json.Marshal`` output of the ``Receipt`` struct; this library
29
+ mirrors it.
30
+ """
31
+
32
+ from ._verify import (
33
+ ChainResult,
34
+ InvalidReceiptError,
35
+ VerifyResult,
36
+ verify,
37
+ verify_chain,
38
+ )
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "ChainResult",
44
+ "InvalidReceiptError",
45
+ "VerifyResult",
46
+ "__version__",
47
+ "verify",
48
+ "verify_chain",
49
+ ]
@@ -0,0 +1,140 @@
1
+ """Command-line interface for ``python -m pipelock_verify``.
2
+
3
+ Verifies a single receipt JSON file or a flight recorder JSONL chain.
4
+ Exit code 0 on success, 1 on failure — same convention as the Go
5
+ ``pipelock verify-receipt`` command.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+ from collections.abc import Sequence
13
+ from pathlib import Path
14
+
15
+ from . import __version__
16
+ from ._verify import ChainResult, VerifyResult, verify, verify_chain
17
+
18
+
19
+ def _detect_mode(path: Path) -> str:
20
+ """Return ``"chain"`` for JSONL files, ``"single"`` otherwise."""
21
+ if path.suffix.lower() == ".jsonl":
22
+ return "chain"
23
+ return "single"
24
+
25
+
26
+ def _print_single(result: VerifyResult, path: Path) -> None:
27
+ if not result.valid:
28
+ print(f"FAILED: {path}: {result.error}")
29
+ return
30
+ print(f"OK: {path}")
31
+ if result.action_id:
32
+ print(f" Action ID: {result.action_id}")
33
+ if result.action_type:
34
+ print(f" Action Type: {result.action_type}")
35
+ if result.verdict:
36
+ print(f" Verdict: {result.verdict}")
37
+ if result.target:
38
+ print(f" Target: {result.target}")
39
+ if result.transport:
40
+ print(f" Transport: {result.transport}")
41
+ if result.timestamp:
42
+ print(f" Timestamp: {result.timestamp}")
43
+ if result.signer_key:
44
+ print(f" Signer: {result.signer_key}")
45
+ if result.chain_seq is not None:
46
+ print(f" Chain seq: {result.chain_seq}")
47
+ if result.chain_prev_hash:
48
+ print(f" Chain prev: {result.chain_prev_hash}")
49
+
50
+
51
+ def _print_chain(result: ChainResult, path: Path) -> None:
52
+ if not result.valid:
53
+ print(f"CHAIN BROKEN: {path}")
54
+ if result.error:
55
+ print(f" Error: {result.error}")
56
+ if result.broken_at_seq is not None:
57
+ print(f" Broke at: seq {result.broken_at_seq}")
58
+ return
59
+ print(f"CHAIN VALID: {path}")
60
+ print(f" Receipts: {result.receipt_count}")
61
+ if result.final_seq is not None:
62
+ print(f" Final seq: {result.final_seq}")
63
+ if result.root_hash:
64
+ print(f" Root hash: {result.root_hash}")
65
+ if result.start_time:
66
+ print(f" Start: {result.start_time}")
67
+ if result.end_time:
68
+ print(f" End: {result.end_time}")
69
+
70
+
71
+ def main(argv: Sequence[str] | None = None) -> int:
72
+ parser = argparse.ArgumentParser(
73
+ prog="pipelock_verify",
74
+ description=(
75
+ "Verify Pipelock action receipts. "
76
+ "Pass a single receipt JSON file or a JSONL flight recorder "
77
+ "chain. Exit 0 on success, 1 on failure."
78
+ ),
79
+ )
80
+ parser.add_argument(
81
+ "path",
82
+ type=Path,
83
+ help="receipt JSON file (single) or JSONL file (chain)",
84
+ )
85
+ parser.add_argument(
86
+ "--key",
87
+ dest="public_key_hex",
88
+ help=(
89
+ "expected signer public key, hex-encoded. When omitted, the "
90
+ "embedded signer_key is trusted; chain mode pins it to the "
91
+ "first receipt and enforces consistency across the chain."
92
+ ),
93
+ )
94
+ parser.add_argument(
95
+ "--mode",
96
+ choices=("single", "chain", "auto"),
97
+ default="auto",
98
+ help="force single or chain mode; default auto-detects from file suffix",
99
+ )
100
+ parser.add_argument(
101
+ "--version",
102
+ action="version",
103
+ version=f"pipelock-verify {__version__}",
104
+ )
105
+
106
+ args = parser.parse_args(argv)
107
+ path: Path = args.path
108
+
109
+ if not path.exists():
110
+ print(f"FAILED: {path}: file not found", file=sys.stderr)
111
+ return 1
112
+
113
+ mode = args.mode if args.mode != "auto" else _detect_mode(path)
114
+
115
+ if mode == "chain":
116
+ chain_result = verify_chain(path, args.public_key_hex)
117
+ # Empty files are rejected at the CLI layer to match the Go CLI's
118
+ # behavior (internal/cli/signing/receipt.go returns an error when
119
+ # the extracted receipt list is empty). The library function keeps
120
+ # the permissive "empty is vacuously valid" shape that mirrors
121
+ # Go's receipt.VerifyChain.
122
+ if chain_result.valid and chain_result.receipt_count == 0:
123
+ print(f"No receipts found in {path}")
124
+ return 1
125
+ _print_chain(chain_result, path)
126
+ return 0 if chain_result.valid else 1
127
+
128
+ try:
129
+ data = path.read_bytes()
130
+ except OSError as exc:
131
+ print(f"FAILED: {path}: {exc}", file=sys.stderr)
132
+ return 1
133
+
134
+ single_result = verify(data, args.public_key_hex)
135
+ _print_single(single_result, path)
136
+ return 0 if single_result.valid else 1
137
+
138
+
139
+ if __name__ == "__main__":
140
+ raise SystemExit(main())