#!/usr/bin/env python3
"""Sign/verify an offline release manifest. Requires OpenSSL 3 with Ed25519.

The signing key belongs outside the repository and must never ship to customers.
A verifier must obtain the public-key fingerprint from a trusted separate channel.
This verifies delivered files, not the identity of a running patched process.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile

PRODUCT = "BMX STAGE AUDIO"
MAX_MANIFEST = 4 * 1024 * 1024


def openssl(*args):
    result = subprocess.run([os.environ.get("BMX_OPENSSL", "openssl"), *map(str, args)],
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if result.returncode:
        raise ValueError("OpenSSL 操作失败：请检查版本、密钥格式或数字签名。")
    return result.stdout


def digest(path):
    h = hashlib.sha256()
    with path.open("rb") as f:
        for block in iter(lambda: f.read(1024 * 1024), b""):
            h.update(block)
    return h.hexdigest()


def fingerprint(public):
    return hashlib.sha256(openssl("pkey", "-pubin", "-in", public, "-outform", "DER")).hexdigest()


def release_file(root, name):
    p = Path(name)
    if p.is_absolute() or "\\" in name or ".." in p.parts or not p.parts or p.as_posix() != name:
        raise ValueError("发行清单包含无效路径。")
    candidate = root / p
    if candidate.is_symlink() or not candidate.is_file() or not candidate.resolve().is_relative_to(root.resolve()):
        raise ValueError("发行文件缺失或路径超出发行目录。")
    return candidate


def keygen(private, public):
    if private.exists() or public.exists():
        raise ValueError("密钥文件已存在，拒绝覆盖。")
    private.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    public.parent.mkdir(parents=True, exist_ok=True)
    # Reserve the secret with owner-only permissions before OpenSSL writes it.
    fd = os.open(private, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
    os.close(fd)
    openssl("genpkey", "-algorithm", "ED25519", "-out", private)
    openssl("pkey", "-in", private, "-pubout", "-out", public)
    print("公钥 SHA-256 (DER):", fingerprint(public))


def sign(root, files, private, manifest):
    signature = Path(str(manifest) + ".sig")
    if manifest.exists() or signature.exists():
        raise ValueError("签名输出已存在，拒绝覆盖。")
    if not 0 < len(files) <= 1000 or len(set(files)) != len(files):
        raise ValueError("发行文件列表为空、重复或过长。")
    entries = []
    for name in sorted(files):
        f = release_file(root, name)
        if f.resolve() in (private.resolve(), manifest.resolve(), signature.resolve()):
            raise ValueError("禁止把密钥或清单本身打包进发行文件列表。")
        entries.append({"file": name, "bytes": f.stat().st_size, "sha256": digest(f)})
    version = json.loads((Path(__file__).resolve().parent.parent / "package.json").read_text())["version"]
    data = {"schema": 1, "product": PRODUCT, "version": version, "algorithm": "Ed25519", "files": entries}
    manifest.write_bytes((json.dumps(data, ensure_ascii=False, indent=2) + "\n").encode())
    openssl("pkeyutl", "-sign", "-rawin", "-inkey", private, "-in", manifest, "-out", signature)
    print("PASS: 发行清单已签名，文件数", len(entries))


def verify(root, public, trusted, manifest):
    if manifest.stat().st_size > MAX_MANIFEST:
        raise ValueError("发行清单过大。")
    # Freeze the bytes being checked; do not reopen a mutable original after verification.
    data = manifest.read_bytes()
    if len(data) > MAX_MANIFEST:
        raise ValueError("发行清单过大。")
    public_data = public.read_bytes()
    signature_data = Path(str(manifest) + ".sig").read_bytes()
    if len(signature_data) != 64 or len(public_data) > 4096:
        raise ValueError("公钥或签名长度无效。")
    with tempfile.TemporaryDirectory(prefix="bmx-verify-") as temp:
        frozen = Path(temp) / "manifest.json"
        frozen.write_bytes(data)
        frozen_public = Path(temp) / "public.pem"
        frozen_public.write_bytes(public_data)
        frozen_sig = Path(temp) / "signature.bin"
        frozen_sig.write_bytes(signature_data)
        if not trusted or fingerprint(frozen_public) != trusted.lower():
            raise ValueError("公钥指纹与可信来源不一致。")
        openssl("pkeyutl", "-verify", "-pubin", "-inkey", frozen_public, "-rawin", "-in", frozen,
                "-sigfile", frozen_sig)
    obj = json.loads(data)
    if obj.get("schema") != 1 or obj.get("product") != PRODUCT or obj.get("algorithm") != "Ed25519":
        raise ValueError("不是受支持的 BMX 发行清单。")
    entries = obj.get("files")
    if not isinstance(entries, list) or not 0 < len(entries) <= 1000:
        raise ValueError("发行清单文件列表无效。")
    seen = set()
    for entry in entries:
        name = entry["file"]
        if name in seen:
            raise ValueError("发行清单文件重复。")
        seen.add(name)
        f = release_file(root, name)
        if f.stat().st_size != entry["bytes"] or digest(f) != entry["sha256"]:
            raise ValueError("发行文件被更改或下载不完整：" + name)
    print("PASS: 可信公钥、Ed25519 签名及全部发行文件校验通过；文件数", len(entries))


def main():
    p = argparse.ArgumentParser(description=__doc__)
    sub = p.add_subparsers(dest="action", required=True)
    k = sub.add_parser("keygen")
    k.add_argument("--private-key", required=True, type=Path)
    k.add_argument("--public-key", required=True, type=Path)
    s = sub.add_parser("sign")
    s.add_argument("--private-key", required=True, type=Path)
    s.add_argument("--root", required=True, type=Path)
    s.add_argument("--manifest", required=True, type=Path)
    s.add_argument("files", nargs="+")
    v = sub.add_parser("verify")
    v.add_argument("--public-key", required=True, type=Path)
    v.add_argument("--trusted-key-sha256", required=True)
    v.add_argument("--root", required=True, type=Path)
    v.add_argument("--manifest", required=True, type=Path)
    a = p.parse_args()
    try:
        if a.action == "keygen":
            keygen(a.private_key, a.public_key)
        elif a.action == "sign":
            sign(a.root, a.files, a.private_key, a.manifest)
        else:
            verify(a.root, a.public_key, a.trusted_key_sha256, a.manifest)
    except (ValueError, OSError, KeyError, TypeError) as e:
        print("FAIL:", e, file=sys.stderr)
        return 1
    return 0


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