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
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
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.
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.
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
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
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
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.
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
cards for both host name and
localhost because, why not. This will give
us some nice flexibility.
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.
The easiest way to set the dates for the object are use the
functions We’ll use a range of now (0) to 5 years in the future.
Associate the key we generated with the cert.
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 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
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
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.
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.