SSL/TLS client certificate verification with Python v3.4+ SSLContext
Saturday, June 2nd, 2018
Normally, an SSL/TLS client verifies the server’s certificate. It’s also possible for the server to require a signed certificate from the client. These are called Client Certificates. This ensures that not only can the client trust the server, but the server can also trusts the client.
Traditionally in Python, you’d pass the ca_certs
parameter to the ssl.wrap_socket()
function on the server to enable client certificates:
# Client ssl.wrap_socket(s, ca_certs="ssl/server.crt", cert_reqs=ssl.CERT_REQUIRED, certfile="ssl/client.crt", keyfile="ssl/client.key") # Server ssl.wrap_socket(connection, server_side=True, certfile="ssl/server.crt", keyfile="ssl/server.key", ca_certs="ssl/client.crt")
Since Python v3.4, the more secure, and thus preferred method of wrapping a socket in the SSL/TLS layer is to create an SSLContext
instance and call SSLContext.wrap_socket()
. However, the SSLContext.wrap_socket()
method does not have the ca_certs
parameter. Neither is it directly obvious how to enable requirement of client certificates on the server-side.
The documentation for SSLContext.load_default_certs()
does mention client certificates:
Purpose.CLIENT_AUTH loads CA certificates for client certificate verification on the server side.
But SSLContext.load_default_certs()
loads the system’s default trusted Certificate Authority chains so that the client can verify the server‘s certificates. You generally don’t want to use these for client certificates.
In the Verifying Certificates section, it mentions that you need to specify CERT_REQUIRED
:
In server mode, if you want to authenticate your clients using the SSL layer (rather than using a higher-level authentication mechanism), you’ll also have to specify CERT_REQUIRED and similarly check the client certificate.
I didn’t spot how to specify CERT_REQUIRED
in either the SSLContext
constructor or the wrap_socket()
method. Turns out you have to manually set a property on the SSLContext
on the server to enable client certificate verification, like this:
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.verify_mode = ssl.CERT_REQUIRED context.load_cert_chain(certfile=server_cert, keyfile=server_key) context.load_verify_locations(cafile=client_certs)
Here’s a full example of a client and server who both validate each other’s certificates:
For this example, we’ll create Self-signed server and client certificates. Normally you’d use a server certificate from a Certificate Authority such as Let’s Encrypt, and would setup your own Certificate Authority so you can sign and revoke client certificates.
Create server certificate:
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt
Make sure to enter ‘example.com’ for the Common Name.
Next, generate a client certificate:
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout client.key -out client.crt
The Common Name for the client certificate doesn’t really matter.
Client code:
#!/usr/bin/python3 import socket import ssl host_addr = '127.0.0.1' host_port = 8082 server_sni_hostname = 'example.com' server_cert = 'server.crt' client_cert = 'client.crt' client_key = 'client.key' context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=server_cert) context.load_cert_chain(certfile=client_cert, keyfile=client_key) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = context.wrap_socket(s, server_side=False, server_hostname=server_sni_hostname) conn.connect((host_addr, host_port)) print("SSL established. Peer: {}".format(conn.getpeercert())) print("Sending: 'Hello, world!") conn.send(b"Hello, world!") print("Closing connection") conn.close()
Server code:
#!/usr/bin/python3 import socket from socket import AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET, SHUT_RDWR import ssl listen_addr = '127.0.0.1' listen_port = 8082 server_cert = 'server.crt' server_key = 'server.key' client_certs = 'client.crt' context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.verify_mode = ssl.CERT_REQUIRED context.load_cert_chain(certfile=server_cert, keyfile=server_key) context.load_verify_locations(cafile=client_certs) bindsocket = socket.socket() bindsocket.bind((listen_addr, listen_port)) bindsocket.listen(5) while True: print("Waiting for client") newsocket, fromaddr = bindsocket.accept() print("Client connected: {}:{}".format(fromaddr[0], fromaddr[1])) conn = context.wrap_socket(newsocket, server_side=True) print("SSL established. Peer: {}".format(conn.getpeercert())) buf = b'' # Buffer to hold received client data try: while True: data = conn.recv(4096) if data: # Client sent us data. Append to buffer buf += data else: # No more data from client. Show buffer and close connection. print("Received:", buf) break finally: print("Closing connection") conn.shutdown(socket.SHUT_RDWR) conn.close()
Output from the server looks like this:
$ python3 ./server.py Waiting for client Client connected: 127.0.0.1:51372 SSL established. Peer: {'subject': ((('countryName', 'AU'),), (('stateOrProvinceName', 'Some-State'),), (('organizationName', 'Internet Widgits Pty Ltd'),), (('commonName', 'someclient'),)), 'issuer': ((('countryName', 'AU'),), (('stateOrProvinceName', 'Some-State'),), (('organizationName', 'Internet Widgits Pty Ltd'),), (('commonName', 'someclient'),)), 'notBefore': 'Jun 1 08:05:39 2018 GMT', 'version': 3, 'serialNumber': 'A564F9767931F3BC', 'notAfter': 'Jun 1 08:05:39 2019 GMT'} Received: b'Hello, world!' Closing connection Waiting for client
Output from the client:
$ python3 ./client.py SSL established. Peer: {'notBefore': 'May 30 20:47:38 2018 GMT', 'notAfter': 'May 30 20:47:38 2019 GMT', 'subject': ((('countryName', 'NL'),), (('stateOrProvinceName', 'GLD'),), (('localityName', 'Ede'),), (('organizationName', 'Electricmonk'),), (('commonName', 'example.com'),)), 'issuer': ((('countryName', 'NL'),), (('stateOrProvinceName', 'GLD'),), (('localityName', 'Ede'),), (('organizationName', 'Electricmonk'),), (('commonName', 'example.com'),)), 'version': 3, 'serialNumber': 'CAEC89334941FD9F'} Sending: 'Hello, world! Closing connection
A few notes:
- You can concatenate multiple client certificates into a single PEM file to authenticate different clients.
- You can re-use the same cert and key on both the server and client. This way, you don’t need to generate a specific client certificate. However, any clients using that certificate will require the key, and will be able to impersonate the server. There’s also no way to distinguish between clients anymore.
- You don’t need to setup your own Certificate Authority and sign client certificates. You can just generate them with the above mentioned openssl command and add them to the trusted certificates file. If you no longer trust the client, just remove the certificate from the file.
- I’m not sure if the server verifies the client certificate’s expiration date.