cpython/Lib/test/certdata/make_ssl_certs.py

"""Make the custom certificate and private key files used by test_ssl
and friends."""

import argparse
import os
import pprint
import shutil
import tempfile
from subprocess import *

startdate = "20180829142316Z"
enddate_default = "25251028142316Z"
days_default = "140000"

req_template = """
    [ default ]
    base_url               = http://testca.pythontest.net/testca

    [req]
    distinguished_name     = req_distinguished_name
    prompt                 = no

    [req_distinguished_name]
    C                      = XY
    L                      = Castle Anthrax
    O                      = Python Software Foundation
    CN                     = {hostname}

    [req_x509_extensions_nosan]

    [req_x509_extensions_simple]
    subjectAltName         = @san

    [req_x509_extensions_full]
    subjectAltName         = @san
    keyUsage               = critical,keyEncipherment,digitalSignature
    extendedKeyUsage       = serverAuth,clientAuth
    basicConstraints       = critical,CA:false
    subjectKeyIdentifier   = hash
    authorityKeyIdentifier = keyid:always,issuer:always
    authorityInfoAccess    = @issuer_ocsp_info
    crlDistributionPoints  = @crl_info

    [ issuer_ocsp_info ]
    caIssuers;URI.0        = $base_url/pycacert.cer
    OCSP;URI.0             = $base_url/ocsp/

    [ crl_info ]
    URI.0                  = $base_url/revocation.crl

    [san]
    DNS.1 = {hostname}
    {extra_san}

    [dir_sect]
    C                      = XY
    L                      = Castle Anthrax
    O                      = Python Software Foundation
    CN                     = dirname example

    [princ_name]
    realm = EXP:0, GeneralString:KERBEROS.REALM
    principal_name = EXP:1, SEQUENCE:principal_seq

    [principal_seq]
    name_type = EXP:0, INTEGER:1
    name_string = EXP:1, SEQUENCE:principals

    [principals]
    princ1 = GeneralString:username

    [ ca ]
    default_ca      = CA_default

    [ CA_default ]
    dir = cadir
    database  = $dir/index.txt
    crlnumber = $dir/crl.txt
    default_md = sha256
    startdate = {startdate}
    default_startdate = {startdate}
    enddate = {enddate}
    default_enddate = {enddate}
    default_days = {days}
    default_crl_days = {days}
    certificate = pycacert.pem
    private_key = pycakey.pem
    serial    = $dir/serial
    RANDFILE  = $dir/.rand
    policy          = policy_match

    [ policy_match ]
    countryName             = match
    stateOrProvinceName     = optional
    organizationName        = match
    organizationalUnitName  = optional
    commonName              = supplied
    emailAddress            = optional

    [ policy_anything ]
    countryName   = optional
    stateOrProvinceName = optional
    localityName    = optional
    organizationName  = optional
    organizationalUnitName  = optional
    commonName    = supplied
    emailAddress    = optional


    [ v3_ca ]

    subjectKeyIdentifier=hash
    authorityKeyIdentifier=keyid:always,issuer
    basicConstraints = critical, CA:true
    keyUsage = critical, digitalSignature, keyCertSign, cRLSign

    """

here = os.path.abspath(os.path.dirname(__file__))


def make_cert_key(cmdlineargs, hostname, sign=False, extra_san='',
                  ext='req_x509_extensions_full', key='rsa:3072'):
    print("creating cert for " + hostname)
    tempnames = []
    for i in range(3):
        with tempfile.NamedTemporaryFile(delete=False) as f:
            tempnames.append(f.name)
    req_file, cert_file, key_file = tempnames
    try:
        req = req_template.format(
            hostname=hostname,
            extra_san=extra_san,
            startdate=startdate,
            enddate=cmdlineargs.enddate,
            days=cmdlineargs.days
        )
        with open(req_file, 'w') as f:
            f.write(req)
        args = ['req', '-new', '-nodes', '-days', cmdlineargs.days,
                '-newkey', key, '-keyout', key_file,
                '-config', req_file]
        if sign:
            with tempfile.NamedTemporaryFile(delete=False) as f:
                tempnames.append(f.name)
                reqfile = f.name
            args += ['-out', reqfile ]

        else:
            args += ['-extensions', ext, '-x509', '-out', cert_file ]
        check_call(['openssl'] + args)

        if sign:
            args = [
                'ca',
                '-config', req_file,
                '-extensions', ext,
                '-out', cert_file,
                '-outdir', 'cadir',
                '-policy', 'policy_anything',
                '-batch', '-infiles', reqfile
            ]
            check_call(['openssl'] + args)


        with open(cert_file, 'r') as f:
            cert = f.read()
        with open(key_file, 'r') as f:
            key = f.read()
        return cert, key
    finally:
        for name in tempnames:
            os.remove(name)

TMP_CADIR = 'cadir'

def unmake_ca():
    shutil.rmtree(TMP_CADIR)

def make_ca(cmdlineargs):
    os.mkdir(TMP_CADIR)
    with open(os.path.join('cadir','index.txt'),'a+') as f:
        pass # empty file
    with open(os.path.join('cadir','crl.txt'),'a+') as f:
        f.write("00")
    with open(os.path.join('cadir','index.txt.attr'),'w+') as f:
        f.write('unique_subject = no')
    # random start value for serial numbers
    with open(os.path.join('cadir','serial'), 'w') as f:
        f.write('CB2D80995A69525B\n')

    with tempfile.NamedTemporaryFile("w") as t:
        req = req_template.format(
            hostname='our-ca-server',
            extra_san='',
            startdate=startdate,
            enddate=cmdlineargs.enddate,
            days=cmdlineargs.days
        )
        t.write(req)
        t.flush()
        with tempfile.NamedTemporaryFile() as f:
            args = ['req', '-config', t.name, '-new',
                    '-nodes',
                    '-newkey', 'rsa:3072',
                    '-keyout', 'pycakey.pem',
                    '-out', f.name,
                    '-subj', '/C=XY/L=Castle Anthrax/O=Python Software Foundation CA/CN=our-ca-server']
            check_call(['openssl'] + args)
            args = ['ca', '-config', t.name,
                    '-out', 'pycacert.pem', '-batch', '-outdir', TMP_CADIR,
                    '-keyfile', 'pycakey.pem',
                    '-selfsign', '-extensions', 'v3_ca', '-infiles', f.name ]
            check_call(['openssl'] + args)
            args = ['ca', '-config', t.name, '-gencrl', '-out', 'revocation.crl']
            check_call(['openssl'] + args)

    # capath hashes depend on subject!
    check_call([
        'openssl', 'x509', '-in', 'pycacert.pem', '-out', 'capath/ceff1710.0'
    ])
    shutil.copy('capath/ceff1710.0', 'capath/b1930218.0')


def write_cert_reference(path):
    import _ssl
    refdata = pprint.pformat(_ssl._test_decode_cert(path))
    print(refdata)
    with open(path + '.reference', 'w') as f:
        print(refdata, file=f)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Make the custom certificate and private key files used by test_ssl and friends.')
    parser.add_argument('--days', default=days_default)
    parser.add_argument('--enddate', default=enddate_default)
    cmdlineargs = parser.parse_args()

    os.chdir(here)
    cert, key = make_cert_key(cmdlineargs, 'localhost', ext='req_x509_extensions_simple')
    with open('ssl_cert.pem', 'w') as f:
        f.write(cert)
    with open('ssl_key.pem', 'w') as f:
        f.write(key)
    print("password protecting ssl_key.pem in ssl_key.passwd.pem")
    check_call(['openssl','pkey','-in','ssl_key.pem','-out','ssl_key.passwd.pem','-aes256','-passout','pass:somepass'])
    check_call(['openssl','pkey','-in','ssl_key.pem','-out','keycert.passwd.pem','-aes256','-passout','pass:somepass'])

    with open('keycert.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    with open('keycert.passwd.pem', 'a+') as f:
        f.write(cert)

    # For certificate matching tests
    make_ca(cmdlineargs)
    cert, key = make_cert_key(cmdlineargs, 'fakehostname', ext='req_x509_extensions_simple')
    with open('keycert2.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    cert, key = make_cert_key(cmdlineargs, 'localhost', sign=True)
    with open('keycert3.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    check_call(['openssl', 'x509', '-outform', 'pem', '-in', 'keycert3.pem', '-out', 'cert3.pem'])

    cert, key = make_cert_key(cmdlineargs, 'fakehostname', sign=True)
    with open('keycert4.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    cert, key = make_cert_key(
        cmdlineargs, 'localhost-ecc', sign=True, key='param:secp384r1.pem'
    )
    with open('keycertecc.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    extra_san = [
        'otherName.1 = 1.2.3.4;UTF8:some other identifier',
        'otherName.2 = 1.3.6.1.5.2.2;SEQUENCE:princ_name',
        'email.1 = [email protected]',
        'DNS.2 = www.example.org',
        # GEN_X400
        'dirName.1 = dir_sect',
        # GEN_EDIPARTY
        'URI.1 = https://www.python.org/',
        'IP.1 = 127.0.0.1',
        'IP.2 = ::1',
        'RID.1 = 1.2.3.4.5',
    ]

    cert, key = make_cert_key(cmdlineargs, 'allsans', sign=True, extra_san='\n'.join(extra_san))
    with open('allsans.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    extra_san = [
        # könig (king)
        'DNS.2 = xn--knig-5qa.idn.pythontest.net',
        # königsgäßchen (king's alleyway)
        'DNS.3 = xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
        'DNS.4 = xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
        # βόλοσ (marble)
        'DNS.5 = xn--nxasmq6b.idna2003.pythontest.net',
        'DNS.6 = xn--nxasmm1c.idna2008.pythontest.net',
    ]

    # IDN SANS, signed
    cert, key = make_cert_key(cmdlineargs, 'idnsans', sign=True, extra_san='\n'.join(extra_san))
    with open('idnsans.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    cert, key = make_cert_key(cmdlineargs, 'nosan', sign=True, ext='req_x509_extensions_nosan')
    with open('nosan.pem', 'w') as f:
        f.write(key)
        f.write(cert)

    unmake_ca()
    print("Writing out reference data for Lib/test/test_ssl.py and Lib/test/test_asyncio/utils.py")
    write_cert_reference('keycert.pem')
    write_cert_reference('keycert3.pem')