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.