aer1-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.
- aer1_verify-0.1.0/PKG-INFO +133 -0
- aer1_verify-0.1.0/README.md +121 -0
- aer1_verify-0.1.0/aer1_verify.egg-info/PKG-INFO +133 -0
- aer1_verify-0.1.0/aer1_verify.egg-info/SOURCES.txt +9 -0
- aer1_verify-0.1.0/aer1_verify.egg-info/dependency_links.txt +1 -0
- aer1_verify-0.1.0/aer1_verify.egg-info/entry_points.txt +2 -0
- aer1_verify-0.1.0/aer1_verify.egg-info/top_level.txt +1 -0
- aer1_verify-0.1.0/aer1_verify.py +267 -0
- aer1_verify-0.1.0/pyproject.toml +23 -0
- aer1_verify-0.1.0/setup.cfg +4 -0
- aer1_verify-0.1.0/tests/test_verifier.py +81 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aer1-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline verifier for AER-1 verifiable execution receipts. No network, no trust in any server.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: aer-1,receipt,verification,ai-agents,audit
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Topic :: Security
|
|
9
|
+
Classifier: Topic :: Security :: Cryptography
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# aer1-verify
|
|
14
|
+
|
|
15
|
+
Verify an AER-1 verifiable execution receipt **completely offline**. No network. No trust in any server, including ours.
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
A verifiable receipt says: this AI did this work, and the record was not changed afterward. That claim should not require you to trust the company that issued it. `aer1-verify` rechecks the cryptography locally, from the receipt data alone. If the math holds, the receipt is intact. If it does not, the receipt is forged or corrupt.
|
|
20
|
+
|
|
21
|
+
The honest boundary, stated plainly: this tool proves the recorded result **was not changed**. It does not prove the recorded result **is correct**. Integrity, not truth. That is what AER-1 receipts claim, and it is all this tool checks.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Single file, standard library only. No dependencies.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Option 1: copy the file, nothing else needed
|
|
29
|
+
curl -O https://gitlab.com/rambozambodotdev/zambo/-/raw/main/aer1-verifier/aer1_verify.py
|
|
30
|
+
|
|
31
|
+
# Option 2: pip install
|
|
32
|
+
pip install .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
Get a receipt as JSON. From zambo.dev, the verify endpoint returns one:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -s "https://zambo.dev/api/v2/receipt/<receipt-id>/verify" -o receipt.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then verify it offline (unplug your network if you want to prove the point):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 aer1_verify.py receipt.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Output:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
aer1-verify 0.1.0
|
|
53
|
+
receipt: c5dadb0a-8f99-40f6-b30a-40852435b752
|
|
54
|
+
[PASS] input shape recognized (envelope.receipt)
|
|
55
|
+
[PASS] required fields present (id, canonical_bytes, output_hash)
|
|
56
|
+
[PASS] canonical_bytes is strict base64 -- 740 bytes
|
|
57
|
+
[PASS] decoded bytes are strict UTF-8 (fail closed)
|
|
58
|
+
[PASS] canonical_byte_length matches decoded length -- field=740 actual=740
|
|
59
|
+
[PASS] canonical JSON parses
|
|
60
|
+
[PASS] canonical form re-encodes byte-exactly
|
|
61
|
+
[PASS] output_hash is sha256: + 64 lowercase hex chars
|
|
62
|
+
[PASS] hash_algorithm agrees with sha256 -- got: 'sha256'
|
|
63
|
+
[PASS] sha256(canonical_bytes) equals output_hash
|
|
64
|
+
[PASS] created_at/timestamp is a real calendar date
|
|
65
|
+
[PASS] timestamp is not in the future
|
|
66
|
+
[PASS] verification_status (server claim, informational only) -- server said: 'verified'; offline verdict rests on the math above
|
|
67
|
+
VERDICT: PASS: fingerprint matches, receipt intact
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Machine-readable output for CI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 aer1_verify.py receipt.json --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Exit codes: `0` = PASS, `1` = FAIL, `2` = usage or input error.
|
|
77
|
+
|
|
78
|
+
## What it checks
|
|
79
|
+
|
|
80
|
+
1. **Required fields**: the receipt carries `id`, `canonical_bytes`, `output_hash`.
|
|
81
|
+
2. **Strict base64**: the committed bytes decode cleanly, no leniency.
|
|
82
|
+
3. **Strict UTF-8**: the bytes are valid UTF-8. Anything else fails closed.
|
|
83
|
+
4. **Byte length agreement**: the declared length matches the decoded bytes.
|
|
84
|
+
5. **Canonical round-trip**: the JSON re-encodes to byte-identical bytes (sorted keys, compact separators, UTF-8). The fingerprint covers exactly what you see; nothing hides outside it.
|
|
85
|
+
6. **Fingerprint shape**: `output_hash` is `sha256:` plus 64 lowercase hex chars.
|
|
86
|
+
7. **Algorithm agreement**: the declared hash algorithm is sha256.
|
|
87
|
+
8. **The core check**: `SHA-256(canonical_bytes)` equals the committed `output_hash`. This is the whole proof.
|
|
88
|
+
9. **Timestamp sanity**: the issue time is a real calendar date and not in the future.
|
|
89
|
+
|
|
90
|
+
The server's `verification_status` field is **reported, never trusted**. An offline verifier checks math, not the issuer's word. That is the point.
|
|
91
|
+
|
|
92
|
+
## Try breaking it
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Take a real receipt, change one character in the recorded result,
|
|
96
|
+
# keep the old fingerprint, and watch verification fail:
|
|
97
|
+
python3 - <<'EOF'
|
|
98
|
+
import json, base64
|
|
99
|
+
r = json.load(open('receipt.json'))['receipt']
|
|
100
|
+
raw = base64.b64decode(r['canonical_bytes']).decode('utf-8')
|
|
101
|
+
raw = raw.replace('164.00', '164.01', 1) # one character
|
|
102
|
+
r['canonical_bytes'] = base64.b64encode(raw.encode()).decode()
|
|
103
|
+
r['canonical_byte_length'] = len(raw.encode())
|
|
104
|
+
json.dump({'receipt': r}, open('forged.json', 'w'))
|
|
105
|
+
EOF
|
|
106
|
+
python3 aer1_verify.py forged.json # -> VERDICT: FAIL
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One character. The fingerprint no longer matches. That is the security property, demonstrated negatively.
|
|
110
|
+
|
|
111
|
+
## Input shapes
|
|
112
|
+
|
|
113
|
+
Liberal on input, strict on math. Accepts:
|
|
114
|
+
|
|
115
|
+
- the bare receipt object (`canonical_bytes` / `output_hash` present),
|
|
116
|
+
- the `{"receipt": {...}}` envelope from the zambo.dev verify endpoint,
|
|
117
|
+
- the MCP `_receipt` shape returned with tool calls.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 tests/test_verifier.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Fixtures include real receipts issued by zambo.dev plus adversarial mutations (tampered bytes, bad base64, wrong digest, non-UTF-8 bytes). The real-receipt fixtures must keep passing: they pin this tool to production.
|
|
126
|
+
|
|
127
|
+
## Relation to AER-1
|
|
128
|
+
|
|
129
|
+
AER-1 is the AI Agent Execution Receipt specification (IETF Internet-Draft `draft-zambo-aer1`). This verifier implements the receipt-integrity checks from the draft's canonicalization and fingerprint rules. It is a verifier, not an issuer: it cannot create receipts, only judge them.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Same as the parent repository.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# aer1-verify
|
|
2
|
+
|
|
3
|
+
Verify an AER-1 verifiable execution receipt **completely offline**. No network. No trust in any server, including ours.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
A verifiable receipt says: this AI did this work, and the record was not changed afterward. That claim should not require you to trust the company that issued it. `aer1-verify` rechecks the cryptography locally, from the receipt data alone. If the math holds, the receipt is intact. If it does not, the receipt is forged or corrupt.
|
|
8
|
+
|
|
9
|
+
The honest boundary, stated plainly: this tool proves the recorded result **was not changed**. It does not prove the recorded result **is correct**. Integrity, not truth. That is what AER-1 receipts claim, and it is all this tool checks.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Single file, standard library only. No dependencies.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Option 1: copy the file, nothing else needed
|
|
17
|
+
curl -O https://gitlab.com/rambozambodotdev/zambo/-/raw/main/aer1-verifier/aer1_verify.py
|
|
18
|
+
|
|
19
|
+
# Option 2: pip install
|
|
20
|
+
pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
Get a receipt as JSON. From zambo.dev, the verify endpoint returns one:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -s "https://zambo.dev/api/v2/receipt/<receipt-id>/verify" -o receipt.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then verify it offline (unplug your network if you want to prove the point):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 aer1_verify.py receipt.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
aer1-verify 0.1.0
|
|
41
|
+
receipt: c5dadb0a-8f99-40f6-b30a-40852435b752
|
|
42
|
+
[PASS] input shape recognized (envelope.receipt)
|
|
43
|
+
[PASS] required fields present (id, canonical_bytes, output_hash)
|
|
44
|
+
[PASS] canonical_bytes is strict base64 -- 740 bytes
|
|
45
|
+
[PASS] decoded bytes are strict UTF-8 (fail closed)
|
|
46
|
+
[PASS] canonical_byte_length matches decoded length -- field=740 actual=740
|
|
47
|
+
[PASS] canonical JSON parses
|
|
48
|
+
[PASS] canonical form re-encodes byte-exactly
|
|
49
|
+
[PASS] output_hash is sha256: + 64 lowercase hex chars
|
|
50
|
+
[PASS] hash_algorithm agrees with sha256 -- got: 'sha256'
|
|
51
|
+
[PASS] sha256(canonical_bytes) equals output_hash
|
|
52
|
+
[PASS] created_at/timestamp is a real calendar date
|
|
53
|
+
[PASS] timestamp is not in the future
|
|
54
|
+
[PASS] verification_status (server claim, informational only) -- server said: 'verified'; offline verdict rests on the math above
|
|
55
|
+
VERDICT: PASS: fingerprint matches, receipt intact
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Machine-readable output for CI:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 aer1_verify.py receipt.json --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Exit codes: `0` = PASS, `1` = FAIL, `2` = usage or input error.
|
|
65
|
+
|
|
66
|
+
## What it checks
|
|
67
|
+
|
|
68
|
+
1. **Required fields**: the receipt carries `id`, `canonical_bytes`, `output_hash`.
|
|
69
|
+
2. **Strict base64**: the committed bytes decode cleanly, no leniency.
|
|
70
|
+
3. **Strict UTF-8**: the bytes are valid UTF-8. Anything else fails closed.
|
|
71
|
+
4. **Byte length agreement**: the declared length matches the decoded bytes.
|
|
72
|
+
5. **Canonical round-trip**: the JSON re-encodes to byte-identical bytes (sorted keys, compact separators, UTF-8). The fingerprint covers exactly what you see; nothing hides outside it.
|
|
73
|
+
6. **Fingerprint shape**: `output_hash` is `sha256:` plus 64 lowercase hex chars.
|
|
74
|
+
7. **Algorithm agreement**: the declared hash algorithm is sha256.
|
|
75
|
+
8. **The core check**: `SHA-256(canonical_bytes)` equals the committed `output_hash`. This is the whole proof.
|
|
76
|
+
9. **Timestamp sanity**: the issue time is a real calendar date and not in the future.
|
|
77
|
+
|
|
78
|
+
The server's `verification_status` field is **reported, never trusted**. An offline verifier checks math, not the issuer's word. That is the point.
|
|
79
|
+
|
|
80
|
+
## Try breaking it
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Take a real receipt, change one character in the recorded result,
|
|
84
|
+
# keep the old fingerprint, and watch verification fail:
|
|
85
|
+
python3 - <<'EOF'
|
|
86
|
+
import json, base64
|
|
87
|
+
r = json.load(open('receipt.json'))['receipt']
|
|
88
|
+
raw = base64.b64decode(r['canonical_bytes']).decode('utf-8')
|
|
89
|
+
raw = raw.replace('164.00', '164.01', 1) # one character
|
|
90
|
+
r['canonical_bytes'] = base64.b64encode(raw.encode()).decode()
|
|
91
|
+
r['canonical_byte_length'] = len(raw.encode())
|
|
92
|
+
json.dump({'receipt': r}, open('forged.json', 'w'))
|
|
93
|
+
EOF
|
|
94
|
+
python3 aer1_verify.py forged.json # -> VERDICT: FAIL
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
One character. The fingerprint no longer matches. That is the security property, demonstrated negatively.
|
|
98
|
+
|
|
99
|
+
## Input shapes
|
|
100
|
+
|
|
101
|
+
Liberal on input, strict on math. Accepts:
|
|
102
|
+
|
|
103
|
+
- the bare receipt object (`canonical_bytes` / `output_hash` present),
|
|
104
|
+
- the `{"receipt": {...}}` envelope from the zambo.dev verify endpoint,
|
|
105
|
+
- the MCP `_receipt` shape returned with tool calls.
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python3 tests/test_verifier.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Fixtures include real receipts issued by zambo.dev plus adversarial mutations (tampered bytes, bad base64, wrong digest, non-UTF-8 bytes). The real-receipt fixtures must keep passing: they pin this tool to production.
|
|
114
|
+
|
|
115
|
+
## Relation to AER-1
|
|
116
|
+
|
|
117
|
+
AER-1 is the AI Agent Execution Receipt specification (IETF Internet-Draft `draft-zambo-aer1`). This verifier implements the receipt-integrity checks from the draft's canonicalization and fingerprint rules. It is a verifier, not an issuer: it cannot create receipts, only judge them.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
Same as the parent repository.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aer1-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline verifier for AER-1 verifiable execution receipts. No network, no trust in any server.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: aer-1,receipt,verification,ai-agents,audit
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Topic :: Security
|
|
9
|
+
Classifier: Topic :: Security :: Cryptography
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# aer1-verify
|
|
14
|
+
|
|
15
|
+
Verify an AER-1 verifiable execution receipt **completely offline**. No network. No trust in any server, including ours.
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
A verifiable receipt says: this AI did this work, and the record was not changed afterward. That claim should not require you to trust the company that issued it. `aer1-verify` rechecks the cryptography locally, from the receipt data alone. If the math holds, the receipt is intact. If it does not, the receipt is forged or corrupt.
|
|
20
|
+
|
|
21
|
+
The honest boundary, stated plainly: this tool proves the recorded result **was not changed**. It does not prove the recorded result **is correct**. Integrity, not truth. That is what AER-1 receipts claim, and it is all this tool checks.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Single file, standard library only. No dependencies.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Option 1: copy the file, nothing else needed
|
|
29
|
+
curl -O https://gitlab.com/rambozambodotdev/zambo/-/raw/main/aer1-verifier/aer1_verify.py
|
|
30
|
+
|
|
31
|
+
# Option 2: pip install
|
|
32
|
+
pip install .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
Get a receipt as JSON. From zambo.dev, the verify endpoint returns one:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -s "https://zambo.dev/api/v2/receipt/<receipt-id>/verify" -o receipt.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then verify it offline (unplug your network if you want to prove the point):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 aer1_verify.py receipt.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Output:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
aer1-verify 0.1.0
|
|
53
|
+
receipt: c5dadb0a-8f99-40f6-b30a-40852435b752
|
|
54
|
+
[PASS] input shape recognized (envelope.receipt)
|
|
55
|
+
[PASS] required fields present (id, canonical_bytes, output_hash)
|
|
56
|
+
[PASS] canonical_bytes is strict base64 -- 740 bytes
|
|
57
|
+
[PASS] decoded bytes are strict UTF-8 (fail closed)
|
|
58
|
+
[PASS] canonical_byte_length matches decoded length -- field=740 actual=740
|
|
59
|
+
[PASS] canonical JSON parses
|
|
60
|
+
[PASS] canonical form re-encodes byte-exactly
|
|
61
|
+
[PASS] output_hash is sha256: + 64 lowercase hex chars
|
|
62
|
+
[PASS] hash_algorithm agrees with sha256 -- got: 'sha256'
|
|
63
|
+
[PASS] sha256(canonical_bytes) equals output_hash
|
|
64
|
+
[PASS] created_at/timestamp is a real calendar date
|
|
65
|
+
[PASS] timestamp is not in the future
|
|
66
|
+
[PASS] verification_status (server claim, informational only) -- server said: 'verified'; offline verdict rests on the math above
|
|
67
|
+
VERDICT: PASS: fingerprint matches, receipt intact
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Machine-readable output for CI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 aer1_verify.py receipt.json --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Exit codes: `0` = PASS, `1` = FAIL, `2` = usage or input error.
|
|
77
|
+
|
|
78
|
+
## What it checks
|
|
79
|
+
|
|
80
|
+
1. **Required fields**: the receipt carries `id`, `canonical_bytes`, `output_hash`.
|
|
81
|
+
2. **Strict base64**: the committed bytes decode cleanly, no leniency.
|
|
82
|
+
3. **Strict UTF-8**: the bytes are valid UTF-8. Anything else fails closed.
|
|
83
|
+
4. **Byte length agreement**: the declared length matches the decoded bytes.
|
|
84
|
+
5. **Canonical round-trip**: the JSON re-encodes to byte-identical bytes (sorted keys, compact separators, UTF-8). The fingerprint covers exactly what you see; nothing hides outside it.
|
|
85
|
+
6. **Fingerprint shape**: `output_hash` is `sha256:` plus 64 lowercase hex chars.
|
|
86
|
+
7. **Algorithm agreement**: the declared hash algorithm is sha256.
|
|
87
|
+
8. **The core check**: `SHA-256(canonical_bytes)` equals the committed `output_hash`. This is the whole proof.
|
|
88
|
+
9. **Timestamp sanity**: the issue time is a real calendar date and not in the future.
|
|
89
|
+
|
|
90
|
+
The server's `verification_status` field is **reported, never trusted**. An offline verifier checks math, not the issuer's word. That is the point.
|
|
91
|
+
|
|
92
|
+
## Try breaking it
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Take a real receipt, change one character in the recorded result,
|
|
96
|
+
# keep the old fingerprint, and watch verification fail:
|
|
97
|
+
python3 - <<'EOF'
|
|
98
|
+
import json, base64
|
|
99
|
+
r = json.load(open('receipt.json'))['receipt']
|
|
100
|
+
raw = base64.b64decode(r['canonical_bytes']).decode('utf-8')
|
|
101
|
+
raw = raw.replace('164.00', '164.01', 1) # one character
|
|
102
|
+
r['canonical_bytes'] = base64.b64encode(raw.encode()).decode()
|
|
103
|
+
r['canonical_byte_length'] = len(raw.encode())
|
|
104
|
+
json.dump({'receipt': r}, open('forged.json', 'w'))
|
|
105
|
+
EOF
|
|
106
|
+
python3 aer1_verify.py forged.json # -> VERDICT: FAIL
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One character. The fingerprint no longer matches. That is the security property, demonstrated negatively.
|
|
110
|
+
|
|
111
|
+
## Input shapes
|
|
112
|
+
|
|
113
|
+
Liberal on input, strict on math. Accepts:
|
|
114
|
+
|
|
115
|
+
- the bare receipt object (`canonical_bytes` / `output_hash` present),
|
|
116
|
+
- the `{"receipt": {...}}` envelope from the zambo.dev verify endpoint,
|
|
117
|
+
- the MCP `_receipt` shape returned with tool calls.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 tests/test_verifier.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Fixtures include real receipts issued by zambo.dev plus adversarial mutations (tampered bytes, bad base64, wrong digest, non-UTF-8 bytes). The real-receipt fixtures must keep passing: they pin this tool to production.
|
|
126
|
+
|
|
127
|
+
## Relation to AER-1
|
|
128
|
+
|
|
129
|
+
AER-1 is the AI Agent Execution Receipt specification (IETF Internet-Draft `draft-zambo-aer1`). This verifier implements the receipt-integrity checks from the draft's canonicalization and fingerprint rules. It is a verifier, not an issuer: it cannot create receipts, only judge them.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Same as the parent repository.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aer1_verify
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""aer1-verify: offline verifier for AER-1 verifiable execution receipts.
|
|
3
|
+
|
|
4
|
+
Verifies a receipt's cryptographic fingerprint with zero network access and
|
|
5
|
+
zero trust in any server. Single file, standard library only.
|
|
6
|
+
|
|
7
|
+
What it proves: the receipt bytes hash to exactly the committed fingerprint,
|
|
8
|
+
so the recorded result was not changed after issuance.
|
|
9
|
+
|
|
10
|
+
What it does NOT prove: that the recorded result is correct. A verifiable
|
|
11
|
+
receipt is proof of integrity, not proof of truth. That is the honest
|
|
12
|
+
boundary of AER-1, and this tool enforces it by checking math, not meaning.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 aer1_verify.py receipt.json
|
|
16
|
+
python3 aer1_verify.py receipt.json --json # machine-readable output
|
|
17
|
+
cat receipt.json | python3 aer1_verify.py -
|
|
18
|
+
|
|
19
|
+
Exit codes: 0 = PASS, 1 = FAIL, 2 = usage/input error.
|
|
20
|
+
|
|
21
|
+
Accepted input shapes (liberal on input, strict on math):
|
|
22
|
+
- the bare receipt object (has canonical_bytes / output_hash)
|
|
23
|
+
- {"receipt": {...}} envelope (zambo.dev verify endpoint shape)
|
|
24
|
+
- the MCP _receipt shape (has canonical_bytes / output_hash / timestamp)
|
|
25
|
+
|
|
26
|
+
Checks performed:
|
|
27
|
+
1. required fields present (id, canonical_bytes, output_hash)
|
|
28
|
+
2. canonical_bytes is strict base64
|
|
29
|
+
3. decoded bytes are strict UTF-8 (fail closed)
|
|
30
|
+
4. canonical_byte_length matches decoded length (when present)
|
|
31
|
+
5. canonical JSON re-encodes byte-exactly (sorted keys, compact
|
|
32
|
+
separators, UTF-8): the fingerprint covers exactly what is shown
|
|
33
|
+
6. output_hash is well-formed (sha256: + 64 lowercase hex chars)
|
|
34
|
+
7. hash_algorithm agrees with sha256 (when present)
|
|
35
|
+
8. sha256(canonical_bytes) equals output_hash <- the core check
|
|
36
|
+
9. created_at/timestamp is a real calendar date (when present)
|
|
37
|
+
|
|
38
|
+
verification_status (the server's own claim) is reported, not trusted:
|
|
39
|
+
an offline verifier checks the math, not the issuer's word.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import sys
|
|
43
|
+
import json
|
|
44
|
+
import base64
|
|
45
|
+
import binascii
|
|
46
|
+
import hashlib
|
|
47
|
+
import re
|
|
48
|
+
from datetime import datetime, timezone
|
|
49
|
+
|
|
50
|
+
VERSION = "0.1.0"
|
|
51
|
+
|
|
52
|
+
HASH_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
53
|
+
B64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
|
|
54
|
+
RFC3339_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def canonical_bytes_of(obj):
|
|
58
|
+
"""AER-1 canonical form: sorted keys, compact separators, UTF-8."""
|
|
59
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"),
|
|
60
|
+
ensure_ascii=False).encode("utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unwrap_envelope(doc):
|
|
64
|
+
"""Accept the verify-endpoint envelope, the bare receipt, or _receipt."""
|
|
65
|
+
if isinstance(doc, dict) and isinstance(doc.get("receipt"), dict):
|
|
66
|
+
return doc["receipt"], "envelope.receipt"
|
|
67
|
+
return doc, "bare"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_calendar(ts):
|
|
71
|
+
"""True only if ts is RFC 3339 shaped AND a real calendar date/time."""
|
|
72
|
+
if not isinstance(ts, str) or not RFC3339_RE.match(ts):
|
|
73
|
+
return False
|
|
74
|
+
try:
|
|
75
|
+
iso = ts.replace("Z", "+00:00")
|
|
76
|
+
dt = datetime.fromisoformat(iso)
|
|
77
|
+
# fromisoformat is lenient about some things; require tz awareness
|
|
78
|
+
# and a round-trip through a strict parse to fail closed.
|
|
79
|
+
if dt.tzinfo is None:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
except ValueError:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify(receipt):
|
|
87
|
+
"""Run every check. Returns (passed: bool, checks: list of dicts)."""
|
|
88
|
+
checks = []
|
|
89
|
+
|
|
90
|
+
def record(name, ok, detail=""):
|
|
91
|
+
checks.append({"check": name, "ok": bool(ok), "detail": detail})
|
|
92
|
+
|
|
93
|
+
# 0. unwrap + required fields
|
|
94
|
+
receipt, shape = unwrap_envelope(receipt)
|
|
95
|
+
if not isinstance(receipt, dict):
|
|
96
|
+
record("input is a JSON object", False, "top-level value is %s" % type(receipt).__name__)
|
|
97
|
+
return False, checks
|
|
98
|
+
record("input shape recognized (%s)" % shape, True)
|
|
99
|
+
missing = [k for k in ("id", "canonical_bytes", "output_hash") if k not in receipt]
|
|
100
|
+
record("required fields present (id, canonical_bytes, output_hash)",
|
|
101
|
+
not missing, "missing: %s" % ", ".join(missing) if missing else "")
|
|
102
|
+
|
|
103
|
+
raw = None
|
|
104
|
+
if missing:
|
|
105
|
+
return False, checks
|
|
106
|
+
|
|
107
|
+
# 1+2. strict base64, then strict UTF-8
|
|
108
|
+
cb = receipt["canonical_bytes"]
|
|
109
|
+
if not isinstance(cb, str) or not B64_RE.match(cb):
|
|
110
|
+
record("canonical_bytes is strict base64", False)
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
raw = base64.b64decode(cb, validate=True)
|
|
114
|
+
except (binascii.Error, ValueError):
|
|
115
|
+
record("canonical_bytes is strict base64", False)
|
|
116
|
+
else:
|
|
117
|
+
record("canonical_bytes is strict base64", True, "%d bytes" % len(raw))
|
|
118
|
+
try:
|
|
119
|
+
raw.decode("utf-8")
|
|
120
|
+
except UnicodeDecodeError:
|
|
121
|
+
record("decoded bytes are strict UTF-8 (fail closed)", False)
|
|
122
|
+
raw = None
|
|
123
|
+
else:
|
|
124
|
+
record("decoded bytes are strict UTF-8 (fail closed)", True)
|
|
125
|
+
|
|
126
|
+
# 3. byte length agreement
|
|
127
|
+
if raw is not None and "canonical_byte_length" in receipt:
|
|
128
|
+
want_len = receipt["canonical_byte_length"]
|
|
129
|
+
record("canonical_byte_length matches decoded length",
|
|
130
|
+
want_len == len(raw),
|
|
131
|
+
"field=%s actual=%d" % (want_len, len(raw)))
|
|
132
|
+
elif raw is not None:
|
|
133
|
+
record("canonical_byte_length matches decoded length", True,
|
|
134
|
+
"field absent, skipped")
|
|
135
|
+
|
|
136
|
+
# 4. canonical JSON round-trip: the fingerprint must cover exactly
|
|
137
|
+
# what a re-parse shows. Any hidden or mis-encoded field fails here.
|
|
138
|
+
if raw is not None:
|
|
139
|
+
try:
|
|
140
|
+
obj = json.loads(raw.decode("utf-8"))
|
|
141
|
+
except (ValueError, UnicodeDecodeError):
|
|
142
|
+
record("canonical JSON parses", False)
|
|
143
|
+
obj = None
|
|
144
|
+
else:
|
|
145
|
+
record("canonical JSON parses", True)
|
|
146
|
+
try:
|
|
147
|
+
re_encoded = canonical_bytes_of(obj)
|
|
148
|
+
except (TypeError, ValueError):
|
|
149
|
+
record("canonical form re-encodes byte-exactly", False,
|
|
150
|
+
"value not JSON-serializable")
|
|
151
|
+
else:
|
|
152
|
+
record("canonical form re-encodes byte-exactly",
|
|
153
|
+
re_encoded == raw,
|
|
154
|
+
"re-encoded %d bytes vs committed %d bytes"
|
|
155
|
+
% (len(re_encoded), len(raw)) if re_encoded != raw else "")
|
|
156
|
+
|
|
157
|
+
# 5. output_hash shape
|
|
158
|
+
oh = receipt["output_hash"]
|
|
159
|
+
record("output_hash is sha256: + 64 lowercase hex chars",
|
|
160
|
+
isinstance(oh, str) and bool(HASH_RE.match(oh)),
|
|
161
|
+
"" if isinstance(oh, str) and HASH_RE.match(oh or "") else "got: %r" % (oh,))
|
|
162
|
+
|
|
163
|
+
# 6. hash algorithm agreement
|
|
164
|
+
algo = receipt.get("hash_algorithm")
|
|
165
|
+
if algo is None:
|
|
166
|
+
record("hash_algorithm agrees with sha256", True, "field absent, skipped")
|
|
167
|
+
else:
|
|
168
|
+
record("hash_algorithm agrees with sha256", algo == "sha256",
|
|
169
|
+
"got: %r" % (algo,))
|
|
170
|
+
|
|
171
|
+
# 7. THE core check
|
|
172
|
+
if raw is not None and isinstance(oh, str) and HASH_RE.match(oh):
|
|
173
|
+
want = "sha256:" + hashlib.sha256(raw).hexdigest()
|
|
174
|
+
record("sha256(canonical_bytes) equals output_hash",
|
|
175
|
+
oh == want,
|
|
176
|
+
"" if oh == want else "committed=%s recomputed=%s" % (oh, want))
|
|
177
|
+
else:
|
|
178
|
+
record("sha256(canonical_bytes) equals output_hash", False,
|
|
179
|
+
"skipped: earlier decode or format check failed")
|
|
180
|
+
|
|
181
|
+
# 8. timestamp sanity (informational strictness: a receipt from the
|
|
182
|
+
# future or with an impossible date is suspicious)
|
|
183
|
+
ts = receipt.get("created_at", receipt.get("timestamp"))
|
|
184
|
+
if ts is None:
|
|
185
|
+
record("created_at/timestamp is a real calendar date", True,
|
|
186
|
+
"field absent, skipped")
|
|
187
|
+
else:
|
|
188
|
+
ok = check_calendar(ts)
|
|
189
|
+
record("created_at/timestamp is a real calendar date", ok,
|
|
190
|
+
"" if ok else "got: %r" % (ts,))
|
|
191
|
+
if ok:
|
|
192
|
+
try:
|
|
193
|
+
dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
|
|
194
|
+
if dt > datetime.now(timezone.utc):
|
|
195
|
+
record("timestamp is not in the future", False,
|
|
196
|
+
"got: %r" % (ts,))
|
|
197
|
+
else:
|
|
198
|
+
record("timestamp is not in the future", True)
|
|
199
|
+
except ValueError:
|
|
200
|
+
record("timestamp is not in the future", False,
|
|
201
|
+
"unparseable: %r" % (ts,))
|
|
202
|
+
|
|
203
|
+
# Server's own claim: reported, never trusted by an offline verifier.
|
|
204
|
+
status = receipt.get("verification_status")
|
|
205
|
+
checks.append({"check": "verification_status (server claim, informational only)",
|
|
206
|
+
"ok": True,
|
|
207
|
+
"detail": "server said: %r; offline verdict rests on the math above"
|
|
208
|
+
% (status,)})
|
|
209
|
+
|
|
210
|
+
passed = all(c["ok"] for c in checks)
|
|
211
|
+
return passed, checks
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv=None):
|
|
215
|
+
if argv is None:
|
|
216
|
+
argv = sys.argv
|
|
217
|
+
args = [a for a in argv[1:] if a != "--json"]
|
|
218
|
+
as_json = "--json" in argv[1:]
|
|
219
|
+
if len(args) != 1:
|
|
220
|
+
print("usage: python3 aer1_verify.py receipt.json [--json]", file=sys.stderr)
|
|
221
|
+
print(" cat receipt.json | python3 aer1_verify.py -", file=sys.stderr)
|
|
222
|
+
return 2
|
|
223
|
+
src = args[0]
|
|
224
|
+
try:
|
|
225
|
+
if src == "-":
|
|
226
|
+
text = sys.stdin.read()
|
|
227
|
+
else:
|
|
228
|
+
with open(src, "r", encoding="utf-8") as f:
|
|
229
|
+
text = f.read()
|
|
230
|
+
except OSError as e:
|
|
231
|
+
print("error: cannot read %s: %s" % (src, e), file=sys.stderr)
|
|
232
|
+
return 2
|
|
233
|
+
try:
|
|
234
|
+
doc = json.loads(text)
|
|
235
|
+
except ValueError as e:
|
|
236
|
+
print("error: input is not valid JSON: %s" % e, file=sys.stderr)
|
|
237
|
+
return 2
|
|
238
|
+
|
|
239
|
+
passed, checks = verify(doc)
|
|
240
|
+
receipt_id = None
|
|
241
|
+
try:
|
|
242
|
+
r = doc.get("receipt", doc) if isinstance(doc, dict) else {}
|
|
243
|
+
receipt_id = r.get("id") if isinstance(r, dict) else None
|
|
244
|
+
except Exception:
|
|
245
|
+
receipt_id = None
|
|
246
|
+
|
|
247
|
+
if as_json:
|
|
248
|
+
print(json.dumps({"verdict": "PASS" if passed else "FAIL",
|
|
249
|
+
"receipt_id": receipt_id,
|
|
250
|
+
"checks": checks}, indent=2))
|
|
251
|
+
else:
|
|
252
|
+
print("aer1-verify %s" % VERSION)
|
|
253
|
+
print("receipt: %s" % (receipt_id or "(unknown id)"))
|
|
254
|
+
for c in checks:
|
|
255
|
+
mark = "PASS" if c["ok"] else "FAIL"
|
|
256
|
+
line = " [%s] %s" % (mark, c["check"])
|
|
257
|
+
if c["detail"]:
|
|
258
|
+
line += " -- %s" % c["detail"]
|
|
259
|
+
print(line)
|
|
260
|
+
print("VERDICT: %s" % ("PASS: fingerprint matches, receipt intact"
|
|
261
|
+
if passed else
|
|
262
|
+
"FAIL: fingerprint does not match, do not trust this receipt"))
|
|
263
|
+
return 0 if passed else 1
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aer1-verify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Offline verifier for AER-1 verifiable execution receipts. No network, no trust in any server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["aer-1", "receipt", "verification", "ai-agents", "audit"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Security",
|
|
16
|
+
"Topic :: Security :: Cryptography",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
aer1-verify = "aer1_verify:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
py-modules = ["aer1_verify"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Tests for aer1_verify.py. Run: python3 -m pytest tests/ (or python3 tests/test_verifier.py)
|
|
2
|
+
|
|
3
|
+
Fixtures are real receipts issued by zambo.dev on 2026-10-06, plus
|
|
4
|
+
adversarial mutations. The two PASS fixtures must keep passing: they pin
|
|
5
|
+
the verifier to live production receipts.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
FIX = os.path.join(HERE, "fixtures")
|
|
14
|
+
VERIFIER = os.path.join(HERE, "..", "aer1_verify.py")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_verifier(fixture):
|
|
18
|
+
p = subprocess.run([sys.executable, VERIFIER, os.path.join(FIX, fixture), "--json"],
|
|
19
|
+
capture_output=True, text=True)
|
|
20
|
+
return p.returncode, json.loads(p.stdout)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_real_receipt_envelope_passes():
|
|
24
|
+
code, out = run_verifier("receipt-real-1.json")
|
|
25
|
+
assert code == 0, out
|
|
26
|
+
assert out["verdict"] == "PASS"
|
|
27
|
+
assert out["receipt_id"] == "c5dadb0a-8f99-40f6-b30a-40852435b752"
|
|
28
|
+
assert all(c["ok"] for c in out["checks"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_real_receipt_mcp_shape_passes():
|
|
32
|
+
code, out = run_verifier("receipt-real-2.json")
|
|
33
|
+
assert code == 0, out
|
|
34
|
+
assert out["verdict"] == "PASS"
|
|
35
|
+
assert out["receipt_id"] == "f2815391-ebc0-4a51-bbe3-613434371534"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tampered_receipt_fails():
|
|
39
|
+
# One character changed in the statement ($86,164.00 -> $86,164.01).
|
|
40
|
+
# The fingerprint must not match anymore.
|
|
41
|
+
code, out = run_verifier("receipt-tampered.json")
|
|
42
|
+
assert code == 1, out
|
|
43
|
+
assert out["verdict"] == "FAIL"
|
|
44
|
+
failed = [c["check"] for c in out["checks"] if not c["ok"]]
|
|
45
|
+
assert any("sha256(canonical_bytes) equals output_hash" in f for f in failed), failed
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_bad_base64_fails():
|
|
49
|
+
code, out = run_verifier("receipt-bad-b64.json")
|
|
50
|
+
assert code == 1
|
|
51
|
+
assert out["verdict"] == "FAIL"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_wrong_hash_fails():
|
|
55
|
+
code, out = run_verifier("receipt-wrong-hash.json")
|
|
56
|
+
assert code == 1
|
|
57
|
+
assert out["verdict"] == "FAIL"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_non_utf8_fails_closed():
|
|
61
|
+
# Valid base64, invalid UTF-8: must fail closed, never pass on bytes
|
|
62
|
+
# it cannot read.
|
|
63
|
+
code, out = run_verifier("receipt-bad-utf8.json")
|
|
64
|
+
assert code == 1
|
|
65
|
+
assert out["verdict"] == "FAIL"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_usage_error():
|
|
69
|
+
p = subprocess.run([sys.executable, VERIFIER], capture_output=True, text=True)
|
|
70
|
+
assert p.returncode == 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
test_real_receipt_envelope_passes()
|
|
75
|
+
test_real_receipt_mcp_shape_passes()
|
|
76
|
+
test_tampered_receipt_fails()
|
|
77
|
+
test_bad_base64_fails()
|
|
78
|
+
test_wrong_hash_fails()
|
|
79
|
+
test_non_utf8_fails_closed()
|
|
80
|
+
test_usage_error()
|
|
81
|
+
print("all aer1-verify tests passed")
|