Spaces:
Restarting
Restarting
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 +94 -1
- szl-cookbook/verify-intoto-receipt.py +611 -0
- szl_intoto.py +662 -0
- szl_intoto_routes.py +307 -0
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"}
|