#!/usr/bin/env python3 """AICEP v2.2 ingest gate (proposed Phase 2 extension). The model never does crypto and never sees a hand paste. 1. Issue a fresh 128-bit nonce. 2. Run aicep_sub.py (pinned by sha256) with --nonce and --attest-out, or accept an attestation file (--attestation). 3. Check: verifier signature against the REGISTERED verifier pubkey; nonce match; ts fresh (<=300 s, not in the future beyond 30 s); script_sha256 == pinned == sha256 of the script on disk; manifest_sha256 == sha256 of manifest.json on disk NOW; every ciphertext on disk NOW matches the manifest (TOCTOU); folder holds only listed files; result_sha256 == hash of the embedded result; exit_code/status consistent (0 <-> accepted). 4. Only then print gate output {"gate":"pass", "attestation":..., "content": }. Otherwise {"gate":"fail","reason":...} and exit 4. No content on failure. Config (JSON, human-maintained): {"verifier_pub": "", "pinned_script_sha256": ""} """ import argparse, json, os, secrets, subprocess, sys, time from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from aicep_common import canonical, key_id, sha256_hex, unb64 HERE = os.path.dirname(os.path.abspath(__file__)) SUB = os.path.join(HERE, "aicep_sub.py") MAX_AGE, MAX_SKEW = 300, 30 class GateFail(Exception): pass def check(bundle, nonce, batch_dir, cfg, now=None, sub_path=SUB): now = int(now if now is not None else time.time()) if not isinstance(bundle, dict) or not {"attestation", "sig", "result"} <= set(bundle): raise GateFail("no attestation/signature (unsigned result)") att, res = bundle["attestation"], bundle["result"] pub = unb64(cfg["verifier_pub"]) if att.get("verifier_key_id") != key_id(pub): raise GateFail("attestation not from the registered verifier key") try: Ed25519PublicKey.from_public_bytes(pub).verify(unb64(bundle["sig"]), canonical(att)) except (InvalidSignature, ValueError): raise GateFail("bad verifier signature") if att.get("nonce") != nonce: raise GateFail("nonce mismatch (stale or replayed attestation)") ts = att.get("ts") if not isinstance(ts, int) or now - ts > MAX_AGE or ts - now > MAX_SKEW: raise GateFail("attestation not fresh") disk_script = sha256_hex(open(sub_path, "rb").read()) if not (att.get("script_sha256") == cfg["pinned_script_sha256"] == disk_script): raise GateFail("verifier script hash mismatch (modified aicep_sub.py)") if att.get("result_sha256") != sha256_hex(canonical(res)): raise GateFail("result does not match attestation") if (att.get("exit_code") == 0) != (att.get("status") == "accepted") or res.get("status") != att.get("status"): raise GateFail("exit code/status inconsistent") mb = open(os.path.join(batch_dir, "manifest.json"), "rb").read() if att.get("manifest_sha256") != sha256_hex(mb): raise GateFail("manifest on disk differs from attested manifest") m = json.loads(mb) # v2.3 QoP floor: signed + encrypted only if m.get("alg") != "age-x25519+ed25519" or any(not f.get("id") for f in m.get("files", [])): raise GateFail("QoP floor violated (sign+encrypt only)") if att.get("batch_id") not in (None, m.get("batch_id")) or (att.get("status") == "accepted" and (att.get("batch_id"), att.get("seq")) != (m.get("batch_id"), m.get("seq"))): raise GateFail("attestation is for a different batch") expected = {f"{f['id']}.age" for f in m.get("files", [])} | {"manifest.json", "manifest.json.sig"} if set(os.listdir(batch_dir)) != expected: raise GateFail("batch folder changed after verify (TOCTOU)") for f in m.get("files", []): if sha256_hex(open(os.path.join(batch_dir, f"{f['id']}.age"), "rb").read()) != f["sha256_ct"]: raise GateFail("file changed after verify (TOCTOU)") if att.get("status") != "accepted": raise GateFail(f"verifier {att.get('status')}: {att.get('reason')}") return {"gate": "pass", "attestation": att, "content": res} def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("batch_dir") ap.add_argument("--config", required=True) ap.add_argument("--compartment", required=True) ap.add_argument("--registry", required=True) ap.add_argument("--state", required=True) ap.add_argument("--folder", required=True, help="v2.5: sender folder the batch came from") ap.add_argument("--ack-gap-to", metavar="SKID:SEQ", help="HUMAN ONLY: exact gap ack") ap.add_argument("--operator", help="named human operator (required with --ack-gap-to)") ap.add_argument("--attest-out", default=None) a = ap.parse_args() cfg = json.load(open(a.config)) nonce = secrets.token_hex(16) out = a.attest_out or os.path.join("/dev/shm", f"aicep-att-{nonce}.json") cmd = [sys.executable, SUB, a.batch_dir, "--compartment", a.compartment, "--registry", a.registry, "--state", a.state, "--folder", a.folder, "--nonce", nonce, "--attest-out", out] + (["--ack-gap-to", a.ack_gap_to, "--operator", a.operator or ""] if a.ack_gap_to else []) subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: bundle = json.load(open(out)) print(json.dumps(check(bundle, nonce, a.batch_dir, cfg), indent=2)) except (GateFail, OSError, ValueError) as e: print(json.dumps({"gate": "fail", "reason": str(e)})) sys.exit(4) finally: if not a.attest_out and os.path.exists(out): os.remove(out) if __name__ == "__main__": main()