#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Creates the test timestamp authority the vectors are signed with.

Run once; the certificates under vectors/keys/ are committed and the vectors
are regenerated from them and the committed tokens byte for byte. The TSA
private keys go to .test-authority/, which is git-ignored and never published:
verifying a token needs only the certificates. The authority is disposable.
Whoever needs a token the committed set lacks, and does not hold the keys, runs
this script again: it makes a new authority and clears vectors/keys/tokens/, and
generate_vectors.py then mints every token afresh. Needs OpenSSL 3.4 or later
(for -not_before/-not_after).

Everything here is TEST material and must never sign anything else.

    vectors/keys/
      trust-anchors.pem   the pinned trust anchor of the vectors (the test root)
      root.pem            test root CA, 2025-01-01 to 2045-12-31
      tsa.pem             test TSA, timeStamping purpose, 2025-01-01 to 2045-12-31
      tsa-expired.pem     same key, certificate valid only during 2024
      other-root.pem      a second root that is NOT a trust anchor
      other-tsa.pem       a TSA under the second root
      tsa.cnf             the openssl ts configuration used to mint tokens
      tokens/             minted tokens, one per statement digest and signer
    .test-authority/      tsa.key, other-tsa.key (git-ignored)

Usage: python3 -I make_test_authority.py [keys_dir]   (keys go to .test-authority/)
Licensed Apache-2.0.
"""
from __future__ import annotations

import os
import subprocess
import sys
import tempfile

EXT = """\
[root]
basicConstraints=critical,CA:true
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
[tsa]
basicConstraints=critical,CA:false
keyUsage=critical,digitalSignature,nonRepudiation
extendedKeyUsage=critical,timeStamping
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
"""

TSA_CNF = """\
# openssl ts configuration for the ADCR test authority (TEST ONLY)
[tsa]
default_tsa = tsa_config
[tsa_config]
serial = ./serial
crypto_device = builtin
signer_digest = sha256
default_policy = 1.3.6.1.4.1.32473.1
other_policies = 1.3.6.1.4.1.32473.1
digests = sha256
accuracy = secs:1
ordering = no
tsa_name = no
ess_cert_id_chain = no
ess_cert_id_alg = sha256
"""

VALID = ("20250101000000Z", "20451231235959Z")


def run(*args, cwd):
    subprocess.run(["openssl", *args], cwd=cwd, check=True, capture_output=True)


def cert(cwd, name, cn, ca, ca_key, key, serial, ext, dates):
    run("req", "-new", "-key", key, "-subj", f"/CN={cn}", "-out", name + ".csr", cwd=cwd)
    signer = ["-signkey", key] if ca is None else ["-CA", ca, "-CAkey", ca_key, "-set_serial", str(serial)]
    run("x509", "-req", "-in", name + ".csr", *signer, "-not_before", dates[0], "-not_after", dates[1],
        "-extfile", "ext.cnf", "-extensions", ext, "-out", name + ".pem", cwd=cwd)
    os.remove(os.path.join(cwd, name + ".csr"))


def main(keys_dir, private_dir):
    tokens = os.path.join(keys_dir, "tokens")
    if os.path.isdir(tokens):
        for f in os.listdir(tokens):
            os.remove(os.path.join(tokens, f))
    os.makedirs(tokens, exist_ok=True)
    os.makedirs(private_dir, exist_ok=True)
    with open(os.path.join(keys_dir, "ext.cnf"), "w") as fh:
        fh.write(EXT)
    for k in ("root", "tsa", "other-root", "other-tsa"):
        run("genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", "-out", k + ".key", cwd=keys_dir)
    cert(keys_dir, "root", "ADCR test root (TEST ONLY)", None, None, "root.key", 1, "root", VALID)
    cert(keys_dir, "tsa", "ADCR test TSA (TEST ONLY)", "root.pem", "root.key", "tsa.key", 2, "tsa", VALID)
    cert(keys_dir, "tsa-expired", "ADCR test TSA, expired (TEST ONLY)", "root.pem", "root.key", "tsa.key", 3, "tsa",
         ("20240101000000Z", "20241231235959Z"))
    cert(keys_dir, "other-root", "ADCR untrusted test root (TEST ONLY)", None, None, "other-root.key", 1, "root", VALID)
    cert(keys_dir, "other-tsa", "ADCR untrusted test TSA (TEST ONLY)", "other-root.pem", "other-root.key",
         "other-tsa.key", 2, "tsa", VALID)
    # The root keys sign nothing further; only the TSA keys mint tokens, and
    # they are kept out of the published tree.
    for k in ("root.key", "other-root.key", "ext.cnf"):
        os.remove(os.path.join(keys_dir, k))
    for k in ("tsa.key", "other-tsa.key"):
        os.replace(os.path.join(keys_dir, k), os.path.join(private_dir, k))
    with open(os.path.join(keys_dir, "tsa.cnf"), "w") as fh:
        fh.write(TSA_CNF)
    with open(os.path.join(keys_dir, "root.pem")) as src, open(os.path.join(keys_dir, "trust-anchors.pem"), "w") as dst:
        dst.write(src.read())
    print("test authority written to", keys_dir)


if __name__ == "__main__":
    here = os.path.dirname(os.path.abspath(__file__))
    main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "vectors", "keys"),
         os.path.join(here, ".test-authority"))
