На этом шаге мы рассмотрим путь решения этой задачи.
Вы хотите реализовать сетевой сервис, использующий сокеты, где серверы и клиенты аутентифицируют друг друга и шифруют передаваемые данные с помощью 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 в реальный сетевой сервис, этот рецепт может лишь кивнуть в направлении, куда вам надлежит копать. Вам обязательно придется свериться с документацией, чтобы разобраться в вопросе. Приготовьтесь провести немало времени в попытках заставить все заработать.
На следующем шаге мы рассмотрим передачу файловых дескрипторов сокетов между процессами.