# TLS encryption with a self-signed PKI and Python’s asyncio module

I want to enable TLS encryption with a custom / self-signed PKI for a self-contained distributed system built with Python 3’s asyncio.

I’ve read Python’s ssl docs as well as several tutorials:

From all this, I extracted the following steps in order to create a minimal example for a simple client and server with a self-signed PKI.

It seems to work as expected, but I’m unsure if I missed anything or create non-obvious bugs which could reduce or nullify the security of the system.

Here is what I did so far:

I'll create one root CA which signs a CSR for each machine. Each machine will have its own certificate and key.

Considerations for the root CA:

• The key length should be 2048 bits, or better, 4096 bits.
• A strong passphrase should be used.
• The key should not leave the machine (except if you store it on an SD card and hide it somewhere save). Level of paranoia should depend on the security needs.
• The certificate should not use the same FQDN as any production system.

Generate the key and a self-signed certificate:

$openssl genrsa -aes256 -out ca.key 4096$ openssl req -new -x509 -nodes -key ca.key -out ca.pem -days 1000


Create a key and a certificate signing request (CSR) on a machine:

$openssl genrsa -out server.key 4096$ openssl genrsa -out client.key 4096
$openssl req -new -key server.key -out server.csr$ openssl req -new -key client.key -out client.csr


Now you can sign the device key with your CA cert:

$openssl x509 -CA ca.pem -CAkey ca.key -CAcreateserial -req -in server.csr -out server.pem -days 365$ openssl x509 -CA ca.pem -CAkey ca.key -CAcreateserial -req -in client.csr -out client.pem -days 365


Minimal Python example. We need different SSLContexts for the client and server, because they do some things differently. Both, client and server, will use TLS 1.2 with ECDH-AESGCM. They will verify each other’s certificates against the root CA cert. The client also checks the server’s host name. ECDH keys are only used once per SSL session and compression is disabled:

import asyncio
import ssl

@asyncio.coroutine
print(data)
writer.write(b'cya')
yield from writer.drain()
writer.close()

@asyncio.coroutine
writer.write(b'ohai')
print(data)
writer.close()

loop = asyncio.get_event_loop()

# Setup server
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
server_ctx.verify_mode = ssl.CERT_REQUIRED
server_ctx.options |= ssl.OP_SINGLE_ECDH_USE
server_ctx.options |= ssl.OP_NO_COMPRESSION
server_ctx.set_ciphers('ECDH+AESGCM')

server = loop.run_until_complete(coro)

# Run client
client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
client_ctx.verify_mode = ssl.CERT_REQUIRED
client_ctx.check_hostname = True
client_ctx.set_ciphers('ECDH+AESGCM')

# Shutdown
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()


The script will output

b'ohai'
b'cya'


as expected and TLS seems to work. Are there any problems with it or things that could be done better?

• This belongs on Code Review, not here. – otus Jun 29 '15 at 16:38
• Thanks, I’ll re-post it there. I was not aware of this community and thought crypto would fit. – Stefan Scherfke Jun 29 '15 at 21:00
• Code Review seems to be the wrong place, too. Since the main focus of my question is SSL certificates and parameters, this might indeed be the right community. codereview.stackexchange.com/questions/95143/… – Stefan Scherfke Jun 30 '15 at 10:40
• Oh, sorry for the misleading pointer in case CR was wrong. However, questions with code generally get closed here. – otus Jun 30 '15 at 12:06
• My question is more about the parameter values for the SSLContext and the PKI creation processs and less about the code (especially that of the server and client) itself. – Stefan Scherfke Jun 30 '15 at 13:20

Since I stumbled on this checking something else, a few minor points

• A CA doesn't sign a CSR; it signs (issues) a cert that has some important data taken from the CSR but much data different.

• The Subject names of the CA cert and all server and client certs it issues should be unique, and preferably mnemonic. The CA and client names don't need to be FQDNs, or domainnames at all, although if they are FQDNs that helps make them unique, and usually mnemonic. For a server, either the CommonName part of the Subject or the SubjectAlternativeName extension if used should -- and for many but not all clients must -- match the 'hostname' used by the client(s) to connect to the server. Normally this is the FQDN, but in test and intranet settings it might not; your example shows connecting to the loopback or 'localhost' address 127.0.0.1.

• ECDH-AESGCM is not a ciphersuite or even a keyexchange in SSL/TLS; ECDH+AESGM is an OpenSSL shorthand for 8 different cipher suites -- only 2 of which work with an RSA key+cert, namely ECDHE-RSA-AES256-GCM-SHA384 and ECDHE-RSA-AES128-GCM-SHA256.

• You don't visibly specify the temporary (ephemeral) ecdh parameters (curve) so you are presumably getting a default from Python; OpenSSL itself doesn't have a default, although 1.0.2 up has a nondefault option to automatically use the client preference. Since you've chosen everything else pretty strong, you should make sure the ECDHE curve isn't unacceptably weak. I would hope it can be found in the Python source; if not, monitor an actual connection with Wireshark or similar to check.

If you're just looking for confirmation that the context has logical values being set, then I could say it does. Can't say I know anything about doing this in python though.