#!/usr/bin/env python3
"""Authoritative RFC-7638 JWK-thumbprint keyid oracle (v2 keyid scheme).

The v2 signing-envelope keyid is the lowercase-hex SHA-256 of the RFC 7638 JWK
thumbprint INPUT for the ed25519 public key:

    canonical JWK = {"crv":"Ed25519","kty":"OKP","x":"<base64url(raw32), no pad>"}
                    (members in lexicographic order, no whitespace, UTF-8)
    keyid         = lowercase_hex( SHA-256( canonical JWK bytes ) )

We adopt the RFC-7638 canonical-JWK INPUT (web-crypto-native, zero ASN.1/DER) with
the project-standard lowercase-hex ENCODING (so the existing hex-typed keyid pins,
conformance vectors, and string compares are unchanged). This SUPERSEDES the two
legacy schemes (raw-32 sha256 and SPKI-DER sha256). The keyid is an unauthenticated
DSSE hint excluded from every signed pre-image, so changing it re-stamps metadata
without invalidating a single signature (DEC-20260626-001).

This script is the single source of truth for the new keyids and the cross-language
conformance vectors. Run it to (re)generate the vectors every implementation asserts
against. Usage: `python spec/v1/gen-keyid-vectors.py`
"""

from __future__ import annotations

import base64
import hashlib
import json
import sys
from pathlib import Path

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import (
    Encoding,
    PublicFormat,
    load_pem_public_key,
)

REPO = Path(__file__).resolve().parents[2]  # probity/

# The three distinct signing keys (PEM/SPKI on disk), keyed by their role.
KEYS = {
    "probity": REPO
    / "website/public/verify/probity.pub",  # reference/sample verdict signer
    "probity-mcp": REPO
    / "website/public/verify-mcp/probity-mcp.pub",  # website-demo host signer (== probity-a2a)
    "probity-signer": REPO
    / "website/public/verify-fusion/probity-signer.pub",  # DAC trace/catch-record signer
}


def raw32_from_pem(path: Path) -> bytes:
    pub = load_pem_public_key(path.read_bytes())
    if not isinstance(pub, Ed25519PublicKey):
        raise SystemExit(f"{path}: not an ed25519 public key")
    return pub.public_bytes(Encoding.Raw, PublicFormat.Raw)


def b64url_nopad(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")


def canonical_jwk_bytes(raw32: bytes) -> bytes:
    # RFC 7638: required OKP members {crv, kty, x} in lexicographic order, no whitespace.
    return json.dumps(
        {"crv": "Ed25519", "kty": "OKP", "x": b64url_nopad(raw32)},
        separators=(",", ":"),
        sort_keys=True,
        ensure_ascii=False,
    ).encode("utf-8")


def jwk_thumbprint_keyid(raw32: bytes) -> str:
    return hashlib.sha256(canonical_jwk_bytes(raw32)).hexdigest()


def legacy_raw_sha256(raw32: bytes) -> str:
    return hashlib.sha256(raw32).hexdigest()


def legacy_spki_sha256(path: Path) -> str:
    der = load_pem_public_key(path.read_bytes()).public_bytes(
        Encoding.DER, PublicFormat.SubjectPublicKeyInfo
    )
    return hashlib.sha256(der).hexdigest()


def main() -> int:
    vectors: dict[str, dict[str, str]] = {}
    for role, path in KEYS.items():
        if not path.exists():
            raise SystemExit(f"missing key file: {path}")
        raw = raw32_from_pem(path)
        vectors[role] = {
            "raw32_b64url": b64url_nopad(raw),
            "canonical_jwk": canonical_jwk_bytes(raw).decode("utf-8"),
            "keyid_v2_jwk_thumbprint": jwk_thumbprint_keyid(raw),
            "legacy_raw_sha256": legacy_raw_sha256(raw),
            "legacy_spki_sha256": legacy_spki_sha256(path),
        }
    print(json.dumps(vectors, indent=2))
    return 0


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