#!/usr/bin/env python3
"""Standalone offline verifier for an AssessQu evidence pack.

Python standard library only. No network, no vendor service, no AssessQu code.

What it checks:
  1. Integrity: recomputes the SHA-256 over the canonical content and header and compares to the sealed
     hashes, so anyone can confirm the pack was not altered after sealing.
  2. Time anchor: decodes the stored RFC 3161 timestamp token offline and confirms that
       (a) the SHA-256 message imprint embedded in the token equals the pack content_hash, and
       (b) the genTime inside the token equals the gen_time printed in the pack,
     so the anchored time binds to these exact bytes. This decode is dependency-free: a pure-Python DER
     walk, with `openssl asn1parse` as a cross-check when openssl is present.
  3. TSA issuer-chain trust: verifies the timestamp token's signature chains to the pinned FreeTSA Root CA
     carried in freetsa-cacert.pem next to this script. The verifier pins the root by SHA-256 fingerprint,
     confirms the token's signing certificate was issued by that exact root (RSA-SHA512 over the signer
     certificate), and verifies the token's own signature over its signed attributes (ECDSA, NIST P-384,
     SHA-512), then confirms those signed attributes bind to the timestamp content checked in step 2. All of
     this is pure Python: DER parsing, RSA PKCS#1 v1.5, and ECDSA, no third-party library. (LibreSSL
     `openssl ts -verify` is unreliable for these tokens, so a Python path is used.) When the issuer chain
     verifies, the anchor is reported as "issuer-verified time": the time was issued by the FreeTSA you
     pinned, not merely a token that happens to carry this imprint.
  4. Key signature (when present): verifies a detached Ed25519 signature (RFC 8032) over the canonical
     content bytes using the public key carried in the pack. The verify is pure Python: no third-party
     library is needed. A pack with no signature still PASSes, reported as "signature: not present".
  5. KAT replay (when present): recomputes the SHA-256 digest of each embedded NIST ACVP test vector and
     compares it to the recorded digest, so a tampered expected value is caught offline. This is the
     vector-integrity replay any machine can run with the standard library. The full algorithmic re-run
     (running ML-KEM / ML-DSA / SLH-DSA on the vectors) needs the vetted PQC libraries and is done at seal
     time; its result is recorded per algorithm as live_pass.

What it does NOT check: binding the timestamp signing key or the Ed25519 key to a named legal identity
(a published certificate tying the key to a registered organization) is a separate trust step. The
issuer-chain check proves the FreeTSA you pinned issued the time; it does not assert who operates FreeTSA.
See the offering page boundaries.

Usage:
    python3 verify.py pack.json
"""
import sys
import os
import re
import json
import base64
import hashlib
import shutil
import subprocess
import tempfile


def canon(obj):
    # Must match the sealing canonicalization exactly: sorted keys, no spaces, raw UTF-8.
    return json.dumps(obj, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False).encode("utf-8")


def sha256(b):
    return hashlib.sha256(b).hexdigest()


# --- Merkle tree-seal: selective disclosure (stdlib only) ----------------------
# A pack may seal its records as Merkle leaves and fold the root into content under
# content["merkle_seal"]["root"]. A holder discloses only chosen records as a
# separate disclosure.json: each disclosed leaf carries its value plus an audit
# path of sibling digests. This recomputes the root from the disclosed leaf and
# confirms it equals the sealed root, with the other records withheld. Leaf hash =
# sha256(0x00||canonical_leaf); node = sha256(0x01||left||right); odd rows pair the
# last node with itself. This must match the sealing rule in report.py exactly.
def merkle_leaf_hash(leaf):
    return hashlib.sha256(b"\x00" + canon(leaf)).hexdigest()


def merkle_node_hash(left_hex, right_hex):
    return hashlib.sha256(b"\x01" + bytes.fromhex(left_hex)
                          + bytes.fromhex(right_hex)).hexdigest()


def verify_disclosure(proof, expected_root=None):
    """Recompute the sealed Merkle root from each disclosed leaf and its audit path.

    Returns (ok, detail, per_leaf). With expected_root set (the root read from the
    pack content) the proof's own root must equal it, binding the disclosure to the
    pack. Withheld leaves never appear in the proof, yet the root still validates.
    """
    seal = proof.get("merkle_seal") or {}
    root = seal.get("root")
    per_leaf = []
    if expected_root is not None and root != expected_root:
        return (False, "disclosed merkle_seal.root does not equal the sealed pack root",
                per_leaf)
    all_ok = True
    for d in proof.get("disclosed", []):
        leaf_hash = merkle_leaf_hash(d["leaf"])
        leaf_hash_ok = leaf_hash == d.get("leaf_hash")
        h = leaf_hash
        for step in d.get("path", []):
            if step["side"] == "right":
                h = merkle_node_hash(h, step["sibling"])
            else:
                h = merkle_node_hash(step["sibling"], h)
        ok = leaf_hash_ok and h == root
        all_ok = all_ok and ok
        per_leaf.append({"index": d.get("index"), "ok": ok})
    detail = (f"{len(per_leaf)} disclosed leaf/leaves recompute the sealed root"
              if all_ok and per_leaf
              else ("no leaves disclosed" if not per_leaf
                    else "a disclosed leaf does NOT recompute the sealed root"))
    return all_ok and bool(per_leaf), detail, per_leaf


def imprint_and_time_from_der(der):
    """Pure-Python DER walk: find the SHA-256 message imprint and genTime inside the TSTInfo.

    The SHA-256 messageImprint is the AlgorithmIdentifier OID {2.16.840.1.101.3.4.2.1} followed by an
    OCTET STRING of the 32 hash bytes. genTime is a GeneralizedTime (tag 0x18) of YYYYMMDDHHMMSSZ.
    Returns (imprint_hex_or_None, gen_time_iso_or_None).
    """
    sha256_oid = bytes.fromhex("0609608648016503040201")
    pos = der.find(sha256_oid)
    imprint_hex = None
    if pos != -1:
        window = der[pos:pos + 64]
        m = window.find(b"\x04\x20")
        if m != -1 and pos + m + 2 + 32 <= len(der):
            imprint_hex = der[pos + m + 2: pos + m + 2 + 32].hex()
    gen_time = None
    gt = re.search(rb"\x18\x0f(\d{14})Z", der)
    if gt:
        d = gt.group(1).decode()
        gen_time = f"{d[0:4]}-{d[4:6]}-{d[6:8]}T{d[8:10]}:{d[10:12]}:{d[12:14]}+00:00"
    return imprint_hex, gen_time


def openssl_has_imprint(der, expected_hex):
    """Cross-check the imprint via `openssl asn1parse` (LibreSSL safe). True/False/None(unavailable)."""
    if not shutil.which("openssl"):
        return None
    try:
        with tempfile.NamedTemporaryFile(suffix=".der", delete=True) as f:
            f.write(der)
            f.flush()
            out = subprocess.run(["openssl", "asn1parse", "-inform", "DER", "-in", f.name],
                                 capture_output=True, text=True, timeout=15)
        if out.returncode != 0 and not out.stdout:
            return None
        return expected_hex.upper() in out.stdout.upper()
    except Exception:
        return None


def _norm_time(s):
    return str(s).replace("+00:00", "Z").replace("Z", "")


# --- Minimal DER parser (recursive TLV), stdlib only ---------------------------
def _der_len(b, i):
    n = b[i]
    i += 1
    if n < 0x80:
        return n, i
    k = n & 0x7f
    return int.from_bytes(b[i:i + k], "big"), i + k


def _der_parse(b, i=0):
    """Return (tag_byte, header_start, content_start, content_end, next_index)."""
    tag = b[i]
    j = i + 1
    if (tag & 0x1f) == 0x1f:  # multi-byte tag number
        while b[j] & 0x80:
            j += 1
        j += 1
    length, j = _der_len(b, j)
    return tag, i, j, j + length, j + length


def _der_children(b, cstart, cend):
    out = []
    i = cstart
    while i < cend:
        tag, hs, cs, ce, nxt = _der_parse(b, i)
        out.append((tag, hs, cs, ce))
        i = nxt
    return out


# --- Pure-Python RSA PKCS#1 v1.5 verify ----------------------------------------
_RSA_HASH_PREFIX = {
    "sha256": bytes.fromhex("3031300d060960864801650304020105000420"),
    "sha384": bytes.fromhex("3041300d060960864801650304020205000430"),
    "sha512": bytes.fromhex("3051300d060960864801650304020305000440"),
}


def _rsa_pkcs1v15_verify(n, e, sig, msg, hashname):
    k = (n.bit_length() + 7) // 8
    if len(sig) != k:
        return False
    s = int.from_bytes(sig, "big")
    if s >= n:
        return False
    em = pow(s, e, n).to_bytes(k, "big")
    t = _RSA_HASH_PREFIX[hashname] + hashlib.new(hashname, msg).digest()
    if len(t) > k - 11:
        return False
    expected = b"\x00\x01" + b"\xff" * (k - len(t) - 3) + b"\x00" + t
    return em == expected


# --- Pure-Python ECDSA verify (NIST P-256 / P-384 / P-521) ---------------------
_CURVES = {
    "1.2.840.10045.3.1.7": dict(  # secp256r1 / prime256v1
        p=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff,
        n=0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551,
        gx=0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
        gy=0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5),
    "1.3.132.0.34": dict(  # secp384r1
        p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff,
        n=0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973,
        gx=0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7,
        gy=0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f),
    "1.3.132.0.35": dict(  # secp521r1
        p=0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
        n=0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e913864409,
        gx=0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66,
        gy=0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650),
}
_CURVE_OID_BYTES = {
    bytes.fromhex("2a8648ce3d030107"): "1.2.840.10045.3.1.7",  # P-256
    bytes.fromhex("2b81040022"): "1.3.132.0.34",               # P-384
    bytes.fromhex("2b81040023"): "1.3.132.0.35",               # P-521
}


def _ec_inv(x, p):
    return pow(x, p - 2, p)


def _ec_add(P, Q, a, p):
    if P is None:
        return Q
    if Q is None:
        return P
    x1, y1 = P
    x2, y2 = Q
    if x1 == x2 and (y1 + y2) % p == 0:
        return None
    if P == Q:
        m = (3 * x1 * x1 + a) * _ec_inv(2 * y1, p) % p
    else:
        m = (y2 - y1) * _ec_inv((x2 - x1) % p, p) % p
    x3 = (m * m - x1 - x2) % p
    return (x3, (m * (x1 - x3) - y1) % p)


def _ec_mul(k, P, a, p):
    R = None
    while k > 0:
        if k & 1:
            R = _ec_add(R, P, a, p)
        P = _ec_add(P, P, a, p)
        k >>= 1
    return R


def _ecdsa_verify(curve_oid, pub_xy, sig_der, msg, hashname):
    """Return True/False, or None when the curve is not one we implement (honest defer)."""
    cv = _CURVES.get(curve_oid)
    if cv is None:
        return None
    p, n = cv["p"], cv["n"]
    a = p - 3
    G = (cv["gx"], cv["gy"])
    try:
        tag, hs, cs, ce, nx = _der_parse(sig_der, 0)
        kids = _der_children(sig_der, cs, ce)
        if len(kids) != 2:
            return False
        r = int.from_bytes(sig_der[kids[0][2]:kids[0][3]], "big")
        s = int.from_bytes(sig_der[kids[1][2]:kids[1][3]], "big")
    except Exception:
        return False
    if not (1 <= r < n and 1 <= s < n):
        return False
    h = hashlib.new(hashname, msg).digest()
    e = int.from_bytes(h, "big")
    excess = len(h) * 8 - n.bit_length()  # FIPS 186: use leftmost n bits if hash is longer
    if excess > 0:
        e >>= excess
    sinv = _ec_inv(s, n)
    X = _ec_add(_ec_mul((e * sinv) % n, G, a, p),
                _ec_mul((r * sinv) % n, pub_xy, a, p), a, p)
    if X is None:
        return False
    return (X[0] % n) == r


# --- X.509 / CMS field extraction via DER walk ---------------------------------
_OID_RSA_ENC = bytes.fromhex("2a864886f70d010101")   # rsaEncryption
_OID_EC_PUBKEY = bytes.fromhex("2a8648ce3d0201")     # ecPublicKey
_OID_SHA512_RSA = bytes.fromhex("2a864886f70d01010d")  # sha512WithRSAEncryption
_OID_MSG_DIGEST = bytes.fromhex("2a864886f70d010904")  # PKCS#9 messageDigest
_OID_SIGNED_DATA = bytes.fromhex("2a864886f70d010702")


def _cert_tbs_and_sig(cert_der):
    """Certificate ::= SEQ { tbsCertificate, sigAlg, sigValue BIT STRING }."""
    tag, hs, cs, ce, nx = _der_parse(cert_der, 0)
    kids = _der_children(cert_der, cs, ce)
    tbs = cert_der[kids[0][1]:kids[0][3]]
    sigalg = _der_children(cert_der, kids[1][2], kids[1][3])[0]
    sig_oid = cert_der[sigalg[2]:sigalg[3]]
    sigbit = kids[2]
    sig = cert_der[sigbit[2] + 1:sigbit[3]]  # BIT STRING: skip unused-bits byte
    return tbs, sig, sig_oid


def _cert_pubkey(cert_der):
    """Return ('rsa',(n,e),None) or ('ec',(x,y),curve_oid_str_or_None)."""
    tag, hs, cs, ce, nx = _der_parse(cert_der, 0)
    tbs = _der_children(cert_der, cs, ce)[0]
    spki = None
    for k in _der_children(cert_der, tbs[2], tbs[3]):
        if k[0] == 0x30:
            sub = _der_children(cert_der, k[2], k[3])
            if len(sub) == 2 and sub[0][0] == 0x30 and sub[1][0] == 0x03:
                algoid = _der_children(cert_der, sub[0][2], sub[0][3])
                if algoid and algoid[0][0] == 0x06:
                    ob = cert_der[algoid[0][2]:algoid[0][3]]
                    if ob in (_OID_RSA_ENC, _OID_EC_PUBKEY):
                        spki = (sub, ob)
    if spki is None:
        raise ValueError("SubjectPublicKeyInfo not found")
    sub, ob = spki
    keybytes = cert_der[sub[1][2] + 1:sub[1][3]]  # drop unused-bits byte
    if ob == _OID_RSA_ENC:
        tg, h2, c2, e2, n2 = _der_parse(keybytes, 0)
        ks = _der_children(keybytes, c2, e2)
        n = int.from_bytes(keybytes[ks[0][2]:ks[0][3]], "big")
        e = int.from_bytes(keybytes[ks[1][2]:ks[1][3]], "big")
        return "rsa", (n, e), None
    algkids = _der_children(cert_der, sub[0][2], sub[0][3])
    curve_oid = None
    if len(algkids) >= 2 and algkids[1][0] == 0x06:
        curve_oid = _CURVE_OID_BYTES.get(cert_der[algkids[1][2]:algkids[1][3]])
    if keybytes[0] != 0x04:
        raise ValueError("EC point not uncompressed")
    flen = (len(keybytes) - 1) // 2
    x = int.from_bytes(keybytes[1:1 + flen], "big")
    y = int.from_bytes(keybytes[1 + flen:1 + 2 * flen], "big")
    return "ec", (x, y), curve_oid


def _cert_subject_issuer(cert_der):
    tag, hs, cs, ce, nx = _der_parse(cert_der, 0)
    tbs = _der_children(cert_der, cs, ce)[0]
    tk = _der_children(cert_der, tbs[2], tbs[3])
    idx = 1 if tk[0][0] == 0xa0 else 0  # skip optional [0] version
    serial = cert_der[tk[idx][2]:tk[idx][3]]
    issuer = cert_der[tk[idx + 2][1]:tk[idx + 2][3]]
    subject = cert_der[tk[idx + 4][1]:tk[idx + 4][3]]
    return serial, issuer, subject


def _load_pinned_root():
    """Load freetsa-cacert.pem next to this script. Returns (der, sha256hex) or (None, None)."""
    here = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(here, "freetsa-cacert.pem")
    if not os.path.exists(path):
        return None, None
    try:
        pem = open(path, "r", encoding="ascii").read()
        m = re.search(r"-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----", pem, re.S)
        der = base64.b64decode("".join(m.group(1).split()))
        return der, hashlib.sha256(der).hexdigest()
    except Exception:
        return None, None


# SHA-256 fingerprint of the FreeTSA Root CA (https://freetsa.org/files/cacert.pem).
# The verifier trusts this exact root; the vendored PEM must match it byte-for-byte.
_PINNED_FREETSA_ROOT_SHA256 = "a6379e7cecc05faa3cbf076013d745e327bbbaa38c0b9af22469d4701d18aabc"


def _extract_token_signed_data(token_der):
    """From a TimeStampResp (or a bare TimeStampToken ContentInfo), return the SignedData SEQUENCE
    span and the original buffer, or None if not found."""
    try:
        tag, hs, cs, ce, nx = _der_parse(token_der, 0)
        top = _der_children(token_der, cs, ce)
    except Exception:
        return None
    # Find the ContentInfo whose first child OID is id-signedData. It may be the whole buffer
    # (bare token) or the optional timeStampToken field of a TimeStampResp.
    candidates = [(tag, hs, cs, ce)]
    candidates += top
    for (t, h0, c0, e0) in candidates:
        if t != 0x30:
            continue
        try:
            sub = _der_children(token_der, c0, e0)
        except Exception:
            continue
        if sub and sub[0][0] == 0x06 and token_der[sub[0][2]:sub[0][3]] == _OID_SIGNED_DATA:
            # sub[1] = [0] EXPLICIT SignedData
            if len(sub) >= 2 and sub[1][0] == 0xa0:
                sd = _der_children(token_der, sub[1][2], sub[1][3])[0]
                return sd
    return None


def check_issuer_chain(token_der, anchor_matched):
    """Verify the timestamp token chains to the pinned FreeTSA root.

    Returns (status, detail). status in:
      'issuer_verified' - signer cert chains to the pinned root and the token signature verifies
      'forged'          - the chain or token signature does NOT verify (real tamper / wrong issuer)
      'deferred'        - the check could not run (no pinned cert, unsupported curve, parse limit);
                          the matched anchor still stands, issuer trust is just not asserted here
    """
    root_der, root_fp = _load_pinned_root()
    if root_der is None:
        return "deferred", "pinned FreeTSA root (freetsa-cacert.pem) not found next to this script"
    if root_fp != _PINNED_FREETSA_ROOT_SHA256:
        return "deferred", ("vendored freetsa-cacert.pem does not match the pinned FreeTSA root "
                            "fingerprint; refusing to trust an unrecognized root")
    sd = _extract_token_signed_data(token_der)
    if sd is None:
        return "deferred", "could not locate the SignedData structure inside the timestamp token"
    try:
        sd_kids = _der_children(token_der, sd[2], sd[3])
        certs, signer_infos, encap = [], None, None
        for k in sd_kids:
            if k[0] == 0xa0:  # [0] IMPLICIT certificates
                for c in _der_children(token_der, k[2], k[3]):
                    certs.append(token_der[c[1]:c[3]])
            elif k[0] == 0x31:  # SET OF SignerInfo
                signer_infos = k
            elif k[0] == 0x30:  # encapContentInfo
                sub = _der_children(token_der, k[2], k[3])
                if sub and sub[0][0] == 0x06:
                    encap = k
        if signer_infos is None or encap is None or not certs:
            return "deferred", "timestamp token is missing signer info or certificates"

        # eContent (TSTInfo) bytes, for the messageDigest binding
        econtent = None
        for e in _der_children(token_der, encap[2], encap[3]):
            if e[0] == 0xa0:
                oct_ = _der_children(token_der, e[2], e[3])[0]
                econtent = token_der[oct_[2]:oct_[3]]
        if econtent is None:
            return "deferred", "could not read the TSTInfo content from the token"

        # first SignerInfo: version, sid, digestAlg, [0]signedAttrs?, sigAlg, signature, [1]unsigned?
        si = _der_children(token_der, signer_infos[2], signer_infos[3])[0]
        order = _der_children(token_der, si[2], si[3])
        sid = order[1]
        rest = order[3:]
        ri = 0
        signed_attrs_tlv = None
        if rest and rest[ri][0] == 0xa0:
            signed_attrs_tlv = rest[ri]
            ri += 1
        ri += 1  # signatureAlgorithm
        sig_value = token_der[rest[ri][2]:rest[ri][3]]
        if signed_attrs_tlv is None:
            return "deferred", "timestamp token has no signed attributes to verify"

        # signed attrs are signed as a SET OF (0x31), stored as [0] IMPLICIT
        sa_for_sig = b"\x31" + token_der[signed_attrs_tlv[1] + 1:signed_attrs_tlv[3]]

        # messageDigest signed-attr must equal hash(TSTInfo); binds signature to imprint+genTime
        md_attr = None
        digest_alg = order[2]
        dg_oid = _der_children(token_der, digest_alg[2], digest_alg[3])[0]
        dg_name = {bytes.fromhex("608648016503040201"): "sha256",
                   bytes.fromhex("608648016503040202"): "sha384",
                   bytes.fromhex("608648016503040203"): "sha512"}.get(
                       token_der[dg_oid[2]:dg_oid[3]])
        if dg_name is None:
            return "deferred", "timestamp token uses a digest algorithm this verifier does not implement"
        for attr in _der_children(token_der, signed_attrs_tlv[2], signed_attrs_tlv[3]):
            ak = _der_children(token_der, attr[2], attr[3])
            if token_der[ak[0][2]:ak[0][3]] == _OID_MSG_DIGEST:
                setv = _der_children(token_der, ak[1][2], ak[1][3])[0]
                md_attr = token_der[setv[2]:setv[3]]
        if md_attr != hashlib.new(dg_name, econtent).digest():
            return "forged", "the token's signed messageDigest does not match its own timestamp content"

        # locate signer cert (issuerAndSerialNumber) and the embedded root
        sid_kids = _der_children(token_der, sid[2], sid[3])
        if sid[0] != 0x30 or len(sid_kids) < 2:
            return "deferred", "timestamp token identifies its signer by a form this verifier does not parse"
        want_issuer = token_der[sid_kids[0][1]:sid_kids[0][3]]
        want_serial = token_der[sid_kids[1][2]:sid_kids[1][3]]
        signer_cert = None
        for c in certs:
            serial, issuer, subject = _cert_subject_issuer(c)
            if issuer == want_issuer and serial == want_serial:
                signer_cert = c
        if signer_cert is None:
            return "deferred", "the token's signing certificate is not bundled in the token"

        # the signer must be issued by the pinned root (subject of pinned root == signer's issuer)
        _, _, root_subject = _cert_subject_issuer(root_der)
        _, signer_issuer, _ = _cert_subject_issuer(signer_cert)
        if signer_issuer != root_subject:
            return "forged", "the token's signing certificate is not issued by the pinned FreeTSA root"

        # verify the signer certificate's signature with the pinned root key (RSA-SHA512)
        tbs, csig, csig_oid = _cert_tbs_and_sig(signer_cert)
        rtype, rparams, _ = _cert_pubkey(root_der)
        if rtype != "rsa" or csig_oid != _OID_SHA512_RSA:
            return "deferred", "the FreeTSA chain uses a certificate signature this verifier does not implement"
        if not _rsa_pkcs1v15_verify(rparams[0], rparams[1], csig, tbs, "sha512"):
            return "forged", "the signing certificate does not verify against the pinned FreeTSA root"

        # verify the token's own signature over the signed attributes (signer EC key)
        stype, sparams, curve_oid = _cert_pubkey(signer_cert)
        if stype != "ec":
            return "deferred", "the token signature uses a key type this verifier does not implement"
        ok = _ecdsa_verify(curve_oid, sparams, sig_value, sa_for_sig, dg_name)
        if ok is None:
            return "deferred", "the token signature uses an EC curve this verifier does not implement"
        if not ok:
            return "forged", "the timestamp token signature does not verify against its signing certificate"

        curve_name = {"1.2.840.10045.3.1.7": "NIST P-256", "1.3.132.0.34": "NIST P-384",
                      "1.3.132.0.35": "NIST P-521"}.get(curve_oid, curve_oid)
        if not anchor_matched:
            # chain is sound but the imprint/time did not match the pack; integrity check governs
            return "issuer_verified", (f"token chains to the pinned FreeTSA root ({curve_name}, SHA-512); "
                                       f"note the imprint/time match is reported separately above")
        return "issuer_verified", (f"signed by FreeTSA, chains to the pinned root "
                                   f"(signer key {curve_name}, SHA-512; root RSA-SHA512)")
    except Exception as e:
        # Never fail a pack on a parser limitation; report honestly and keep the matched anchor.
        return "deferred", f"issuer-chain check could not complete on this token ({e})"


def check_anchor(pack):
    """Return (status, detail, issuer_status, issuer_detail).
    status in {'matched','mismatch','not_checked','none'}.
    """
    ts = pack.get("timestamp") or {}
    if not (ts.get("anchored") and ts.get("token_b64")):
        return "none", "no external anchor on this pack", "none", ""
    try:
        der = base64.b64decode(ts["token_b64"])
    except Exception as e:
        return "not_checked", f"token base64 decode failed: {e}", "deferred", ""
    content_hash = pack["content_hash"]
    imp, gen_time = imprint_and_time_from_der(der)
    matched = False
    if imp is not None:
        imp_ok = imp == content_hash
        time_ok = gen_time is not None and _norm_time(gen_time) == _norm_time(ts.get("gen_time"))
        if imp_ok and time_ok:
            status, detail, matched = "matched", (
                f"TSA {ts.get('tsa')}; embedded imprint matches content_hash; "
                f"genTime {gen_time} matches the pack"), True
        elif not imp_ok:
            status, detail = "mismatch", "embedded imprint does NOT match content_hash"
        else:
            status, detail = "mismatch", (f"imprint matches but token genTime {gen_time} != pack "
                                          f"gen_time {ts.get('gen_time')}")
    else:
        ossl = openssl_has_imprint(der, content_hash)
        if ossl is True:
            status, detail, matched = "matched", (
                f"TSA {ts.get('tsa')}; imprint confirmed in token via openssl asn1parse"), True
        elif ossl is False:
            status, detail = "mismatch", "openssl read the token but content_hash imprint is absent"
        else:
            status, detail = "not_checked", ("no RFC 3161 decoder available "
                                             "(stdlib DER walk and openssl both unavailable)")
    issuer_status, issuer_detail = check_issuer_chain(der, matched)
    return status, detail, issuer_status, issuer_detail


# --- Ed25519 verification, pure stdlib (RFC 8032 reference, public domain) ------
_ED_P = 2 ** 255 - 19
_ED_L = 2 ** 252 + 27742317777372353535851937790883648493


def _ed_inv(x):
    return pow(x, _ED_P - 2, _ED_P)


_ED_D = -121665 * _ed_inv(121666) % _ED_P


def _ed_recover_x(y, sign):
    if y >= _ED_P:
        return None
    x2 = (y * y - 1) * _ed_inv(_ED_D * y * y + 1) % _ED_P
    if x2 == 0:
        return None if sign else 0
    x = pow(x2, (_ED_P + 3) // 8, _ED_P)
    if (x * x - x2) % _ED_P != 0:
        x = x * pow(2, (_ED_P - 1) // 4, _ED_P) % _ED_P
    if (x * x - x2) % _ED_P != 0:
        return None
    if (x & 1) != sign:
        x = _ED_P - x
    return x


def _ed_add(P, Q):
    A = (P[1] - P[0]) * (Q[1] - Q[0]) % _ED_P
    B = (P[1] + P[0]) * (Q[1] + Q[0]) % _ED_P
    C = 2 * P[3] * Q[3] * _ED_D % _ED_P
    D = 2 * P[2] * Q[2] % _ED_P
    E, F, G, H = B - A, D - C, D + C, B + A
    return (E * F % _ED_P, G * H % _ED_P, F * G % _ED_P, E * H % _ED_P)


_ED_GY = 4 * _ed_inv(5) % _ED_P
_ED_G = (_ed_recover_x(_ED_GY, 0), _ED_GY, 1, _ed_recover_x(_ED_GY, 0) * _ED_GY % _ED_P)


def _ed_mul(s, P):
    Q = (0, 1, 1, 0)
    while s > 0:
        if s & 1:
            Q = _ed_add(Q, P)
        P = _ed_add(P, P)
        s >>= 1
    return Q


def _ed_equal(P, Q):
    return ((P[0] * Q[2] - Q[0] * P[2]) % _ED_P == 0 and
            (P[1] * Q[2] - Q[1] * P[2]) % _ED_P == 0)


def _ed_decompress(s):
    if len(s) != 32:
        return None
    y = int.from_bytes(s, "little")
    sign = y >> 255
    y &= (1 << 255) - 1
    x = _ed_recover_x(y, sign)
    return None if x is None else (x, y, 1, x * y % _ED_P)


def ed25519_verify(public, msg, sig):
    """RFC 8032 Ed25519 verify. public=32B, sig=64B, msg=bytes. True/False."""
    if len(public) != 32 or len(sig) != 64:
        return False
    A = _ed_decompress(public)
    if A is None:
        return False
    R = _ed_decompress(sig[:32])
    if R is None:
        return False
    s = int.from_bytes(sig[32:], "little")
    if s >= _ED_L:
        return False
    h = int.from_bytes(hashlib.sha512(sig[:32] + public + msg).digest(), "little") % _ED_L
    return _ed_equal(_ed_mul(s, _ED_G), _ed_add(R, _ed_mul(h, A)))


def check_signature(pack):
    """Return (status, detail). status in {'verified','failed','not_present'}."""
    sig = pack.get("signature")
    if not (sig and sig.get("present")):
        return "not_present", "signature: not present"
    try:
        pub = bytes.fromhex(sig["public_key_hex"])
        raw = bytes.fromhex(sig["signature_hex"])
        ok = ed25519_verify(pub, canon(pack["content"]), raw)
    except Exception as e:
        return "failed", f"signature check error: {e}"
    if ok:
        return "verified", (f"Ed25519 signature over canonical content verifies "
                            f"(key {sig['public_key_hex'][:16]}...)")
    return "failed", "Ed25519 signature does NOT verify against the content"


def replay_kat(pack):
    """Recompute each embedded ACVP vector digest and compare. Stdlib only.

    Returns (status, detail, lines). status in {'replayed','failed','not_present'}.
    Proves the recorded NIST ACVP vectors were not altered after sealing. The full
    algorithmic re-run needs the vetted PQC libraries and was done at seal time; its
    result is the per-algorithm live_pass shown below.
    """
    kat = pack.get("kat")
    if not kat or not kat.get("algorithms"):
        return "not_present", "kat: not present", []
    lines = []
    all_ok = True
    n = 0
    for alg in kat["algorithms"]:
        for vec in alg.get("vectors", []):
            n += 1
            recomputed = sha256(canon(vec.get("fields", {})))
            ok = recomputed == vec.get("vector_digest")
            all_ok = all_ok and ok
            lines.append(f"  {alg.get('algorithm')} {vec.get('name')} "
                         f"(ACVP tcId {vec.get('acvp_tcId')}): digest "
                         f"{'match' if ok else 'MISMATCH'}; "
                         f"seal-time live_pass={alg.get('live_pass')}")
    detail = (f"{n} ACVP vector(s) re-checked; all digests match" if all_ok
              else "an embedded ACVP vector digest does NOT match")
    return ("replayed" if all_ok else "failed"), detail, lines


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "pack.json"
    with open(path, encoding="utf-8") as f:
        pack = json.load(f)

    content_ok = sha256(canon(pack["content"])) == pack["content_hash"]
    header = {"format": pack["format"], "generated": pack["generated"],
              "seq": pack["seq"], "prev_hash": pack["prev_hash"],
              "content_hash": pack["content_hash"]}
    entry_ok = sha256(canon(header)) == pack["entry_hash"]
    integrity_ok = content_ok and entry_ok

    anchor_status, anchor_detail, issuer_status, issuer_detail = check_anchor(pack)
    sig_status, sig_detail = check_signature(pack)
    kat_status, kat_detail, kat_lines = replay_kat(pack)

    print("content_hash recomputed match:", content_ok)
    print("entry_hash recomputed match:  ", entry_ok)
    if anchor_status == "matched":
        if issuer_status == "issuer_verified":
            print("time anchor (issuer-verified time): VERIFIED -", anchor_detail)
            print("  TSA issuer chain:              VERIFIED -", issuer_detail)
        else:
            print("time anchor (integrity + time): VERIFIED -", anchor_detail)
            print("  TSA issuer chain:              not asserted -", issuer_detail)
    elif anchor_status == "mismatch":
        print("time anchor:                    FAIL -", anchor_detail)
    elif anchor_status == "not_checked":
        print("time anchor present but NOT cryptographically checked by this verifier -", anchor_detail)
    else:
        print("time anchor:                   ", anchor_detail)

    if sig_status == "verified":
        print("key signature (Ed25519):       VERIFIED -", sig_detail)
    elif sig_status == "failed":
        print("key signature (Ed25519):       FAIL -", sig_detail)
    else:
        print("key signature:                ", sig_detail)

    if kat_status == "replayed":
        print("KAT replay (NIST ACVP):        REPLAYED -", kat_detail)
        for ln in kat_lines:
            print(ln)
    elif kat_status == "failed":
        print("KAT replay (NIST ACVP):        FAIL -", kat_detail)
        for ln in kat_lines:
            print(ln)
    else:
        print("KAT replay:                   ", kat_detail)

    # Merkle tree-seal + optional selective disclosure (stdlib only).
    sealed_root = (pack.get("content") or {}).get("merkle_seal", {}).get("root")
    disclosure_ok = True
    if sealed_root:
        print("merkle tree-seal:               present - root", sealed_root[:16] + "...")
        disc_path = os.path.join(os.path.dirname(os.path.abspath(path)), "disclosure.json")
        if os.path.exists(disc_path):
            with open(disc_path, encoding="utf-8") as f:
                proof = json.load(f)
            d_ok, d_detail, d_lines = verify_disclosure(proof, expected_root=sealed_root)
            disclosure_ok = d_ok
            print("selective disclosure:          ",
                  ("VERIFIED - " if d_ok else "FAIL - ") + d_detail)
            for ln in d_lines:
                print(f"  disclosed leaf index {ln['index']}: "
                      f"{'recomputes sealed root' if ln['ok'] else 'does NOT recompute root'}")
        else:
            print("selective disclosure:           no disclosure.json beside the pack "
                  "(records are sealed; nothing disclosed on this run)")

    if issuer_status == "issuer_verified":
        print("note: this verifier checks integrity + the anchored time imprint + the FreeTSA issuer "
              "chain (pinned root) + (when present) the key signature and KAT replay. It does not bind "
              "the timestamp or signing key to a named legal identity.")
    else:
        print("note: this verifier checks integrity + the anchored time imprint + (when present) the key "
              "signature and KAT replay. TSA issuer-chain trust was not asserted on this run "
              "(see the line above); it does not bind keys to a named legal identity.")

    # An intact pack fails only on a decoded imprint mismatch, a present signature that does not
    # verify, an embedded KAT vector that was altered, or a timestamp whose issuer chain is forged.
    ok = (integrity_ok and anchor_status != "mismatch" and issuer_status != "forged"
          and sig_status != "failed" and kat_status != "failed" and disclosure_ok)
    print("RESULT:", "PASS, pack intact" if ok else "FAIL, pack altered after sealing")
    if ok:
        print("note: this verifier attests integrity + anchored time (and, when shown above, FreeTSA "
              "issuer trust); it does NOT attest that the control-to-DORA-article mapping in this pack "
              "is legally correct or regulator-accepted (DeployQuantum documented interpretation, "
              "counsel sign-off pending).")
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()
