agent-receipts 0.2.1__py3-none-any.whl

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,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-receipts
3
+ Version: 0.2.1
4
+ Summary: Python SDK for the Action Receipts protocol
5
+ Project-URL: Homepage, https://github.com/agnt-rcpt/sdk-py
6
+ Project-URL: Repository, https://github.com/agnt-rcpt/sdk-py
7
+ Project-URL: Issues, https://github.com/agnt-rcpt/sdk-py/issues
8
+ Project-URL: Spec, https://github.com/agnt-rcpt/spec
9
+ Author: Otto Jongerius
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: agent,ai,audit,receipts,verifiable-credentials
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: cryptography>=41.0
23
+ Requires-Dist: pydantic>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pyright>=1.1; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.9; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <div align="center">
31
+
32
+ # attest-protocol
33
+
34
+ ### Python SDK for the Action Receipts protocol
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/attest-protocol)](https://pypi.org/project/attest-protocol/)
37
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
38
+ [![Python](https://img.shields.io/badge/Python-3.11+-3776AB?logo=python&logoColor=white)](https://www.python.org/)
39
+ [![CI](https://github.com/attest-protocol/attest-py/actions/workflows/ci.yml/badge.svg)](https://github.com/attest-protocol/attest-py/actions/workflows/ci.yml)
40
+
41
+ ---
42
+
43
+ Create, sign, hash-chain, store, and verify cryptographically signed audit trails for AI agent actions.
44
+
45
+ [Spec](https://github.com/attest-protocol/spec) &bull; [TypeScript SDK](https://github.com/attest-protocol/attest-ts) &bull; [Reference Implementation](https://github.com/ojongerius/attest)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## Why receipts?
52
+
53
+ If you're building with AI agents, you're probably already logging what they do. Receipts go further: they're **cryptographically signed, hash-chained records** that can't be quietly altered after the fact — and they follow a standard format that works across languages, agents, and systems.
54
+
55
+ Here's where that matters in practice:
56
+
57
+ - **Post-incident review** — An agent ran overnight and something broke. The receipt chain shows exactly which actions it took, in what order, and whether each succeeded or failed — with cryptographic proof the log hasn't been tampered with after the fact.
58
+
59
+ - **Compliance and audit** — Regulated environments require evidence of what systems did and why. Receipts are W3C Verifiable Credentials with Ed25519 signatures, giving auditors a tamper-evident trail they can independently verify.
60
+
61
+ - **Safer autonomous agents** — Agents can query their own audit trail mid-session. Before taking a high-risk action, an agent can check what it has already done and whether previous steps succeeded, enabling self-correcting workflows.
62
+
63
+ - **Multi-agent trust** — When agents collaborate, receipts serve as proof of prior actions. Agent B can verify that Agent A actually completed step 1 before proceeding to step 2, without trusting a shared log.
64
+
65
+ - **Usage tracking** — Every action is classified by type and risk level, giving you a structured breakdown of what agents spent their time on.
66
+
67
+ ### Beyond local storage
68
+
69
+ The protocol is designed for receipts to travel — publishing to a shared ledger, forwarding to a compliance system, or exchanging between agents as proof of prior actions. Receipts are portable W3C Verifiable Credentials, but where they go is always under the user's control.
70
+
71
+ ---
72
+
73
+ ## Install
74
+
75
+ ```sh
76
+ pip install attest-protocol
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ### Create and sign a receipt
82
+
83
+ ```python
84
+ from attest_protocol import (
85
+ create_receipt,
86
+ generate_key_pair,
87
+ hash_receipt,
88
+ sign_receipt,
89
+ CreateReceiptInput,
90
+ Chain,
91
+ Issuer,
92
+ Outcome,
93
+ Principal,
94
+ )
95
+ from attest_protocol.receipt.create import ActionInput
96
+
97
+ # Generate an Ed25519 key pair
98
+ keys = generate_key_pair()
99
+
100
+ # Create an unsigned receipt
101
+ unsigned = create_receipt(CreateReceiptInput(
102
+ issuer=Issuer(id="did:agent:my-agent"),
103
+ principal=Principal(id="did:user:alice"),
104
+ action=ActionInput(
105
+ type="filesystem.file.read",
106
+ risk_level="low",
107
+ ),
108
+ outcome=Outcome(status="success"),
109
+ chain=Chain(
110
+ sequence=1,
111
+ previous_receipt_hash=None,
112
+ chain_id="chain_session-1",
113
+ ),
114
+ ))
115
+
116
+ # Sign and hash
117
+ receipt = sign_receipt(unsigned, keys.private_key, "did:agent:my-agent#key-1")
118
+ receipt_hash = hash_receipt(receipt)
119
+ ```
120
+
121
+ ### Verify a receipt
122
+
123
+ ```python
124
+ from attest_protocol import verify_receipt
125
+
126
+ valid = verify_receipt(receipt, keys.public_key)
127
+ print(f"Signature valid: {valid}") # True
128
+ ```
129
+
130
+ ### Verify a chain
131
+
132
+ ```python
133
+ from attest_protocol import verify_chain
134
+
135
+ # Verify a list of receipts (e.g. [receipt] from the example above)
136
+ result = verify_chain([receipt], keys.public_key)
137
+ print(f"Chain valid: {result.valid}")
138
+ print(f"Receipts verified: {result.length}")
139
+ if not result.valid:
140
+ print(f"Broken at index: {result.broken_at}")
141
+ ```
142
+
143
+ ### Action taxonomy
144
+
145
+ The standardized action taxonomy (action types and risk levels) is defined in the
146
+ [protocol specification](https://github.com/attest-protocol/spec/tree/main/spec/taxonomy).
147
+ Taxonomy classification will be added in a future milestone (M3).
148
+
149
+ ## What is an Action Receipt?
150
+
151
+ A [W3C Verifiable Credential](https://www.w3.org/TR/vc-data-model-2.0/) signed with Ed25519, recording:
152
+
153
+ | Field | What it captures |
154
+ |:---|:---|
155
+ | **Action** | What happened, classified by a [standardized taxonomy](https://github.com/attest-protocol/spec/tree/main/spec/taxonomy) |
156
+ | **Principal** | Who authorized it (human or organization) |
157
+ | **Issuer** | Which agent performed it |
158
+ | **Outcome** | Success/failure, reversibility, undo method |
159
+ | **Chain** | SHA-256 hash link to the previous receipt (tamper-evident) |
160
+ | **Privacy** | Parameters are hashed, never stored in plaintext |
161
+
162
+ ## API reference
163
+
164
+ ### Receipt creation and signing
165
+
166
+ ```python
167
+ from attest_protocol import (
168
+ create_receipt, # Build an unsigned receipt from input fields
169
+ generate_key_pair, # Ed25519 key pair (PEM-encoded)
170
+ sign_receipt, # Sign with Ed25519Signature2020 proof
171
+ verify_receipt, # Verify a receipt's signature
172
+ )
173
+ ```
174
+
175
+ ### Hashing and canonicalization
176
+
177
+ ```python
178
+ from attest_protocol import (
179
+ canonicalize, # RFC 8785 JSON canonicalization
180
+ hash_receipt, # Hash receipt (excluding proof) -> "sha256:<hex>"
181
+ sha256, # Hash arbitrary data -> "sha256:<hex>"
182
+ )
183
+ ```
184
+
185
+ ### Chain verification
186
+
187
+ ```python
188
+ from attest_protocol import (
189
+ verify_chain, # Verify signatures, hash links, and sequence numbering
190
+ )
191
+ ```
192
+
193
+ ### Types (Pydantic v2 models)
194
+
195
+ ```python
196
+ from attest_protocol import (
197
+ ActionReceipt, # Signed receipt with proof
198
+ UnsignedActionReceipt, # Receipt before signing
199
+ Action, ActionTarget, Authorization, Chain,
200
+ CredentialSubject, Intent, Issuer, Operator,
201
+ Outcome, Principal, Proof, StateChange,
202
+ )
203
+ ```
204
+
205
+ ### Subpackage imports
206
+
207
+ ```python
208
+ from attest_protocol.receipt import create_receipt, sign_receipt
209
+ from attest_protocol.receipt.hash import canonicalize
210
+ from attest_protocol.receipt.types import CONTEXT, CREDENTIAL_TYPE
211
+ ```
212
+
213
+ ### TypeScript SDK compatibility
214
+
215
+ camelCase aliases are available for users coming from the TS SDK:
216
+
217
+ ```python
218
+ from attest_protocol import (
219
+ createReceipt, # = create_receipt
220
+ generateKeyPair, # = generate_key_pair
221
+ signReceipt, # = sign_receipt
222
+ verifyReceipt, # = verify_receipt
223
+ hashReceipt, # = hash_receipt
224
+ verifyChain, # = verify_chain
225
+ )
226
+ ```
227
+
228
+ ## Cross-language compatibility
229
+
230
+ This SDK produces **byte-identical** output to [`@attest-protocol/attest-ts`](https://github.com/attest-protocol/attest-ts):
231
+
232
+ - RFC 8785 canonical JSON matches exactly
233
+ - SHA-256 hashes are identical
234
+ - Ed25519 signatures from either SDK verify in the other
235
+
236
+ Cross-language compatibility is verified by test vectors generated from the TypeScript SDK.
237
+
238
+ ## Project structure
239
+
240
+ ```
241
+ src/attest_protocol/
242
+ receipt/
243
+ types.py # Pydantic models for all receipt types
244
+ create.py # Receipt creation with auto-generated IDs
245
+ signing.py # Ed25519 signing and verification
246
+ hash.py # RFC 8785 canonicalization + SHA-256
247
+ chain.py # Chain verification
248
+ ```
249
+
250
+ ## Development
251
+
252
+ ```sh
253
+ uv sync --all-extras
254
+ uv run pytest # run tests
255
+ uv run ruff check . # lint
256
+ uv run ruff format . # format
257
+ uv run pyright # type check
258
+ ```
259
+
260
+ | | |
261
+ |:---|:---|
262
+ | **Language** | Python 3.11+ |
263
+ | **Types** | Pydantic v2, pyright strict mode |
264
+ | **Linting** | ruff |
265
+ | **Testing** | pytest |
266
+ | **Dependencies** | `pydantic>=2.0`, `cryptography>=41.0` |
267
+
268
+ ## Ecosystem
269
+
270
+ | Repository | Description |
271
+ |:---|:---|
272
+ | [attest-protocol/spec](https://github.com/attest-protocol/spec) | Protocol specification, JSON Schemas, canonical taxonomy |
273
+ | [attest-protocol/attest-ts](https://github.com/attest-protocol/attest-ts) | TypeScript SDK ([npm](https://www.npmjs.com/package/@attest-protocol/attest-ts)) |
274
+ | **attest-protocol/attest-py** (this package) | Python SDK |
275
+ | [ojongerius/attest](https://github.com/ojongerius/attest) | MCP proxy + CLI (reference implementation) |
276
+
277
+ ## License
278
+
279
+ Apache 2.0 — see [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
1
+ attest_protocol/__init__.py,sha256=kwKaNQHb7xg5-tanXkI-Z4dfYcoU5_vL3fe2YKGyMIc,3343
2
+ attest_protocol/_version.py,sha256=w9Iw7QVvd8lme2wKwEbCo5IgetVjSfFBOOYAcA_Q0Ns,18
3
+ attest_protocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ attest_protocol/receipt/__init__.py,sha256=l1RPZgdyALgAJ49qWSoCuz0FJulfnOIMh6KMgvJl3cs,1362
5
+ attest_protocol/receipt/chain.py,sha256=6pL26ksKoHTW0nyHbYd-IGsVQjVTuP9mNg1w3qKPDrc,2661
6
+ attest_protocol/receipt/create.py,sha256=kEvk7sj534nT3W7ssj8qQ6SGE15p1Hw_Z9IaAeRUS0s,2932
7
+ attest_protocol/receipt/hash.py,sha256=gB8tCvTH2rAleK7GU0mli2Mq85yDVRSUs5ApJVUTh_g,3697
8
+ attest_protocol/receipt/signing.py,sha256=vT62mnnqOxOMhU3ZgvEf6bzvwd2IDYYz8GhsHfP0fDc,4226
9
+ attest_protocol/receipt/types.py,sha256=c9ILfSSfSrffn9-XEs232vk_exyNjSB-FJFFWg0v7dE,3588
10
+ attest_protocol/store/__init__.py,sha256=i50VW5hkZeVNInLOUfL2r2rgqAlZs2hnR2mxbww_DwU,364
11
+ attest_protocol/store/store.py,sha256=BuU_EI7OgcmaZeOS1wHt-vXOgwR4yYSDO4XHrzwamLM,7164
12
+ attest_protocol/store/verify.py,sha256=kzz_3pZCklTivAeDa1SmqFLhyLXuc77FTy9332S02FM,587
13
+ attest_protocol/taxonomy/__init__.py,sha256=M2DLT_4mnVOlblPbVlPz9Z_7TUgWdObAnpjbzRziRR8,791
14
+ attest_protocol/taxonomy/actions.py,sha256=LKgFV_yrbtsfv4LJSZBnkNpIhZ3HJW9GlZDB7QIFYEU,2949
15
+ attest_protocol/taxonomy/classify.py,sha256=lZR6Dv2z59vXkEpjSPayIr_oP5B1hW5d2ZS93xCbt3s,1058
16
+ attest_protocol/taxonomy/config.py,sha256=1NzzNo9U407xEKDnzF3VztOarlamlj_N9dbTmZ-9BA4,1973
17
+ attest_protocol/taxonomy/types.py,sha256=e70VsA-YMLqJfbJPmsRo2i0Ho4LyQv8HtiPmpg3AZR4,512
18
+ agent_receipts-0.2.1.dist-info/METADATA,sha256=9KwactIRXDzVpfAgmJma1HtvV3CbqsJft00EM1NPMV8,9559
19
+ agent_receipts-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
20
+ agent_receipts-0.2.1.dist-info/licenses/LICENSE,sha256=ZNP4Wy2jzAlWYAvfws5JqirwxpamE_Xo-BZfjgZ39i0,1072
21
+ agent_receipts-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 attest-protocol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ """Python SDK for the Action Receipts protocol."""
2
+
3
+ from attest_protocol._version import VERSION
4
+ from attest_protocol.receipt.chain import (
5
+ ChainVerification,
6
+ ReceiptVerification,
7
+ verify_chain,
8
+ )
9
+ from attest_protocol.receipt.create import CreateReceiptInput, create_receipt
10
+ from attest_protocol.receipt.hash import canonicalize, hash_receipt, sha256
11
+ from attest_protocol.receipt.signing import (
12
+ KeyPair,
13
+ generate_key_pair,
14
+ sign_receipt,
15
+ verify_receipt,
16
+ )
17
+ from attest_protocol.receipt.types import (
18
+ CONTEXT,
19
+ CREDENTIAL_TYPE,
20
+ Action,
21
+ ActionReceipt,
22
+ ActionTarget,
23
+ Authorization,
24
+ Chain,
25
+ CredentialSubject,
26
+ Intent,
27
+ Issuer,
28
+ Operator,
29
+ Outcome,
30
+ Principal,
31
+ Proof,
32
+ StateChange,
33
+ UnsignedActionReceipt,
34
+ )
35
+ from attest_protocol.store.store import (
36
+ ReceiptQuery,
37
+ ReceiptStore,
38
+ StoreStats,
39
+ open_store,
40
+ )
41
+ from attest_protocol.store.verify import verify_stored_chain
42
+ from attest_protocol.taxonomy.actions import (
43
+ ALL_ACTIONS,
44
+ FILESYSTEM_ACTIONS,
45
+ SYSTEM_ACTIONS,
46
+ UNKNOWN_ACTION,
47
+ get_action_type,
48
+ resolve_action_type,
49
+ )
50
+ from attest_protocol.taxonomy.classify import ClassificationResult, classify_tool_call
51
+ from attest_protocol.taxonomy.config import load_taxonomy_config
52
+ from attest_protocol.taxonomy.types import ActionTypeEntry, TaxonomyMapping
53
+
54
+ # camelCase aliases for users coming from the TypeScript SDK
55
+ createReceipt = create_receipt
56
+ generateKeyPair = generate_key_pair
57
+ signReceipt = sign_receipt
58
+ verifyReceipt = verify_receipt
59
+ hashReceipt = hash_receipt
60
+ verifyChain = verify_chain
61
+ openStore = open_store
62
+ verifyStoredChain = verify_stored_chain
63
+ classifyToolCall = classify_tool_call
64
+ getActionType = get_action_type
65
+ resolveActionType = resolve_action_type
66
+ loadTaxonomyConfig = load_taxonomy_config
67
+
68
+ # RECEIPT_VERSION is the receipt schema version (from types), not the package version
69
+ from attest_protocol.receipt.types import VERSION as RECEIPT_VERSION # noqa: E402
70
+
71
+ __all__ = [
72
+ # Version
73
+ "VERSION",
74
+ "RECEIPT_VERSION",
75
+ # Types
76
+ "Action",
77
+ "ActionReceipt",
78
+ "ActionTarget",
79
+ "Authorization",
80
+ "Chain",
81
+ "CredentialSubject",
82
+ "Intent",
83
+ "Issuer",
84
+ "Operator",
85
+ "Outcome",
86
+ "Principal",
87
+ "Proof",
88
+ "StateChange",
89
+ "UnsignedActionReceipt",
90
+ # Constants
91
+ "CONTEXT",
92
+ "CREDENTIAL_TYPE",
93
+ # Creation
94
+ "CreateReceiptInput",
95
+ "create_receipt",
96
+ "createReceipt",
97
+ # Hashing
98
+ "canonicalize",
99
+ "hash_receipt",
100
+ "hashReceipt",
101
+ "sha256",
102
+ # Signing
103
+ "KeyPair",
104
+ "generate_key_pair",
105
+ "generateKeyPair",
106
+ "sign_receipt",
107
+ "signReceipt",
108
+ "verify_receipt",
109
+ "verifyReceipt",
110
+ # Chain
111
+ "ChainVerification",
112
+ "ReceiptVerification",
113
+ "verify_chain",
114
+ "verifyChain",
115
+ # Store
116
+ "ReceiptQuery",
117
+ "ReceiptStore",
118
+ "StoreStats",
119
+ "open_store",
120
+ "openStore",
121
+ "verify_stored_chain",
122
+ "verifyStoredChain",
123
+ # Taxonomy
124
+ "ALL_ACTIONS",
125
+ "ActionTypeEntry",
126
+ "ClassificationResult",
127
+ "FILESYSTEM_ACTIONS",
128
+ "SYSTEM_ACTIONS",
129
+ "TaxonomyMapping",
130
+ "UNKNOWN_ACTION",
131
+ "classify_tool_call",
132
+ "classifyToolCall",
133
+ "get_action_type",
134
+ "getActionType",
135
+ "load_taxonomy_config",
136
+ "loadTaxonomyConfig",
137
+ "resolve_action_type",
138
+ "resolveActionType",
139
+ ]
@@ -0,0 +1 @@
1
+ VERSION = "0.2.0"
File without changes
@@ -0,0 +1,70 @@
1
+ from attest_protocol.receipt.chain import (
2
+ ChainVerification,
3
+ ReceiptVerification,
4
+ verify_chain,
5
+ )
6
+ from attest_protocol.receipt.create import CreateReceiptInput, create_receipt
7
+ from attest_protocol.receipt.hash import canonicalize, hash_receipt, sha256
8
+ from attest_protocol.receipt.signing import (
9
+ KeyPair,
10
+ generate_key_pair,
11
+ sign_receipt,
12
+ verify_receipt,
13
+ )
14
+ from attest_protocol.receipt.types import (
15
+ CONTEXT,
16
+ CREDENTIAL_TYPE,
17
+ VERSION,
18
+ Action,
19
+ ActionReceipt,
20
+ ActionTarget,
21
+ Authorization,
22
+ Chain,
23
+ CredentialSubject,
24
+ Intent,
25
+ Issuer,
26
+ Operator,
27
+ Outcome,
28
+ Principal,
29
+ Proof,
30
+ StateChange,
31
+ UnsignedActionReceipt,
32
+ )
33
+
34
+ __all__ = [
35
+ # Types
36
+ "Action",
37
+ "ActionReceipt",
38
+ "ActionTarget",
39
+ "Authorization",
40
+ "Chain",
41
+ "CredentialSubject",
42
+ "Intent",
43
+ "Issuer",
44
+ "Operator",
45
+ "Outcome",
46
+ "Principal",
47
+ "Proof",
48
+ "StateChange",
49
+ "UnsignedActionReceipt",
50
+ # Constants
51
+ "CONTEXT",
52
+ "CREDENTIAL_TYPE",
53
+ "VERSION",
54
+ # Creation
55
+ "CreateReceiptInput",
56
+ "create_receipt",
57
+ # Hashing
58
+ "canonicalize",
59
+ "hash_receipt",
60
+ "sha256",
61
+ # Signing
62
+ "KeyPair",
63
+ "generate_key_pair",
64
+ "sign_receipt",
65
+ "verify_receipt",
66
+ # Chain
67
+ "ChainVerification",
68
+ "ReceiptVerification",
69
+ "verify_chain",
70
+ ]
@@ -0,0 +1,95 @@
1
+ """Chain verification — validate receipt chains for integrity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ from attest_protocol.receipt.hash import hash_receipt
9
+ from attest_protocol.receipt.signing import verify_receipt
10
+
11
+ if TYPE_CHECKING:
12
+ from attest_protocol.receipt.types import ActionReceipt
13
+
14
+
15
+ @dataclass
16
+ class ReceiptVerification:
17
+ """Result of verifying a single receipt in a chain."""
18
+
19
+ index: int
20
+ receipt_id: str
21
+ signature_valid: bool
22
+ hash_link_valid: bool
23
+ sequence_valid: bool
24
+
25
+
26
+ @dataclass
27
+ class ChainVerification:
28
+ """Result of verifying an entire chain."""
29
+
30
+ valid: bool
31
+ length: int
32
+ receipts: list[ReceiptVerification] = field(default_factory=list)
33
+ broken_at: int = -1
34
+
35
+
36
+ def verify_chain(
37
+ receipts: list[ActionReceipt],
38
+ public_key: str,
39
+ ) -> ChainVerification:
40
+ """Verify a chain of signed receipts.
41
+
42
+ Checks for each receipt:
43
+ 1. Ed25519 signature validity
44
+ 2. Hash linkage: previous_receipt_hash matches SHA-256 of prior receipt
45
+ 3. Sequence numbers are strictly incrementing
46
+
47
+ Receipts must be provided in chain order (by sequence number).
48
+ """
49
+ if not receipts:
50
+ return ChainVerification(valid=True, length=0)
51
+
52
+ results: list[ReceiptVerification] = []
53
+ broken_at = -1
54
+ previous: ActionReceipt | None = None
55
+
56
+ for i, receipt in enumerate(receipts):
57
+ chain = receipt.credentialSubject.chain
58
+
59
+ signature_valid = verify_receipt(receipt, public_key)
60
+
61
+ if previous is None:
62
+ hash_link_valid = chain.previous_receipt_hash is None
63
+ else:
64
+ previous_hash = hash_receipt(previous)
65
+ hash_link_valid = chain.previous_receipt_hash == previous_hash
66
+
67
+ current_sequence = chain.sequence
68
+ if previous is None:
69
+ sequence_valid = current_sequence >= 1
70
+ else:
71
+ prev_sequence = previous.credentialSubject.chain.sequence
72
+ sequence_valid = current_sequence == prev_sequence + 1
73
+
74
+ verification = ReceiptVerification(
75
+ index=i,
76
+ receipt_id=receipt.id,
77
+ signature_valid=signature_valid,
78
+ hash_link_valid=hash_link_valid,
79
+ sequence_valid=sequence_valid,
80
+ )
81
+ results.append(verification)
82
+
83
+ if broken_at == -1 and (
84
+ not signature_valid or not hash_link_valid or not sequence_valid
85
+ ):
86
+ broken_at = i
87
+
88
+ previous = receipt
89
+
90
+ return ChainVerification(
91
+ valid=broken_at == -1,
92
+ length=len(receipts),
93
+ receipts=results,
94
+ broken_at=broken_at,
95
+ )