#!/usr/bin/env python3
"""Create example self-signed X.509 certificate
"""

from argparse import ArgumentParser
from base64 import standard_b64decode
from base64 import standard_b64encode
from datetime import datetime
from datetime import timedelta
from os import urandom
from sys import exit as sys_exit
from sys import stdout
from textwrap import fill

from pyderasn import Any
from pyderasn import BitString
from pyderasn import Boolean
from pyderasn import IA5String
from pyderasn import Integer
from pyderasn import OctetString
from pyderasn import PrintableString
from pyderasn import UTCTime

from pygost.asn1schemas.oids import id_at_commonName
from pygost.asn1schemas.oids import id_at_countryName
from pygost.asn1schemas.oids import id_ce_authorityKeyIdentifier
from pygost.asn1schemas.oids import id_ce_basicConstraints
from pygost.asn1schemas.oids import id_ce_keyUsage
from pygost.asn1schemas.oids import id_ce_subjectAltName
from pygost.asn1schemas.oids import id_ce_subjectKeyIdentifier
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_256
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_256_paramSetA
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_256_paramSetB
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_256_paramSetC
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_256_paramSetD
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_512
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_512_paramSetA
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_512_paramSetB
from pygost.asn1schemas.oids import id_tc26_gost3410_2012_512_paramSetC
from pygost.asn1schemas.oids import id_tc26_signwithdigest_gost3410_2012_256
from pygost.asn1schemas.oids import id_tc26_signwithdigest_gost3410_2012_512
from pygost.asn1schemas.prvkey import PrivateKey
from pygost.asn1schemas.prvkey import PrivateKeyAlgorithmIdentifier
from pygost.asn1schemas.prvkey import PrivateKeyInfo
from pygost.asn1schemas.x509 import AlgorithmIdentifier
from pygost.asn1schemas.x509 import AttributeType
from pygost.asn1schemas.x509 import AttributeTypeAndValue
from pygost.asn1schemas.x509 import AttributeValue
from pygost.asn1schemas.x509 import AuthorityKeyIdentifier
from pygost.asn1schemas.x509 import BasicConstraints
from pygost.asn1schemas.x509 import Certificate
from pygost.asn1schemas.x509 import CertificateSerialNumber
from pygost.asn1schemas.x509 import Extension
from pygost.asn1schemas.x509 import Extensions
from pygost.asn1schemas.x509 import GeneralName
from pygost.asn1schemas.x509 import GostR34102012PublicKeyParameters
from pygost.asn1schemas.x509 import KeyIdentifier
from pygost.asn1schemas.x509 import KeyUsage
from pygost.asn1schemas.x509 import Name
from pygost.asn1schemas.x509 import RDNSequence
from pygost.asn1schemas.x509 import RelativeDistinguishedName
from pygost.asn1schemas.x509 import SubjectAltName
from pygost.asn1schemas.x509 import SubjectKeyIdentifier
from pygost.asn1schemas.x509 import SubjectPublicKeyInfo
from pygost.asn1schemas.x509 import TBSCertificate
from pygost.asn1schemas.x509 import Time
from pygost.asn1schemas.x509 import Validity
from pygost.asn1schemas.x509 import Version
from pygost.gost3410 import CURVES
from pygost.gost3410 import prv_unmarshal
from pygost.gost3410 import pub_marshal
from pygost.gost3410 import public_key
from pygost.gost3410 import sign
from pygost.gost34112012256 import GOST34112012256
from pygost.gost34112012512 import GOST34112012512

parser = ArgumentParser(description="Self-signed X.509 certificate creator")
parser.add_argument(
    "--ca",
    action="store_true",
    help="Enable BasicConstraints.cA",
)
parser.add_argument(
    "--cn",
    required=True,
    help="Subject's CommonName",
)
parser.add_argument(
    "--country",
    help="Subject's Country",
)
parser.add_argument(
    "--serial",
    help="Serial number",
)
parser.add_argument(
    "--ai",
    required=True,
    help="Signing algorithm: {256[ABCD],512[ABC]}",
)
parser.add_argument(
    "--issue-with",
    help="Path to PEM with CA to issue the child",
)
parser.add_argument(
    "--reuse-key",
    help="Path to PEM with the key to reuse",
)
parser.add_argument(
    "--out-key",
    help="Path to PEM with the resulting key",
)
parser.add_argument(
    "--only-key",
    action="store_true",
    help="Only generate the key",
)
parser.add_argument(
    "--out-cert",
    help="Path to PEM with the resulting certificate",
)
args = parser.parse_args()
AIs = {
    "256A": {
        "publicKeyParamSet": id_tc26_gost3410_2012_256_paramSetA,
        "key_algorithm": id_tc26_gost3410_2012_256,
        "prv_len": 32,
        "curve": CURVES["id-tc26-gost-3410-2012-256-paramSetA"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_256,
        "hasher": GOST34112012256,
    },
    "256B": {
        "publicKeyParamSet": id_tc26_gost3410_2012_256_paramSetB,
        "key_algorithm": id_tc26_gost3410_2012_256,
        "prv_len": 32,
        "curve": CURVES["id-tc26-gost-3410-2012-256-paramSetB"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_256,
        "hasher": GOST34112012256,
    },
    "256C": {
        "publicKeyParamSet": id_tc26_gost3410_2012_256_paramSetC,
        "key_algorithm": id_tc26_gost3410_2012_256,
        "prv_len": 32,
        "curve": CURVES["id-tc26-gost-3410-2012-256-paramSetC"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_256,
        "hasher": GOST34112012256,
    },
    "256D": {
        "publicKeyParamSet": id_tc26_gost3410_2012_256_paramSetD,
        "key_algorithm": id_tc26_gost3410_2012_256,
        "prv_len": 32,
        "curve": CURVES["id-tc26-gost-3410-2012-256-paramSetD"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_256,
        "hasher": GOST34112012256,
    },
    "512A": {
        "publicKeyParamSet": id_tc26_gost3410_2012_512_paramSetA,
        "key_algorithm": id_tc26_gost3410_2012_512,
        "prv_len": 64,
        "curve": CURVES["id-tc26-gost-3410-12-512-paramSetA"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_512,
        "hasher": GOST34112012512,
    },
    "512B": {
        "publicKeyParamSet": id_tc26_gost3410_2012_512_paramSetB,
        "key_algorithm": id_tc26_gost3410_2012_512,
        "prv_len": 64,
        "curve": CURVES["id-tc26-gost-3410-12-512-paramSetB"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_512,
        "hasher": GOST34112012512,
    },
    "512C": {
        "publicKeyParamSet": id_tc26_gost3410_2012_512_paramSetC,
        "key_algorithm": id_tc26_gost3410_2012_512,
        "prv_len": 64,
        "curve": CURVES["id-tc26-gost-3410-2012-512-paramSetC"],
        "sign_algorithm": id_tc26_signwithdigest_gost3410_2012_512,
        "hasher": GOST34112012512,
    },
}
ai = AIs[args.ai]

ca_prv = None
ca_cert = None
ca_subj = None
ca_ai = None
if args.issue_with is not None:
    with open(args.issue_with, "rb") as fd:
        lines = fd.read().decode("ascii").split("-----")
    idx = lines.index("BEGIN PRIVATE KEY")
    if idx == -1:
        raise ValueError("PEM has no PRIVATE KEY")
    prv_raw = standard_b64decode(lines[idx + 1])
    idx = lines.index("BEGIN CERTIFICATE")
    if idx == -1:
        raise ValueError("PEM has no CERTIFICATE")
    cert_raw = standard_b64decode(lines[idx + 1])
    pki = PrivateKeyInfo().decod(prv_raw)
    ca_prv = prv_unmarshal(bytes(OctetString().decod(bytes(pki["privateKey"]))))
    ca_cert = Certificate().decod(cert_raw)
    tbs = ca_cert["tbsCertificate"]
    ca_subj = tbs["subject"]
    curve_oid = GostR34102012PublicKeyParameters().decod(bytes(
        tbs["subjectPublicKeyInfo"]["algorithm"]["parameters"]
    ))["publicKeyParamSet"]
    ca_ai = next(iter([
        params for params in AIs.values()
        if params["publicKeyParamSet"] == curve_oid
    ]))

key_params = GostR34102012PublicKeyParameters((
    ("publicKeyParamSet", ai["publicKeyParamSet"]),
))


def pem(obj):
    return fill(standard_b64encode(obj.encode()).decode("ascii"), 64)


if args.reuse_key is not None:
    with open(args.reuse_key, "rb") as fd:
        lines = fd.read().decode("ascii").split("-----")
    idx = lines.index("BEGIN PRIVATE KEY")
    if idx == -1:
        raise ValueError("PEM has no PRIVATE KEY")
    prv_raw = standard_b64decode(lines[idx + 1])
    pki = PrivateKeyInfo().decod(prv_raw)
    prv = prv_unmarshal(bytes(OctetString().decod(bytes(pki["privateKey"]))))
else:
    prv_raw = urandom(ai["prv_len"])
    out = stdout if args.out_key is None else open(args.out_key, "w")
    print("-----BEGIN PRIVATE KEY-----", file=out)
    print(pem(PrivateKeyInfo((
        ("version", Integer(0)),
        ("privateKeyAlgorithm", PrivateKeyAlgorithmIdentifier((
            ("algorithm", ai["key_algorithm"]),
            ("parameters", Any(key_params)),
        ))),
        ("privateKey", PrivateKey(OctetString(prv_raw).encode())),
    ))), file=out)
    print("-----END PRIVATE KEY-----", file=out)
    if args.only_key:
        sys_exit()
    prv = prv_unmarshal(prv_raw)

curve = ai["curve"]
pub_raw = pub_marshal(public_key(curve, prv))
rdn = [RelativeDistinguishedName((
    AttributeTypeAndValue((
        ("type", AttributeType(id_at_commonName)),
        ("value", AttributeValue(PrintableString(args.cn))),
    )),
))]
if args.country:
    rdn.append(RelativeDistinguishedName((
        AttributeTypeAndValue((
            ("type", AttributeType(id_at_countryName)),
            ("value", AttributeValue(PrintableString(args.country))),
        )),
    )))
subj = Name(("rdnSequence", RDNSequence(rdn)))
not_before = datetime.utcnow()
not_after = not_before + timedelta(days=365 * (10 if args.ca else 1))
ai_sign = AlgorithmIdentifier((
    ("algorithm", (ai if ca_ai is None else ca_ai)["sign_algorithm"]),
))
exts = [
    Extension((
        ("extnID", id_ce_subjectKeyIdentifier),
        ("extnValue", OctetString(
            SubjectKeyIdentifier(GOST34112012256(pub_raw).digest()[:20]).encode()
        )),
    )),
    Extension((
        ("extnID", id_ce_keyUsage),
        ("critical", Boolean(True)),
        ("extnValue", OctetString(KeyUsage(
            ("keyCertSign" if args.ca else "digitalSignature",),
        ).encode())),
    )),
]
if args.ca:
    exts.append(Extension((
        ("extnID", id_ce_basicConstraints),
        ("critical", Boolean(True)),
        ("extnValue", OctetString(BasicConstraints((
            ("cA", Boolean(True)),
        )).encode())),
    )))
else:
    exts.append(Extension((
        ("extnID", id_ce_subjectAltName),
        ("extnValue", OctetString(
            SubjectAltName((
                GeneralName(("dNSName", IA5String(args.cn))),
            )).encode()
        )),
    )))
if ca_ai is not None:
    caKeyId = [
        bytes(SubjectKeyIdentifier().decod(bytes(ext["extnValue"])))
        for ext in ca_cert["tbsCertificate"]["extensions"]
        if ext["extnID"] == id_ce_subjectKeyIdentifier
    ][0]
    exts.append(Extension((
        ("extnID", id_ce_authorityKeyIdentifier),
        ("extnValue", OctetString(AuthorityKeyIdentifier((
            ("keyIdentifier", KeyIdentifier(caKeyId)),
        )).encode())),
    )))

serial = (
    int.from_bytes(urandom(20), "big")
    if args.serial is None else int(args.serial)
)
tbs = TBSCertificate((
    ("version", Version("v3")),
    ("serialNumber", CertificateSerialNumber(serial)),
    ("signature", ai_sign),
    ("issuer", subj if ca_ai is None else ca_subj),
    ("validity", Validity((
        ("notBefore", Time(("utcTime", UTCTime(not_before)))),
        ("notAfter", Time(("utcTime", UTCTime(not_after)))),
    ))),
    ("subject", subj),
    ("subjectPublicKeyInfo", SubjectPublicKeyInfo((
        ("algorithm", AlgorithmIdentifier((
            ("algorithm", ai["key_algorithm"]),
            ("parameters", Any(key_params)),
        ))),
        ("subjectPublicKey", BitString(OctetString(pub_raw).encode())),
    ))),
    ("extensions", Extensions(exts)),
))
cert = Certificate((
    ("tbsCertificate", tbs),
    ("signatureAlgorithm", ai_sign),
    ("signatureValue", BitString(
        sign(curve, prv, ai["hasher"](tbs.encode()).digest()[::-1])
        if ca_ai is None else
        sign(ca_ai["curve"], ca_prv, ca_ai["hasher"](tbs.encode()).digest()[::-1])
    )),
))
out = stdout if args.out_cert is None else open(args.out_cert, "w")
print("-----BEGIN CERTIFICATE-----", file=out)
print(pem(cert), file=out)
print("-----END CERTIFICATE-----", file=out)
