Introduction

Sometimes I need to write a simple network server to emulate an application I’m integrating with. Typically, this is ends up being a throw away Python script that allows me to easily inspect at a request and returns a basic response. It’s handy to verify what I’m sending isn’t malformed. Also, it helps to ensure my response parser is at least somewhat sane.

The software I work on requires a TLS secured connection to all remote end points. Since these are throw away scripts I find myself running the openssl command line more of often than I’d like.

It would be ideal to have a Python module that would generate the certificate and key files for me. Something I could keep around, drop into one of these scripts, and have TLS without the external steps of running openssl.

Unfortunately, Python does not have a built in module for generating or manipulating x509 certificates. This is a shame because it means we need to rely on externally installed components.

I realize I’m saving myself 2 minutes of by not running openssl. That said, having the script take care of everything makes it easier to share with coworkers. Giving them instructions like “run the script” vs “run this command, then run the script” make it a little easier on me.

Libraries

For cert generation, the two libraries I’ve found are PyOpenSSL and pyca/cryptography. Since neither is part of standard Python I’ve implemented detection and fallback with these two libraries. If neither is installed you’re out of luck.

If you work with Python a lot, most likely one or both are already installed. When I went to install Cryptography I found out that it had already been installed as a dependency for another application I have.

The code

self_sign_cert.py

import socket

def _gen_openssl():
    import random
    from OpenSSL import crypto

    pkey = crypto.PKey()
    pkey.generate_key(crypto.TYPE_RSA, 2048)

    x509 = crypto.X509()
    subject = x509.get_subject()
    subject.commonName = socket.gethostname()
    x509.set_issuer(subject)
    x509.gmtime_adj_notBefore(0)
    x509.gmtime_adj_notAfter(5*365*24*60*60)
    x509.set_pubkey(pkey)
    x509.set_serial_number(random.randrange(100000))
    x509.set_version(2)
    x509.add_extensions([
        crypto.X509Extension(b'subjectAltName', False,
            ','.join([
                'DNS:%s' % socket.gethostname(),
                'DNS:*.%s' % socket.gethostname(),
                'DNS:localhost',
                'DNS:*.localhost']).encode()),
        crypto.X509Extension(b"basicConstraints", True, b"CA:false")])

    x509.sign(pkey, 'SHA256')

    return (crypto.dump_certificate(crypto.FILETYPE_PEM, x509),
        crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))

def _gen_cryptography():
    import datetime
    from cryptography import x509
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.primitives import serialization
    from cryptography.x509.oid import NameOID

    one_day = datetime.timedelta(1, 0, 0)
    private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
            backend=default_backend())
    public_key = private_key.public_key()

    builder = x509.CertificateBuilder()
    builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, socket.gethostname())]))
    builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, socket.gethostname())]))
    builder = builder.not_valid_before(datetime.datetime.today() - one_day)
    builder = builder.not_valid_after(datetime.datetime.today() + (one_day*365*5))
    builder = builder.serial_number(x509.random_serial_number())
    builder = builder.public_key(public_key)
    builder = builder.add_extension(
        x509.SubjectAlternativeName([
            x509.DNSName(socket.gethostname()),
            x509.DNSName('*.%s' % socket.gethostname()),
            x509.DNSName('localhost'),
            x509.DNSName('*.localhost'),
        ]),
        critical=False)
    builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)

    certificate = builder.sign(
        private_key=private_key, algorithm=hashes.SHA256(),
        backend=default_backend())

    return (certificate.public_bytes(serialization.Encoding.PEM),
        private_key.private_bytes(serialization.Encoding.PEM,
            serialization.PrivateFormat.PKCS8,
            serialization.NoEncryption()))

def gen_self_signed_cert():
    '''
    Returns (cert, key) as ASCII PEM strings
    '''
    try:
        return _gen_openssl()
    except:
        try:
            return _gen_cryptography()
        except:
            return (None, None)
    return (None, None)

if __name__ == '__main__':
    print(gen_self_signed_cert())

Lazy loading for fallback

PyOpenSSL and Cryptography are both lazy loaded within their respective functions. This allows detection by trying to call the function in a try..except block. If it fails to run then, most likely, it’s because the module isn’t present. This is exactly how the main function gen_self_signed_cert operates. It tires to generate with PyOpenSSL and if that fails tries Cryptography. If both fail then you’re out of luck and the cert and key returned are both None.

The preference of PyOpenSSL over Cryptography is arbitrary. Both are capable of generating self signed certificate. I’m not worried which one performs better because these functions don’t do a whole lot. Also, it’s not like these will be called repeatedly in performance critical code.

Ultimately, it really doesn’t matter which ends up being used.

A note about SubjectAlternativeName

A subjectAltName is very important. We’ll use the host name as the common name which is great if you only want to access the system using your host name. I find localhost to be a bit easier most of the time. Having it listed as a subjectAltName makes things easier. While we’re at it we can add *. wild cards for both host name and localhost because, why not. This will give us some nice flexibility.

PyOpenSSL

    import random
    from OpenSSL import crypto

Start off by importing PyOpenSSL!

    pkey = crypto.PKey()
    pkey.generate_key(crypto.TYPE_RSA, 2048)

Next we’ll generate the key for the cert.

    x509 = crypto.X509()
    subject = x509.get_subject()
    subject.commonName = socket.gethostname()
    x509.set_issuer(subject)

We’ll need a OpenSSL.crypto.X509Name object to set the subject and issuer. However, the constructor for the X509Name class requires another X509Name object to duplicate. We can’t make one directly so we’ll just pull the empty subject out of the x509 we’re working on. The subject object is still owned by the x509 so modifying it will modify the x509 too.

Since this is self signed we’re also the issuer.

    x509.gmtime_adj_notBefore(0)
    x509.gmtime_adj_notAfter(5*365*24*60*60)

The easiest way to set the dates for the object are use the gmtime_adj_* functions We’ll use a range of now (0) to 5 years in the future.

    x509.set_pubkey(pkey)

Associate the key we generated with the cert.

    x509.set_serial_number(random.randrange(100000))

Serial numbers need to be unique so we’ll use a random number.

    x509.set_version(2)
    x509.add_extensions([
        crypto.X509Extension(b'subjectAltName', False,
            ','.join([
                'DNS:%s' % socket.gethostname(),
                'DNS:*.%s' % socket.gethostname(),
                'DNS:localhost',
                'DNS:*.localhost']).encode()),
        crypto.X509Extension(b"basicConstraints", True, b"CA:false")])

We must set the certificate version to 3. The code says 2 but PyOpenSSL starts counting the version at 0. So, set_version(2) means this is a version 3 cert. This is necessary for the subjectAltName to be honored.

Then we’ll add all the subject alternative names we discussed earlier.

    x509.sign(pkey, 'SHA256')

    return (crypto.dump_certificate(crypto.FILETYPE_PEM, x509),
        crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))

Finally, we’ll sign and dump the cert and key data.

Cryptography

Cryptography makes the certificate generate process a lot easier than OpenSSl because it has a handy x509.CertificateBuilder class. The vast majority of the code here is from the documentation example. I’m not going to go into too much detail because this mirrors the process described in the PyOpenSSL section.

Create the builder, and start setting the attributes using the easy to use setter functions. That said, it’s a bit verbose at times. Specifying the RSA public_exponent is required even thought he documentation says to always use 65537. I’m surprised this isn’t an optional argument with that as the default value. Another place is setting the common and issuer names requires specifying the proper NameOID.

    return (certificate.public_bytes(serialization.Encoding.PEM),
        private_key.private_bytes(serialization.Encoding.PEM,
            serialization.PrivateFormat.PKCS8,
            serialization.NoEncryption()))

Not covered in the documentation example is outputting the final certificate.

Conclusion

I found it interesting how the PyOpenSSL and Cryptography APIs differed. While PyOpenSSL isn’t as clean Cryptography is a lot more verbose. That said, they both turned out to be equally easy to use.

Now I have a very easy to use, drop in module for generating self sign certificates. This will make writing test endpoints a bit easier and definitely easier to share the scripts.