#!/usr/bin/env python3
"""Independent Open Print v1 conformance verifier.

This file uses only the Python standard library and does not import Open Print
or JavaScript code. Its Ed25519 routine exists to reproduce the public test
fixture. It is not a production signing or general-purpose security library.
"""

import argparse
import base64
import binascii
import copy
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path


FIELD = 2**255 - 19
ORDER = 2**252 + 27742317777372353535851937790883648493
MAX_SAFE_INTEGER = 9007199254740991
CURVE_D = (-121665 * pow(121666, FIELD - 2, FIELD)) % FIELD
SQRT_M1 = pow(2, (FIELD - 1) // 4, FIELD)
IDENTITY = (0, 1, 1, 0)


def _json_string(value):
    return json.dumps(value, ensure_ascii=False, separators=(",", ":"))


def _utf16_sort_key(value):
    return value.encode("utf-16-be", "surrogatepass")


def canonicalize(value):
    """Serialize the JSON types used by Open Print conformance records."""
    if value is None:
        return "null"
    if value is True:
        return "true"
    if value is False:
        return "false"
    if isinstance(value, str):
        return _json_string(value)
    if isinstance(value, int) and not isinstance(value, bool):
        if abs(value) > MAX_SAFE_INTEGER:
            raise TypeError("Integer is outside the portable JSON range.")
        return str(value)
    if isinstance(value, float):
        raise TypeError(
            "The Python conformance verifier rejects floats; public v1 records "
            "use integer numeric fields."
        )
    if isinstance(value, list):
        return "[" + ",".join(canonicalize(item) for item in value) + "]"
    if isinstance(value, dict):
        if any(not isinstance(key, str) for key in value):
            raise TypeError("Canonical JSON object keys must be strings.")
        keys = sorted(value, key=_utf16_sort_key)
        return "{" + ",".join(
            _json_string(key) + ":" + canonicalize(value[key])
            for key in keys
        ) + "}"
    raise TypeError("Value is outside the JSON data model.")


def sha256_hex(value):
    if isinstance(value, str):
        value = value.encode("utf-8")
    return hashlib.sha256(value).hexdigest()


def decode_base64url(value):
    if not re.fullmatch(r"[A-Za-z0-9_-]+", value) or len(value) % 4 == 1:
        raise ValueError("Expected canonical unpadded base64url.")
    padding = "=" * ((4 - len(value) % 4) % 4)
    try:
        decoded = base64.b64decode(
            value.replace("-", "+").replace("_", "/") + padding,
            validate=True,
        )
    except binascii.Error as error:
        raise ValueError("Expected canonical unpadded base64url.") from error
    canonical = base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii")
    if canonical != value:
        raise ValueError("Expected canonical unpadded base64url.")
    return decoded


def _recover_x(y, sign):
    numerator = (y * y - 1) % FIELD
    denominator = (CURVE_D * y * y + 1) % FIELD
    xx = numerator * pow(denominator, FIELD - 2, FIELD) % FIELD
    x = pow(xx, (FIELD + 3) // 8, FIELD)
    if (x * x - xx) % FIELD != 0:
        x = x * SQRT_M1 % FIELD
    if (x * x - xx) % FIELD != 0:
        raise ValueError("Point is not on the Ed25519 curve.")
    if (x & 1) != sign:
        x = FIELD - x
    if x == 0 and sign:
        raise ValueError("Point uses a non-canonical sign bit.")
    return x


def _decode_point(encoded):
    if len(encoded) != 32:
        raise ValueError("Ed25519 points must contain 32 bytes.")
    integer = int.from_bytes(encoded, "little")
    sign = integer >> 255
    y = integer & ((1 << 255) - 1)
    if y >= FIELD:
        raise ValueError("Ed25519 point is not canonical.")
    x = _recover_x(y, sign)
    return (x, y, 1, x * y % FIELD)


def _add_points(left, right):
    x1, y1, z1, t1 = left
    x2, y2, z2, t2 = right
    a = (y1 - x1) * (y2 - x2) % FIELD
    b = (y1 + x1) * (y2 + x2) % FIELD
    c = 2 * CURVE_D * t1 * t2 % FIELD
    d = 2 * z1 * z2 % FIELD
    e = b - a
    f = d - c
    g = d + c
    h = b + a
    return (
        e * f % FIELD,
        g * h % FIELD,
        f * g % FIELD,
        e * h % FIELD,
    )


def _multiply_point(scalar, point):
    result = IDENTITY
    addend = point
    while scalar:
        if scalar & 1:
            result = _add_points(result, addend)
        addend = _add_points(addend, addend)
        scalar >>= 1
    return result


def _points_equal(left, right):
    return (
        (left[0] * right[2] - right[0] * left[2]) % FIELD == 0
        and (left[1] * right[2] - right[1] * left[2]) % FIELD == 0
    )


BASE_POINT = _decode_point(bytes.fromhex("58" + "66" * 31))


def verify_ed25519(public_key, message, signature):
    """Verify one Ed25519 signature using the RFC 8032 verification equation."""
    if len(public_key) != 32 or len(signature) != 64:
        return False
    encoded_r = signature[:32]
    scalar_s = int.from_bytes(signature[32:], "little")
    if scalar_s >= ORDER:
        return False
    try:
        point_a = _decode_point(public_key)
        point_r = _decode_point(encoded_r)
    except ValueError:
        return False
    challenge = int.from_bytes(
        hashlib.sha512(encoded_r + public_key + message).digest(),
        "little",
    ) % ORDER
    left = _multiply_point(scalar_s, BASE_POINT)
    right = _add_points(point_r, _multiply_point(challenge, point_a))
    return _points_equal(left, right)


def _parse_time(value):
    return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(
        timezone.utc
    )


def _trusted_public_key(issuer, issuer_id, key_id, signed_at):
    if issuer.get("id") != issuer_id:
        raise ValueError("Issuer ID does not match the signed record.")
    key = next(
        (candidate for candidate in issuer["keys"] if candidate["id"] == key_id),
        None,
    )
    if key is None or key["status"] == "revoked":
        raise ValueError("Signing key is not trusted.")
    signed_time = _parse_time(signed_at)
    if signed_time < _parse_time(key["validFrom"]):
        raise ValueError("Record was signed before the key became valid.")
    if "validUntil" in key and signed_time > _parse_time(key["validUntil"]):
        raise ValueError("Record was signed after the key expired.")
    public_jwk = key["publicKey"]
    if (
        set(public_jwk) != {"kty", "crv", "x", "key_ops", "ext"}
        or public_jwk.get("kty") != "OKP"
        or public_jwk.get("crv") != "Ed25519"
        or public_jwk.get("key_ops") != ["verify"]
        or public_jwk.get("ext") is not True
    ):
        raise ValueError("Issuer key is not an Ed25519 public key.")
    decoded = decode_base64url(public_jwk["x"])
    if len(decoded) != 32:
        raise ValueError("Ed25519 public keys must contain 32 bytes.")
    return decoded


def _verify_envelope(
    payload,
    integrity,
    signature,
    issuer,
    issuer_id,
    label,
    integrity_scope,
    signature_scope,
):
    if (
        integrity.get("algorithm") != "SHA-256"
        or integrity.get("scope") != integrity_scope
        or signature.get("algorithm") != "Ed25519"
        or signature.get("scope") != signature_scope
    ):
        raise ValueError(label + " uses unsupported envelope methods.")
    digest = sha256_hex(canonicalize(payload))
    if digest != integrity.get("digest"):
        raise ValueError(label + " digest does not match.")
    signature_metadata = {
        key: value for key, value in signature.items() if key != "value"
    }
    signing_input = canonicalize(
        {
            "payload": payload,
            "integrity": integrity,
            "signature": signature_metadata,
        }
    )
    public_key = _trusted_public_key(
        issuer,
        issuer_id,
        signature["keyId"],
        signature["createdAt"],
    )
    if not verify_ed25519(
        public_key,
        signing_input.encode("utf-8"),
        decode_base64url(signature["value"]),
    ):
        raise ValueError(label + " signature does not match.")
    return digest


def verify_certificate(certificate, issuer):
    claims = certificate["claims"]
    integrity = certificate["integrity"]
    signature = certificate["signature"]
    if (
        integrity.get("algorithm") != "SHA-256"
        or integrity.get("canonicalization") != "open-print-c14n-1"
        or integrity.get("scope") != "claims"
        or signature.get("algorithm") != "Ed25519"
        or signature.get("scope")
        != "claims, integrity, and signature metadata"
    ):
        raise ValueError("Certificate uses unsupported envelope methods.")
    digest = sha256_hex(canonicalize(claims))
    if digest != integrity.get("digest"):
        raise ValueError("Certificate digest does not match.")
    signature_metadata = {
        key: value for key, value in signature.items() if key != "value"
    }
    signing_input = canonicalize(
        {
            "claims": claims,
            "integrity": integrity,
            "signature": signature_metadata,
        }
    ).encode("utf-8")
    public_key = _trusted_public_key(
        issuer,
        claims["issuer"]["id"],
        signature["keyId"],
        signature["createdAt"],
    )
    if not verify_ed25519(
        public_key,
        signing_input,
        decode_base64url(signature["value"]),
    ):
        raise ValueError("Certificate signature does not match.")
    return digest


def verify_registry(registry, issuer):
    previous_digest = None
    digests = []
    for index, event in enumerate(registry):
        payload = {
            key: value
            for key, value in event.items()
            if key not in ("integrity", "signature")
        }
        if payload["previousEventDigest"] != previous_digest:
            raise ValueError(
                "Registry event {} does not follow its predecessor.".format(
                    index
                )
            )
        digest = _verify_envelope(
            payload,
            event["integrity"],
            event["signature"],
            issuer,
            event["issuerId"],
            "Registry event {}".format(index),
            "registry event payload",
            "registry event payload, integrity, and signature metadata",
        )
        previous_digest = digest
        digests.append(digest)
    return digests


def verify_checkpoint(checkpoint, issuer, registry):
    payload = {
        key: value
        for key, value in checkpoint.items()
        if key not in ("integrity", "signature")
    }
    if checkpoint["registryLength"] != len(registry):
        raise ValueError("Checkpoint does not cover the registry.")
    expected_head = registry[-1]["integrity"]["digest"] if registry else None
    if checkpoint["headEventDigest"] != expected_head:
        raise ValueError("Checkpoint head does not match the registry.")
    return _verify_envelope(
        payload,
        checkpoint["integrity"],
        checkpoint["signature"],
        issuer,
        checkpoint["issuerId"],
        "Checkpoint",
        "registry checkpoint payload",
        "registry checkpoint payload, integrity, and signature metadata",
    )


def verify_archive(archive):
    contents = {
        key: value for key, value in archive.items() if key != "integrity"
    }
    integrity = archive["integrity"]
    if (
        integrity.get("algorithm") != "SHA-256"
        or integrity.get("canonicalization") != "open-print-c14n-1"
        or integrity.get("scope") != "archive contents"
    ):
        raise ValueError("Archive uses unsupported integrity methods.")
    digest = sha256_hex(canonicalize(contents))
    if digest != integrity.get("digest"):
        raise ValueError("Archive digest does not match.")
    return digest


def _read_json(path):
    with path.open("r", encoding="utf-8") as handle:
        return json.load(handle)


def verify_fixture(fixture_root):
    claims = _read_json(fixture_root / "claims.json")
    certificate = _read_json(fixture_root / "certificate.json")
    issuer = _read_json(fixture_root / "issuer.json")
    registry = _read_json(fixture_root / "registry.json")
    checkpoint = _read_json(fixture_root / "checkpoint.json")
    archive = _read_json(fixture_root / "archive.json")
    errors = []
    report = {
        "implementation": "python-standard-library",
        "fixture": "open-print-v1",
    }

    try:
        astral_key = "\U00010000"
        private_use_key = "\ue000"
        expected_key_order = (
            '{"' + astral_key + '":2,"' + private_use_key + '":1}'
        )
        if canonicalize({private_use_key: 1, astral_key: 2}) != expected_key_order:
            raise ValueError("UTF-16 object key ordering does not match.")
        try:
            canonicalize(MAX_SAFE_INTEGER + 1)
            raise ValueError("An unsafe integer passed canonicalization.")
        except TypeError:
            pass
        report["canonicalization"] = {
            "utf16KeyOrder": True,
            "unsafeIntegerRejected": True,
        }

        if canonicalize(claims) != canonicalize(certificate["claims"]):
            raise ValueError("Standalone claims differ from the certificate.")
        certificate_digest = verify_certificate(certificate, issuer)
        report["certificate"] = {
            "digest": certificate_digest,
            "signature": True,
        }

        registry_digests = verify_registry(registry, issuer)
        issuance = registry[0]
        if issuance["certificateId"] != certificate["claims"]["id"]:
            raise ValueError("Registry certificate ID does not match.")
        if issuance["fingerprint"] != "sha256:" + certificate_digest:
            raise ValueError("Registry certificate fingerprint does not match.")
        if issuance["sequence"] != certificate["claims"]["sequence"]:
            raise ValueError("Registry certificate sequence does not match.")
        report["registry"] = {
            "events": len(registry),
            "digests": registry_digests,
            "signatures": True,
            "chain": True,
        }

        checkpoint_digest = verify_checkpoint(checkpoint, issuer, registry)
        report["checkpoint"] = {
            "digest": checkpoint_digest,
            "signature": True,
        }

        archive_digest = verify_archive(archive)
        if canonicalize(archive["issuers"][0]) != canonicalize(issuer):
            raise ValueError("Archived issuer differs from the fixture.")
        if canonicalize(archive["certificates"][0]) != canonicalize(certificate):
            raise ValueError("Archived certificate differs from the fixture.")
        if canonicalize(archive["registry"]) != canonicalize(registry):
            raise ValueError("Archived registry differs from the fixture.")
        if canonicalize(archive["checkpoints"][0]) != canonicalize(checkpoint):
            raise ValueError("Archived checkpoint differs from the fixture.")
        report["archive"] = {"digest": archive_digest}

        changed = copy.deepcopy(certificate)
        changed["claims"]["artworkTitle"] = "Changed by independent verifier"
        try:
            verify_certificate(changed, issuer)
            raise ValueError("A changed certificate passed verification.")
        except ValueError as error:
            if "digest does not match" not in str(error):
                raise
        report["changedCertificateRejected"] = True

        changed_metadata = copy.deepcopy(certificate)
        changed_metadata["signature"]["createdAt"] = (
            "2026-07-29T12:00:01.500Z"
        )
        try:
            verify_certificate(changed_metadata, issuer)
            raise ValueError("Changed signature metadata passed verification.")
        except ValueError as error:
            if "signature does not match" not in str(error):
                raise
        report["changedSignatureMetadataRejected"] = True
    except (KeyError, TypeError, ValueError) as error:
        errors.append(str(error))

    report["valid"] = not errors
    report["errors"] = errors
    return report


def main():
    default_fixture = Path(__file__).resolve().parents[1] / "v1"
    parser = argparse.ArgumentParser(
        description="Verify the Open Print v1 conformance fixture independently."
    )
    parser.add_argument(
        "fixture",
        nargs="?",
        type=Path,
        default=default_fixture,
        help="Path to the conformance/v1 directory.",
    )
    arguments = parser.parse_args()
    report = verify_fixture(arguments.fixture.resolve())
    json.dump(report, sys.stdout, indent=2)
    sys.stdout.write("\n")
    return 0 if report["valid"] else 1


if __name__ == "__main__":
    sys.exit(main())
