#!/usr/bin/env python3
"""Generate the experimental *bound-gate* fixture.

EXPERIMENTAL. This is not a frozen conformance fixture and is not part of the
Warranted Crossings website. It reuses three published wire profiles verbatim
(request-binding.v1.candidate, context-snapshot.v1 frozen wire binding, and
gate-verdict.v1.candidate) but supplies NEW values that actually bind:

  * retrievable *policy* bytes whose content hash equals the committed policy_hash;
  * retrievable *authority-state* bytes whose content hash equals the committed
    authority_state_hash;
  * a declared, experimental invariant set with definitions, inputs, measurements
    and results;
  * a RequestBinding and ContextSnapshot;
  * a GateVerdict whose signed inputs_hash equals the independently recomputable
    SHA-256("gate-inputs:v1\\0" || request_hash || context_hash) commitment.

The signing key is a TEST key derived from a published label (no secret custody).
Its TEST trust root is declared in trust-root.json, OUTSIDE the signed verdict.
This is test authority: it makes no claim of production identity or key custody.

Determinism: Ed25519 (RFC 8032) and a fixed seed make every artifact byte-stable,
so this generator is reproducible and its outputs are checked in as immutable
vectors. `verify_bound_gate.py` re-implements the checks independently; it does
not import this module.
"""
from __future__ import annotations

import hashlib
import json
from pathlib import Path

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

HERE = Path(__file__).resolve().parent

# ---- published wire-profile domains (verbatim from the source profiles) -----
REQUEST_DOMAIN = b"crossing-graph:request:v1\0"
CONTEXT_DOMAIN = b"crossing-graph:context:v1\0"
INPUTS_DOMAIN = b"gate-inputs:v1\0"
VERDICT_DOMAIN = b"crossing-graph:gate-verdict:v1\0"

# ---- test signing key (derived from a published label; NOT a secret) --------
TEST_SEED = hashlib.sha256(b"bound-gate-fixture:test-signing-seed:v1").digest()


def head(major: int, value: int) -> bytes:
    prefix = major << 5
    if value < 24:
        return bytes([prefix | value])
    if value < 256:
        return bytes([prefix | 24, value])
    if value < 65536:
        return bytes([prefix | 25]) + value.to_bytes(2, "big")
    if value < 2**32:
        return bytes([prefix | 26]) + value.to_bytes(4, "big")
    return bytes([prefix | 27]) + value.to_bytes(8, "big")


def cbor_text(s: str) -> bytes:
    raw = s.encode("utf-8")
    return head(3, len(raw)) + raw


def cbor_bytes(b: bytes) -> bytes:
    return head(2, len(b)) + b


def encode_request(instance_id: str, class_id: str, evidence_tier: int, payload: bytes) -> bytes:
    """request-binding.v1.candidate: deterministic CBOR map, fields 0..5."""
    vals = [head(0, 1), head(0, 1), cbor_text(instance_id), cbor_text(class_id),
            head(0, evidence_tier), cbor_bytes(payload)]
    return head(5, 6) + b"".join(head(0, i) + v for i, v in enumerate(vals))


def encode_context(request_hash: bytes, policy_hash: bytes, evaluated_at_ms: int,
                   freshness_valid_until_ms, graph_head_sequence: int,
                   graph_head_hash: bytes, authority_state_hash: bytes,
                   gate_profile_version: str) -> bytes:
    """context-snapshot.v1 (frozen wire binding): deterministic CBOR map, fields 0..9."""
    fresh = b"\xf6" if freshness_valid_until_ms is None else head(0, freshness_valid_until_ms)
    vals = [head(0, 1), head(0, 5), cbor_bytes(request_hash), cbor_bytes(policy_hash),
            head(0, evaluated_at_ms), fresh, head(0, graph_head_sequence),
            cbor_bytes(graph_head_hash), cbor_bytes(authority_state_hash),
            cbor_text(gate_profile_version)]
    return head(5, 10) + b"".join(head(0, i) + v for i, v in enumerate(vals))


def encode_verdict(gate_id: str, outcome: int, denial_code, evaluated_invariants: bytes,
                   policy_hash: bytes, inputs_hash: bytes, timestamp_ms: int,
                   key_identifier: str) -> bytes:
    """gate-verdict.v1.candidate: deterministic CBOR map, fields 0..9."""
    denial = b"\xf6" if denial_code is None else head(0, denial_code)
    vals = [head(0, 1), head(0, 4), cbor_text(gate_id), head(0, outcome), denial,
            cbor_bytes(evaluated_invariants), cbor_bytes(policy_hash),
            cbor_bytes(inputs_hash), head(0, timestamp_ms), cbor_text(key_identifier)]
    return head(5, 10) + b"".join(head(0, i) + v for i, v in enumerate(vals))


def canonical_material(obj: dict) -> bytes:
    """Byte-stable serialization of retrievable material. The content hash is the
    SHA-256 of exactly these bytes."""
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")


def sha256(*parts: bytes) -> bytes:
    h = hashlib.sha256()
    for p in parts:
        h.update(p)
    return h.digest()


# ---- experimental invariant vocabulary (declared here, measured by the verifier
#      independently). These are THIS fixture's invariants over the supplied test
#      inputs; they are NOT the source spec's I1..I8. -------------------------
INVARIANT_DEFS = [
    {"bit": 0, "id": "BG-I1", "name": "ClassRegistered",
     "predicate": "request.class_id in policy.registered_classes",
     "inputs": ["request.class_id", "policy.registered_classes"]},
    {"bit": 1, "id": "BG-I2", "name": "EvidenceSufficient",
     "predicate": "request.evidence_tier >= policy.min_evidence_tier",
     "inputs": ["request.evidence_tier", "policy.min_evidence_tier"]},
    {"bit": 2, "id": "BG-I3", "name": "AuthorityEndorsesKey",
     "predicate": "authority_state.status=='active' and authority_state.authority_id==policy.authority_id and verdict_signing_key in authority_state.authorized_gate_keys",
     "inputs": ["authority_state.status", "authority_state.authority_id",
                "authority_state.authorized_gate_keys", "verdict.signing_public_key",
                "policy.authority_id"]},
]


def measure_invariants(class_id, evidence_tier, policy, authority_state, signing_key_hex):
    i1 = class_id in policy["registered_classes"]
    i2 = evidence_tier >= policy["min_evidence_tier"]
    i3 = (authority_state["status"] == "active"
          and authority_state["authority_id"] == policy["authority_id"]
          and signing_key_hex in authority_state["authorized_gate_keys"])
    results = [i1, i2, i3]
    mask = 0
    for d, r in zip(INVARIANT_DEFS, results):
        if r:
            mask |= (1 << d["bit"])
    return results, mask


def main() -> None:
    priv = Ed25519PrivateKey.from_private_bytes(TEST_SEED)
    pub_hex = priv.public_key().public_bytes(
        serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex()

    # --- retrievable material -------------------------------------------------
    policy = {
        "policy_id": "bound-gate-test-policy",
        "version": "1",
        "authority_id": "bound-gate-test-authority",
        "registered_classes": ["CRX-4A-17"],
        "min_evidence_tier": 3,
        "required_invariants": ["BG-I1", "BG-I2", "BG-I3"],
        "note": "TEST policy for the experimental bound-gate fixture. Not a production policy.",
    }
    authority_state = {
        "authority_id": "bound-gate-test-authority",
        "status": "active",
        "authorized_gate_keys": [pub_hex],
        "note": "TEST authority-state snapshot. Endorses the test gate key only.",
    }
    policy_bytes = canonical_material(policy)
    authority_bytes = canonical_material(authority_state)
    policy_hash = sha256(policy_bytes)
    authority_state_hash = sha256(authority_bytes)

    material_dir = HERE / "material"
    material_dir.mkdir(exist_ok=True)
    (material_dir / "policy.json").write_bytes(policy_bytes)
    (material_dir / "authority-state.json").write_bytes(authority_bytes)

    # --- test trust root, declared OUTSIDE the signed verdict -----------------
    trust_root = {
        "trust_root_version": "bound-gate-trust-root:v0",
        "test_authority": True,
        "production_identity_claimed": False,
        "key_custody": "none (seed published in generate.py)",
        "roots": [{
            "key_hex": pub_hex,
            "key_identifier": "bound-gate-test-key-v1",
            "label": "bound-gate experimental test trust root",
            "declared_by": "experiments/bound-gate-fixture/trust-root.json",
            "illustrative": False,
        }],
        "note": "TEST authority only. Lists which test key the verifier may treat "
                "as an endorsed test signer. It confers no production authority.",
    }
    (HERE / "trust-root.json").write_bytes(canonical_material(trust_root) + b"\n")

    # --- shared context parameters (genesis head: no graph history claimed) ---
    evaluated_at_ms = 1706000000000
    freshness_valid_until_ms = 1706086400000
    graph_head_sequence = 0
    graph_head_hash = bytes(32)
    gate_profile_version = "gate-verdict:v1"

    def build(instance_id, class_id, evidence_tier, payload, verdict_outcome,
              verdict_denial, verdict_mask):
        req = encode_request(instance_id, class_id, evidence_tier, payload)
        request_hash = sha256(REQUEST_DOMAIN, req)
        ctx = encode_context(request_hash, policy_hash, evaluated_at_ms,
                             freshness_valid_until_ms, graph_head_sequence,
                             graph_head_hash, authority_state_hash, gate_profile_version)
        context_hash = sha256(CONTEXT_DOMAIN, ctx)
        inputs_hash = sha256(INPUTS_DOMAIN, request_hash, context_hash)
        verdict = encode_verdict("adm", verdict_outcome, verdict_denial,
                                 bytes([verdict_mask]), policy_hash, inputs_hash,
                                 evaluated_at_ms, "bound-gate-test-key-v1")
        message = VERDICT_DOMAIN + verdict
        signature = priv.sign(message)
        verdict_hash = hashlib.sha256(message).digest()
        results, measured_mask = measure_invariants(class_id, evidence_tier, policy,
                                                     authority_state, pub_hex)
        return {
            "request": {
                "instance_id": instance_id, "class_id": class_id,
                "evidence_tier": evidence_tier, "payload_hex": payload.hex(),
                "canonical_cbor_hex": req.hex(), "request_hash_hex": request_hash.hex(),
            },
            "context": {
                "canonical_cbor_hex": ctx.hex(), "context_hash_hex": context_hash.hex(),
                "policy_hash_hex": policy_hash.hex(),
                "authority_state_hash_hex": authority_state_hash.hex(),
                "evaluated_at_ms": evaluated_at_ms,
                "freshness_valid_until_ms": freshness_valid_until_ms,
                "graph_head_sequence": graph_head_sequence,
                "graph_head_hash_hex": graph_head_hash.hex(),
                "gate_profile_version": gate_profile_version,
            },
            "verdict": {
                "gate_id": "adm",
                "outcome": "positive" if verdict_outcome == 0 else "denied",
                "denial_code": verdict_denial,
                "evaluated_invariants_hex": bytes([verdict_mask]).hex(),
                "policy_hash_hex": policy_hash.hex(),
                "inputs_hash_hex": inputs_hash.hex(),
                "timestamp_ms": evaluated_at_ms,
                "key_identifier": "bound-gate-test-key-v1",
                "canonical_cbor_hex": verdict.hex(),
                "gate_verdict_hash_hex": verdict_hash.hex(),
                "signing_public_key_hex": pub_hex,
                "signature_hex": signature.hex(),
            },
            "invariant_measurement": {
                "measured": {d["id"]: r for d, r in zip(INVARIANT_DEFS, results)},
                "measured_mask_hex": bytes([measured_mask]).hex(),
            },
            "policy_bytes_hex": policy_bytes.hex(),
            "authority_state_bytes_hex": authority_bytes.hex(),
        }

    # --- one valid vector: all invariants pass, verdict positive, mask 0x07 ---
    valid = build("bound-gate-crossing-001", "CRX-4A-17", 3, b"bound-gate", 0, None, 0x07)
    valid["name"] = "bound-gate-valid"
    valid["policy_file"] = "material/policy.json"
    valid["authority_state_file"] = "material/authority-state.json"

    # --- negatives: each isolates one failing check; none may warrant ---------
    negatives = []

    # 1. input binding: mutate the request payload; verdict (signed) still commits
    #    to the original inputs_hash. Signature stays valid; binding breaks.
    n = build("bound-gate-crossing-001", "CRX-4A-17", 3, b"bound-gate", 0, None, 0x07)
    tampered_req = encode_request("bound-gate-crossing-001", "CRX-4A-17", 3, b"tampered!!")
    n["request"]["canonical_cbor_hex"] = tampered_req.hex()
    n["request"]["request_hash_hex"] = sha256(REQUEST_DOMAIN, tampered_req).hex()
    n["request"]["payload_hex"] = b"tampered!!".hex()
    n["name"] = "input-binding-mismatch"
    n["description"] = ("Request payload differs from what the signed verdict "
                        "committed to; recomputed inputs_hash no longer matches.")
    n["expected_failed_checks"] = ["input_binding"]
    negatives.append(n)

    # 2. policy bytes: tamper a benign field of the retrievable policy material so
    #    its content hash no longer equals the committed policy_hash.
    n = build("bound-gate-crossing-001", "CRX-4A-17", 3, b"bound-gate", 0, None, 0x07)
    tampered_policy = dict(policy, note="TAMPERED policy bytes; content hash will not match.")
    n["policy_bytes_hex"] = canonical_material(tampered_policy).hex()
    n["name"] = "policy-bytes-tampered"
    n["description"] = ("Retrievable policy bytes were altered; SHA-256(policy) no "
                        "longer equals the committed policy_hash.")
    n["expected_failed_checks"] = ["material_availability"]
    negatives.append(n)

    # 3. authority-state bytes: tamper a benign field so its content hash no longer
    #    equals the committed authority_state_hash.
    n = build("bound-gate-crossing-001", "CRX-4A-17", 3, b"bound-gate", 0, None, 0x07)
    tampered_auth = dict(authority_state, note="TAMPERED authority-state bytes.")
    n["authority_state_bytes_hex"] = canonical_material(tampered_auth).hex()
    n["name"] = "authority-state-tampered"
    n["description"] = ("Retrievable authority-state bytes were altered; "
                        "SHA-256(authority_state) no longer equals the committed hash.")
    n["expected_failed_checks"] = ["material_availability"]
    negatives.append(n)

    # 4. invariant measurement: a FULLY bound + signed set whose measured invariants
    #    contradict the verdict's claim. evidence_tier=2 < min 3, so BG-I2 fails and
    #    the true mask is 0x05, but the verdict over-claims 0x07 and outcome positive.
    n = build("bound-gate-crossing-002", "CRX-4A-17", 2, b"bound-gate", 0, None, 0x07)
    n["name"] = "invariant-measurement-mismatch"
    n["description"] = ("Signature, input binding and material all verify, but the "
                        "invariants measured over the supplied inputs (mask 0x05, "
                        "BG-I2 fails) contradict the verdict's claimed mask 0x07 and "
                        "positive outcome.")
    n["expected_failed_checks"] = ["invariant_evaluation"]
    negatives.append(n)

    # 5. signature: flip one signature byte; verdict bytes unchanged.
    n = build("bound-gate-crossing-001", "CRX-4A-17", 3, b"bound-gate", 0, None, 0x07)
    sig = bytearray(bytes.fromhex(n["verdict"]["signature_hex"]))
    sig[0] ^= 0x01
    n["verdict"]["signature_hex"] = bytes(sig).hex()
    n["name"] = "signature-invalid"
    n["description"] = ("The Ed25519 signature was corrupted; every other check "
                        "still passes, yet no warrant may issue.")
    n["expected_failed_checks"] = ["signature"]
    negatives.append(n)

    fixture = {
        "fixture": "bound-gate.v1",
        "status": "EXPERIMENTAL_not_frozen_not_published",
        "purpose": ("Smallest crossing whose signed verdict binds to real, "
                    "retrievable TEST inputs and measured invariants. Reuses the "
                    "request-binding.v1 / context-snapshot.v1 / gate-verdict.v1 wire "
                    "profiles with new bound values."),
        "test_authority": True,
        "production_identity_claimed": False,
        "decision_replay": "DECISION_REPLAY_UNAVAILABLE",
        "decision_replay_note": (
            "This fixture evaluates declared invariants over the supplied test inputs "
            "at the recorded instant. It does NOT reconstruct historical event "
            "encoding, authority history, or source policy evaluation over a log. The "
            "graph head is genesis (sequence 0, zero hash): no graph history is "
            "claimed. A regenerated digest is not independent source-decision replay."),
        "domains": {
            "request": REQUEST_DOMAIN.decode("latin-1"),
            "context": CONTEXT_DOMAIN.decode("latin-1"),
            "inputs": INPUTS_DOMAIN.decode("latin-1"),
            "verdict": VERDICT_DOMAIN.decode("latin-1"),
        },
        "hash_rules": {
            "request_hash": "SHA-256(request_domain || request_cbor)",
            "context_hash": "SHA-256(context_domain || context_cbor)",
            "inputs_hash": "SHA-256(inputs_domain || request_hash || context_hash)",
            "verdict_message": "verdict_domain || verdict_cbor (Ed25519 signed)",
            "policy_hash": "SHA-256(policy material file bytes)  [content hash]",
            "authority_state_hash": "SHA-256(authority-state material file bytes)  [content hash]",
        },
        "trust_root_ref": "trust-root.json",
        "invariant_definitions": INVARIANT_DEFS,
        "gate_rule": ("EXECUTE (test-domain warrant) iff byte_conformance, signature, "
                      "input_binding, material_availability, definition_binding, "
                      "invariant_evaluation, measurement_binding and test_authority "
                      "all PASS; otherwise HOLD. A passing signature "
                      "alone never warrants."),
        "valid_vector": valid,
        "negative_vectors": negatives,
    }
    fixture_bytes = json.dumps(fixture, indent=2, ensure_ascii=False).encode("utf-8") + b"\n"
    (HERE / "bound-gate.v1.json").write_bytes(fixture_bytes)
    print("wrote bound-gate.v1.json")
    print("  fixture sha256   :", hashlib.sha256(fixture_bytes).hexdigest())
    print("  policy_hash      :", policy_hash.hex())
    print("  authority_state  :", authority_state_hash.hex())
    print("  test public key  :", pub_hex)
    print("  valid inputs_hash:", valid["verdict"]["inputs_hash_hex"])


if __name__ == "__main__":
    main()
