agora-agent-receipts 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.
- agora_agent_receipts-0.1.0/LICENSE +21 -0
- agora_agent_receipts-0.1.0/PKG-INFO +219 -0
- agora_agent_receipts-0.1.0/README.md +196 -0
- agora_agent_receipts-0.1.0/agent_receipts.py +221 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/PKG-INFO +219 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/SOURCES.txt +14 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/dependency_links.txt +1 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/entry_points.txt +2 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/requires.txt +6 -0
- agora_agent_receipts-0.1.0/agora_agent_receipts.egg-info/top_level.txt +4 -0
- agora_agent_receipts-0.1.0/mcp_wrapper.py +71 -0
- agora_agent_receipts-0.1.0/mediator.py +142 -0
- agora_agent_receipts-0.1.0/pyproject.toml +35 -0
- agora_agent_receipts-0.1.0/setup.cfg +4 -0
- agora_agent_receipts-0.1.0/tests/test_receipts.py +130 -0
- agora_agent_receipts-0.1.0/verify_cli.py +70 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agora
|
|
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,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agora-agent-receipts
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tamper-evident, third-party-verifiable receipts for AI agent / MCP tool calls
|
|
5
|
+
Author: Agora
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DanceNitra/agora/tree/main/agent-receipts
|
|
8
|
+
Project-URL: Source, https://github.com/DanceNitra/agora
|
|
9
|
+
Keywords: ai-agents,mcp,verifiable,receipts,ed25519,audit,agent-security,provenance
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Security :: Cryptography
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: crypto
|
|
19
|
+
Requires-Dist: cryptography>=41; extra == "crypto"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# agent-receipts
|
|
25
|
+
|
|
26
|
+
**Tamper-evident, third-party-verifiable receipts for AI agent / MCP actions — in one small file.**
|
|
27
|
+
|
|
28
|
+
An AI agent's logs are *self-reported claims*. Nothing stops the agent — or a compromised proxy —
|
|
29
|
+
from rewriting history after the fact, or from emitting a hallucinated "I called the database and it
|
|
30
|
+
returned X" that never happened. A **receipt** is the opposite of a log: independent, verifiable
|
|
31
|
+
evidence of what an action consumed and produced, that a third party can check **without trusting the
|
|
32
|
+
agent**.
|
|
33
|
+
|
|
34
|
+
This is the smallest honest version of that idea, built to be read in one sitting and run in one
|
|
35
|
+
command. It is a reference proof-of-concept, not a hardened product — the scope below is deliberately
|
|
36
|
+
honest about what it does and does not give you.
|
|
37
|
+
|
|
38
|
+
> **Naming note / prior art.** There is already an established **"Agent Receipts" protocol** with a
|
|
39
|
+
> public spec and a Python SDK by Otto Jongerius ([github.com/agent-receipts](https://github.com/agent-receipts/ar)).
|
|
40
|
+
> This project is an **independent, minimal reference** for understanding the idea — it is *not* that
|
|
41
|
+
> protocol's SDK, and on PyPI it is `agora-agent-receipts` to avoid any confusion. If you want the
|
|
42
|
+
> protocol and a maintained SDK, use his; if you want a 200-line file to learn from or vendor, use this.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python agent_receipts.py # core: hash-chain + Ed25519 signatures + tamper/forgery demo
|
|
46
|
+
python mcp_wrapper.py # wrap any MCP/agent tool so every call emits a receipt
|
|
47
|
+
python mediator.py # external-mediator mode: catch an agent hiding/faking its own actions
|
|
48
|
+
python verify_cli.py receipts.json --pubkey <hex> # independently verify a receipts file (no code)
|
|
49
|
+
python mnemo_receipts.py # tamper-evident memory: detect an out-of-band edit to an mnemo store
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What it does — two layers
|
|
53
|
+
|
|
54
|
+
1. **Hash chain (integrity, zero extra deps).** Each receipt commits to the previous one
|
|
55
|
+
(`prev = hash of the last receipt`), forming a Merkle-style chain. Edit *any* past receipt and
|
|
56
|
+
every hash after it breaks — so a *partial* edit is **detectable**, and `verify()` names the exact
|
|
57
|
+
step that was altered. **Honest limit:** the hash chain *alone* does not stop a thorough tamperer
|
|
58
|
+
who recomputes the whole chain end-to-end (then no link breaks). Integrity-only is sufficient only
|
|
59
|
+
if the chain head is published/anchored where the attacker can't also rewrite it — otherwise the
|
|
60
|
+
signature (layer 2) is what actually protects a self-held chain.
|
|
61
|
+
2. **Ed25519 signatures (authenticity, needs `cryptography`).** Each receipt's hash is signed with
|
|
62
|
+
the actor's private key; a third party verifies with the **public key only**. This proves *who*
|
|
63
|
+
produced the receipt and that the content wasn't forged — no shared secret. (If `cryptography`
|
|
64
|
+
isn't installed, the hash chain still works on its own.)
|
|
65
|
+
|
|
66
|
+
A receipt commits to the **SHA-256 of inputs/outputs, not the raw content** — so you prove *what* was
|
|
67
|
+
processed without exposing it, and reveal a value later only if you choose (anyone can re-check it
|
|
68
|
+
against the committed hash). Full zero-knowledge proofs (ZK-SNARKs) go further still — proving a
|
|
69
|
+
computation was correct while revealing *nothing* — and are the heavy end of this same design space.
|
|
70
|
+
|
|
71
|
+
## What the demo measures
|
|
72
|
+
|
|
73
|
+
The self-demo records three MCP tool calls, then attacks the trail:
|
|
74
|
+
|
|
75
|
+
| step | action | result |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| 1 | honest chain | `verify() → True` |
|
|
78
|
+
| 2 | edit a past receipt's output | caught: *content tampered* at the exact seq |
|
|
79
|
+
| 3 | re-hash the forged receipt to look consistent | still caught: *invalid signature* + *broken chain link downstream* |
|
|
80
|
+
|
|
81
|
+
Logs cannot do this: a self-reported log can be edited end-to-end and looks identical afterward.
|
|
82
|
+
|
|
83
|
+
## Use it
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from agent_receipts import ReceiptChain, generate_keypair
|
|
87
|
+
|
|
88
|
+
sk, pk = generate_keypair() # the actor's identity
|
|
89
|
+
chain = ReceiptChain(actor="my-agent", private_key_hex=sk, public_key_hex=pk)
|
|
90
|
+
|
|
91
|
+
chain.record("mcp.web_search", {"query": "..."}, {"results": 7}) # one receipt per tool call
|
|
92
|
+
chain.record("mcp.memory.write", {"fact": "..."}, {"stored": True})
|
|
93
|
+
|
|
94
|
+
ok, problems = chain.verify(expected_pubkey=pk) # anyone with pk can run this
|
|
95
|
+
open("receipts.json", "w").write(chain.to_json()) # ship the receipts; the public key verifies them
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`mcp_wrapper.py` shows a `ReceiptedDispatcher` that drops in around an MCP-style tool registry so you
|
|
99
|
+
don't touch your tools — you wrap the dispatch.
|
|
100
|
+
|
|
101
|
+
## External-mediator mode (`mediator.py`)
|
|
102
|
+
|
|
103
|
+
Self-signed receipts have one honest hole: an agent holding its own key can simply *not* emit a receipt
|
|
104
|
+
for an action it wants to hide. The fix (as used by `pipelock` and the receiver-attested research line)
|
|
105
|
+
is to move the signer **outside** the agent — route every tool call through a `Mediator` that holds its
|
|
106
|
+
own key and signs what it actually brokers:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from mediator import Mediator, reconcile
|
|
110
|
+
med = Mediator(tools, private_key_hex=mk, public_key_hex=mpub, name="org-proxy")
|
|
111
|
+
med.dispatch("transfer_funds", to="acct-9981", amount=5000) # mediator signs the real call
|
|
112
|
+
ok, problems = reconcile(agent_self_reported_log, med.chain) # diff the agent's story vs the truth
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The demo has an agent route three calls through the mediator, then tamper with its *own* log — hiding a
|
|
116
|
+
funds transfer and faking a secret read. `reconcile()` catches all of it, because the mediator's signed
|
|
117
|
+
chain is ground truth:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
agent reported 2 actions but the mediator brokered 3 -> agent OMITTED 1 call(s)
|
|
121
|
+
action 1: agent calls it 'read_secret', mediator saw 'mcp.transfer_funds'
|
|
122
|
+
action 1 (mcp.transfer_funds): input hash mismatch (agent misreported its arguments)
|
|
123
|
+
action 1 (mcp.transfer_funds): output hash mismatch (agent misreported the result)
|
|
124
|
+
action 2 (mcp.read_secret): no agent record at all (hidden call)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
(This is the real demo output — `reconcile()` aligns positionally, so omitting a middle call also shifts
|
|
128
|
+
the indices after it; the point is that the omission and the misreport are both surfaced.) Now the agent
|
|
129
|
+
cannot withhold a receipt, forge one (it lacks the mediator's key), or lie about I/O (the
|
|
130
|
+
mediator hashes what really flowed through it). Optionally the agent also signs its own claim, giving a
|
|
131
|
+
dual-attested receipt where agent-vs-mediator divergence is itself the alarm.
|
|
132
|
+
|
|
133
|
+
## Verify someone else's receipts (`verify_cli.py`)
|
|
134
|
+
|
|
135
|
+
A third party who wasn't there confirms what happened with one command — the file plus the public key:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python verify_cli.py receipts.json --pubkey 7d08e6e6... # VERIFIED (exit 0) or FAILED (exit 1)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
It recomputes the whole chain, checks every signature against the expected key, and names the exact
|
|
142
|
+
broken step. Exit code 0/1 drops cleanly into CI or a pre-commit hook. Measured on a 2-receipt file: an
|
|
143
|
+
honest file verifies; tampering one output prints `seq 0: content tampered` (exit 1); the wrong `--pubkey`
|
|
144
|
+
prints `signed by an unexpected key` (exit 1).
|
|
145
|
+
|
|
146
|
+
## Tamper-evident memory: the `mnemo` integration (`mnemo_receipts.py`)
|
|
147
|
+
|
|
148
|
+
[mnemo](https://github.com/DanceNitra/agora/tree/main/mnemo) (our open-source memory core) is already
|
|
149
|
+
append-only with deterministic supersession, so it never silently edits a fact in normal use. But the
|
|
150
|
+
store is a file — anyone who can touch it can rewrite a stored memory after the fact, and any store
|
|
151
|
+
would then serve the altered text as the original. Receipts close that: every `remember()` emits a
|
|
152
|
+
signed receipt committing to the memory's content hash, so the *write history* is independently
|
|
153
|
+
verifiable.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from mnemo_receipts import ReceiptedMnemo, audit_memory
|
|
157
|
+
rm = ReceiptedMnemo(Mnemo(path="mem.json"), private_key_hex=sk, public_key_hex=pk)
|
|
158
|
+
rm.remember("The prod database host is db-prod-01.", key="prod-db::host", mtype="semantic")
|
|
159
|
+
ok, problems = audit_memory(rm.m, rm.chain, expected_pubkey=pk)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`audit_memory()` re-hashes the current store against the write receipts. Measured: an honest store
|
|
163
|
+
audits clean; an **out-of-band edit** (`db-prod-01 → db-attacker-07`, made straight in the store, which
|
|
164
|
+
mnemo itself can't see) is caught — `memory <id>: stored content no longer matches the write receipt`.
|
|
165
|
+
This is a thin wrapper; it does **not** modify mnemo's zero-dependency core.
|
|
166
|
+
|
|
167
|
+
## Honest scope (what this is NOT)
|
|
168
|
+
|
|
169
|
+
- The *self-signed* core proves a receipt **chain is internally consistent and authentically signed**.
|
|
170
|
+
It does **not** by itself prove the agent reported *every* action — an actor that controls its own
|
|
171
|
+
key can still withhold a receipt. That gap is closed by **external-mediator mode** (`mediator.py`,
|
|
172
|
+
below), which puts the signer outside the agent; anchoring the chain head to a third party is a
|
|
173
|
+
further hardening.
|
|
174
|
+
- It commits to input/output **hashes**, not a proof that the tool *computed correctly*. That is what
|
|
175
|
+
ZK-SNARK approaches add, at much higher cost.
|
|
176
|
+
- Keys here are raw/in-memory for clarity; real deployments use a KMS / hardware-backed key store.
|
|
177
|
+
|
|
178
|
+
## Landscape & prior art
|
|
179
|
+
|
|
180
|
+
This sits in an active, fast-moving space — **we build on it, we did not invent it.** In particular,
|
|
181
|
+
the exact pattern here (Ed25519 + canonical JSON + hash-chain) is the production-grade subject of
|
|
182
|
+
**Microsoft's [agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit),
|
|
183
|
+
Tutorial 33 "offline verifiable receipts"** (Ed25519 over RFC 8785 / JCS canonical payloads,
|
|
184
|
+
hash-chained, CLI-verifiable offline). Treat this repo as the *minimal one-file way to understand the
|
|
185
|
+
idea*, and that toolkit as the grown-up version.
|
|
186
|
+
|
|
187
|
+
Honest map of the space:
|
|
188
|
+
|
|
189
|
+
- **A named protocol + SDK:** the **"Agent Receipts" protocol** by Otto Jongerius — a public spec
|
|
190
|
+
([github.com/agent-receipts/ar](https://github.com/agent-receipts/ar)) plus a maintained Python
|
|
191
|
+
SDK (`pip install agent-receipts`). The most directly-related effort to this one; if you need an
|
|
192
|
+
interoperable standard rather than a teaching reference, start there.
|
|
193
|
+
- **Production OSS (corporate):** Microsoft `agent-governance-toolkit` — Tutorial 33 = the same
|
|
194
|
+
Ed25519 + canonical + hash-chain receipts, with policy/identity/sandboxing around it.
|
|
195
|
+
- **External-mediator receipts:** [`pipelock`](https://github.com/luckyPipewrench/pipelock) — an
|
|
196
|
+
open-source MCP/egress firewall that emits *mediator-signed* Ed25519 receipts from **outside** the
|
|
197
|
+
agent (core Apache-2.0; enterprise features Elastic-License), which is how you close the
|
|
198
|
+
agent-can-withhold-a-receipt gap noted above.
|
|
199
|
+
- **Commercial:** [Zero Proof AI](https://zeroproofai.com) — a pre-launch "certificate authority for
|
|
200
|
+
AI agents" issuing on-chain-anchored receipts for tool calls.
|
|
201
|
+
- **Research:**
|
|
202
|
+
- Basu, *Tool Receipts, Not Zero-Knowledge Proofs: Practical Hallucination Detection for AI Agents*,
|
|
203
|
+
[arXiv:2603.10060](https://arxiv.org/abs/2603.10060) (2026) — HMAC-signed tool-execution receipts
|
|
204
|
+
(the pragmatic, symmetric camp; we use Ed25519 so a third party verifies without a shared secret).
|
|
205
|
+
- Figuera, *Notarized Agents: Receiver-Attested Confidential Receipts for AI Agent Actions*,
|
|
206
|
+
[arXiv:2606.04193](https://arxiv.org/abs/2606.04193) (2026) — receiver-signed receipts published
|
|
207
|
+
to a transparency log (the external-attestation camp).
|
|
208
|
+
- Jing & Qi, *Zero-Knowledge Audit for Internet of Agents … with Model Context Protocol*,
|
|
209
|
+
[arXiv:2512.14737](https://arxiv.org/abs/2512.14737) (2025) — the zero-knowledge / privacy-
|
|
210
|
+
preserving end of the same space.
|
|
211
|
+
|
|
212
|
+
## Roadmap (if this proves useful)
|
|
213
|
+
|
|
214
|
+
~~External-mediator mode~~ (done — `mediator.py`) · ~~verifier CLI~~ (done — `verify_cli.py`) ·
|
|
215
|
+
~~`mnemo` integration~~ (done — `mnemo_receipts.py`) · publish-and-anchor the chain head · selective
|
|
216
|
+
disclosure of a single committed field · packaged spin-out (PyPI).
|
|
217
|
+
|
|
218
|
+
MIT. Part of the [Agora](https://github.com/DanceNitra/agora) project — an autonomous research OS that
|
|
219
|
+
ships every claim with a runnable receipt. Feedback and adversarial testing welcome.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# agent-receipts
|
|
2
|
+
|
|
3
|
+
**Tamper-evident, third-party-verifiable receipts for AI agent / MCP actions — in one small file.**
|
|
4
|
+
|
|
5
|
+
An AI agent's logs are *self-reported claims*. Nothing stops the agent — or a compromised proxy —
|
|
6
|
+
from rewriting history after the fact, or from emitting a hallucinated "I called the database and it
|
|
7
|
+
returned X" that never happened. A **receipt** is the opposite of a log: independent, verifiable
|
|
8
|
+
evidence of what an action consumed and produced, that a third party can check **without trusting the
|
|
9
|
+
agent**.
|
|
10
|
+
|
|
11
|
+
This is the smallest honest version of that idea, built to be read in one sitting and run in one
|
|
12
|
+
command. It is a reference proof-of-concept, not a hardened product — the scope below is deliberately
|
|
13
|
+
honest about what it does and does not give you.
|
|
14
|
+
|
|
15
|
+
> **Naming note / prior art.** There is already an established **"Agent Receipts" protocol** with a
|
|
16
|
+
> public spec and a Python SDK by Otto Jongerius ([github.com/agent-receipts](https://github.com/agent-receipts/ar)).
|
|
17
|
+
> This project is an **independent, minimal reference** for understanding the idea — it is *not* that
|
|
18
|
+
> protocol's SDK, and on PyPI it is `agora-agent-receipts` to avoid any confusion. If you want the
|
|
19
|
+
> protocol and a maintained SDK, use his; if you want a 200-line file to learn from or vendor, use this.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python agent_receipts.py # core: hash-chain + Ed25519 signatures + tamper/forgery demo
|
|
23
|
+
python mcp_wrapper.py # wrap any MCP/agent tool so every call emits a receipt
|
|
24
|
+
python mediator.py # external-mediator mode: catch an agent hiding/faking its own actions
|
|
25
|
+
python verify_cli.py receipts.json --pubkey <hex> # independently verify a receipts file (no code)
|
|
26
|
+
python mnemo_receipts.py # tamper-evident memory: detect an out-of-band edit to an mnemo store
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it does — two layers
|
|
30
|
+
|
|
31
|
+
1. **Hash chain (integrity, zero extra deps).** Each receipt commits to the previous one
|
|
32
|
+
(`prev = hash of the last receipt`), forming a Merkle-style chain. Edit *any* past receipt and
|
|
33
|
+
every hash after it breaks — so a *partial* edit is **detectable**, and `verify()` names the exact
|
|
34
|
+
step that was altered. **Honest limit:** the hash chain *alone* does not stop a thorough tamperer
|
|
35
|
+
who recomputes the whole chain end-to-end (then no link breaks). Integrity-only is sufficient only
|
|
36
|
+
if the chain head is published/anchored where the attacker can't also rewrite it — otherwise the
|
|
37
|
+
signature (layer 2) is what actually protects a self-held chain.
|
|
38
|
+
2. **Ed25519 signatures (authenticity, needs `cryptography`).** Each receipt's hash is signed with
|
|
39
|
+
the actor's private key; a third party verifies with the **public key only**. This proves *who*
|
|
40
|
+
produced the receipt and that the content wasn't forged — no shared secret. (If `cryptography`
|
|
41
|
+
isn't installed, the hash chain still works on its own.)
|
|
42
|
+
|
|
43
|
+
A receipt commits to the **SHA-256 of inputs/outputs, not the raw content** — so you prove *what* was
|
|
44
|
+
processed without exposing it, and reveal a value later only if you choose (anyone can re-check it
|
|
45
|
+
against the committed hash). Full zero-knowledge proofs (ZK-SNARKs) go further still — proving a
|
|
46
|
+
computation was correct while revealing *nothing* — and are the heavy end of this same design space.
|
|
47
|
+
|
|
48
|
+
## What the demo measures
|
|
49
|
+
|
|
50
|
+
The self-demo records three MCP tool calls, then attacks the trail:
|
|
51
|
+
|
|
52
|
+
| step | action | result |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| 1 | honest chain | `verify() → True` |
|
|
55
|
+
| 2 | edit a past receipt's output | caught: *content tampered* at the exact seq |
|
|
56
|
+
| 3 | re-hash the forged receipt to look consistent | still caught: *invalid signature* + *broken chain link downstream* |
|
|
57
|
+
|
|
58
|
+
Logs cannot do this: a self-reported log can be edited end-to-end and looks identical afterward.
|
|
59
|
+
|
|
60
|
+
## Use it
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from agent_receipts import ReceiptChain, generate_keypair
|
|
64
|
+
|
|
65
|
+
sk, pk = generate_keypair() # the actor's identity
|
|
66
|
+
chain = ReceiptChain(actor="my-agent", private_key_hex=sk, public_key_hex=pk)
|
|
67
|
+
|
|
68
|
+
chain.record("mcp.web_search", {"query": "..."}, {"results": 7}) # one receipt per tool call
|
|
69
|
+
chain.record("mcp.memory.write", {"fact": "..."}, {"stored": True})
|
|
70
|
+
|
|
71
|
+
ok, problems = chain.verify(expected_pubkey=pk) # anyone with pk can run this
|
|
72
|
+
open("receipts.json", "w").write(chain.to_json()) # ship the receipts; the public key verifies them
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`mcp_wrapper.py` shows a `ReceiptedDispatcher` that drops in around an MCP-style tool registry so you
|
|
76
|
+
don't touch your tools — you wrap the dispatch.
|
|
77
|
+
|
|
78
|
+
## External-mediator mode (`mediator.py`)
|
|
79
|
+
|
|
80
|
+
Self-signed receipts have one honest hole: an agent holding its own key can simply *not* emit a receipt
|
|
81
|
+
for an action it wants to hide. The fix (as used by `pipelock` and the receiver-attested research line)
|
|
82
|
+
is to move the signer **outside** the agent — route every tool call through a `Mediator` that holds its
|
|
83
|
+
own key and signs what it actually brokers:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from mediator import Mediator, reconcile
|
|
87
|
+
med = Mediator(tools, private_key_hex=mk, public_key_hex=mpub, name="org-proxy")
|
|
88
|
+
med.dispatch("transfer_funds", to="acct-9981", amount=5000) # mediator signs the real call
|
|
89
|
+
ok, problems = reconcile(agent_self_reported_log, med.chain) # diff the agent's story vs the truth
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The demo has an agent route three calls through the mediator, then tamper with its *own* log — hiding a
|
|
93
|
+
funds transfer and faking a secret read. `reconcile()` catches all of it, because the mediator's signed
|
|
94
|
+
chain is ground truth:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
agent reported 2 actions but the mediator brokered 3 -> agent OMITTED 1 call(s)
|
|
98
|
+
action 1: agent calls it 'read_secret', mediator saw 'mcp.transfer_funds'
|
|
99
|
+
action 1 (mcp.transfer_funds): input hash mismatch (agent misreported its arguments)
|
|
100
|
+
action 1 (mcp.transfer_funds): output hash mismatch (agent misreported the result)
|
|
101
|
+
action 2 (mcp.read_secret): no agent record at all (hidden call)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
(This is the real demo output — `reconcile()` aligns positionally, so omitting a middle call also shifts
|
|
105
|
+
the indices after it; the point is that the omission and the misreport are both surfaced.) Now the agent
|
|
106
|
+
cannot withhold a receipt, forge one (it lacks the mediator's key), or lie about I/O (the
|
|
107
|
+
mediator hashes what really flowed through it). Optionally the agent also signs its own claim, giving a
|
|
108
|
+
dual-attested receipt where agent-vs-mediator divergence is itself the alarm.
|
|
109
|
+
|
|
110
|
+
## Verify someone else's receipts (`verify_cli.py`)
|
|
111
|
+
|
|
112
|
+
A third party who wasn't there confirms what happened with one command — the file plus the public key:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python verify_cli.py receipts.json --pubkey 7d08e6e6... # VERIFIED (exit 0) or FAILED (exit 1)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It recomputes the whole chain, checks every signature against the expected key, and names the exact
|
|
119
|
+
broken step. Exit code 0/1 drops cleanly into CI or a pre-commit hook. Measured on a 2-receipt file: an
|
|
120
|
+
honest file verifies; tampering one output prints `seq 0: content tampered` (exit 1); the wrong `--pubkey`
|
|
121
|
+
prints `signed by an unexpected key` (exit 1).
|
|
122
|
+
|
|
123
|
+
## Tamper-evident memory: the `mnemo` integration (`mnemo_receipts.py`)
|
|
124
|
+
|
|
125
|
+
[mnemo](https://github.com/DanceNitra/agora/tree/main/mnemo) (our open-source memory core) is already
|
|
126
|
+
append-only with deterministic supersession, so it never silently edits a fact in normal use. But the
|
|
127
|
+
store is a file — anyone who can touch it can rewrite a stored memory after the fact, and any store
|
|
128
|
+
would then serve the altered text as the original. Receipts close that: every `remember()` emits a
|
|
129
|
+
signed receipt committing to the memory's content hash, so the *write history* is independently
|
|
130
|
+
verifiable.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from mnemo_receipts import ReceiptedMnemo, audit_memory
|
|
134
|
+
rm = ReceiptedMnemo(Mnemo(path="mem.json"), private_key_hex=sk, public_key_hex=pk)
|
|
135
|
+
rm.remember("The prod database host is db-prod-01.", key="prod-db::host", mtype="semantic")
|
|
136
|
+
ok, problems = audit_memory(rm.m, rm.chain, expected_pubkey=pk)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`audit_memory()` re-hashes the current store against the write receipts. Measured: an honest store
|
|
140
|
+
audits clean; an **out-of-band edit** (`db-prod-01 → db-attacker-07`, made straight in the store, which
|
|
141
|
+
mnemo itself can't see) is caught — `memory <id>: stored content no longer matches the write receipt`.
|
|
142
|
+
This is a thin wrapper; it does **not** modify mnemo's zero-dependency core.
|
|
143
|
+
|
|
144
|
+
## Honest scope (what this is NOT)
|
|
145
|
+
|
|
146
|
+
- The *self-signed* core proves a receipt **chain is internally consistent and authentically signed**.
|
|
147
|
+
It does **not** by itself prove the agent reported *every* action — an actor that controls its own
|
|
148
|
+
key can still withhold a receipt. That gap is closed by **external-mediator mode** (`mediator.py`,
|
|
149
|
+
below), which puts the signer outside the agent; anchoring the chain head to a third party is a
|
|
150
|
+
further hardening.
|
|
151
|
+
- It commits to input/output **hashes**, not a proof that the tool *computed correctly*. That is what
|
|
152
|
+
ZK-SNARK approaches add, at much higher cost.
|
|
153
|
+
- Keys here are raw/in-memory for clarity; real deployments use a KMS / hardware-backed key store.
|
|
154
|
+
|
|
155
|
+
## Landscape & prior art
|
|
156
|
+
|
|
157
|
+
This sits in an active, fast-moving space — **we build on it, we did not invent it.** In particular,
|
|
158
|
+
the exact pattern here (Ed25519 + canonical JSON + hash-chain) is the production-grade subject of
|
|
159
|
+
**Microsoft's [agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit),
|
|
160
|
+
Tutorial 33 "offline verifiable receipts"** (Ed25519 over RFC 8785 / JCS canonical payloads,
|
|
161
|
+
hash-chained, CLI-verifiable offline). Treat this repo as the *minimal one-file way to understand the
|
|
162
|
+
idea*, and that toolkit as the grown-up version.
|
|
163
|
+
|
|
164
|
+
Honest map of the space:
|
|
165
|
+
|
|
166
|
+
- **A named protocol + SDK:** the **"Agent Receipts" protocol** by Otto Jongerius — a public spec
|
|
167
|
+
([github.com/agent-receipts/ar](https://github.com/agent-receipts/ar)) plus a maintained Python
|
|
168
|
+
SDK (`pip install agent-receipts`). The most directly-related effort to this one; if you need an
|
|
169
|
+
interoperable standard rather than a teaching reference, start there.
|
|
170
|
+
- **Production OSS (corporate):** Microsoft `agent-governance-toolkit` — Tutorial 33 = the same
|
|
171
|
+
Ed25519 + canonical + hash-chain receipts, with policy/identity/sandboxing around it.
|
|
172
|
+
- **External-mediator receipts:** [`pipelock`](https://github.com/luckyPipewrench/pipelock) — an
|
|
173
|
+
open-source MCP/egress firewall that emits *mediator-signed* Ed25519 receipts from **outside** the
|
|
174
|
+
agent (core Apache-2.0; enterprise features Elastic-License), which is how you close the
|
|
175
|
+
agent-can-withhold-a-receipt gap noted above.
|
|
176
|
+
- **Commercial:** [Zero Proof AI](https://zeroproofai.com) — a pre-launch "certificate authority for
|
|
177
|
+
AI agents" issuing on-chain-anchored receipts for tool calls.
|
|
178
|
+
- **Research:**
|
|
179
|
+
- Basu, *Tool Receipts, Not Zero-Knowledge Proofs: Practical Hallucination Detection for AI Agents*,
|
|
180
|
+
[arXiv:2603.10060](https://arxiv.org/abs/2603.10060) (2026) — HMAC-signed tool-execution receipts
|
|
181
|
+
(the pragmatic, symmetric camp; we use Ed25519 so a third party verifies without a shared secret).
|
|
182
|
+
- Figuera, *Notarized Agents: Receiver-Attested Confidential Receipts for AI Agent Actions*,
|
|
183
|
+
[arXiv:2606.04193](https://arxiv.org/abs/2606.04193) (2026) — receiver-signed receipts published
|
|
184
|
+
to a transparency log (the external-attestation camp).
|
|
185
|
+
- Jing & Qi, *Zero-Knowledge Audit for Internet of Agents … with Model Context Protocol*,
|
|
186
|
+
[arXiv:2512.14737](https://arxiv.org/abs/2512.14737) (2025) — the zero-knowledge / privacy-
|
|
187
|
+
preserving end of the same space.
|
|
188
|
+
|
|
189
|
+
## Roadmap (if this proves useful)
|
|
190
|
+
|
|
191
|
+
~~External-mediator mode~~ (done — `mediator.py`) · ~~verifier CLI~~ (done — `verify_cli.py`) ·
|
|
192
|
+
~~`mnemo` integration~~ (done — `mnemo_receipts.py`) · publish-and-anchor the chain head · selective
|
|
193
|
+
disclosure of a single committed field · packaged spin-out (PyPI).
|
|
194
|
+
|
|
195
|
+
MIT. Part of the [Agora](https://github.com/DanceNitra/agora) project — an autonomous research OS that
|
|
196
|
+
ships every claim with a runnable receipt. Feedback and adversarial testing welcome.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""agent-receipts — tamper-evident, third-party-verifiable receipts for AI agent / MCP actions.
|
|
2
|
+
|
|
3
|
+
The problem: an AI agent's own logs are *self-reported claims*. Nothing stops the agent (or a
|
|
4
|
+
compromised proxy) from rewriting history after the fact, or from a hallucinated "I called the
|
|
5
|
+
database and it returned X" that never happened. A *receipt* is the opposite of a log: independent,
|
|
6
|
+
verifiable evidence of what an action consumed and produced, that a third party can check without
|
|
7
|
+
trusting the agent.
|
|
8
|
+
|
|
9
|
+
This single file gives the smallest honest version of that, in two layers:
|
|
10
|
+
|
|
11
|
+
1. HASH CHAIN (integrity, zero extra deps). Each receipt commits to the previous one
|
|
12
|
+
(prev = hash of the last receipt), so the whole sequence is a Merkle-style chain. Edit ANY
|
|
13
|
+
past receipt and every hash after it breaks -> a *partial* edit is detectable, and you can
|
|
14
|
+
name the exact step that was altered. IMPORTANT, honest limit: the hash chain ALONE does not
|
|
15
|
+
stop a thorough tamperer who recomputes the whole chain end-to-end (no link breaks then) -- so
|
|
16
|
+
integrity-only is enough only if the chain head is published/anchored somewhere the attacker
|
|
17
|
+
can't also rewrite. The SIGNATURE (layer 2) is what protects a self-held chain.
|
|
18
|
+
|
|
19
|
+
2. ED25519 SIGNATURES (authenticity, needs `cryptography`). Each receipt's hash is signed with
|
|
20
|
+
the actor's private key. A third party verifies with the PUBLIC key only -> it proves *who*
|
|
21
|
+
produced the receipt and that the content wasn't forged, without anyone sharing a secret.
|
|
22
|
+
(Optional: if `cryptography` is not installed, the hash chain still works on its own.)
|
|
23
|
+
|
|
24
|
+
Privacy nod (the "zero-knowledge-ish" part): receipts store the SHA-256 of inputs/outputs, not the
|
|
25
|
+
raw content. You commit to *what* was processed; you reveal the content only if/when you choose, and
|
|
26
|
+
anyone can later check a revealed value against the committed hash. Real zero-knowledge proofs (ZK-
|
|
27
|
+
SNARKs) go further — proving a computation was correct without revealing inputs at all — and are the
|
|
28
|
+
heavier end of this same design space (see the README for the landscape and prior art).
|
|
29
|
+
|
|
30
|
+
Run the self-demo: python agent_receipts.py
|
|
31
|
+
MIT. Part of the Agora project. Honest scope: this is a reference PoC, not a hardened product.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
import json
|
|
35
|
+
import time
|
|
36
|
+
import hashlib
|
|
37
|
+
from typing import Any, Optional
|
|
38
|
+
|
|
39
|
+
GENESIS = "0" * 64
|
|
40
|
+
|
|
41
|
+
# --- optional asymmetric signing (graceful if the lib is absent) ---
|
|
42
|
+
try:
|
|
43
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
44
|
+
Ed25519PrivateKey, Ed25519PublicKey,
|
|
45
|
+
)
|
|
46
|
+
from cryptography.hazmat.primitives import serialization
|
|
47
|
+
_HAVE_CRYPTO = True
|
|
48
|
+
except Exception: # pragma: no cover - exercised only on installs without `cryptography`
|
|
49
|
+
_HAVE_CRYPTO = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sha256_hex(data: bytes) -> str:
|
|
53
|
+
return hashlib.sha256(data).hexdigest()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _canonical(obj: Any) -> bytes:
|
|
57
|
+
"""Deterministic JSON so the same content always hashes the same (sorted keys, no spaces)."""
|
|
58
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def hash_content(content: Any) -> str:
|
|
62
|
+
"""Hash arbitrary input/output content. Bytes are hashed as-is; everything else is canonicalized."""
|
|
63
|
+
if isinstance(content, (bytes, bytearray)):
|
|
64
|
+
return sha256_hex(bytes(content))
|
|
65
|
+
return sha256_hex(_canonical(content))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate_keypair() -> tuple[str, str]:
|
|
69
|
+
"""Return (private_key_hex, public_key_hex) for an Ed25519 actor identity."""
|
|
70
|
+
if not _HAVE_CRYPTO:
|
|
71
|
+
raise RuntimeError("signing requires the `cryptography` package (pip install cryptography)")
|
|
72
|
+
sk = Ed25519PrivateKey.generate()
|
|
73
|
+
sk_hex = sk.private_bytes(
|
|
74
|
+
serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()
|
|
75
|
+
).hex()
|
|
76
|
+
pk_hex = sk.public_key().public_bytes(
|
|
77
|
+
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
|
78
|
+
).hex()
|
|
79
|
+
return sk_hex, pk_hex
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _sign(private_key_hex: str, message: bytes) -> str:
|
|
83
|
+
sk = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex))
|
|
84
|
+
return sk.sign(message).hex()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _verify_sig(public_key_hex: str, signature_hex: str, message: bytes) -> bool:
|
|
88
|
+
try:
|
|
89
|
+
pk = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key_hex))
|
|
90
|
+
pk.verify(bytes.fromhex(signature_hex), message)
|
|
91
|
+
return True
|
|
92
|
+
except Exception:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def receipt_hash(r: dict) -> str:
|
|
97
|
+
"""The receipt's own hash commits to its content AND the previous receipt (the chain link)."""
|
|
98
|
+
core = {
|
|
99
|
+
"seq": r["seq"], "ts": r["ts"], "action": r["action"], "actor": r.get("actor"),
|
|
100
|
+
"input_sha256": r["input_sha256"], "output_sha256": r["output_sha256"],
|
|
101
|
+
"meta": r.get("meta"), "prev": r["prev"],
|
|
102
|
+
}
|
|
103
|
+
return sha256_hex(_canonical(core))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ReceiptChain:
|
|
107
|
+
"""An append-only, hash-chained, optionally-signed sequence of action receipts."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, actor: Optional[str] = None, private_key_hex: Optional[str] = None,
|
|
110
|
+
public_key_hex: Optional[str] = None):
|
|
111
|
+
self.actor = actor
|
|
112
|
+
self._sk = private_key_hex
|
|
113
|
+
self.public_key_hex = public_key_hex
|
|
114
|
+
self.receipts: list[dict] = []
|
|
115
|
+
|
|
116
|
+
def record(self, action: str, inputs: Any, output: Any, meta: Optional[dict] = None,
|
|
117
|
+
ts: Optional[float] = None) -> dict:
|
|
118
|
+
"""Record one action (e.g. an MCP tool call): commit to its input/output hashes, chain, sign."""
|
|
119
|
+
prev = self.receipts[-1]["hash"] if self.receipts else GENESIS
|
|
120
|
+
r = {
|
|
121
|
+
"seq": len(self.receipts),
|
|
122
|
+
"ts": ts if ts is not None else time.time(),
|
|
123
|
+
"action": action,
|
|
124
|
+
"actor": self.actor,
|
|
125
|
+
"input_sha256": hash_content(inputs),
|
|
126
|
+
"output_sha256": hash_content(output),
|
|
127
|
+
"meta": meta,
|
|
128
|
+
"prev": prev,
|
|
129
|
+
}
|
|
130
|
+
r["hash"] = receipt_hash(r)
|
|
131
|
+
if self._sk:
|
|
132
|
+
r["pubkey"] = self.public_key_hex
|
|
133
|
+
r["sig"] = _sign(self._sk, bytes.fromhex(r["hash"]))
|
|
134
|
+
self.receipts.append(r)
|
|
135
|
+
return r
|
|
136
|
+
|
|
137
|
+
def verify(self, expected_pubkey: Optional[str] = None) -> tuple[bool, list[str]]:
|
|
138
|
+
"""Recompute the chain from scratch. Returns (ok, problems). Names the exact broken step."""
|
|
139
|
+
problems: list[str] = []
|
|
140
|
+
prev = GENESIS
|
|
141
|
+
for i, r in enumerate(self.receipts):
|
|
142
|
+
if r.get("seq") != i:
|
|
143
|
+
problems.append(f"seq {i}: out-of-order (claims seq={r.get('seq')})")
|
|
144
|
+
if r.get("prev") != prev:
|
|
145
|
+
problems.append(f"seq {i}: broken chain link (prev mismatch -> a prior receipt was altered/removed)")
|
|
146
|
+
if receipt_hash(r) != r.get("hash"):
|
|
147
|
+
problems.append(f"seq {i}: content tampered (hash does not match this receipt's fields)")
|
|
148
|
+
if "sig" in r:
|
|
149
|
+
pk = r.get("pubkey")
|
|
150
|
+
if expected_pubkey and pk != expected_pubkey:
|
|
151
|
+
problems.append(f"seq {i}: signed by an unexpected key (possible impersonation)")
|
|
152
|
+
if not _HAVE_CRYPTO:
|
|
153
|
+
problems.append(f"seq {i}: signature present but `cryptography` not installed to verify it")
|
|
154
|
+
elif not _verify_sig(pk, r["sig"], bytes.fromhex(r["hash"])):
|
|
155
|
+
problems.append(f"seq {i}: invalid signature (forged or wrong key)")
|
|
156
|
+
elif expected_pubkey:
|
|
157
|
+
problems.append(f"seq {i}: unsigned, but a signature was required")
|
|
158
|
+
prev = r.get("hash")
|
|
159
|
+
return (len(problems) == 0, problems)
|
|
160
|
+
|
|
161
|
+
def to_json(self) -> str:
|
|
162
|
+
return json.dumps(self.receipts, indent=2, ensure_ascii=False)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_receipts(cls, receipts: list[dict]) -> "ReceiptChain":
|
|
166
|
+
c = cls()
|
|
167
|
+
c.receipts = receipts
|
|
168
|
+
return c
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _demo() -> None:
|
|
172
|
+
print("=== agent-receipts: self-demo ===\n")
|
|
173
|
+
signed = _HAVE_CRYPTO
|
|
174
|
+
if signed:
|
|
175
|
+
sk, pk = generate_keypair()
|
|
176
|
+
chain = ReceiptChain(actor="research-agent-01", private_key_hex=sk, public_key_hex=pk)
|
|
177
|
+
print(f"actor identity (public key): {pk[:16]}... (third parties verify with this, no secret shared)\n")
|
|
178
|
+
else:
|
|
179
|
+
chain = ReceiptChain(actor="research-agent-01")
|
|
180
|
+
pk = None
|
|
181
|
+
print("(`cryptography` not installed -> hash-chain only, no signatures)\n")
|
|
182
|
+
|
|
183
|
+
# An agent does three MCP tool calls. Each gets a receipt.
|
|
184
|
+
chain.record("mcp.web_search", {"query": "supersession blind spot AUROC"},
|
|
185
|
+
{"results": 7, "top": "arXiv:2606.26511"})
|
|
186
|
+
chain.record("mcp.memory.write", {"fact": "Pro tier costs 39 USD/mo", "source": "billing:tool"},
|
|
187
|
+
{"stored": True, "id": "m-1042"})
|
|
188
|
+
chain.record("mcp.code.run", {"cmd": "python probe.py"}, {"exit": 0, "stdout_sha": "9f1c..."})
|
|
189
|
+
print(f"recorded {len(chain.receipts)} receipts (one per tool call)")
|
|
190
|
+
|
|
191
|
+
ok, problems = chain.verify(expected_pubkey=pk)
|
|
192
|
+
print(f"[1] honest chain verifies? {ok} {problems if problems else ''}")
|
|
193
|
+
|
|
194
|
+
# TAMPER: someone edits a past receipt's output AFTER the fact (e.g. to hide what really happened).
|
|
195
|
+
chain.receipts[1]["output_sha256"] = hash_content({"stored": True, "id": "m-DIFFERENT"})
|
|
196
|
+
ok2, problems2 = chain.verify(expected_pubkey=pk)
|
|
197
|
+
print(f"[2] after editing receipt #1's output: verifies? {ok2}")
|
|
198
|
+
for p in problems2:
|
|
199
|
+
print(f" - {p}")
|
|
200
|
+
|
|
201
|
+
# FORGE: rebuild #1 so its own hash matches the edit. The hash chain is now internally consistent,
|
|
202
|
+
# but the SIGNATURE was made over the original hash by the real key -> still caught (if signed).
|
|
203
|
+
chain.receipts[1]["hash"] = receipt_hash(chain.receipts[1])
|
|
204
|
+
ok3, problems3 = chain.verify(expected_pubkey=pk)
|
|
205
|
+
print(f"[3] after also recomputing the hash to match: verifies? {ok3}")
|
|
206
|
+
for p in problems3:
|
|
207
|
+
print(f" - {p}")
|
|
208
|
+
if signed:
|
|
209
|
+
print(" -> the signature (made by the real key over the ORIGINAL hash) exposes the forgery,")
|
|
210
|
+
print(" and #2's broken chain link points downstream. Integrity + authenticity together.")
|
|
211
|
+
else:
|
|
212
|
+
print(" -> this lazy edit broke a chain link, so it's caught. But WITHOUT signatures a")
|
|
213
|
+
print(" thorough tamperer would recompute the WHOLE chain (no link breaks) and it would")
|
|
214
|
+
print(" pass -- integrity-only needs an anchored/published head. Ed25519 signing closes this.")
|
|
215
|
+
|
|
216
|
+
print("\nMEASURED: an honest receipt chain verifies; a partial edit is detected at the exact step;")
|
|
217
|
+
print("a re-hashed forgery is caught by the SIGNATURE (a full recompute defeats the hash chain alone).")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
_demo()
|