#!/usr/bin/env python3
"""Verify a downloaded Symvek release artifact before installing it.

This script is published on symvek.com next to the artifact it checks. It is
deliberately standalone: the person running it has not installed anything yet,
so it depends on nothing but the Python standard library and `cryptography`.

    python3 verify-symvek-release.py symvek_watchpoint-0.0.9-py3-none-any.whl

It expects the detached manifest `<artifact>.psf.json` alongside the artifact.

WHAT THIS PROVES, AND WHAT IT DOES NOT. It proves the file you downloaded is
byte-identical to the one Symvek signed with the release key embedded below.
The trust root is that embedded key, which reaches you over HTTPS from
symvek.com — so verify the fingerprint against a second channel if the download
itself is what you are worried about. It says nothing about whether the
software is good, only about whether it is the software Symvek published.

Exit codes:
    0  signature checked and passed
    1  verification FAILED — do not install this file
    2  usage error, or the manifest is missing/unreadable
    3  BLIND — a post-quantum signature is present but could not be checked
"""

from __future__ import annotations

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

# Symvek release-signing public keys, base64url without padding.
# FILLED AT KEY-GENERATION TIME. The script refuses to run while these are
# placeholders, so a release can never ship with an unpinned verifier.
RELEASE_ED25519_PUBKEY = "SbxEvOmIj44jRF1Rrae_PvHexEqsPeIbqMODp01ESck"
RELEASE_MLDSA65_PUBKEY = "_65-PHLh73twkCG7e18rDhn89HZT9PVGi05BFDRIv7g5VMfZAEaeDfHBeMrYzd_k5hC9VS5SBTXuOgmftddaUnqN4TITmUDV7hDlEhE0qfiLPKV_vAu2TcFz6CY2CJe0NryHvwZoBsf6Z6Oejn1AGkig6tkRJ-6g3g3PFAOfZQpqyy0a58n7m4qHME8Sl0jmJsySD3x_fn04HCsdK6rRtoc3H5KEB7fNhzGYUDQv46C2rbmazbxKE5qE7tq1lJzCIjg45eRqWrpVJq3VCGxVzCKe2y_bkZTkPXbYnt2kkibz5aL9rMwHAYH-pYJbjQvfZl7o9v4LfawVIOTZScfi9ZhsUX7a9FmWaCax1Da08ZM5_b1sbO4F9e2MtEkUTkJameIj673SLSg6dxEic06TYq6GtW7GmEqeED9_NIkDiqBHTDUrbUiSC_igNlcK9mLY1wjqnvg3cYNNT5cl5GKleeB31ghhFZLcAnu3mHCfpp6yyJeoz2rey9sDItgXSZlJFx8ZG1L3ED99JlxEo8HsFKNIIQBUr9Cnc6wOX0mYsBg3BxPWKQaAshfjFF_EBu96HS9xY18LaPatB5w-iCZ7VWLNcXGD_bCtYUpehAixNZ06i6KK6_xeYydpIPFeqEDUpuL6UGQ1t3LXRUxpB0PaSFxKPTqtXlN3FZtF1wbteWfQMlmSpwpoHAK_zXJ4f8LUkqk5iJfhRIYq8Fpl84o_duZH0EfFpnDuMn4W6qS3dtHb9K0raCoWaEpnCG9JTESqXNbtsoqAKeuyK0zPEy7kkfcXpO4H-LfQVFjXjODDM1tY6clXzUcdJ3igPBipALY4cc9ugjod7Zz3VFaBk3iEG-4RG6yQ7AZKpvUg30wrt3dH2iyrPsfM8Os78dNzwNC6uOYMxX4rhqPK2X_lQLl-_LojU6GTDMbunWooZDHXzBw004Y7D9RJSr2ITtKLq2Vey-LakOpT-cRzag1RJBtgC6gm_I0cSFG2bgbHE6NmMeaqc3MoUPHlMIXFnnTR99ZT6zMDnhbAkmJCtX8i5zC6fuvfegOO0t03OD24_79vZSjDZJF0iXfmY9MoYvny3wkE8O6gmEvW3Eo6B_egyI5jwnKWW029LAm1JQsWsXaPAM8nwuCge0v9oGcbv-mT8PG687G0Ml1fCmZWiWS-_NUOvknkeMaWwfEvqznicjbvO6SK2zzwPEyBOHwEeC-sYH4GWaBS32eP4wvhNHct-WHtNxdujCvvhxNhG9zIw-tfcUh8wavRpnSZzV1mLyqF_2aVoSSiIaajILXd4BE9oncd-hZyqg4TZRQDsRaKMlIi5DOFhwUm85xChlGP3cJKk7n96PvTr6XwlOETYTTah2kDjNeTW_M4APvbdS85e3AM6rfHXvM4laZoURZvrzJ_j9vFYXsHFcjHXIyFMq-KMWBJwvNptjuk-wVuqVKHNM82Ai8aK8cvqDuMYF_T9PVRC6Wmg0PPOKvaG0SKdtXCzJG3HCjmWW33hD2i9lVmbucoR5rNzj5PAY96s6h-kw9yedxuzExM-_igKUMOJ1OU8Gz6MJUida4R9AudMFTJbocl9JgJK3PWkLrwhCM1L1Ji3CksDE6She5rAuyZoilgY6JJ1OnICOPmHCZh_h2rAwIrgjwpRMXlDgf8zCk-N5ZXH9zkTEsdFwUo3xSfS7oJ3pruUWcGvZiHZ9kKBusTAyuBQpLvQ5jrtyTWHGxQvjuWY2Geqy0zrUGFKajuVqUxJtO0pL1TjOEgc_6O9RrOKSNqRWZDZxQEChcdSAK8XhUFKpLzVSNZ4W-ScbZLqyM86yD6OILU7OmCZD1z47bB8VJ1_BqQguMfIsDtlqVsws5bHE56N0munBrUG3OZavMg34pQ3-GyFuIGHfSjblSgSmyF8zbKFJVCO9YEjsQDeOHBuOZiN-z-fgt7Bp_pNWQj3Y4Nlu7V9HvXjCpaExCH1G67YDXfobD9rHST6w1Lr57l8cnIyEUCxBieQD6lCNjUG-NaDoTc7N6WndbOhZWrsoAIIkZvaZdsSdWfJR3BT1umtsuzDvlFrCkJNGyQyg9JG6Ss9iVIiSevOSmZb9tcrQf-AYtp26Gpl-quze0O-P2jQeD7YtB1w1-trl0htBEyzNxfCZbDtNr3sP_yrK099fachhDZK5_ElpmqlaWTYQLeK1n5LCKctRZ9I4DzEYjL4tyAWUoq9RRV7wOXQHwgaoYQK3Zatc-wdCFe9sj4vVVb-MKiKBjclQdiL2nD0mZ7aSKbH-QevyGIalTS74qtgX2l7NaAne4pQmN5G4V7vHdI54Ume7udaXsxsJiIdFQF6rQZ0AOuwXfEEhvQEg2uXFU1CMf2ftddj5j8vPuQLPbG-hrw4vPTmH09xkItnC9FxDaXKnlSEaMhvCGI3pYmRahBhiASy-zpwQnXY0PYpVjWy4L-NhnbUWoZBYAivjx0qpfpXynoIlGPmpGTzVAZmdCXmr9qgip1VmQu9hv--TZXXdyKJuI4gWHJaP-GXpeU51b8CAGDTWppS2aNcf5DEbzvfvNoz_twGQ4G16fUKy_mep1_JnnJkvXEsfDRRVYAwOnRWNazMq5rlem0M4C35QOXj98"

_PLACEHOLDER = "REPLACE_AT_KEY_GENERATION"
SIGNATURE_FIELDS = ("signature", "pq_signature")
PQ_ALG = "ML-DSA-65"


def die(msg: str, code: int = 2) -> "None":
    print(f"ERROR: {msg}", file=sys.stderr)
    sys.exit(code)


def b64url_decode(s: str) -> bytes:
    pad = 4 - (len(s) % 4)
    return base64.urlsafe_b64decode(s + ("=" * pad if pad != 4 else ""))


def canonical(obj: object) -> bytes:
    """Must match the signer byte for byte."""
    return json.dumps(obj, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False).encode("utf-8")


def sha256_file(p: Path) -> str:
    h = hashlib.sha256()
    with p.open("rb") as f:
        while chunk := f.read(65536):
            h.update(chunk)
    return h.hexdigest()


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
    ap.add_argument("artifact", help="The downloaded file (e.g. a .whl)")
    ap.add_argument("--manifest", default=None,
                    help="Detached manifest. Default: <artifact>.psf.json")
    # Testing hooks. A real user never needs these; the keys are pinned above.
    ap.add_argument("--pubkey", default=None, help=argparse.SUPPRESS)
    ap.add_argument("--mldsa-pubkey", default=None, help=argparse.SUPPRESS)
    args = ap.parse_args()

    ed_pub = args.pubkey or RELEASE_ED25519_PUBKEY
    pq_pub = args.mldsa_pubkey or RELEASE_MLDSA65_PUBKEY
    if _PLACEHOLDER in (ed_pub, pq_pub):
        die("this verifier has no pinned release key — it was published "
            "unfinished and cannot verify anything. Report this to "
            "hello@symvek.com.")

    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    except ImportError:
        die("needs the 'cryptography' package: pip install cryptography")

    artifact = Path(args.artifact)
    if not artifact.is_file():
        die(f"no such file: {artifact}")
    manifest_path = Path(args.manifest) if args.manifest else Path(str(artifact) + ".psf.json")
    if not manifest_path.is_file():
        die(f"no manifest at {manifest_path} — download it alongside the artifact")

    try:
        event = json.loads(manifest_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
        die(f"manifest is not readable JSON: {exc}")

    # 1. Does the manifest describe THIS file?
    payload = event.get("payload") or {}
    recorded = payload.get("sha256")
    actual = sha256_file(artifact)
    if recorded != actual:
        print(f"FAILED: {artifact.name} does not match the manifest.", file=sys.stderr)
        print(f"  signed sha256:   {recorded}", file=sys.stderr)
        print(f"  your file:       {actual}", file=sys.stderr)
        print("Do not install this file.", file=sys.stderr)
        return 1
    if payload.get("filename") not in (None, artifact.name):
        # Guards a rename attack: a genuinely signed OLD release presented under
        # a new name. The filename carries the version, so it is signed too.
        print(f"FAILED: manifest was signed for '{payload.get('filename')}', "
              f"not '{artifact.name}'.", file=sys.stderr)
        return 1

    # 2. Is the manifest itself genuinely from Symvek?
    sig_block = event.get("signature") or {}
    if sig_block.get("alg") != "Ed25519" or "value" not in sig_block:
        die("manifest has no usable Ed25519 signature block")
    message = canonical({k: v for k, v in event.items() if k not in SIGNATURE_FIELDS})
    try:
        Ed25519PublicKey.from_public_bytes(b64url_decode(ed_pub)).verify(
            b64url_decode(sig_block["value"]), message
        )
    except Exception:
        print("FAILED: the manifest signature does not match the Symvek "
              "release key. Do not install this file.", file=sys.stderr)
        return 1

    # 3. The post-quantum half. Present-but-unchecked is not a pass, and
    #    ABSENT is not a pass either — see below.
    pq_block = event.get("pq_signature") or {}
    if not pq_block.get("value"):
        # SILENT-DOWNGRADE DEFENCE (added 2026-08-04, found by break-testing the
        # first real signed release).
        #
        # This branch used to print "OK … (Ed25519)" and return 0. That voided
        # the entire reason for hybrid signing. The standard's rationale is that
        # a forgery must break BOTH layers — but an attacker who breaks only
        # Ed25519 could DELETE the pq_signature block from the manifest and this
        # verifier would accept the result as a lesser-but-fine pass.
        #
        # A pinned ML-DSA key is an assertion that Symvek signs hybrid. If the
        # key is pinned and the block is missing, the manifest has been altered
        # or the release was mis-produced. Either way it is not verifiable, and
        # `sign_release.py` already refuses to emit classical-only without an
        # explicit --allow-classical-only, so a legitimate release cannot land
        # here. Exit 1: this is a failure, not a blind spot.
        if RELEASE_MLDSA65_PUBKEY != _PLACEHOLDER:
            print(f"FAILED: {artifact.name} carries NO {PQ_ALG} signature, but this "
                  f"verifier pins a Symvek {PQ_ALG} release key — every genuine "
                  "Symvek release is signed with both. The manifest has been "
                  "altered or this artifact did not come from a Symvek release "
                  "build. Do not install this file.", file=sys.stderr)
            return 1
        print(f"OK: {artifact.name} is signed by Symvek (Ed25519).")
        print("Note: this release carries no post-quantum signature.")
        return 0
    if pq_block.get("alg") != PQ_ALG:
        die(f"unsupported post-quantum algorithm: {pq_block.get('alg')}")

    try:
        from cryptography.hazmat.primitives.asymmetric import mldsa
    except ImportError:
        print(f"UNCHECKED: this release carries a {PQ_ALG} signature that your "
              "environment cannot verify (needs cryptography>=45). Ed25519 "
              "passed, which is a real but lesser result.", file=sys.stderr)
        return 3
    try:
        mldsa.MLDSA65PublicKey.from_public_bytes(b64url_decode(pq_pub)).verify(
            b64url_decode(pq_block["value"]), message
        )
    except Exception:
        print(f"FAILED: the {PQ_ALG} signature does not match the Symvek "
              "release key. Do not install this file.", file=sys.stderr)
        return 1

    print(f"OK: {artifact.name} is signed by Symvek (Ed25519 + {PQ_ALG}).")
    print(f"  sha256 {actual}")
    return 0


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