An upgradable stream starts life as a plain old socket connection, but is capable of being “upgraded” to use Transport Layer Security (TLS). This is sometimes known as STARTTLS. Common examples of this are SMTP, LDAP, and HTTP proxy tunneling with CONNECT.
The has been broken in Python, but is fixed in version 3.11!
To make things work you will need an SSL certificate and key, and for that certificate to be trusted by a certificate chain.
You can find a gist for this here.
Server
The server starts without TLS. When a client connects, the server responds to three messages:
PING
— the server responds withPONG
.STARTLS
— the server upgrades the connection to TLS.QUIT
— the server closes the client connection.
Let’s see the code.
1 | import asyncio |
Looking at the run_server
function, the first job is to build the SLL context. After creating the context, the certificate authority bundle is loaded, then the certificate and key.
The partial function binds the SSL context as the first argument to the handle_client
callback.
The server is then started without TLS and set running. The fully qualified domain name (FQDN) is used for the host (the “any” address “0.0.0.0” would be fine, but the FQDN works better on Windows).
When a client connects the handle_client
function is called. The function begins a loop. For each iteration the loop starts by reading a line from the client. If it reads PING
it writes PONG
. If it reads QUIT
it breaks out of the loop and closes the connection. If it reads STARTTLS
it calls await start_tls(ctx)
to upgrade the connection. That’s all there is to it! Very neat.
Client
This time let’s start with the code.
1 | import asyncio |
The client starts by opening a connection without TLS. As with the server the FQDN is used, but this time it’s important. When the client upgrades to TLS the host must match the certificate, so an IP address won’t work. There is a choice here though, as the start_tls
call takes the host name as an optional argument. After connecting the client checks to see if there’s an SSL server certificate with get_extra_info(“peercert”)
call. This should return None
.
Next the client writes a PING
to the server over the unencrypted stream and reads the result (which should be PONG
).
The next step is to upgrade the connection. The client writes STARTTLS
to instruct the server to start the handshake. An SSL context is then made, and the start_tls(ctx)
function is called on the writer. The client then checks for an SSL server certificate with get_extra_info("peercert”)
which should now exist.
The client then writes PING
over the now encrypted stream and reads the result (which should be PONG
).
Finally the client writes QUIT
and closes the connection.
Thoughts
It’s been a long time coming, but the result is so simple!
Good luck with your coding.