Skip to main content

Python SSL Socket Server

Published

I recently had to build a small server application in Python. It did not need to be anything complicated. It needed to run on about one hundred servers and receive a tiny command to do something when signalled and that was all. A web server would have been overkill and anyway there wasn’t one available on all of the servers in question.

So as it turns out, writing a socket server in Python is pretty trivial and the documentation includes example code for you, too. The caveat that I had to deal with is that I needed to validate that the client was who they said they were and I wanted to do it with an SSL certificate so that SSL would handle all of the authentication for me. The authorization still has to be handled by the program.

The documentation in Python for writing an SSL server is all over the place. With each version of Python 3 the library has changed in some subtle way that deprecates what was previously the preferred way so if you’re going to do this then first you should verify that what I’m showing you here is actually up to date. I’m pretty certain that this code is valid in Python 3.7, though we are running it in a 3.6 environment.

First, the server.

import socketserver
import ssl


class RequestServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    # faster re-binding
    allow_reuse_address = True

    # kick connections when we exit
    daemon_threads = True

    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
        super().__init__(server_address, RequestHandlerClass, False)

        # create an ssl context that using the dart.s.uw.edu cert that requires
        # the client to present a certificate and validates it against uwca.
        ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        ctx.verify_mode = ssl.CERT_REQUIRED
        ctx.load_verify_locations("/usr/local/ssl/certs/ca-uwca.pem")
        ctx.load_cert_chain("/usr/local/ssl/certs/dart.s.uw.edu.pem")

        # replace the socket with an ssl version of itself
        self.socket = ctx.wrap_socket(self.socket, server_side=True)

        # bind the socket and start the server
        if bind_and_activate:
            self.server_bind()
            self.server_activate()


class RequestHandler(socketserver.StreamRequestHandler):
    def handle(self):
        print("connection from {}:{}".format(
            self.client_address[0],
            self.client_address[1],
        ))

        try:
            common_name = self._get_common_name(self.request.getpeercert())
            if common_name is None or common_name != "dart.s.uw.edu":
                print("rejecting {}".format(common_name))
                self.wfile.write('{"accepted": false}\n'.encode())
                return

            # now we're going to listen to what they have to say
            data = self.rfile.readline().strip()
            print("data: {}".format(data))
            self.wfile.write('{"accepted": true}\n'.encode())
        except BrokenPipeError:
            print("broken pipe from {}:{}".format(
                self.client_address[0],
                self.client_address[1],
            ))

    def _get_common_name(self, cert):
        if cert is None:
            return None

        for sub in cert.get("subject", ()):
            for key, value in sub:
                if key == "commonName":
                    return value


# this is the server. it handles the sockets. it passes requests to the
# listener (the second argument). the server will run in its own thread so
# that we can kill it when we need to.
server = RequestServer(("0.0.0.0", 3278), RequestHandler)
server.serve_forever()

It listens on port 3278 and it listens for SSL connections. It will tell SSL clients that its hostname is “dart.s.uw.edu”. You should use whatever certificate it is that you have lying around for your server to identify itself.

You will notice the line that says load_verify_locations and the preceding line that says CERT_REQUIRED. This means that all incoming connections must present a client certificate and that certificate must have been signed by the CA indicated by load_verify_locations. This server will accept any client certificate signed by the UW Certificate Authority. That is the authentication component.

But I only want to allow connections from a certificate that I deem authorized. This is the authorization component. That’s what the private method called _get_common_name does. When given the certificate details from the client connection it will extract the client’s common name and returns that. We make sure that common name matches something authorized. In this case our server identifies itself as “dart.s.uw.edu” and only allows clients that are using that same certificate. (Is this a good idea? Probably not. But I don’t have the infrastructure to maintain lots of certificates for just this purpose. This is effective for me.)

What does a client look like to all of this? Super simple.

import socket
import ssl


ctx = ssl.create_default_context()
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True
ctx.load_verify_locations("/usr/local/ssl/certs/ca-uwca.pem")
ctx.load_cert_chain("/usr/local/ssl/certs/dart.s.uw.edu.pem")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    with ctx.wrap_socket(sock, server_hostname="dart.s.uw.edu") as ssl_sock:
        ssl_sock.connect(("localhost", 3278))
        ssl_sock.sendall(bytes("this is a test\n", "utf-8"))

This verifies that our server is presenting a valid UW Certificate Authority signed certificate. It also presents our server with a certificate with the common name dart.s.uw.edu. Finally, we tell our client that our server will identify itself as dart.s.uw.edu. If we didn’t set a server_hostname argument then the client would only validate the connection to the server if the server identified itself as localhost as that is the hostname we are connecting to. But our server is identifying itself as dart.s.uw.edu because that’s the certificate that we made it use.

One interesting note to this: I don’t know about the server code (because I haven’t tried) but the client code does NOT work with eventlet, unless I’m doing something wrong. We’ll find out when they respond to my issue request.