349 lines
12 KiB
Python
Executable File
349 lines
12 KiB
Python
Executable File
#!/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
|
|
from pygost.utils import bytes2long
|
|
|
|
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 = (
|
|
bytes2long(GOST34112012256(urandom(16)).digest()[:20])
|
|
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)
|