Python SSL Socket Server

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 and then be done. A web server would have been overkill and a was definitely not available on all of the hundred servers. 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 would still have to be handled by me.)

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 verify that what I’m showing you here is 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

    # make this bigger than five
    request_queue_size = 10

    # 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_certificate_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_certificate_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 laying around for your server to identify itself.

You’ll 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_certificate_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 ssock:
        ssock.connect(("localhost", 3278))
        ssock.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 the 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.