#!/usr/bin/env python3
"""17 forged Quorum-QaaS receipts, built from a real one, run against the standalone verifier. No CAIN code.

    curl -sO https://cainstudio.online/verify_qaas_receipt.py
    curl -sO https://cainstudio.online/verify_pbft_qc_bundle.py
    curl -sO https://cainstudio.online/verify-yourself-receipt.json
    curl -sO https://cainstudio.online/qaas-forgeries.py
    PIN=$(curl -s https://cainstudio.online/api/v1/quorum/status | python3 -c 'import json,sys;print(json.load(sys.stdin)["configuration_hash"])')
    pip install cryptography && python3 -I qaas-forgeries.py verify-yourself-receipt.json "$PIN"

Pass = the real receipt is NOTARIZED and every forgery is NOT_VERIFIED. Exit 0 only then.
Forgery 17 is a whole cluster re-signed with the forger's own four keys: it is internally consistent, so it is
caught ONLY by the pin. That is why the pin is mandatory.
"""
import os, sys; sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import hashlib
import base64, copy, json, sys
import verify_qaas_receipt as V
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization as S
Q = V.Q
real = json.load(open(sys.argv[1])); PIN = sys.argv[2]
def C(): return copy.deepcopy(real)
cases = {}
r = C(); s = r["commit_qc"]["bundle"]["individual_signatures"]; s["cain-mr2-node-3"] = copy.deepcopy(s["cain-mr2-node-1"]); cases["duplicate signer: node-1's vote filed under node-3"] = r
r = C(); s = r["commit_qc"]["bundle"]["individual_signatures"]; v = s.pop("cain-mr2-node-3"); v2 = copy.deepcopy(s["cain-mr2-node-1"]); v2["sender_id"] = "cain-mr2-node-4"; s["cain-mr2-node-4"] = v2; cases["vote relabelled as another member (sender_id swapped)"] = r
k = Ed25519PrivateKey.generate(); pk = base64.b64encode(k.public_key().public_bytes(S.Encoding.Raw, S.PublicFormat.Raw)).decode()
r = C(); s = r["commit_qc"]["bundle"]["individual_signatures"]; s.pop("cain-mr2-node-3"); v = copy.deepcopy(s["cain-mr2-node-1"]); v["sender_id"] = "cain-mr2-node-4"; v["public_key_b64"] = pk; v["signature_b64"] = base64.b64encode(k.sign(Q.msg_digest(v).encode())).decode(); s["cain-mr2-node-4"] = v; cases["3rd vote signed by an outsider key embedded in the vote"] = r
r = C(); s = r["commit_qc"]["bundle"]["individual_signatures"]; s["cain-mr2-node-1"] = copy.deepcopy(r["prepare_qc"]["bundle"]["individual_signatures"]["cain-mr2-node-1"]); cases["PREPARE vote passed off as a COMMIT vote"] = r
r = C(); r["commit_qc"]["bundle"]["individual_signatures"]["cain-mr2-node-9"] = copy.deepcopy(r["commit_qc"]["bundle"]["individual_signatures"]["cain-mr2-node-1"]); cases["extra vote from a non-member id"] = r
r = C(); r["commit_qc"]["view"] = 181; cases["certificate view changed"] = r
r = C(); r["commit_qc"]["quorum"] = 2; r["commit_qc"]["bundle"]["individual_signatures"].pop("cain-mr2-node-3"); cases["quorum lowered to 2 with 2 votes"] = r
r = C(); r["operation"]["data"]["digest"] = "0" * 64; cases["operation digest differs from receipt digest"] = r
r = C(); r["prepare_qc"]["leader_proposal"]["sender_id"] = "cain-mr2-node-2"; cases["leader proposal attributed to a non-primary"] = r
r = C(); del r["prepare_qc"]; cases["PREPARE_QC removed"] = r
r = C(); r["commit_qc"] = None; cases["COMMIT_QC null"] = r
r = C(); r["sequence"] = str(r["sequence"]); cases["sequence as a string"] = r
r = C(); r["commit_qc"]["bundle"]["individual_signatures"]["cain-mr2-node-1"]["signature_b64"] += "="; cases["signature with extra padding (malleated encoding)"] = r
r = C(); r["request_id"] = "other-request"; cases["request_id changed"] = r
r = C(); r["client_id"] = "qaas-someone-else"; cases["client_id changed"] = r
r = C(); r["type"] = "CAIN42/QUORUM-QAAS/RECEIPT/v2"; cases["receipt type changed"] = r
# 17: the forger's own cluster (self-consistent; only the pin catches it)
r = C(); keys = {m["node_id"]: Ed25519PrivateKey.generate() for m in r["membership"]["members"]}
pub = lambda kk: base64.b64encode(kk.public_key().public_bytes(S.Encoding.Raw, S.PublicFormat.Raw)).decode()
for m in r["membership"]["members"]: m["public_key_b64"] = pub(keys[m["node_id"]])
mb = Q.Membership(r["membership"]); r["configuration_hash"] = mb.configuration_hash
fake = hashlib.sha256(b"a document the cluster never notarized").hexdigest(); r["digest"] = fake; r["operation"]["data"]["digest"] = fake
pd = Q.proposal_request_digest({"operation": r["operation"], "request_id": r["request_id"], "client_id": r["client_id"]})
def _sign(m):
    m["request_digest"] = pd; m["public_key_b64"] = pub(keys[m["sender_id"]])
    m["signature_b64"] = base64.b64encode(keys[m["sender_id"]].sign(Q.msg_digest(m).encode())).decode()
for qc in (r["prepare_qc"], r["commit_qc"]):
    qc["configuration_hash"] = mb.configuration_hash; qc["proposal_digest"] = pd
    if qc.get("leader_proposal"):
        qc["leader_proposal"]["payload"]["operation"] = r["operation"]; _sign(qc["leader_proposal"])
    for v in qc["bundle"]["individual_signatures"].values(): _sign(v)
    qc["decision_hash"] = Q.qc_decision_hash(qc); qc["certificate_hash"] = Q.qc_hash(qc)
cases["whole cluster re-signed with the forger's own 4 keys, for a digest never notarized"] = r
out = []
res = V.verify_receipt(real, pin=PIN); out.append(("control: real receipt, pinned", res["verdict"]))
for name, r in cases.items():
    try: v = V.verify_receipt(r, pin=PIN)["verdict"]
    except Exception as e: v = "CRASH " + type(e).__name__
    out.append((name, v))
for n, v in out: print(("OK  " if (v == "NOTARIZED") == n.startswith("control") else "BAD ") + f"{v:13s} {n}")
bad = [n for n, v in out if (v == "NOTARIZED") != n.startswith("control")]
print(f"\n{len(cases)} new forgeries, {len(cases) - len([b for b in bad if not b.startswith('control')])} rejected; control {'OK' if not any(b.startswith('control') for b in bad) else 'FAILED'}")
sys.exit(1 if bad else 0)
