简体   繁体   中英

How can I create a Python SSL Client/Server pair where only the server authenticates the client

I have been trying to get a simple Python SSL example working for a day now with no luck. I want to create an SSL server and SSL client. The server should authenticate the client. The Python docs are pretty light on examples for the SSL module, and in general I can't find many working examples. The code I am working with is as follows;

Client:

import socket
import ssl


class SSLClient:
    def __init__(self, server_host, server_port, client_cert, client_key):
        self.server_host = server_host
        self.server_port = server_port
        self._context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        self._context.load_cert_chain(client_cert, client_key)
        self._sock = None
        self._ssock = None

    def __del__(self):
        self.close()

    def connect(self):
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._ssock = self._context.wrap_socket(
            self._sock, server_hostname=self.server_host
        )
        self._ssock.connect((self.server_host, self.server_port))

    def send(self, msg):
        self._ssock.send(msg.encode())

    def close(self):
        self._ssock.close()

Server:

import socket
import ssl
from threading import Thread


class SSLServer:
    def __init__(self, host, port, cafile, chunk_size=1024):
        self.host = host
        self.port = port
        self.chunk_size = chunk_size
        self._context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        self._context.load_verify_locations(cafile)
        self._ssock = None

    def __del__(self):
        self.close()

    def connect(self):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
            sock.bind((self.host, self.port))
            sock.listen(5)
            with self._context.wrap_socket(sock, server_side=True) as self._ssock:
                conn, _ = self._ssock.accept()

                while True:
                    data = conn.recv(self.chunk_size).decode()
                    print(data)
                    if data is None:
                        break

    def close(self):
        self._ssock.close()


class SSLServerThread(Thread):
    def __init__(self, server):
        super().__init__()
        self._server = server
        self.daemon = True

    def run(self):
        self._server.connect()

    def stop(self):
        self._server.close()

Test script:

import client, server
from os import path
from time import sleep

server_host = "localhost"
server_port = 11234
client_cert = path.join(path.dirname(__file__), "client.crt")
client_key = path.join(path.dirname(__file__), "client.key")

s = server.SSLServer(server_host, server_port, client_cert)
s_thread = server.SSLServerThread(s)
s_thread.start()
sleep(2)
c = client.SSLClient(server_host, server_port, client_cert, client_key)
c.connect()

c.send("This is a test message!")

c.close()
s.close()

I generated my client certificate and key using the following command:

openssl req -newkey rsa:2048 \
            -x509 \
            -sha256 \
            -days 3650 \
            -nodes \
            -out client.crt \
            -keyout client.key \
            -subj "/C=UK/ST=Scotland/L=Glasgow/O=Company A/OU=Testing/CN=MyName"

The test script seems to start the server and allow the client to connect, but I am getting a BrokenPipeError when I try to send the test message.

Annoyingly I have been getting various different error messages as I go, so it's likely a combination of things. This is a simple example I created to try and get something working. On my more complex example I get "NO_SHARED_CIPHERS" when the client attempts to connect to the server. Annoyingly I can't see why this simple example seems to get further than the more complex one (ie the connection seems to be established successfully) even though they are set up almost identically.

I have uploaded a repo at git@github.com:stevengillies87/python-ssl-client-auth-example.git if anyone would like to test it.


I realised the first bug came from copy pasting and example and not realising how it differed from my code in its setup. It used socket.socket() to create the socket whereas my example used socket.create_connection(), which also connects the socket. This was the reason I was getting a BrokenPipeError. Now both my simple example and the actual code I am writing both have a consistent NO_SHARED_CIPHER error. I added a line to the source code to connect the client after the socket has been wrapped.

So, as expected it was a combination of things.

Before I added the SSL layer to my code it worked with TCP sockets. I was using socket.create_connection() in the client to create and connect a socket in one call. When I added SSL I continued to do this but because I was attempting to connect to an SSL server via a TCP socket I was getting a NO_SHARED_CIPHER error.

The solution to this problem was to only create the TCP socket with sock = socket.socket() , wrap it with ssock = ssl_context.wrap_context(sock) and then call connect on the SSL layer, ssock.connect((host, port)) .

However, I was still getting a handshaking error on connection. I found this link, https://www.electricmonk.nl/log/2018/06/02/ssl-tls-client-certificate-verification-with-python-v3-4-sslcontext/ , which provided a detailed example of how to create mutually authenticating SSL client/server. Crucially, the author pointed out that hostname used for server authentication must match the "common name" entered when creating the server.crt and server.key files. Previously I had just been using the same host that I was connecting to, "localhost" in this case. They also noted that the ssl_context verify mode should be set to verify_mode = ssl.CERT_REQUIRED for client auth.

Once the example worked I set about removing the client auth of the server. This was done by changing the client SSL context from ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) to ssl.SSLContext() . The client now does not require the server.crt file to connect successfully.

Frustratingly I still need to create server cert/key files and load them into the server using ssl_context.load_cert_chain() , even though I do not need the server to be authenticated. If I try to remove this step from the server I get a NO_SHARED_CIPHER error again. If anyone knows how I can avoid this then please let me know, or explain why it is necessary.

Working code below, and updated at the github link in the question.

Client:

import socket
import ssl

class SSLClient:
    def __init__(
        self, server_host, server_port, sni_hostname, client_cert, client_key,
    ):
        self.server_host = server_host
        self.server_port = server_port
        self.sni_hostname = sni_hostname
        self._context = ssl.SSLContext()
        self._context.load_cert_chain(client_cert, client_key)
        self._sock = None
        self._ssock = None

    def __del__(self):
        self.close()

    def connect(self):
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._ssock = self._context.wrap_socket(self._sock,)
        self._ssock.connect((self.server_host, self.server_port))

    def send(self, msg):
        self._ssock.send(msg.encode())

    def close(self):
        self._ssock.close()

Server:

import socket
import ssl
from threading import Thread


class SSLServer:
    def __init__(
        self, host, port, server_cert, server_key, client_cert, chunk_size=1024
    ):
        self.host = host
        self.port = port
        self.chunk_size = chunk_size
        self._context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        self._context.verify_mode = ssl.CERT_REQUIRED
        self._context.load_cert_chain(server_cert, server_key)
        self._context.load_verify_locations(client_cert)

    def connect(self):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
            sock.bind((self.host, self.port))
            sock.listen(5)
            while True:
                conn, _ = sock.accept()
                with self._context.wrap_socket(conn, server_side=True) as sconn:
                    self._recv(sconn)

    def _recv(self, sock):
        while True:
            data = sock.recv(self.chunk_size)
            if data:
                print(data.decode())
            else:
                break


class SSLServerThread(Thread):
    def __init__(self, server):
        super().__init__()
        self._server = server
        self.daemon = True

    def run(self):
        self._server.connect()

Test:

import client, server
from os import path
from time import sleep

server_host = "127.0.0.1"
server_port = 35689
server_sni_hostname = "www.company-b.com"
client_cert = path.join(path.dirname(__file__), "client.crt")
client_key = path.join(path.dirname(__file__), "client.key")
server_cert = path.join(path.dirname(__file__), "server.crt")
server_key = path.join(path.dirname(__file__), "server.key")

s = server.SSLServer(server_host, server_port, server_cert, server_key, client_cert)
s_thread = server.SSLServerThread(s)
s_thread.start()
sleep(2)
c = client.SSLClient(
    server_host, server_port, server_sni_hostname, client_cert, client_key
)
c.connect()

c.send("This is a test message!")

c.close()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM