Шаг 213.
Python: сборник рецептов.
Сети и веб-программирование. Добавление SSL в сетевые сервисы

    На этом шаге мы рассмотрим путь решения этой задачи.

Задача

    Вы хотите реализовать сетевой сервис, использующий сокеты, где серверы и клиенты аутентифицируют друг друга и шифруют передаваемые данные с помощью SSL.

Решение

    Модуль ssl предоставляет поддержку для добавления SSL к низкоуровневым соединениям на базе сокетов. В частности, функция ssl.wrap_socket() принимает существующий сокет и оборачивает его слоем SSL. Вот, например, простой эхо-сервер, который предоставляет серверный сертификат подсоединяющимся клиентам:

from socket import socket, AF_INET, SOCK_STREAM
from socket import SOL_SOCKET, SO_REUSEADDR
import ssl

KEYFILE = 'server_key.pem'   # Приватный ключ сервера
CERTFILE = 'server_cert.pem' # Сертификат сервера (передаваемый клиенту)

def echo_client(s):
    while True:
        data = s.recv(8192)
        if data == b'':
            break
        s.send(data)
    s.close()
    print('Connection closed')

def echo_server(address):
    s = socket(AF_INET, SOCK_STREAM)
    s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    s.bind(address)
    s.listen(1)
    
    # Оборачивает слоем SSL, требуя клиентских сертификатов
    s_ssl = ssl.wrap_socket(s, 
                            keyfile=KEYFILE, 
                            certfile=CERTFILE, 
                            server_side=True
                            )
    # Ждет соединений
    while True:
        try:
            c,a = s_ssl.accept()
            print('Got connection', c, a)
            echo_client(c)
        except Exception as e:
            print('{}: {}'.format(e.__class__.__name__, e))

echo_server(('', 20000))

    Вот интерактивный сеанс, который показывает, как клиент соединяется с сервером. Клиент запрашивает у сервера его сертификат и проверяет его:

>>> from socket import socket, AF_INET, SOCK_STREAM 
>>> import ssl
>>> s = socket(AF_INET, SOCK_STREAM)
>>> s_ssl = ssl.wrap_socket(s,
        cert_reqs=ssl.CERT_REQUIRED,
        ca_certs = 'server_cert.pem')
>>> s_ssl.connect(('localhost', 20000))
>>> s_ssl.send(b'Hello World?')
12
>>> s_ssl.recv(8192) 
b'Hello World?'
>>>

    Проблема со всем этим низкоуровневым сокетным хакерством в том, что это не очень хорошо работает с существующими сетевыми сервисами, уже реализованными в стандартной библиотеке. Например, большая часть серверного кода (HTTP, XML-RPC и т. д.) базируется на библиотеке socketserver. Код клиентов тоже реализуется на более высоком уровне. В существующие сервисы можно добавить SSL, но для этого требуется немного другой подход.

    Во-первых, в серверы SSL может быть добавлен с помощью класса-миксина:

import ssl

class SSLMixin:
    '''
    Класс-миксин, который добавляет поддержку SSL 
    существующим серверам, основанным на модулe socketserver.
    '''
    def __init__(self, *args, 
                 keyfile=None, certfile=None, ca_certs=None, cert_reqs=ssl.CERT_NONE,
                 **kwargs):
        self._keyfile = keyfile
        self._certfile = certfile
        self._ca_certs = ca_certs
        self._cert_reqs = cert_reqs
        super().__init__(*args, **kwargs)

    def get_request(self):
        client, addr = super().get_request()
        client_ssl = ssl.wrap_socket(client,
                                     keyfile = self._keyfile,
                                     certfile = self._certfile,
                                     ca_certs = self._ca_certs,
                                     cert_reqs = self._cert_reqs,
                                     server_side =True)
        return client_ssl, addr

    Чтобы использовать этот миксин, вы должны добавить его к другим классам сервера. Например, вот как определить XML-RPC-сервер, который работает через SSL:

# Пример SSL-XMLRPC-сервера.

import ssl
from xmlrpc.server import SimpleXMLRPCServer
from sslmixin import SSLMixin

class SSLSimpleXMLRPCServer(SSLMixin, SimpleXMLRPCServer):
    pass

class KeyValueServer:
    _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys']
    def __init__(self, *args, **kwargs):
        self._data = {}
        self._serv = SSLSimpleXMLRPCServer(*args, allow_none=True, **kwargs)
        for name in self._rpc_methods_:
            self._serv.register_function(getattr(self, name))

    def get(self, name):
        return self._data[name]

    def set(self, name, value):
        self._data[name] = value

    def delete(self, name):
        del self._data[name]

    def exists(self, name):
        return name in self._data

    def keys(self):
        return list(self._data)

    def serve_forever(self):
        self._serv.serve_forever()

if __name__ == '__main__':
    KEYFILE='server_key.pem'   # Private key of the server
    CERTFILE='server_cert.pem' # Server certificate
    CA_CERTS='client_cert.pem' # Certificates of accepted clients

    kvserv = KeyValueServer(('', 15000),
                            keyfile=KEYFILE,
                            certfile=CERTFILE,
                            ca_certs=CA_CERTS,
                            cert_reqs=ssl.CERT_REQUIRED,
                            )
    kvserv.serve_forever()

    Чтобы использовать этот сервер, вы можете соединиться с ним с помощью обычного модуля xmlrpc.client. Просто напишите https: в URL. Например:

>>> from xmlrpc.client import ServerProxy
>>> s = ServerProxy('https://localhost:15000', allow_none=True)
>>> s.set('foo', 'bar')
>>> s.set('spam', [1, 2, 3])
>>> s.keys()
['spam', 'foo']
>>> s.get('foo')
'bar'
>>> s.get('spam')
[1, 2, 3]
>>> s.delete('spam')
>>> s.exists('spam')
False
>>>

    Сложность работы с SSL-клиентами состоит в том, что требуются дополнительные шаги, чтобы верифицировать сертификат сервера или предоставить серверу опознавательную информацию о клиенте (такую как клиентский сертификат). К сожалению, для реализации этого нет стандартного пути, так что вопрос потребует некоторого изучения. Вот, однако, пример того, как установить безопасное XML-RPC-соединение, которое проверяет сертификат сервера:

from xmlrpc.client import SafeTransport, ServerProxy
import ssl

class VerifyCertSafeTransport(SafeTransport):
    def __init__(self, cafile, certfile=None, keyfile=None):
        super().__init__()
        self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
        self._ssl_context.load_verify_locations(cafile)
        if certfile:
            self._ssl_context.load_cert_chain(certfile, keyfile)
        self._ssl_context.verify_mode = ssl.CERT_REQUIRED

    def make_connection(self, host):
        s = super().make_connection((host, {'context': self._ssl_context}))

        return s

# Создаем клиентский прокси
s = ServerProxy('https://localhost:15000', 
                transport=VerifyCertSafeTransport('server_cert.pem'),
                allow_none=True)

    Как показано выше, сервер предоставляет сертификат клиенту, и клиент его проверяет. Эта верификация может быть двунаправленной. Если сервер хочет верифицировать клиента, измените процесс его запуска на следующий:

# Создаем клиентский прокси
s = ServerProxy('https://localhost:15000', 
                transport=VerifyCertSafeTransport('server_cert.pem', 
                                                  'client_cert.pem', 
                                                  'client_key.pem'),
                allow_none=True)


Обсуждение

    Этот рецепт испытает на прочность ваши знания в области системного конфигурирования и SSL. Самое сложное - произвести первоначальную конфигурацию ключей, сертификатов и всех прочих необходимых компонентов по порядку.

    Поясним: каждая конечная точка SSL-соединения обычно имеет приватный ключ и подписанный файл сертификата. Файл сертификата содержит публичный ключ. Он предоставляется удаленному пиру (peer) при каждом соединении. Сертификаты публичных серверов обычно подписываются центром выдачи сертификатов типа Verisign, Equifax или похожими организациями (это стоит денег). Чтобы верифицировать серверные сертификаты, клиенты поддерживают файл, в котором хранятся сертификаты доверенных центров сертификации. Например, веб-браузеры поддерживают сертификаты, соответствующие главным центрам выдачи сертификатов, и используют их для проверки целостности сертификатов, предоставляемых веб-серверами при HTTPS-соединениях.

    Для целей этого рецепта вы можете создать самоподписанный сертификат. Вот как это делается:

bash % openssl req -new -x509 -days 365 -nodes -out server_cert.pem \
-keyout server_key.pem
Generating a 1024 bit RSA private key
..........................................++++++
...++++++
writing new private key to 'server_key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Illinois
Locality Name (eg, city) []:Chicago
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Dabeaz, LLC
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:localhost
Email Address []:
bash %

    При создании сертификата значения для различных полей часто могут быть произвольными. Однако поле Common Name часто содержит имя хоста DNS-сервера. Если вы просто экспериментируете на своем компьютере, используйте localhost. В противном случае используйте доменное имя компьютера, на котором будет запущен сервер.

    В качестве результата этой конфигурации вы получите файл server_key.pem, содержащий приватный ключ. Он выглядит примерно так:

-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCZrCNLoEyAKF+f9UNcFaz5Osa6jf7qkbUl8si5xQrY3ZYC7juu
nL1dZLn/VbEFIITaUOgvBtPv1qUWTJGwga62VSG1oFE0ODIx3g2Nh4sRf+rySsx2
L4442nx0z4O5vJQ7k6eRNHAZUUnCL50+YvjyLyt7ryLSjSuKhCcJsbZgPwIDAQAB
AoGAB5evrr7eyL4160tM5rHTeATlaLY3UBOe5Z8XN8Z6gLiB/ucSX9AysviVD/6F
3oD6z2aL8jbeJc1vHqjt0dC2dwwm32vVl8mRdyoAsQpWmiqXrkvP4Bsl04VpBeHw
Qt8xNSW9SFhceL3LEvw9M8i9MV39viih1ILyH8OuHdvJyFECQQDLEjl2d2ppxND9
PoLqVFAirDfX2JnLTdWbc+M11a9Jdn3hKF8TcxfEnFVs5Gav1MusicY5KB0ylYPb
YbTvqKc7AkEAwbnRBO2VYEZsJZp2X0IZqP9ovWokkpYx+PE4+c6MySDgaMcigL7v
WDIHJG1CHudD09GbqENasDzyb2HAIW4CzQJBAKDdkv+xoW6gJx42Auc2WzTcUHCA
eXR/+BLpPrhKykzbvOQ8YvS5W764SUO1u1LWs3G+wnRMvrRvlMCZKgggBjkCQQCG
Jewto2+a+WkOKQXrNNScCDE5aPTmZQc5waCYq4UmCZQcOjkUOiN3ST1U5iuxRqfb
V/yX6fw0qh+fLWtkOs/JAkA+okMSxZwqRtfgOFGBfwQ8/iKrnizeanTQ3L6scFXI
CHZXdJ3XQ6qUmNxNn7iJ7S/LDawo1QfWkCfD9FYoxBlg
-----END RSA PRIVATE KEY----

    Серверный сертификат в файле server_cert.pem выглядит похожим образом:

-----BEGIN CERTIFICATE-----
MIIC+DCCAmGgAwIBAgIJAPMd+vi45js3MA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNV
BAYTAlVTMREwDwYDVQQIEwhJbGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIG
A1UEChMLRGFiZWF6LCBMTEMxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMzAxMTEx
ODQyMjdaFw0xNDAxMTExODQyMjdaMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJ
bGxpbm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMx
EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
mawjS6BMgChfn/VDXBWs+TrGuo3+6pG1JfLIucUK2N2WAu47rpy9XWS5/1WxBSCE
2lDoLwbT79alFkyRsIGutlUhtaBRNDgyMd4NjYeLEX/q8krMdi+OONp8dM+DubyU
O5OnkTRwGVFJwi+dPmL48i8re68i0o0rioQnCbG2YD8CAwEAAaOBwTCBvjAdBgNV
HQ4EFgQUrtoLHHgXiDZTr26NMmgKJLJLFtIwgY4GA1UdIwSBhjCBg4AUrtoLHHgX
iDZTr26NMmgKJLJLFtKhYKReMFwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhJbGxp
bm9pczEQMA4GA1UEBxMHQ2hpY2FnbzEUMBIGA1UEChMLRGFiZWF6LCBMTEMxEjAQ
BgNVBAMTCWxvY2FsaG9zdIIJAPMd+vi45js3MAwGA1UdEwQFMAMBAf8wDQYJKoZI
hvcNAQEFBQADgYEAFci+dqvMG4xF8UTnbGVvZJPIzJDRee6Nbt6AHQo9pOdAIMAu
WsGCplSOaDNdKKzl+b2UT2Zp3AIW4Qd51bouSNnR4M/gnr9ZD1ZctFd3jS+C5XRp
D3vvcW5lAnCCC80P6rXy7d7hTeFu5EYKtRGXNvVNd/06NALGDflrrOwxF3Y=
-----END CERTIFICATE-----

    В серверном коде приватный ключ и файл сертификата будут передаваться различным связанным с SSL функциям-оберткам. Сертификат - это то, что будет представлено клиентам. Приватный ключ должен быть защищен и оставаться на сервере.

    В коде клиента нужно поддерживать специальный файл валидных сертифицирующих организаций - он нужен, чтобы верифицировать сертификат сервера. Если такого файла у вас нет, то, по крайней мере, вы должны поместить копию серверного сертификата на клиентском компьютере и использовать его как способ верификации. Во время соединения сервер представит свой сертификат, и затем вы используете сохраненный сертификат, который у вас есть, чтобы проверить корректность серверного.

    Серверы также могут верифицировать клиентов. Чтобы сделать это, клиентам нужно иметь собственный приватный ключ и сертификатный ключ. Сервер также поддерживает файл с доверенными центрами сертификации, чтобы проверять клиентские сертификаты.

    Если вы намереваетесь добавить поддержку SSL в реальный сетевой сервис, этот рецепт может лишь кивнуть в направлении, куда вам надлежит копать. Вам обязательно придется свериться с документацией, чтобы разобраться в вопросе. Приготовьтесь провести немало времени в попытках заставить все заработать.

    На следующем шаге мы рассмотрим передачу файловых дескрипторов сокетов между процессами.




Предыдущий шаг Содержание Следующий шаг