betterwithage commited on
Commit
f2c745e
·
verified ·
1 Parent(s): 235fa8e

DEV2: add szl_intoto.py + szl_intoto_routes.py + verify-intoto-receipt.py + patched serve.py (in-toto Statement v1 + Merkle log + verify guide endpoint). Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>

Browse files
serve.py CHANGED
@@ -8297,11 +8297,81 @@ except Exception as _kl_e:
8297
 
8298
  _LOCAL_ONLY_A11OY_PREFIXES = ("v1/warhacker/", "v1/observability/", "v1/sec/",
8299
  "v1/live/", "v1/code/", "v1/seismic/", "v1/feeds/",
8300
- "v1/govern/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8301
 
8302
 
8303
  @app.api_route("/api/a11oy/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
8304
  async def api_proxy(request: Request, path: str) -> Response:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8305
  if path.startswith(_LOCAL_ONLY_A11OY_PREFIXES):
8306
  return JSONResponse(
8307
  {"error": "local route unmatched — not proxied to Node backend",
@@ -10037,6 +10107,29 @@ except Exception as _szl_lake_e: # pragma: no cover
10037
  # ============================================================================
10038
  # END: a11oy UNIFIED RECEIPT LEDGER sink
10039
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10040
  # ============================================================================
10041
  # BEGIN: Tier-1 Demo Features (BVIR + Honest Refusal + Verifiable Thesis)
10042
  # ADDITIVE. Path namespace /api/a11oy/v1/demo — no overlap with any existing
 
8297
 
8298
  _LOCAL_ONLY_A11OY_PREFIXES = ("v1/warhacker/", "v1/observability/", "v1/sec/",
8299
  "v1/live/", "v1/code/", "v1/seismic/", "v1/feeds/",
8300
+ "v1/govern/",
8301
+ "v1/verify/intoto", # in-toto verify guide (DEV2)
8302
+ "v1/khipu/intoto/", # in-toto receipt views (DEV2)
8303
+ )
8304
+
8305
+
8306
+ # ============================================================================
8307
+ # DEV2: in-toto verify guide (inline, proven register pattern)
8308
+ # Registered BEFORE /api/a11oy/{path:path} so it wins over the Node proxy.
8309
+ # ============================================================================
8310
+ @app.get("/api/a11oy/v1/verify/intoto")
8311
+ async def _intoto_verify_guide(request: Request) -> Response:
8312
+ """in-toto verification guide: what is now verifiable vs roadmap."""
8313
+ from starlette.responses import JSONResponse as _JSONResponse
8314
+ _pub_key_url = "https://github.com/szl-holdings/.github/blob/main/cosign.pub"
8315
+ return _JSONResponse({
8316
+ "title": "SZL a11oy in-toto Verification Guide",
8317
+ "what_is_now_verifiable": {
8318
+ "1_dsse_signature": {
8319
+ "status": "LIVE",
8320
+ "description": "DSSE-signed with SZL ECDSA P-256 keypair. payloadType=application/vnd.in-toto+json. Verifiable with cosign verify-blob.",
8321
+ "command": "cosign verify-blob --key https://a-11-oy.com/cosign.pub --bundle <receipt.bundle.json> <statement.json>",
8322
+ },
8323
+ "2_intoto_statement_v1": {
8324
+ "status": "LIVE",
8325
+ "description": "_type: https://in-toto.io/Statement/v1, subject:[{name, digest:{sha3-256:<output_hash>}}], predicateType: https://szl.holdings/khipu-governed-inference/v1",
8326
+ "fetch_endpoint": "/khipu/intoto/<receipt_id>",
8327
+ },
8328
+ "3_hard_binding": {
8329
+ "status": "LIVE",
8330
+ "description": "subject.digest = SHA3-256(model_output). C2PA pattern: receipt cannot be recycled for a different output. Verify offline: SHA3-256(answer) == statement.subject[0].digest",
8331
+ },
8332
+ "4_self_hosted_merkle_log": {
8333
+ "status": "LIVE",
8334
+ "description": "RFC 6962 SHA3-256 Merkle transparency log. Inclusion proofs at /api/lake/v1/proof/<receipt_id>.",
8335
+ "proof_endpoint": "/api/lake/v1/proof/<receipt_id>",
8336
+ "log_endpoint": "/api/lake/v1/log",
8337
+ "honest_label": "szl-lake-merkle (self-hosted) — NOT Sigstore public Rekor",
8338
+ },
8339
+ },
8340
+ "what_is_roadmap": {
8341
+ "per_receipt_public_rekor": "ROADMAP: submit each receipt to rekor.sigstore.dev on Lake publish.",
8342
+ "slsa_l2_container": "ROADMAP: actions/attest-build-provenance in CI (~3 YAML lines).",
8343
+ "tee_attestation": "ROADMAP Phase II: AWS Nitro PCR-bound inference attestation.",
8344
+ },
8345
+ "offline_verifier": "szl-cookbook/verify-intoto-receipt.py (Apache-2.0)",
8346
+ "pr": "https://github.com/szl-holdings/a11oy/pull/567",
8347
+ "public_key_url": _pub_key_url,
8348
+ })
8349
+ print("[a11oy] in-toto verify guide registered: /api/a11oy/v1/verify/intoto (DEV2)", file=__import__("sys").stderr)
8350
 
8351
 
8352
  @app.api_route("/api/a11oy/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
8353
  async def api_proxy(request: Request, path: str) -> Response:
8354
+ # DEV2: in-toto verify guide + inclusion proof — served in-process, NOT proxied
8355
+ if path == "v1/verify/intoto":
8356
+ return JSONResponse({
8357
+ "title": "SZL a11oy in-toto Verification Guide",
8358
+ "what_is_now_verifiable": {
8359
+ "1_dsse_signature": {"status": "LIVE", "description": "payloadType=application/vnd.in-toto+json, ECDSA-P256-SHA256 DSSE sig"},
8360
+ "2_intoto_statement_v1": {"status": "LIVE", "description": "_type: https://in-toto.io/Statement/v1, predicateType: https://szl.holdings/khipu-governed-inference/v1", "endpoint": "/khipu/intoto/<receipt_id>"},
8361
+ "3_hard_binding": {"status": "LIVE", "description": "subject.digest = SHA3-256(output). C2PA pattern."},
8362
+ "4_merkle_log": {"status": "LIVE", "description": "RFC 6962 SHA3-256 self-hosted log.", "proof_endpoint": "/api/lake/v1/proof/<id>", "log_endpoint": "/api/lake/v1/log", "honest_label": "szl-lake-merkle (self-hosted) — NOT Sigstore Rekor"},
8363
+ },
8364
+ "what_is_roadmap": {
8365
+ "per_receipt_public_rekor": "ROADMAP: submit to rekor.sigstore.dev on Lake publish",
8366
+ "slsa_l2": "ROADMAP: actions/attest-build-provenance in CI",
8367
+ "tee": "ROADMAP Phase II: AWS Nitro PCR",
8368
+ },
8369
+ "offline_verifier": "szl-cookbook/verify-intoto-receipt.py (Apache-2.0)",
8370
+ "pr": "https://github.com/szl-holdings/a11oy/pull/567",
8371
+ "public_key_url": "https://github.com/szl-holdings/.github/blob/main/cosign.pub",
8372
+ "_dev": "DEV2 in-toto attestation layer (szl_intoto.py)",
8373
+ })
8374
+
8375
  if path.startswith(_LOCAL_ONLY_A11OY_PREFIXES):
8376
  return JSONResponse(
8377
  {"error": "local route unmatched — not proxied to Node backend",
 
10107
  # ============================================================================
10108
  # END: a11oy UNIFIED RECEIPT LEDGER sink
10109
  # ============================================================================
10110
+ # ============================================================================
10111
+ # BEGIN: in-toto Statement v1 routes + Merkle transparency log (DEV2, additive)
10112
+ # Mounts in-toto-compatible receipt views and per-receipt inclusion proofs:
10113
+ # GET /khipu/intoto/<receipt_id> -> Statement v1 + DSSE envelope + proof
10114
+ # GET /api/lake/v1/proof/<receipt_id> -> Merkle inclusion proof (RFC 6962)
10115
+ # GET /api/lake/v1/log -> self-hosted log state
10116
+ # GET /api/a11oy/v1/verify/intoto -> verification guide
10117
+ # REGISTERED AFTER szl_lake_ingest: because each register() uses insert(0,),
10118
+ # the LAST registered module wins (its routes are at index 0 = highest priority).
10119
+ # So intoto routes correctly WIN over any /api/lake/v1/* catch-alls.
10120
+ # Pattern: in-toto Attestation Framework v1 (Apache-2.0). No lib import, no AGPL.
10121
+ # Additive, try/except-guarded. HONEST LABELS: rekor-public vs szl-lake-merkle.
10122
+ # ============================================================================
10123
+ try:
10124
+ import szl_intoto_routes as _szl_intoto_routes
10125
+ _intoto_status = _szl_intoto_routes.register(app, ns="a11oy")
10126
+ print(f"[a11oy] in-toto routes registered: {_intoto_status}", file=__import__("sys").stderr)
10127
+ except Exception as _it_e:
10128
+ print(f"[a11oy] in-toto routes NOT registered (non-fatal): {_it_e!r}", file=__import__("sys").stderr)
10129
+ # ============================================================================
10130
+ # END: in-toto Statement v1 routes + Merkle transparency log
10131
+ # ============================================================================
10132
+
10133
  # ============================================================================
10134
  # BEGIN: Tier-1 Demo Features (BVIR + Honest Refusal + Verifiable Thesis)
10135
  # ADDITIVE. Path namespace /api/a11oy/v1/demo — no overlap with any existing
szl-cookbook/verify-intoto-receipt.py ADDED
@@ -0,0 +1,611 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173
4
+ # Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>
5
+ """
6
+ szl-cookbook: verify-intoto-receipt.py
7
+ =======================================
8
+
9
+ OFFLINE verifier for SZL a11oy Khipu receipts in in-toto Statement v1 format.
10
+
11
+ WHAT THIS PROVES (without trusting SZL at runtime):
12
+ 1. DSSE signature — receipt was signed by the SZL ECDSA P-256 keypair.
13
+ Verified against the published cosign.pub (embedded below; also at
14
+ https://github.com/szl-holdings/.github/blob/main/cosign.pub).
15
+ 2. in-toto Statement v1 structure — receipt payload is a valid Statement v1
16
+ (_type, subject, predicateType, predicate all present and typed correctly).
17
+ 3. Hard binding — subject.digest["sha3-256"] matches SHA3-256(answer_text)
18
+ (C2PA pattern: receipt cannot be recycled for a different model output).
19
+ 4. Merkle inclusion proof — leaf_hash(statement) walks the audit_path to the
20
+ root_hash. OFFLINE math: no trust in SZL required for the proof check.
21
+
22
+ USAGE:
23
+ # Fetch a receipt bundle from the live API
24
+ curl -s https://szlholdings-a11oy.hf.space/khipu/intoto/<receipt_id> > receipt.json
25
+
26
+ # Run the verifier (stdlib only; no pip installs required except `cryptography`)
27
+ python3 verify-intoto-receipt.py receipt.json
28
+
29
+ # Or pipe directly
30
+ curl -s https://szlholdings-a11oy.hf.space/khipu/intoto/<receipt_id> | python3 verify-intoto-receipt.py -
31
+
32
+ # Verbose output
33
+ python3 verify-intoto-receipt.py receipt.json --verbose
34
+
35
+ DEPENDENCIES:
36
+ stdlib only for chain verification.
37
+ `cryptography` (pip install cryptography) for DSSE signature verification.
38
+ NO in-toto library, NO sigstore library, NO network call required once you
39
+ have the receipt JSON.
40
+
41
+ EXIT CODES:
42
+ 0 — all checks PASSED
43
+ 1 — one or more checks FAILED (details printed to stdout)
44
+ 2 — usage error or unreadable input
45
+
46
+ WHAT IS NOT VERIFIED BY THIS SCRIPT (honest limits):
47
+ - Whether the model output is correct or the governance decision was right.
48
+ - Whether the SZL keypair itself is trustworthy (trust anchor is cosign.pub).
49
+ - Public Rekor inclusion (per-receipt Rekor submission is ROADMAP).
50
+ The self-hosted Merkle log inclusion proof IS verified here.
51
+ - TEE attestation (AWS Nitro PCR) — not yet implemented (Phase II roadmap).
52
+
53
+ Apache-2.0 — SZL Holdings 2026. Reimplement, modify, redistribute freely.
54
+ """
55
+ from __future__ import annotations
56
+
57
+ import base64
58
+ import hashlib
59
+ import json
60
+ import sys
61
+ from typing import Any
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # EMBEDDED PUBLIC KEY (szl-holdings/.github/cosign.pub — public, not a secret)
65
+ # Also fetchable at https://a-11-oy.com/cosign.pub
66
+ # ---------------------------------------------------------------------------
67
+ _COSIGN_PUBLIC_PEM = """
68
+ -----BEGIN PUBLIC KEY-----
69
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyq9ALpZuegbE67GRpWp8FfGSX1IJ
70
+ bt5gw4jQ3RuBuIYIZchnfn9XLZf5KKw+zRfq5EJ8S+5cqwai5Wz0FDSyyA==
71
+ -----END PUBLIC KEY-----
72
+ """.strip()
73
+
74
+ _PUBLIC_KEY_URL = "https://github.com/szl-holdings/.github/blob/main/cosign.pub"
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # CONSTANTS
78
+ # ---------------------------------------------------------------------------
79
+ STATEMENT_TYPE = "https://in-toto.io/Statement/v1"
80
+ GOVERNED_INFERENCE_PREDICATE = "https://szl.holdings/khipu-governed-inference/v1"
81
+ INTOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json"
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # HELPER: DSSE PAE
86
+ # ---------------------------------------------------------------------------
87
+ def _pae(payload_type: str, body: bytes) -> bytes:
88
+ """DSSE Pre-Authentication Encoding (DSSEv1)."""
89
+ t = payload_type.encode("utf-8")
90
+ return (b"DSSEv1 " + str(len(t)).encode() + b" " + t
91
+ + b" " + str(len(body)).encode() + b" " + body)
92
+
93
+
94
+ def _canonical_json(obj: Any) -> bytes:
95
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"),
96
+ ensure_ascii=False).encode("utf-8")
97
+
98
+
99
+ def _sha3_256(b: bytes) -> str:
100
+ return hashlib.sha3_256(b).hexdigest()
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # CHECK 1: DSSE SIGNATURE
105
+ # ---------------------------------------------------------------------------
106
+ def check_dsse_signature(envelope: dict, verbose: bool = False) -> dict:
107
+ """
108
+ Verify the DSSE envelope signature against the embedded SZL cosign public key.
109
+
110
+ The DSSE protocol:
111
+ PAE = DSSEv1 SP LEN(payloadType) SP payloadType SP LEN(payload_bytes) SP payload_bytes
112
+ signature = ECDSA-P256-SHA256(PAE)
113
+ payload_bytes = base64.decode(envelope["payload"])
114
+
115
+ Returns {passed, reason, ...}
116
+ """
117
+ result = {"check": "dsse_signature", "passed": False, "reason": ""}
118
+
119
+ payload_type = envelope.get("payloadType", "")
120
+ payload_b64 = envelope.get("payload", "")
121
+ signatures = envelope.get("signatures", [])
122
+
123
+ if not payload_b64:
124
+ result["reason"] = "envelope missing 'payload' field"
125
+ return result
126
+ if not signatures:
127
+ # Check for honest unsigned label
128
+ if not envelope.get("signed", True):
129
+ result["passed"] = False
130
+ result["reason"] = f"receipt is UNSIGNED (honesty: {envelope.get('honesty', 'no signing key')})"
131
+ result["warning"] = "Receipt was emitted without signing key present — not a forgery, but unverified."
132
+ return result
133
+ result["reason"] = "no signatures in envelope"
134
+ return result
135
+
136
+ try:
137
+ payload_bytes = base64.b64decode(payload_b64 + "==")
138
+ except Exception as exc:
139
+ result["reason"] = f"base64 decode failed: {exc}"
140
+ return result
141
+
142
+ try:
143
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
144
+ from cryptography.hazmat.primitives.asymmetric import ec
145
+ from cryptography.hazmat.primitives import hashes
146
+ from cryptography.exceptions import InvalidSignature
147
+ except ImportError:
148
+ result["passed"] = None
149
+ result["reason"] = (
150
+ "cryptography library not installed — DSSE sig check skipped. "
151
+ "Install with: pip install cryptography"
152
+ )
153
+ result["skipped"] = True
154
+ return result
155
+
156
+ pub_key = load_pem_public_key(_COSIGN_PUBLIC_PEM.encode("utf-8"))
157
+ pae_bytes = _pae(payload_type, payload_bytes)
158
+
159
+ verified = False
160
+ last_exc = None
161
+ for sig_entry in signatures:
162
+ sig_b64 = sig_entry.get("sig", "")
163
+ if not sig_b64:
164
+ continue
165
+ try:
166
+ sig_bytes = base64.b64decode(sig_b64 + "==")
167
+ pub_key.verify(sig_bytes, pae_bytes, ec.ECDSA(hashes.SHA256()))
168
+ verified = True
169
+ if verbose:
170
+ print(f" [sig] keyid={sig_entry.get('keyid')} → VALID (ECDSA-P256-SHA256)")
171
+ break
172
+ except InvalidSignature:
173
+ last_exc = "InvalidSignature"
174
+ except Exception as exc:
175
+ last_exc = str(exc)
176
+
177
+ if verified:
178
+ result["passed"] = True
179
+ result["reason"] = f"ECDSA-P256-SHA256 signature verified against {_PUBLIC_KEY_URL}"
180
+ result["payload_type"] = payload_type
181
+ result["payload_sha3_256"] = _sha3_256(payload_bytes)
182
+ else:
183
+ result["passed"] = False
184
+ result["reason"] = f"signature verification failed: {last_exc}"
185
+
186
+ return result
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # CHECK 2: in-toto Statement v1 STRUCTURE
191
+ # ---------------------------------------------------------------------------
192
+ def check_intoto_statement(statement: dict, verbose: bool = False) -> dict:
193
+ """
194
+ Verify the in-toto Statement v1 structure.
195
+
196
+ Required fields per spec (https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md):
197
+ _type: "https://in-toto.io/Statement/v1"
198
+ subject: [{name: str, digest: {alg: hex}}]
199
+ predicateType: URI string
200
+ predicate: object
201
+ """
202
+ result = {"check": "intoto_statement_v1", "passed": False, "reason": ""}
203
+ errors = []
204
+
205
+ _type = statement.get("_type")
206
+ if _type != STATEMENT_TYPE:
207
+ errors.append(f"_type is {_type!r}, expected {STATEMENT_TYPE!r}")
208
+
209
+ subject = statement.get("subject")
210
+ if not isinstance(subject, list) or len(subject) == 0:
211
+ errors.append("subject must be a non-empty list")
212
+ else:
213
+ for i, s in enumerate(subject):
214
+ if not isinstance(s.get("name"), str):
215
+ errors.append(f"subject[{i}].name missing or not a string")
216
+ digest = s.get("digest", {})
217
+ if not isinstance(digest, dict) or not digest:
218
+ errors.append(f"subject[{i}].digest missing or empty")
219
+
220
+ predicate_type = statement.get("predicateType")
221
+ if not isinstance(predicate_type, str) or not predicate_type.startswith("http"):
222
+ errors.append(f"predicateType is {predicate_type!r}, expected a URI")
223
+
224
+ predicate = statement.get("predicate")
225
+ if not isinstance(predicate, dict):
226
+ errors.append("predicate must be an object")
227
+
228
+ if errors:
229
+ result["passed"] = False
230
+ result["reason"] = "; ".join(errors)
231
+ result["errors"] = errors
232
+ else:
233
+ result["passed"] = True
234
+ result["reason"] = "valid in-toto Statement v1"
235
+ result["_type"] = _type
236
+ result["predicate_type"] = predicate_type
237
+ result["subject_count"] = len(subject)
238
+ result["subject_names"] = [s.get("name") for s in subject]
239
+ if verbose:
240
+ print(f" [stmt] predicateType={predicate_type}")
241
+ for s in subject:
242
+ print(f" [stmt] subject: {s.get('name')} → digest={s.get('digest')}")
243
+
244
+ return result
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # CHECK 3: HARD BINDING (C2PA pattern)
249
+ # ---------------------------------------------------------------------------
250
+ def check_hard_binding(statement: dict, answer: str | None = None,
251
+ verbose: bool = False) -> dict:
252
+ """
253
+ Verify that subject.digest["sha3-256"] matches SHA3-256(answer_text).
254
+
255
+ C2PA hard-binding pattern: the receipt binds to the EXACT model output.
256
+ If the answer was replaced after signing, the digest will not match.
257
+
258
+ If answer is None (e.g. denied turn), we verify the digest is a valid hex
259
+ string but cannot check the content binding (labeled PARTIAL).
260
+ """
261
+ result = {"check": "hard_binding", "passed": False, "reason": ""}
262
+
263
+ subject = statement.get("subject", [{}])
264
+ if not subject:
265
+ result["reason"] = "no subject in statement"
266
+ return result
267
+
268
+ digest = subject[0].get("digest", {})
269
+ stored_hash = digest.get("sha3-256", "")
270
+
271
+ if not stored_hash or len(stored_hash) != 64:
272
+ result["passed"] = False
273
+ result["reason"] = f"subject.digest[sha3-256] missing or invalid: {stored_hash!r}"
274
+ return result
275
+
276
+ if answer is None:
277
+ # Try to extract from predicate
278
+ predicate = statement.get("predicate", {})
279
+ answer = predicate.get("answer") or predicate.get("output") or None
280
+
281
+ if answer is None:
282
+ result["passed"] = True
283
+ result["reason"] = (
284
+ "PARTIAL: no answer text available to verify content binding. "
285
+ "Digest is present and well-formed. "
286
+ "To fully verify: supply the model output text."
287
+ )
288
+ result["stored_digest"] = stored_hash
289
+ result["partial"] = True
290
+ return result
291
+
292
+ computed_hash = _sha3_256(answer.encode("utf-8"))
293
+ # Also check alternative binding methods documented in szl_intoto.py
294
+ if stored_hash == computed_hash:
295
+ result["passed"] = True
296
+ result["reason"] = "output hash verified: SHA3-256(answer) matches subject.digest"
297
+ result["computed_hash"] = computed_hash
298
+ result["stored_hash"] = stored_hash
299
+ if verbose:
300
+ print(f" [bind] SHA3-256(answer)={computed_hash} ✓")
301
+ else:
302
+ # Check fallback binding (receipt_id + canonical_json of predicate)
303
+ predicate = statement.get("predicate", {})
304
+ receipt_id = (predicate.get("receipt_id") or predicate.get("id")
305
+ or predicate.get("hash") or "")
306
+ _STRIP_KEYS = frozenset({"dsse", "signatures", "_dsse", "_pae_sha256",
307
+ "honesty", "verify_key_url"})
308
+ stripped_predicate = {k: v for k, v in predicate.items()
309
+ if k not in _STRIP_KEYS}
310
+ fallback_input = (receipt_id + ":").encode("utf-8") + _canonical_json(stripped_predicate)
311
+ fallback_hash = _sha3_256(fallback_input)
312
+ if stored_hash == fallback_hash:
313
+ result["passed"] = True
314
+ result["reason"] = "output hash verified via fallback binding (receipt_id + predicate)"
315
+ result["computed_hash"] = fallback_hash
316
+ result["binding_method"] = "fallback"
317
+ if verbose:
318
+ print(f" [bind] fallback binding={fallback_hash} ✓")
319
+ else:
320
+ result["passed"] = False
321
+ result["reason"] = (
322
+ f"output hash MISMATCH: stored={stored_hash!r}, "
323
+ f"computed_direct={computed_hash!r}. "
324
+ "Receipt may have been signed for a different output (tampering)."
325
+ )
326
+ result["stored_hash"] = stored_hash
327
+ result["computed_hash"] = computed_hash
328
+
329
+ return result
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # CHECK 4: MERKLE INCLUSION PROOF
334
+ # ---------------------------------------------------------------------------
335
+ def _leaf_hash_sha3(data: bytes) -> bytes:
336
+ """RFC 6962 leaf hash: SHA3-256(0x00 || data)."""
337
+ return hashlib.sha3_256(b"\x00" + data).digest()
338
+
339
+
340
+ def _node_hash_sha3(left: bytes, right: bytes) -> bytes:
341
+ """RFC 6962 interior node: SHA3-256(0x01 || left || right)."""
342
+ return hashlib.sha3_256(b"\x01" + left + right).digest()
343
+
344
+
345
+ def check_merkle_inclusion(statement: dict, transparency: dict,
346
+ verbose: bool = False) -> dict:
347
+ """
348
+ Verify the Merkle inclusion proof for the self-hosted SZL transparency log.
349
+
350
+ Algorithm (RFC 6962):
351
+ 1. Compute leaf_hash = SHA3-256(0x00 || canonical_json(statement))
352
+ 2. Walk audit_path: for each sibling, compute parent = node_hash(node, sibling)
353
+ (left/right determined by leaf_index parity at each level)
354
+ 3. The final computed node should equal root_hash
355
+
356
+ This is pure math — no trust in SZL required for verification.
357
+ """
358
+ result = {"check": "merkle_inclusion", "passed": False, "reason": ""}
359
+
360
+ log_type = transparency.get("transparency_log", "")
361
+ if log_type == "rekor-public":
362
+ result["passed"] = True
363
+ result["reason"] = (
364
+ f"Rekor public log inclusion: logIndex={transparency.get('log_index')}. "
365
+ f"Verify at {transparency.get('log_url', 'rekor.sigstore.dev')}. "
366
+ "Note: per-receipt Rekor submission may not be available in all deployments."
367
+ )
368
+ result["transparency_log"] = "rekor-public"
369
+ return result
370
+
371
+ if "error" in transparency:
372
+ result["passed"] = False
373
+ result["reason"] = f"No inclusion proof: {transparency['error']}"
374
+ return result
375
+
376
+ hashes_hex = transparency.get("hashes", [])
377
+ root_hash_hex = transparency.get("root_hash", "")
378
+ leaf_index = transparency.get("leaf_index")
379
+ leaf_hash_stored_hex = transparency.get("leaf_hash", "")
380
+
381
+ if not root_hash_hex:
382
+ result["reason"] = "root_hash missing from proof"
383
+ return result
384
+ if leaf_index is None:
385
+ result["reason"] = "leaf_index missing from proof"
386
+ return result
387
+
388
+ # Step 1: compute leaf hash from the statement
389
+ leaf_data = _canonical_json(statement)
390
+ computed_leaf = _leaf_hash_sha3(leaf_data)
391
+ computed_leaf_hex = computed_leaf.hex()
392
+
393
+ if verbose:
394
+ print(f" [merkle] computed leaf_hash={computed_leaf_hex}")
395
+ if leaf_hash_stored_hex:
396
+ print(f" [merkle] stored leaf_hash ={leaf_hash_stored_hex}")
397
+
398
+ if leaf_hash_stored_hex and computed_leaf_hex != leaf_hash_stored_hex:
399
+ result["passed"] = False
400
+ result["reason"] = (
401
+ f"Leaf hash mismatch: computed={computed_leaf_hex}, "
402
+ f"stored={leaf_hash_stored_hex}. "
403
+ "Statement was modified after log append."
404
+ )
405
+ return result
406
+
407
+ # Step 2: walk audit path
408
+ audit_hashes = [bytes.fromhex(h) for h in hashes_hex]
409
+ node = computed_leaf
410
+ i = leaf_index
411
+ for level_idx, sibling in enumerate(audit_hashes):
412
+ if i % 2 == 0:
413
+ # current node is left child
414
+ node = _node_hash_sha3(node, sibling)
415
+ if verbose:
416
+ print(f" [merkle] level {level_idx}: node(L,sibling) → {node.hex()[:16]}…")
417
+ else:
418
+ # current node is right child
419
+ node = _node_hash_sha3(sibling, node)
420
+ if verbose:
421
+ print(f" [merkle] level {level_idx}: node(sibling,R) → {node.hex()[:16]}…")
422
+ i //= 2
423
+
424
+ computed_root = node.hex()
425
+ if verbose:
426
+ print(f" [merkle] computed root={computed_root}")
427
+ print(f" [merkle] stored root ={root_hash_hex}")
428
+
429
+ if computed_root == root_hash_hex:
430
+ result["passed"] = True
431
+ result["reason"] = (
432
+ f"Merkle inclusion proof verified: "
433
+ f"leaf_index={leaf_index}, tree_size={transparency.get('tree_size')}, "
434
+ f"root_hash={root_hash_hex}. "
435
+ f"transparency_log={log_type}"
436
+ )
437
+ result["transparency_log"] = log_type
438
+ result["root_hash"] = root_hash_hex
439
+ result["leaf_index"] = leaf_index
440
+ else:
441
+ result["passed"] = False
442
+ result["reason"] = (
443
+ f"Merkle root mismatch: computed={computed_root}, "
444
+ f"stored={root_hash_hex}. "
445
+ "Proof is invalid or the log was mutated."
446
+ )
447
+
448
+ return result
449
+
450
+
451
+ # ---------------------------------------------------------------------------
452
+ # MAIN VERIFIER
453
+ # ---------------------------------------------------------------------------
454
+ def verify(receipt_bundle: dict, verbose: bool = False) -> dict:
455
+ """
456
+ Run all verification checks on a receipt bundle from /khipu/intoto/<id>.
457
+
458
+ Returns:
459
+ {
460
+ "overall": "PASS" | "FAIL" | "PARTIAL",
461
+ "checks": [...],
462
+ "receipt_id": "<id>",
463
+ "honest_limits": ["..."]
464
+ }
465
+ """
466
+ checks = []
467
+ receipt_id = receipt_bundle.get("receipt_id", "unknown")
468
+ intoto_envelope = receipt_bundle.get("intoto_envelope", {})
469
+ intoto_statement = receipt_bundle.get("intoto_statement", {})
470
+ transparency = receipt_bundle.get("transparency", {})
471
+
472
+ if not intoto_envelope and not intoto_statement:
473
+ return {
474
+ "overall": "FAIL",
475
+ "receipt_id": receipt_id,
476
+ "checks": [],
477
+ "error": (
478
+ "Input does not contain 'intoto_envelope' or 'intoto_statement'. "
479
+ "Fetch from /khipu/intoto/<receipt_id> to get the in-toto format."
480
+ ),
481
+ }
482
+
483
+ # If statement is embedded in the envelope payload, decode it
484
+ if not intoto_statement and intoto_envelope.get("payload"):
485
+ try:
486
+ payload_bytes = base64.b64decode(intoto_envelope["payload"] + "==")
487
+ intoto_statement = json.loads(payload_bytes)
488
+ except Exception:
489
+ pass
490
+
491
+ if verbose:
492
+ print(f"\n[verify] receipt_id: {receipt_id}")
493
+ print(f"[verify] transparency_log: {transparency.get('transparency_log', 'none')}")
494
+
495
+ # CHECK 1: DSSE signature
496
+ c1 = check_dsse_signature(intoto_envelope, verbose=verbose)
497
+ checks.append(c1)
498
+ if verbose:
499
+ status = "PASS" if c1["passed"] else ("SKIP" if c1.get("skipped") else "FAIL")
500
+ print(f"[1] DSSE Signature: {status} — {c1['reason']}")
501
+
502
+ # CHECK 2: in-toto Statement v1 structure
503
+ c2 = check_intoto_statement(intoto_statement, verbose=verbose)
504
+ checks.append(c2)
505
+ if verbose:
506
+ print(f"[2] Statement v1: {'PASS' if c2['passed'] else 'FAIL'} — {c2['reason']}")
507
+
508
+ # CHECK 3: Hard binding
509
+ c3 = check_hard_binding(intoto_statement, verbose=verbose)
510
+ checks.append(c3)
511
+ if verbose:
512
+ partial = " (PARTIAL)" if c3.get("partial") else ""
513
+ print(f"[3] Hard Binding: {'PASS' if c3['passed'] else 'FAIL'}{partial} — {c3['reason']}")
514
+
515
+ # CHECK 4: Merkle inclusion proof
516
+ if transparency:
517
+ c4 = check_merkle_inclusion(intoto_statement, transparency, verbose=verbose)
518
+ checks.append(c4)
519
+ if verbose:
520
+ print(f"[4] Merkle Proof: {'PASS' if c4['passed'] else 'FAIL'} — {c4['reason']}")
521
+
522
+ # Determine overall result
523
+ failed = [c for c in checks if c["passed"] is False]
524
+ skipped = [c for c in checks if c.get("skipped")]
525
+ partial = [c for c in checks if c.get("partial")]
526
+
527
+ if failed:
528
+ overall = "FAIL"
529
+ elif skipped or partial:
530
+ overall = "PARTIAL"
531
+ else:
532
+ overall = "PASS"
533
+
534
+ return {
535
+ "overall": overall,
536
+ "receipt_id": receipt_id,
537
+ "checks": checks,
538
+ "failed_checks": [c["check"] for c in failed],
539
+ "honest_limits": [
540
+ "Per-receipt public Rekor inclusion is ROADMAP (self-hosted Merkle log is current).",
541
+ "TEE attestation (AWS Nitro PCR) is Phase II roadmap.",
542
+ "SZL keypair trust depends on cosign.pub authenticity — verify at " + _PUBLIC_KEY_URL,
543
+ "governance correctness (Λ, gates) is NOT verified by this script.",
544
+ ],
545
+ }
546
+
547
+
548
+ # ---------------------------------------------------------------------------
549
+ # CLI
550
+ # ---------------------------------------------------------------------------
551
+ def main():
552
+ import argparse
553
+
554
+ parser = argparse.ArgumentParser(
555
+ description="Offline verifier for SZL a11oy in-toto Khipu receipts.",
556
+ formatter_class=argparse.RawDescriptionHelpFormatter,
557
+ epilog=__doc__,
558
+ )
559
+ parser.add_argument(
560
+ "receipt_file",
561
+ nargs="?",
562
+ default="-",
563
+ help="Path to receipt JSON file (or '-' to read from stdin). "
564
+ "Fetch from: curl -s https://szlholdings-a11oy.hf.space/khipu/intoto/<id>",
565
+ )
566
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
567
+ parser.add_argument("--json", "-j", action="store_true", help="Output raw JSON result")
568
+ args = parser.parse_args()
569
+
570
+ # Read input
571
+ try:
572
+ if args.receipt_file == "-":
573
+ raw = sys.stdin.read()
574
+ else:
575
+ with open(args.receipt_file, "r", encoding="utf-8") as fh:
576
+ raw = fh.read()
577
+ receipt_bundle = json.loads(raw)
578
+ except FileNotFoundError:
579
+ print(f"ERROR: file not found: {args.receipt_file}", file=sys.stderr)
580
+ sys.exit(2)
581
+ except json.JSONDecodeError as exc:
582
+ print(f"ERROR: invalid JSON: {exc}", file=sys.stderr)
583
+ sys.exit(2)
584
+
585
+ # Run verification
586
+ result = verify(receipt_bundle, verbose=args.verbose)
587
+
588
+ if args.json:
589
+ print(json.dumps(result, indent=2, default=str))
590
+ else:
591
+ print(f"\n{'='*60}")
592
+ print(f"SZL a11oy in-toto Receipt Verifier")
593
+ print(f"{'='*60}")
594
+ print(f"Receipt ID : {result['receipt_id']}")
595
+ print(f"Overall : {result['overall']}")
596
+ print()
597
+ for check in result["checks"]:
598
+ status = "PASS" if check["passed"] else ("SKIP" if check.get("skipped") else "FAIL")
599
+ print(f" [{status:4s}] {check['check']}: {check['reason']}")
600
+ if result.get("failed_checks"):
601
+ print(f"\nFailed checks: {result['failed_checks']}")
602
+ print(f"\nHonest limits:")
603
+ for limit in result["honest_limits"]:
604
+ print(f" - {limit}")
605
+ print(f"{'='*60}\n")
606
+
607
+ sys.exit(0 if result["overall"] == "PASS" else (0 if result["overall"] == "PARTIAL" else 1))
608
+
609
+
610
+ if __name__ == "__main__":
611
+ main()
szl_intoto.py ADDED
@@ -0,0 +1,662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v13
3
+ # Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>
4
+ """
5
+ szl_intoto.py — in-toto Statement v1 serialization + per-receipt transparency log.
6
+
7
+ Reimplemented from the Apache-2.0 in-toto Attestation Framework v1 spec
8
+ (https://github.com/in-toto/attestation) — NO library copied, NO AGPL imported.
9
+ Pattern: adopt the spec, reimplement in SZL's own code.
10
+
11
+ THREE PUBLIC CAPABILITIES:
12
+
13
+ 1. STATEMENT SERIALIZATION
14
+ wrap_as_intoto_statement(receipt) -> dict
15
+ Returns a valid in-toto Statement v1 object:
16
+ {_type, subject:[{name, digest:{sha3-256}}], predicateType, predicate}
17
+ The MODEL OUTPUT hash goes in subject.digest (C2PA hard-binding pattern:
18
+ a receipt cannot be recycled for a different output).
19
+ predicateType = https://szl.holdings/khipu-governed-inference/v1
20
+
21
+ 2. DSSE ENVELOPE (in-toto-compatible)
22
+ build_intoto_envelope(receipt, sign_fn) -> dict
23
+ payloadType = "application/vnd.in-toto+json"
24
+ Uses the caller-supplied sign_fn (szl_dsse.sign_payload) — no new key.
25
+
26
+ 3. TRANSPARENCY LOG — DUAL PATH (honest labels)
27
+ a) Try public Rekor (rekor.sigstore.dev) via DSSE entry submission.
28
+ Returns transparency_log: "rekor-public" if successful.
29
+ NEVER claims Rekor inclusion if the HTTP call failed.
30
+ b) If Rekor unreachable (HF Space egress blocked / timeout), fall back to
31
+ SZL in-memory Merkle transparency log (SHA3-256 RFC 6962 leaf/node hash).
32
+ Returns transparency_log: "szl-lake-merkle (self-hosted)".
33
+ The self-hosted log grows monotonically per process lifetime; restart
34
+ reseeds from khipu/ NDJSON partitions (if present).
35
+ An anchor-to-Rekor-on-publish path is documented below.
36
+
37
+ HONEST LABELS (never weaken):
38
+ - transparency_log: "rekor-public" — ONLY if the entry was ACTUALLY accepted
39
+ - transparency_log: "szl-lake-merkle (self-hosted)" — internal log, third-party
40
+ verifiable against /api/lake/v1/proof/<receipt_id>
41
+ - transparency_log: "none" — if log submission failed and no fallback
42
+
43
+ REKOR ANCHOR ON PUBLISH PATH (ROADMAP — do not claim as current):
44
+ When the a11oy Lake is published to HF Dataset (currently manual / CI-triggered),
45
+ a CI job should: for each receipt in khipu/*.ndjson, compute its in-toto Statement,
46
+ submit to Rekor, store the returned logIndex + inclusionProof back in the NDJSON.
47
+ Until that pipeline is wired, individual receipts use the self-hosted Merkle log.
48
+ The self-hosted log is honest and independently verifiable; it is NOT Sigstore.
49
+
50
+ Stdlib + cryptography (already a dep in a11oy for szl_dsse) only.
51
+ No AGPL. No in_toto library import. Pattern from spec, reimplement fresh.
52
+ """
53
+ from __future__ import annotations
54
+
55
+ import base64
56
+ import hashlib
57
+ import json
58
+ import os
59
+ import threading
60
+ import time
61
+ from datetime import datetime, timezone
62
+ from typing import Any, Callable, Optional
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # CONSTANTS — in-toto Statement v1 spec
66
+ # ---------------------------------------------------------------------------
67
+
68
+ STATEMENT_TYPE = "https://in-toto.io/Statement/v1"
69
+ GOVERNED_INFERENCE_PREDICATE = "https://szl.holdings/khipu-governed-inference/v1"
70
+ INTOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json"
71
+
72
+ REKOR_BASE = "https://rekor.sigstore.dev"
73
+ REKOR_TIMEOUT = float(os.environ.get("SZL_REKOR_TIMEOUT", "15"))
74
+
75
+ _TRANSPARENCY_LOG_VERSION = "szl-intoto/v1"
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # 1. STATEMENT SERIALIZATION
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _canonical_json(obj: Any) -> bytes:
82
+ """Deterministic canonical JSON: sorted keys, no extra whitespace, UTF-8."""
83
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"),
84
+ ensure_ascii=False).encode("utf-8")
85
+
86
+
87
+ def _sha3_256(b: bytes) -> str:
88
+ return hashlib.sha3_256(b).hexdigest()
89
+
90
+
91
+ def _sha256(b: bytes) -> str:
92
+ return hashlib.sha256(b).hexdigest()
93
+
94
+
95
+ def compute_output_hash(receipt: dict) -> str | None:
96
+ """
97
+ Derive the model output hash for the in-toto subject.digest.
98
+
99
+ C2PA hard-binding pattern: the hash MUST cover the actual output content,
100
+ not just metadata, so a receipt cannot be recycled for a different output.
101
+
102
+ Resolution order (honest fallback chain):
103
+ 1. receipt["output_sha3_256"] — pre-computed canonical hash (preferred)
104
+ 2. SHA3-256(receipt["answer"]) — recompute from the stored answer text
105
+ 3. SHA3-256(receipt["receipt_id"] + canonical_json(payload fields))
106
+ — fallback when answer is absent (e.g. denied turn, no answer emitted)
107
+ """
108
+ # 1) Pre-computed (best case)
109
+ h = receipt.get("output_sha3_256")
110
+ if isinstance(h, str) and len(h) == 64:
111
+ return h
112
+
113
+ # 2) Recompute from stored answer
114
+ answer = receipt.get("answer")
115
+ if isinstance(answer, str) and answer:
116
+ return _sha3_256(answer.encode("utf-8"))
117
+
118
+ # 3) Deterministic fallback from receipt identity fields
119
+ rid = receipt.get("receipt_id") or receipt.get("id") or receipt.get("hash") or ""
120
+ payload = {k: v for k, v in receipt.items()
121
+ if k not in ("signature", "dsse", "signatures", "honesty")}
122
+ fallback_input = (rid + ":").encode("utf-8") + _canonical_json(payload)
123
+ return _sha3_256(fallback_input)
124
+
125
+
126
+ def wrap_as_intoto_statement(receipt: dict) -> dict:
127
+ """
128
+ Convert a Khipu receipt to a valid in-toto Statement v1.
129
+
130
+ Returns the statement dict (NOT yet DSSE-signed). Call build_intoto_envelope()
131
+ to get a signed DSSE envelope with payloadType="application/vnd.in-toto+json".
132
+
133
+ Structure per in-toto Attestation Framework v1 (Apache-2.0):
134
+ {
135
+ "_type": "https://in-toto.io/Statement/v1",
136
+ "subject": [{
137
+ "name": "governed-inference-<receipt_id>",
138
+ "digest": {"sha3-256": "<output_hash>"}
139
+ }],
140
+ "predicateType": "https://szl.holdings/khipu-governed-inference/v1",
141
+ "predicate": { <all governance/Λ/energy/gate fields> }
142
+ }
143
+
144
+ The MODEL OUTPUT hash goes in subject.digest — C2PA hard binding.
145
+ All SZL-specific fields (Λ, gate results, chain metadata, Lean proof hashes,
146
+ energy) are preserved in predicate.
147
+ """
148
+ receipt_id = (receipt.get("receipt_id") or receipt.get("id")
149
+ or receipt.get("hash") or "unknown")
150
+ output_hash = compute_output_hash(receipt)
151
+
152
+ # Build the predicate from ALL existing receipt fields.
153
+ # Strip large binary/envelope fields that would double-encode.
154
+ _STRIP_KEYS = frozenset({"dsse", "signatures", "_dsse", "_pae_sha256",
155
+ "honesty", "verify_key_url"})
156
+ predicate = {k: v for k, v in receipt.items() if k not in _STRIP_KEYS}
157
+
158
+ # Explicitly document the hard-binding method so verifiers know how to check
159
+ predicate["_binding_method"] = "sha3-256(output_content|receipt_identity_fallback)"
160
+ predicate["_intoto_version"] = "Statement/v1"
161
+
162
+ statement = {
163
+ "_type": STATEMENT_TYPE,
164
+ "subject": [{
165
+ "name": f"governed-inference-{receipt_id}",
166
+ "digest": {"sha3-256": output_hash},
167
+ }],
168
+ "predicateType": GOVERNED_INFERENCE_PREDICATE,
169
+ "predicate": predicate,
170
+ }
171
+ return statement
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # 2. DSSE ENVELOPE (in-toto-compatible)
176
+ # ---------------------------------------------------------------------------
177
+
178
+ def _pae(payload_type: str, body: bytes) -> bytes:
179
+ """DSSE Pre-Authentication Encoding (DSSEv1)."""
180
+ t = payload_type.encode("utf-8")
181
+ return (b"DSSEv1 " + str(len(t)).encode() + b" " + t
182
+ + b" " + str(len(body)).encode() + b" " + body)
183
+
184
+
185
+ def build_intoto_envelope(
186
+ receipt: dict,
187
+ sign_fn: Callable[[Any, str], dict] | None = None,
188
+ ) -> dict:
189
+ """
190
+ Build a DSSE envelope over an in-toto Statement v1.
191
+
192
+ payloadType is "application/vnd.in-toto+json" (standard in-toto type).
193
+ sign_fn, if provided, must accept (payload_obj, payload_type) and return a
194
+ DSSE envelope dict (szl_dsse.sign_payload matches this signature).
195
+
196
+ If sign_fn is None, imports szl_dsse.sign_payload automatically (prefer the
197
+ existing Cosign keypair so the payloadType change is the ONLY delta).
198
+
199
+ The returned envelope is a valid in-toto DSSE bundle:
200
+ {
201
+ "payloadType": "application/vnd.in-toto+json",
202
+ "payload": "<base64(statement_json)>",
203
+ "signatures": [{...}],
204
+ ...honesty/signed meta from szl_dsse
205
+ }
206
+ """
207
+ statement = wrap_as_intoto_statement(receipt)
208
+
209
+ if sign_fn is None:
210
+ try:
211
+ import szl_dsse as _dsse
212
+ sign_fn = _dsse.sign_payload
213
+ except ImportError:
214
+ sign_fn = None
215
+
216
+ if sign_fn is not None:
217
+ envelope = sign_fn(statement, INTOTO_PAYLOAD_TYPE)
218
+ else:
219
+ # No signing available — emit unsigned envelope, honest label
220
+ body = _canonical_json(statement)
221
+ envelope = {
222
+ "payloadType": INTOTO_PAYLOAD_TYPE,
223
+ "payload": base64.b64encode(body).decode("ascii"),
224
+ "signatures": [],
225
+ "signed": False,
226
+ "honesty": "UNSIGNED — szl_dsse not importable; no signature fabricated.",
227
+ }
228
+
229
+ envelope["_intoto_statement_v1"] = True
230
+ envelope["_predicate_type"] = GOVERNED_INFERENCE_PREDICATE
231
+ return envelope
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # 3a. TRANSPARENCY LOG — public Rekor submission
236
+ # ---------------------------------------------------------------------------
237
+
238
+ def _load_public_pem() -> str | None:
239
+ """Load the SZL cosign public key PEM for Rekor verifier entry."""
240
+ try:
241
+ import szl_dsse as _dsse
242
+ pem = getattr(_dsse, "COSIGN_PUBLIC_PEM", None)
243
+ if pem and "BEGIN" in pem:
244
+ return pem.strip()
245
+ except ImportError:
246
+ pass
247
+ return os.environ.get("SZL_COSIGN_PUBLIC_PEM", "").strip() or None
248
+
249
+
250
+ def submit_to_rekor(dsse_envelope: dict, receipt_id: str) -> dict:
251
+ """
252
+ Submit a DSSE in-toto envelope to the public Sigstore Rekor log.
253
+
254
+ Returns a transparency log entry dict with:
255
+ {
256
+ "transparency_log": "rekor-public",
257
+ "log_index": <int>,
258
+ "log_url": "https://rekor.sigstore.dev/api/v1/log/entries?logIndex=<N>",
259
+ "inclusion_proof": {"checkpoint": ..., "hashes": [...], "root_hash": ...},
260
+ "submitted_at": "<ISO>"
261
+ }
262
+
263
+ On failure returns:
264
+ {"transparency_log": "none", "rekor_error": "<reason>", ...}
265
+
266
+ HONESTY: NEVER reports rekor-public unless the HTTP POST actually succeeded
267
+ and returned a logIndex. No fake inclusion proofs.
268
+ """
269
+ pub_pem = _load_public_pem()
270
+ if not pub_pem:
271
+ return {
272
+ "transparency_log": "none",
273
+ "rekor_error": "SZL cosign public key not available; cannot submit to Rekor",
274
+ "receipt_id": receipt_id,
275
+ }
276
+
277
+ # Build the Rekor DSSE entry request (Rekor v2 DSSE type)
278
+ entry_request = {
279
+ "kind": "dsse",
280
+ "apiVersion": "0.0.1",
281
+ "spec": {
282
+ "proposedContent": {
283
+ "envelope": json.dumps(dsse_envelope),
284
+ "verifiers": [{"publicKeyPem": pub_pem}],
285
+ }
286
+ },
287
+ }
288
+
289
+ try:
290
+ import httpx
291
+ with httpx.Client(follow_redirects=True, timeout=REKOR_TIMEOUT,
292
+ headers={"User-Agent": "a11oy-intoto/1.0 (+https://szlholdings-a11oy.hf.space)",
293
+ "Content-Type": "application/json"}) as client:
294
+ resp = client.post(
295
+ REKOR_BASE + "/api/v1/log/entries",
296
+ content=json.dumps(entry_request).encode("utf-8"),
297
+ )
298
+ if resp.status_code not in (200, 201):
299
+ return {
300
+ "transparency_log": "none",
301
+ "rekor_error": f"Rekor returned HTTP {resp.status_code}: {resp.text[:200]}",
302
+ "receipt_id": receipt_id,
303
+ }
304
+ log_entry = resp.json()
305
+ except Exception as exc:
306
+ return {
307
+ "transparency_log": "none",
308
+ "rekor_error": f"Rekor unreachable: {exc!r}",
309
+ "receipt_id": receipt_id,
310
+ }
311
+
312
+ # Parse the returned entry (Rekor returns {<uuid>: {body, ...}})
313
+ try:
314
+ uuid, entry = next(iter(log_entry.items()))
315
+ verification = entry.get("verification", {})
316
+ inclusion = verification.get("inclusionProof", {})
317
+ log_index = entry.get("logIndex") or entry.get("logID") or uuid
318
+ return {
319
+ "transparency_log": "rekor-public",
320
+ "log_index": log_index,
321
+ "log_url": f"{REKOR_BASE}/api/v1/log/entries?logIndex={log_index}",
322
+ "inclusion_proof": {
323
+ "checkpoint": inclusion.get("checkpoint"),
324
+ "hashes": inclusion.get("hashes", []),
325
+ "root_hash": inclusion.get("rootHash"),
326
+ "tree_size": inclusion.get("treeSize"),
327
+ "log_index": inclusion.get("logIndex"),
328
+ },
329
+ "receipt_id": receipt_id,
330
+ "submitted_at": datetime.now(timezone.utc).isoformat(),
331
+ }
332
+ except Exception as exc:
333
+ return {
334
+ "transparency_log": "none",
335
+ "rekor_error": f"Rekor response parse error: {exc!r}",
336
+ "receipt_id": receipt_id,
337
+ }
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # 3b. TRANSPARENCY LOG — self-hosted Merkle log (RFC 6962 leaf/node hash)
342
+ # ---------------------------------------------------------------------------
343
+
344
+ class SZLMerkleLog:
345
+ """
346
+ In-memory append-only Merkle transparency log (RFC 6962 leaf/node hashing).
347
+
348
+ Each leaf = SHA3-256(0x00 || canonical_json(statement)).
349
+ Interior node = SHA3-256(0x01 || left || right).
350
+ Inclusion proof = ordered list of sibling hashes from leaf to root.
351
+
352
+ This is NOT Sigstore Rekor. It is a self-hosted, deterministic, independently
353
+ verifiable log. The inclusion proof is standard RFC 6962 audit path math;
354
+ any third party can verify it with the published root hash.
355
+
356
+ Honest label: "szl-lake-merkle (self-hosted)" — NOT public Rekor.
357
+ Rekor-anchor-on-publish: when the Lake is published to HF Dataset / GitHub
358
+ release, a CI job should submit each receipt's in-toto Statement to Rekor and
359
+ store the logIndex. Until then, this self-hosted log is the inclusion mechanism.
360
+ """
361
+
362
+ def __init__(self) -> None:
363
+ self._lock = threading.Lock()
364
+ self._leaves: list[bytes] = [] # raw 32-byte leaf hashes
365
+ self._index: dict[str, int] = {} # receipt_id -> leaf index
366
+ self._tree: list[list[bytes]] = [] # tree[level][node_index]
367
+ self._log_id = "szl-lake-merkle-v1"
368
+
369
+ @staticmethod
370
+ def _leaf_hash(data: bytes) -> bytes:
371
+ """RFC 6962: SHA3-256(0x00 || data)."""
372
+ return hashlib.sha3_256(b"\x00" + data).digest()
373
+
374
+ @staticmethod
375
+ def _node_hash(left: bytes, right: bytes) -> bytes:
376
+ """RFC 6962: SHA3-256(0x01 || left || right)."""
377
+ return hashlib.sha3_256(b"\x01" + left + right).digest()
378
+
379
+ def _rebuild_tree(self) -> None:
380
+ """Rebuild the Merkle tree from the current leaves."""
381
+ if not self._leaves:
382
+ self._tree = []
383
+ return
384
+ level = list(self._leaves)
385
+ self._tree = [level]
386
+ while len(level) > 1:
387
+ next_level = []
388
+ for i in range(0, len(level) - 1, 2):
389
+ next_level.append(self._node_hash(level[i], level[i + 1]))
390
+ if len(level) % 2 == 1:
391
+ next_level.append(level[-1]) # promote odd node
392
+ level = next_level
393
+ self._tree.append(level)
394
+
395
+ def append(self, receipt_id: str, statement: dict) -> dict:
396
+ """
397
+ Append a statement to the log. Idempotent on receipt_id.
398
+
399
+ Returns the inclusion proof dict.
400
+ """
401
+ with self._lock:
402
+ if receipt_id in self._index:
403
+ return self.inclusion_proof(receipt_id)
404
+
405
+ leaf_data = _canonical_json(statement)
406
+ leaf = self._leaf_hash(leaf_data)
407
+ idx = len(self._leaves)
408
+ self._leaves.append(leaf)
409
+ self._index[receipt_id] = idx
410
+ self._rebuild_tree()
411
+ return self.inclusion_proof(receipt_id)
412
+
413
+ def root_hash(self) -> str | None:
414
+ """Current root hash (hex). None if log is empty."""
415
+ if not self._tree:
416
+ return None
417
+ return self._tree[-1][0].hex()
418
+
419
+ def tree_size(self) -> int:
420
+ return len(self._leaves)
421
+
422
+ def inclusion_proof(self, receipt_id: str) -> dict:
423
+ """
424
+ RFC 6962 audit path for the given receipt_id.
425
+
426
+ Returns:
427
+ {
428
+ "transparency_log": "szl-lake-merkle (self-hosted)",
429
+ "log_id": "szl-lake-merkle-v1",
430
+ "leaf_index": <int>,
431
+ "tree_size": <int>,
432
+ "root_hash": "<hex>",
433
+ "hashes": ["<hex>", ...], # sibling hashes from leaf to root
434
+ "leaf_hash": "<hex>",
435
+ "receipt_id": "<id>",
436
+ "verify_endpoint": "/api/lake/v1/proof/<receipt_id>",
437
+ "honest_label": "self-hosted; NOT Sigstore Rekor; rekor-anchor-on-publish is ROADMAP"
438
+ }
439
+ """
440
+ if not self._leaves:
441
+ return {
442
+ "transparency_log": "szl-lake-merkle (self-hosted)",
443
+ "error": "log empty",
444
+ "receipt_id": receipt_id,
445
+ }
446
+ idx = self._index.get(receipt_id)
447
+ if idx is None:
448
+ return {
449
+ "transparency_log": "szl-lake-merkle (self-hosted)",
450
+ "error": "receipt_id not in log",
451
+ "receipt_id": receipt_id,
452
+ }
453
+
454
+ # Compute audit path (sibling hashes from leaf level to root)
455
+ audit_path: list[str] = []
456
+ tree_size = len(self._leaves)
457
+ i = idx
458
+ for level in self._tree[:-1]: # skip root level
459
+ sibling = i ^ 1 # XOR with 1 flips the last bit = sibling index
460
+ if sibling < len(level):
461
+ audit_path.append(level[sibling].hex())
462
+ # else: odd node — no sibling at this level
463
+ i //= 2
464
+
465
+ root = self.root_hash()
466
+ return {
467
+ "transparency_log": "szl-lake-merkle (self-hosted)",
468
+ "log_id": self._log_id,
469
+ "leaf_index": idx,
470
+ "tree_size": tree_size,
471
+ "root_hash": root,
472
+ "hashes": audit_path,
473
+ "leaf_hash": self._leaves[idx].hex(),
474
+ "receipt_id": receipt_id,
475
+ "verify_endpoint": f"/api/lake/v1/proof/{receipt_id}",
476
+ "honest_label": (
477
+ "self-hosted SZL Merkle log; NOT Sigstore public Rekor. "
478
+ "Third-party verifiable against root_hash + audit path (RFC 6962). "
479
+ "Rekor-anchor-on-publish (CI batch submit to rekor.sigstore.dev) "
480
+ "is ROADMAP — documented in szl_intoto.py ANCHOR PATH."
481
+ ),
482
+ }
483
+
484
+ def verify_proof(self, receipt_id: str, statement: dict) -> dict:
485
+ """
486
+ Offline-verifiable inclusion proof check.
487
+
488
+ Recomputes the leaf hash from the statement, walks the audit path,
489
+ and checks against the stored root hash. Returns {verified, ...}.
490
+ """
491
+ idx = self._index.get(receipt_id)
492
+ if idx is None:
493
+ return {"verified": False, "reason": "receipt_id not in log"}
494
+
495
+ proof = self.inclusion_proof(receipt_id)
496
+ leaf_data = _canonical_json(statement)
497
+ computed_leaf = self._leaf_hash(leaf_data)
498
+
499
+ if computed_leaf != self._leaves[idx]:
500
+ return {
501
+ "verified": False,
502
+ "reason": "leaf hash mismatch (statement was modified after log append)",
503
+ "computed_leaf": computed_leaf.hex(),
504
+ "stored_leaf": self._leaves[idx].hex(),
505
+ }
506
+
507
+ # Walk the audit path
508
+ node = computed_leaf
509
+ tree_size = len(self._leaves)
510
+ i = idx
511
+ for level_idx, level in enumerate(self._tree[:-1]):
512
+ sibling_idx = i ^ 1
513
+ if sibling_idx < len(level):
514
+ sibling = level[sibling_idx]
515
+ if i % 2 == 0:
516
+ node = self._node_hash(node, sibling)
517
+ else:
518
+ node = self._node_hash(sibling, node)
519
+ i //= 2
520
+
521
+ computed_root = node.hex()
522
+ stored_root = self.root_hash()
523
+ if computed_root != stored_root:
524
+ return {
525
+ "verified": False,
526
+ "reason": "recomputed root does not match stored root",
527
+ "computed_root": computed_root,
528
+ "stored_root": stored_root,
529
+ }
530
+
531
+ return {
532
+ "verified": True,
533
+ "leaf_index": idx,
534
+ "tree_size": tree_size,
535
+ "root_hash": stored_root,
536
+ "transparency_log": "szl-lake-merkle (self-hosted)",
537
+ "receipt_id": receipt_id,
538
+ }
539
+
540
+
541
+ # Global Merkle log singleton (one per process lifetime)
542
+ _MERKLE_LOG = SZLMerkleLog()
543
+
544
+ # Seed the Merkle log from existing khipu/ NDJSON partitions (if present) on import.
545
+ # This ensures inclusion proofs survive process restart for receipts already on disk.
546
+ def _seed_from_disk(lake_dir: str = "khipu") -> int:
547
+ """Seed the global Merkle log from existing NDJSON receipts on disk."""
548
+ seeded = 0
549
+ if not os.path.isdir(lake_dir):
550
+ return 0
551
+ try:
552
+ for organ_name in sorted(os.listdir(lake_dir)):
553
+ organ_dir = os.path.join(lake_dir, organ_name)
554
+ if not os.path.isdir(organ_dir):
555
+ continue
556
+ for fname in sorted(os.listdir(organ_dir)):
557
+ if not fname.endswith(".ndjson"):
558
+ continue
559
+ fpath = os.path.join(organ_dir, fname)
560
+ with open(fpath, "r", encoding="utf-8") as fh:
561
+ for line in fh:
562
+ line = line.strip()
563
+ if not line:
564
+ continue
565
+ try:
566
+ envelope = json.loads(line)
567
+ except json.JSONDecodeError:
568
+ continue
569
+ rid = envelope.get("receipt_id") or envelope.get("id") or ""
570
+ if not rid:
571
+ continue
572
+ # Build a minimal statement for seeding (no re-signing needed)
573
+ receipt = envelope.get("receipt", envelope)
574
+ statement = wrap_as_intoto_statement(receipt)
575
+ _MERKLE_LOG.append(rid, statement)
576
+ seeded += 1
577
+ except Exception:
578
+ pass
579
+ return seeded
580
+
581
+
582
+ # Seed on import (non-blocking; tolerate any error gracefully)
583
+ try:
584
+ _lake_dir = os.environ.get("SZL_LAKE_DIR", "khipu")
585
+ _MERKLE_LOG_SEED_COUNT = _seed_from_disk(_lake_dir)
586
+ except Exception:
587
+ _MERKLE_LOG_SEED_COUNT = 0
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # 4. HIGH-LEVEL: produce in-toto statement + log proof for a receipt
592
+ # ---------------------------------------------------------------------------
593
+
594
+ def attest_receipt(
595
+ receipt: dict,
596
+ sign_fn: Callable[[Any, str], dict] | None = None,
597
+ try_rekor: bool = True,
598
+ ) -> dict:
599
+ """
600
+ Full in-toto attestation for a Khipu receipt.
601
+
602
+ 1. Wraps the receipt as an in-toto Statement v1.
603
+ 2. Builds a DSSE envelope with payloadType="application/vnd.in-toto+json".
604
+ 3. Attempts public Rekor submission (if try_rekor=True).
605
+ On success: transparency_log="rekor-public" + inclusion proof.
606
+ On failure/unreachable: falls back to self-hosted Merkle log.
607
+ 4. Appends to the self-hosted Merkle log always (belt+suspenders).
608
+
609
+ Returns:
610
+ {
611
+ "intoto_statement": <Statement v1 dict>,
612
+ "intoto_envelope": <DSSE envelope dict>,
613
+ "transparency": <inclusion proof dict>,
614
+ "receipt_id": "<id>",
615
+ "_version": "szl-intoto/v1"
616
+ }
617
+ """
618
+ receipt_id = (receipt.get("receipt_id") or receipt.get("id")
619
+ or receipt.get("hash") or _sha3_256(_canonical_json(receipt)))
620
+
621
+ statement = wrap_as_intoto_statement(receipt)
622
+ envelope = build_intoto_envelope(receipt, sign_fn=sign_fn)
623
+
624
+ # Always append to self-hosted Merkle log
625
+ merkle_proof = _MERKLE_LOG.append(receipt_id, statement)
626
+
627
+ # Attempt public Rekor submission
628
+ transparency = merkle_proof # default to self-hosted
629
+ if try_rekor and envelope.get("signed"):
630
+ rekor_result = submit_to_rekor(envelope, receipt_id)
631
+ if rekor_result.get("transparency_log") == "rekor-public":
632
+ transparency = rekor_result
633
+ # else: Rekor unavailable, self-hosted merkle proof is used
634
+
635
+ return {
636
+ "intoto_statement": statement,
637
+ "intoto_envelope": envelope,
638
+ "transparency": transparency,
639
+ "receipt_id": receipt_id,
640
+ "_version": _TRANSPARENCY_LOG_VERSION,
641
+ }
642
+
643
+
644
+ def get_inclusion_proof(receipt_id: str) -> dict:
645
+ """Retrieve the inclusion proof for a receipt from the self-hosted Merkle log."""
646
+ return _MERKLE_LOG.inclusion_proof(receipt_id)
647
+
648
+
649
+ def verify_inclusion_proof(receipt_id: str, statement: dict) -> dict:
650
+ """Verify an inclusion proof for a receipt against the self-hosted Merkle log."""
651
+ return _MERKLE_LOG.verify_proof(receipt_id, statement)
652
+
653
+
654
+ def merkle_log_state() -> dict:
655
+ """Current state of the self-hosted Merkle log (for monitoring/audit)."""
656
+ return {
657
+ "log_id": "szl-lake-merkle-v1",
658
+ "tree_size": _MERKLE_LOG.tree_size(),
659
+ "root_hash": _MERKLE_LOG.root_hash(),
660
+ "honest_label": "self-hosted SZL Merkle log; NOT Sigstore Rekor",
661
+ "seed_count": _MERKLE_LOG_SEED_COUNT,
662
+ }
szl_intoto_routes.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v13
3
+ # Signed-off-by: Stephen Lutar <stephenlutar2@gmail.com>
4
+ """
5
+ szl_intoto_routes.py — FastAPI/Starlette endpoints for in-toto receipt views
6
+ and transparency log inclusion proofs.
7
+
8
+ ADDITIVE — mounts new routes, does NOT modify existing /khipu/* endpoints.
9
+
10
+ ENDPOINTS:
11
+ GET /khipu/intoto/<receipt_id>
12
+ Returns the in-toto Statement v1 + DSSE envelope for a stored receipt.
13
+ Looks up the receipt from the in-memory KhipuDAG organs first, then the
14
+ szl_lake_store ReceiptLedger (disk). Honors back-compat: the original
15
+ receipt JSON is also returned under "receipt" key.
16
+
17
+ GET /api/lake/v1/proof/<receipt_id>
18
+ Returns the Merkle inclusion proof for a receipt from the self-hosted
19
+ transparency log (szl_intoto.SZLMerkleLog). Third-party verifiable.
20
+
21
+ GET /api/lake/v1/log
22
+ Current state of the self-hosted Merkle log (tree size, root hash).
23
+
24
+ GET /api/a11oy/v1/verify/intoto
25
+ Documentation endpoint: explains what is now third-party-verifiable vs
26
+ what is roadmap. Returns verification instructions and example curl commands.
27
+
28
+ HONEST LABELS (never weaken):
29
+ - transparency_log: "rekor-public" only if actually submitted to Rekor.
30
+ - transparency_log: "szl-lake-merkle (self-hosted)" for self-hosted log.
31
+ - Every response declares its honesty constraints.
32
+
33
+ Stdlib + szl_intoto + szl_dsse + szl_khipu + szl_lake_store. No new pip deps.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import json
38
+ import os
39
+ from typing import Any
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # REGISTER
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def register(app, ns: str = "a11oy") -> dict: # pragma: no cover
46
+ """Attach in-toto receipt + transparency log endpoints to the a11oy FastAPI app.
47
+
48
+ Inserts routes at HEAD of router (before SPA catch-all), following the
49
+ established register() contract in the a11oy codebase.
50
+ """
51
+ try:
52
+ from starlette.routing import Route
53
+ from starlette.responses import JSONResponse
54
+ except Exception:
55
+ return {"registered": [], "status": "starlette-absent"}
56
+
57
+ # ------------------------------------------------------------------
58
+ # Lazy imports (guarded — missing modules degrade gracefully)
59
+ # ------------------------------------------------------------------
60
+ try:
61
+ import szl_intoto as _intoto
62
+ except ImportError:
63
+ _intoto = None
64
+
65
+ try:
66
+ import szl_khipu as _khipu
67
+ except ImportError:
68
+ _khipu = None
69
+
70
+ try:
71
+ import szl_lake_store as _lake
72
+ _ledger = _lake.ReceiptLedger()
73
+ except ImportError:
74
+ _lake = None
75
+ _ledger = None
76
+
77
+ # ------------------------------------------------------------------
78
+ # Helper: look up a receipt by ID across all organs + lake
79
+ # ------------------------------------------------------------------
80
+ def _find_receipt(receipt_id: str) -> dict | None:
81
+ """Search KhipuDAG organs then disk ledger for receipt_id."""
82
+ # 1) Check in-memory KhipuDAG
83
+ if _khipu is not None:
84
+ try:
85
+ for (organ, ns_key), dag in _khipu._REGISTRY.items():
86
+ for r in dag._chain:
87
+ rid = (r.get("receipt_id") or r.get("id")
88
+ or r.get("hash") or r.get("digest") or "")
89
+ if rid == receipt_id:
90
+ return r
91
+ except Exception:
92
+ pass
93
+
94
+ # 2) Check disk ledger
95
+ if _ledger is not None:
96
+ try:
97
+ envelopes = _ledger.query(limit=10000)
98
+ for env in envelopes:
99
+ rid = env.get("receipt_id", "")
100
+ if rid == receipt_id:
101
+ return env.get("receipt", env)
102
+ except Exception:
103
+ pass
104
+
105
+ return None
106
+
107
+ # ------------------------------------------------------------------
108
+ # GET /khipu/intoto/<receipt_id>
109
+ # ------------------------------------------------------------------
110
+ async def _khipu_intoto(request):
111
+ if _intoto is None:
112
+ return JSONResponse(
113
+ {"error": "szl_intoto module not available", "receipt_id": None},
114
+ status_code=503,
115
+ )
116
+ receipt_id = request.path_params.get("receipt_id", "")
117
+ if not receipt_id:
118
+ return JSONResponse({"error": "receipt_id required"}, status_code=400)
119
+
120
+ receipt = _find_receipt(receipt_id)
121
+ if receipt is None:
122
+ return JSONResponse(
123
+ {"error": "receipt not found", "receipt_id": receipt_id,
124
+ "honesty": "Receipt not found in any in-memory organ or disk ledger. "
125
+ "It may belong to a prior process run."},
126
+ status_code=404,
127
+ )
128
+
129
+ # Build in-toto attestation (use Merkle log only; Rekor submit is async roadmap)
130
+ try:
131
+ attestation = _intoto.attest_receipt(receipt, try_rekor=False)
132
+ except Exception as exc:
133
+ return JSONResponse(
134
+ {"error": f"attestation failed: {exc!r}", "receipt_id": receipt_id},
135
+ status_code=500,
136
+ )
137
+
138
+ return JSONResponse({
139
+ "receipt_id": receipt_id,
140
+ "intoto_statement": attestation["intoto_statement"],
141
+ "intoto_envelope": attestation["intoto_envelope"],
142
+ "transparency": attestation["transparency"],
143
+ "receipt": receipt, # back-compat: original receipt preserved
144
+ "_version": attestation["_version"],
145
+ "honesty": (
146
+ "in-toto Statement v1 with payloadType=application/vnd.in-toto+json. "
147
+ "Subject digest binds to model output (C2PA hard-binding). "
148
+ "predicateType=https://szl.holdings/khipu-governed-inference/v1. "
149
+ "transparency_log field declares the log used honestly."
150
+ ),
151
+ })
152
+
153
+ # ------------------------------------------------------------------
154
+ # GET /api/lake/v1/proof/<receipt_id>
155
+ # ------------------------------------------------------------------
156
+ async def _lake_proof(request):
157
+ if _intoto is None:
158
+ return JSONResponse(
159
+ {"error": "szl_intoto module not available"}, status_code=503
160
+ )
161
+ receipt_id = request.path_params.get("receipt_id", "")
162
+ if not receipt_id:
163
+ return JSONResponse({"error": "receipt_id required"}, status_code=400)
164
+
165
+ proof = _intoto.get_inclusion_proof(receipt_id)
166
+ return JSONResponse(proof)
167
+
168
+ # ------------------------------------------------------------------
169
+ # GET /api/lake/v1/log
170
+ # ------------------------------------------------------------------
171
+ async def _lake_log(request):
172
+ if _intoto is None:
173
+ return JSONResponse(
174
+ {"error": "szl_intoto module not available"}, status_code=503
175
+ )
176
+ return JSONResponse(_intoto.merkle_log_state())
177
+
178
+ # ------------------------------------------------------------------
179
+ # GET /api/a11oy/v1/verify/intoto
180
+ # ------------------------------------------------------------------
181
+ async def _verify_intoto_docs(request):
182
+ pub_key_url = "https://github.com/szl-holdings/.github/blob/main/cosign.pub"
183
+ return JSONResponse({
184
+ "title": "SZL a11oy in-toto Verification Guide",
185
+ "what_is_now_verifiable": {
186
+ "1_dsse_signature": {
187
+ "status": "LIVE",
188
+ "description": (
189
+ "Every receipt is DSSE-signed with the SZL ECDSA P-256 keypair. "
190
+ "payloadType is now 'application/vnd.in-toto+json'. "
191
+ "Verifiable with standard cosign verify-blob."
192
+ ),
193
+ "command": (
194
+ "cosign verify-blob "
195
+ "--key https://a-11-oy.com/cosign.pub "
196
+ "--bundle <receipt.bundle.json> "
197
+ "<statement.json>"
198
+ ),
199
+ },
200
+ "2_intoto_statement": {
201
+ "status": "LIVE",
202
+ "description": (
203
+ "Receipt payload is now a valid in-toto Statement v1: "
204
+ "{_type: https://in-toto.io/Statement/v1, "
205
+ "subject:[{name, digest:{sha3-256:<output_hash>}}], "
206
+ "predicateType: https://szl.holdings/khipu-governed-inference/v1, "
207
+ "predicate:{...all governance/Λ/energy/gate fields...}}. "
208
+ "Standard tools (cosign verify-attestation, Ratify) can parse the envelope."
209
+ ),
210
+ "fetch_endpoint": "/khipu/intoto/<receipt_id>",
211
+ },
212
+ "3_hard_binding": {
213
+ "status": "LIVE",
214
+ "description": (
215
+ "Subject digest is SHA3-256 of the model output content. "
216
+ "C2PA hard-binding pattern: receipt cannot be recycled for a different output. "
217
+ "Verifiable offline: compute SHA3-256(answer_text) and compare to statement.subject[0].digest."
218
+ ),
219
+ },
220
+ "4_merkle_inclusion_proof": {
221
+ "status": "LIVE (self-hosted)",
222
+ "description": (
223
+ "Every receipt is appended to the SZL self-hosted Merkle transparency log "
224
+ "(RFC 6962 SHA3-256 leaf/node hashing). "
225
+ "Inclusion proofs retrievable at /api/lake/v1/proof/<receipt_id>. "
226
+ "Third-party verifiable: compute leaf_hash(statement), walk audit_path, "
227
+ "check against root_hash. NO trust in SZL required for the math."
228
+ ),
229
+ "proof_endpoint": "/api/lake/v1/proof/<receipt_id>",
230
+ "log_endpoint": "/api/lake/v1/log",
231
+ "honest_label": "szl-lake-merkle (self-hosted) — NOT Sigstore public Rekor",
232
+ },
233
+ "5_sha3_chain": {
234
+ "status": "LIVE",
235
+ "description": (
236
+ "SHA3-256 prev_hash chain from genesis. "
237
+ "Tamper-evident linked list verifiable by replaying the NDJSON stream. "
238
+ "Chain verification endpoint: /khipu/verify/<digest>"
239
+ ),
240
+ },
241
+ },
242
+ "what_is_roadmap": {
243
+ "per_receipt_public_rekor": {
244
+ "status": "ROADMAP",
245
+ "description": (
246
+ "Public Rekor submission per inference receipt "
247
+ "(transparency_log: rekor-public). "
248
+ "Blocked by: HF Space egress restrictions may prevent live Rekor POSTs "
249
+ "during inference. Planned path: CI batch job submits each receipt "
250
+ "to rekor.sigstore.dev on Lake publish, stores logIndex back in NDJSON. "
251
+ "The self-hosted Merkle log bridges this gap with the SAME math."
252
+ ),
253
+ "unblock_path": (
254
+ "Add SZL_REKOR_SUBMIT=1 env var to HF Space; "
255
+ "szl_intoto.submit_to_rekor() is wired and will attempt live submission. "
256
+ "If HF egress allows it, transparency_log automatically upgrades to rekor-public."
257
+ ),
258
+ },
259
+ "slsa_l2_container": {
260
+ "status": "ROADMAP",
261
+ "description": (
262
+ "SLSA Build L2 provenance for the container via GitHub Actions OIDC "
263
+ "(actions/attest-build-provenance@v2). ~3 lines of YAML. "
264
+ "Currently SLSA L1 (Rekor entry 1710339915 for container build)."
265
+ ),
266
+ },
267
+ "tee_attestation": {
268
+ "status": "ROADMAP (Phase II)",
269
+ "description": (
270
+ "TEE remote attestation (AWS Nitro PCR measurements / Intel TDX) — "
271
+ "proves WHICH model ran in WHICH environment without trusting SZL. "
272
+ "Required for court-martial-grade DoD audit trail."
273
+ ),
274
+ },
275
+ },
276
+ "verify_offline_recipe": (
277
+ "szl-cookbook/verify-intoto-receipt.py — "
278
+ "Apache-2.0 offline verifier: verifies DSSE sig with cosign.pub, "
279
+ "checks in-toto Statement structure, checks Merkle inclusion proof. "
280
+ "NO network call required after downloading the receipt + proof."
281
+ ),
282
+ "public_key_url": pub_key_url,
283
+ "cosign_pub_endpoint": "/cosign.pub",
284
+ })
285
+
286
+ # ------------------------------------------------------------------
287
+ # Route registration
288
+ # ------------------------------------------------------------------
289
+ paths = [
290
+ ("/khipu/intoto/{receipt_id}", _khipu_intoto, ["GET"]),
291
+ ("/api/a11oy/v1/khipu/intoto/{receipt_id}", _khipu_intoto, ["GET"]),
292
+ ("/api/lake/v1/proof/{receipt_id}", _lake_proof, ["GET"]),
293
+ ("/api/lake/v1/log", _lake_log, ["GET"]),
294
+ ("/api/a11oy/v1/verify/intoto", _verify_intoto_docs, ["GET"]),
295
+ ("/v1/verify/intoto", _verify_intoto_docs, ["GET"]),
296
+ ]
297
+
298
+ registered = []
299
+ for path, fn, methods in paths:
300
+ try:
301
+ from starlette.routing import Route as _Route
302
+ app.router.routes.insert(0, _Route(path, fn, methods=methods))
303
+ registered.append(path)
304
+ except Exception:
305
+ pass
306
+
307
+ return {"registered": registered, "status": "ok"}