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.
@@ -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,9 @@
1
+ README.md
2
+ aer1_verify.py
3
+ pyproject.toml
4
+ aer1_verify.egg-info/PKG-INFO
5
+ aer1_verify.egg-info/SOURCES.txt
6
+ aer1_verify.egg-info/dependency_links.txt
7
+ aer1_verify.egg-info/entry_points.txt
8
+ aer1_verify.egg-info/top_level.txt
9
+ tests/test_verifier.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aer1-verify = aer1_verify:main
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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")