|
| 1 | +#!/usr/bin/env python |
| 2 | +# coding: UTF-8 |
| 3 | + |
| 4 | +import six |
| 5 | +import os |
| 6 | +import shutil |
| 7 | +import argparse |
| 8 | +import datetime |
| 9 | + |
| 10 | +from cryptography import x509 |
| 11 | +from cryptography.hazmat.backends import default_backend |
| 12 | +from cryptography.hazmat.primitives import hashes, serialization |
| 13 | +from cryptography.hazmat.primitives.asymmetric import rsa, ed448, ed25519 |
| 14 | +from cryptography.x509.oid import NameOID |
| 15 | + |
| 16 | +DEFAULT_PDIR = '.pretend-certs' |
| 17 | + |
| 18 | +def genkey(key_type='rsa', rsa_key_size=1024, rsa_public_exponent=65537, **args): |
| 19 | + if key_type == 'rsa': |
| 20 | + return rsa.generate_private_key( |
| 21 | + public_exponent=rsa_public_exponent, |
| 22 | + key_size=rsa_key_size, backend=default_backend()) |
| 23 | + elif key_type == 'ed448': |
| 24 | + return ed448.Ed448PrivateKey.generate() |
| 25 | + elif key_type == 'ed25519': |
| 26 | + return ed25519.Ed25519PrivateKey.generate() |
| 27 | + raise ValueError('Unknown key_type={}'.format(key_type)) |
| 28 | + |
| 29 | +def as_pem(key): |
| 30 | + if isinstance(key, (rsa.RSAPrivateKey, ed448.Ed448PrivateKey, ed25519.Ed25519PrivateKey)): |
| 31 | + return key.private_bytes( |
| 32 | + encoding=serialization.Encoding.PEM, |
| 33 | + format=serialization.PrivateFormat.PKCS8, |
| 34 | + encryption_algorithm=serialization.NoEncryption()) |
| 35 | + elif isinstance(key, (rsa.RSAPublicKey, ed448.Ed448PublicKey, ed25519.Ed25519PublicKey)): |
| 36 | + return key.public_bytes( |
| 37 | + encoding=serialization.Encoding.PEM, |
| 38 | + format=serialization.PublicFormat.SubjectPublicKeyInfo) |
| 39 | + elif isinstance(key, x509.Certificate): |
| 40 | + return key.public_bytes(encoding=serialization.Encoding.PEM) |
| 41 | + raise ValueError('Unhandled key class {}'.format(type(key))) |
| 42 | + |
| 43 | +class Authority: |
| 44 | + def __init__(self, key, crt): |
| 45 | + self.key = key |
| 46 | + self.crt = crt |
| 47 | + |
| 48 | +def gen_CA(fname='ca-root', cn='ca-root', path_length=0, authority=None, pdir=DEFAULT_PDIR, **args): |
| 49 | + private_key = genkey(**args) |
| 50 | + public_key = private_key.public_key() |
| 51 | + |
| 52 | + with open(os.path.join(pdir, fname + '.key'), 'w') as fh: |
| 53 | + fh.write( as_pem(private_key) ) |
| 54 | + |
| 55 | + with open(os.path.join(pdir, fname + '.unsigned'), 'w') as fh: |
| 56 | + fh.write( as_pem(public_key) ) |
| 57 | + |
| 58 | + ksec_100 = datetime.timedelta(0, 100e3, 0) |
| 59 | + Msec_300 = datetime.timedelta(0, 300e6, 0) |
| 60 | + |
| 61 | + builder = x509.CertificateBuilder() |
| 62 | + |
| 63 | + subject = issuer = x509.Name([ |
| 64 | + x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'), |
| 65 | + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'State'), |
| 66 | + x509.NameAttribute(NameOID.LOCALITY_NAME, u'City'), |
| 67 | + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Org'), |
| 68 | + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Group'), |
| 69 | + x509.NameAttribute(NameOID.COMMON_NAME, six.text_type(cn)), |
| 70 | + ]) |
| 71 | + |
| 72 | + if authority: |
| 73 | + issuer = authority.crt.subject |
| 74 | + |
| 75 | + builder = builder.subject_name(subject) |
| 76 | + builder = builder.issuer_name(issuer) |
| 77 | + builder = builder.not_valid_before(datetime.datetime.today() - ksec_100) |
| 78 | + builder = builder.not_valid_after(datetime.datetime.today() + Msec_300) |
| 79 | + builder = builder.serial_number(x509.random_serial_number()) |
| 80 | + builder = builder.public_key(public_key) |
| 81 | + |
| 82 | + authority_public_key = authority.crt.public_key() if authority else public_key |
| 83 | + builder = builder.add_extension( |
| 84 | + x509.AuthorityKeyIdentifier.from_issuer_public_key(authority_public_key), critical=False |
| 85 | + ) |
| 86 | + builder = builder.add_extension( |
| 87 | + x509.SubjectKeyIdentifier.from_public_key(public_key), critical=False |
| 88 | + ) |
| 89 | + builder = builder.add_extension( |
| 90 | + x509.BasicConstraints(ca=True, path_length=path_length), critical=True, |
| 91 | + ) |
| 92 | + builder = builder.add_extension( |
| 93 | + x509.KeyUsage( |
| 94 | + digital_signature=True, |
| 95 | + key_cert_sign=True, |
| 96 | + crl_sign=False, |
| 97 | + key_agreement=False, |
| 98 | + key_encipherment=False, |
| 99 | + content_commitment=False, |
| 100 | + data_encipherment=False, |
| 101 | + encipher_only=False, |
| 102 | + decipher_only=False, |
| 103 | + ), critical=True |
| 104 | + ) |
| 105 | + |
| 106 | + signing_args = { |
| 107 | + 'private_key': authority.key if authority else private_key, |
| 108 | + 'backend': default_backend(), |
| 109 | + 'algorithm': None, |
| 110 | + } |
| 111 | + |
| 112 | + if isinstance(signing_args['private_key'], rsa.RSAPrivateKey): |
| 113 | + signing_args['algorithm'] = hashes.SHA256() |
| 114 | + |
| 115 | + certificate = builder.sign(**signing_args) |
| 116 | + |
| 117 | + with open(os.path.join(pdir, fname + '.crt'), 'w') as fh: |
| 118 | + fh.write( as_pem(certificate) ) |
| 119 | + |
| 120 | + return Authority(private_key, certificate) |
| 121 | + |
| 122 | +def gen_leaf(authority, fname_template='{}', cn='Certy Cert McCertFace', pdir=DEFAULT_PDIR, **args): |
| 123 | + private_key = genkey(**args) |
| 124 | + public_key = private_key.public_key() |
| 125 | + |
| 126 | + private_name = fname_template.format('private') |
| 127 | + public_name = fname_template.format('public') |
| 128 | + |
| 129 | + with open(os.path.join(pdir, private_name + '.key'), 'w') as fh: |
| 130 | + fh.write( as_pem(private_key) ) |
| 131 | + |
| 132 | + with open(os.path.join(pdir, public_name + '.unsigned'), 'w') as fh: |
| 133 | + fh.write( as_pem(public_key) ) |
| 134 | + |
| 135 | + ksec_100 = datetime.timedelta(0, 100e3, 0) |
| 136 | + Msec_300 = datetime.timedelta(0, 300e6, 0) |
| 137 | + |
| 138 | + builder = x509.CertificateBuilder() |
| 139 | + subject = x509.Name([ |
| 140 | + x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'), |
| 141 | + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'State'), |
| 142 | + x509.NameAttribute(NameOID.LOCALITY_NAME, u'City'), |
| 143 | + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Org'), |
| 144 | + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Group'), |
| 145 | + x509.NameAttribute(NameOID.COMMON_NAME, six.text_type(cn)), |
| 146 | + ]) |
| 147 | + |
| 148 | + builder = builder.subject_name(subject) |
| 149 | + builder = builder.issuer_name(authority.crt.subject) |
| 150 | + builder = builder.not_valid_before(datetime.datetime.today() - ksec_100) |
| 151 | + builder = builder.not_valid_after(datetime.datetime.today() + Msec_300) |
| 152 | + builder = builder.serial_number(x509.random_serial_number()) |
| 153 | + builder = builder.public_key(public_key) |
| 154 | + |
| 155 | + authority_public_key = authority.crt.public_key() |
| 156 | + # this would pin us to exactly one issuer; without it, any matching issuer |
| 157 | + # CN should do the trick |
| 158 | + # builder = builder.add_extension( |
| 159 | + # x509.AuthorityKeyIdentifier.from_issuer_public_key(authority_public_key), critical=False |
| 160 | + # ) |
| 161 | + builder = builder.add_extension( |
| 162 | + x509.SubjectKeyIdentifier.from_public_key(public_key), critical=False |
| 163 | + ) |
| 164 | + builder = builder.add_extension( |
| 165 | + x509.KeyUsage( |
| 166 | + digital_signature=True, |
| 167 | + data_encipherment=True, |
| 168 | + content_commitment=True, |
| 169 | + key_cert_sign=False, |
| 170 | + crl_sign=False, |
| 171 | + key_agreement=False, |
| 172 | + key_encipherment=False, |
| 173 | + encipher_only=False, |
| 174 | + decipher_only=False, |
| 175 | + ), critical=True |
| 176 | + ) |
| 177 | + |
| 178 | + signing_args = { |
| 179 | + 'private_key': authority.key, |
| 180 | + 'backend': default_backend(), |
| 181 | + 'algorithm': None, |
| 182 | + } |
| 183 | + |
| 184 | + if isinstance(signing_args['private_key'], rsa.RSAPrivateKey): |
| 185 | + signing_args['algorithm'] = hashes.SHA256() |
| 186 | + |
| 187 | + certificate = builder.sign(**signing_args) |
| 188 | + |
| 189 | + with open(os.path.join(pdir, public_name + '.crt'), 'w') as fh: |
| 190 | + fh.write( as_pem(certificate) ) |
| 191 | + |
| 192 | + return Authority(private_key, certificate) |
| 193 | + |
| 194 | +def main(root_cn, int1_cn, int2_cn, **args): |
| 195 | + if os.path.isdir(args['pdir']): |
| 196 | + shutil.rmtree(args['pdir']) |
| 197 | + os.mkdir(args['pdir']) |
| 198 | + |
| 199 | + ca = gen_CA(cn=root_cn, fname='ca-root', path_length=1, **args) |
| 200 | + ia1 = gen_CA(cn=int1_cn, fname='intermediate-1', authority=ca, path_length=0, **args) |
| 201 | + ia2 = gen_CA(cn=int2_cn, fname='intermediate-2', authority=ca, path_length=0, **args) |
| 202 | + |
| 203 | + lf1 = gen_leaf(cn='Certy Cert #1', fname_template='{}-1', authority=ia1, **args) |
| 204 | + lf2 = gen_leaf(cn='Certy Cert #2', fname_template='{}-2', authority=ia2, **args) |
| 205 | + |
| 206 | + with open(os.path.join(args['pdir'], 'bundle.pem'), 'w') as ofh: |
| 207 | + for i in range(1,3): |
| 208 | + with open(os.path.join(args['pdir'], 'intermediate-{}.crt'.format(i)), 'r') as ifh: |
| 209 | + ofh.write(ifh.read()) |
| 210 | + |
| 211 | +if __name__ == '__main__': |
| 212 | + parser = argparse.ArgumentParser( # description='this program', |
| 213 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 214 | + parser.add_argument('-v', '--verbose', action='store_true') |
| 215 | + parser.add_argument('-o', '--output-dir', dest='pdir', type=str, default=DEFAULT_PDIR) |
| 216 | + parser.add_argument('-R', '--root-cn', type=six.text_type, default='car.hubblestack.io') |
| 217 | + parser.add_argument('-I', '--int1-cn', type=six.text_type, default='ia1.hubblestack.io') |
| 218 | + parser.add_argument('-J', '--int2-cn', type=six.text_type, default='ia2.hubblestack.io') |
| 219 | + parser.add_argument('-t', '--key-type', type=six.text_type, |
| 220 | + choices=['rsa', 'ed448', 'ed25519'], default='rsa') |
| 221 | + parser.add_argument('-z', '--rsa-key-size', type=int, default=1024) |
| 222 | + parser.add_argument('-p', '--rsa-public-exponent', type=int, default=65537) |
| 223 | + |
| 224 | + args = parser.parse_args() |
| 225 | + |
| 226 | + try: main(**args.__dict__) |
| 227 | + except KeyboardInterrupt: pass |
0 commit comments