#!/usr/bin/env python3
"""Build a fresh, offline CA policy directory for the eMudhra mitigation.

Usage: python3 filter-ca-bundle.py public-ca.pem --out ca-policy
       Add --private-ca company.pem for each vetted private CA bundle.

Download the input on an updated, trusted host. This script performs no network
requests and refuses to overwrite an existing policy directory. Review policy
changes when retiring this temporary root-removal mitigation.
Fingerprint source, reviewed 2026-10-22:
https://emudhra.we-have-a-problem.com/roots.json
"""

import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import ssl
import tempfile

BLOCKED = {
    "7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D": "emSign Root TLS CA - G3",
    "F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5": "emSign Root CS CA - G3",
    "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4": "AAA Certificate Services",
    "86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B": "emSign ECC Root CA - G3",
}
PEM = re.compile(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", re.S)


def build_policy(inputs, output):
    if output.exists() or output.is_symlink():
        raise ValueError("Choose a fresh output directory for this policy revision")
    kept, removed, sources = {}, {}, []
    validator = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    for path in inputs:
        raw = path.read_bytes()
        text = raw.decode("utf-8")
        certs = PEM.findall(text)
        if not certs or len(certs) != text.count("-----BEGIN CERTIFICATE-----"):
            raise ValueError("Missing or incomplete PEM certificates in " + str(path))
        if text.count("-----END CERTIFICATE-----") != len(certs):
            raise ValueError("Unmatched PEM certificate ending in " + str(path))
        sources.append({"file": str(path), "sha256": hashlib.sha256(raw).hexdigest()})
        for cert in certs:
            validator.load_verify_locations(cadata=cert)
            der = ssl.PEM_cert_to_DER_cert(cert)
            fingerprint = hashlib.sha256(der).hexdigest().upper()
            if fingerprint in BLOCKED:
                removed[fingerprint] = BLOCKED[fingerprint]
            else:
                kept[fingerprint] = ssl.DER_cert_to_PEM_cert(der)
    if not kept:
        raise ValueError("Filtering would leave an empty CA bundle")
    bundle = "".join(kept[key] for key in sorted(kept)).encode("ascii")
    manifest = {
        "policy": "emudhra-root-removal-2026-10-22",
        "fingerprint_source": "https://emudhra.we-have-a-problem.com/roots.json",
        "inputs": sources,
        "blocked_fingerprints": BLOCKED,
        "removed": removed,
        "retained_certificates": len(kept),
        "bundle_sha256": hashlib.sha256(bundle).hexdigest(),
    }
    # Assemble before publishing. The output is an immutable policy revision.
    staging = Path(tempfile.mkdtemp(prefix=".ca-policy-", dir=output.parent))
    try:
        (staging / "ca.pem").write_bytes(bundle)
        (staging / "ca.pem").chmod(0o644)
        (staging / "empty").mkdir(mode=0o755)
        (staging / "empty").chmod(0o755)
        (staging / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
        (staging / "manifest.json").chmod(0o644)
        staging.chmod(0o755)
        # Re-check immediately before rename; normal use has one policy owner.
        if output.exists() or output.is_symlink():
            raise ValueError("Output appeared during generation; choose a fresh directory")
        os.rename(staging, output)
    finally:
        if staging.exists():
            shutil.rmtree(staging)
    print(f"Created {output}: {len(kept)} certificates; removed {len(removed)} blocked roots")
    print("Bundle SHA-256: " + manifest["bundle_sha256"])


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("bundle", type=Path)
    parser.add_argument("--private-ca", type=Path, action="append", default=[])
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    try:
        build_policy([args.bundle, *args.private_ca], args.out)
    except (OSError, ValueError) as error:
        parser.exit(1, str(error) + "\n")


if __name__ == "__main__":
    main()
