9.17. Зашифрованные сокеты и TLS¶
Всё, что рассматривалось до этого момента, передаёт байты в открытом виде. Любое устройство на пути между камерой и сервером – домашний маршрутизатор, интернет-провайдер, вредоносная точка доступа в кафе – в принципе может прочитать или изменить то, что проходит через него. Для большей части интернет-трафика это неприемлемо. Стандартное решение – обернуть соединение в слой шифрования: TLS, протокол Transport Layer Security. Значок замка «HTTPS» в браузере – это TLS, работающий поверх TCP, и та же самая обёртка делает «безопасным» любой другой интернет-протокол. Модуль ssl камеры – это то, что оборачивает socket в TLS.
9.17.1. Что добавляет TLS и с чем поставляется камера¶
TLS располагается между TCP и приложением – приложение записывает байты в обёрнутый в TLS сокет, TLS шифрует их и передаёт результат в TCP, а на другой стороне процесс выполняется в обратном порядке. В своей полной форме TLS даёт три гарантии поверх обычного TCP:
Конфиденциальность. Перехватчики на пути не могут прочитать то, чем обмениваются две конечные точки.
Целостность. Любое изменение трафика в процессе передачи обнаруживается; соединение разрывается вместо того, чтобы доставить искажённые данные.
Аутентификация. Сервер доказывает, что он является именно тем сервером, за который себя выдаёт, а не самозванцем (и, при необходимости, клиент тоже доказывает, кто он такой).
Первые две гарантии обеспечивает само шифрование. Для третьей требуются сертификаты хотя бы на одной стороне, а также нечто заранее доверенное, по чему эти сертификаты можно проверить. Камера OpenMV поставляется вообще без встроенного хранилища сертификатов: только что прошитая камера не доверяет ни одному центру сертификации, не имеет собственного серверного сертификата, а режим проверки по умолчанию (ssl.CERT_NONE) не проверяет сертификат партнёра ни по чему. Поэтому из коробки TLS на камере даёт вам первые две гарантии – шифрование от прослушивания и подмены пассивным наблюдателем – но не третью.
9.17.2. Шифрование исходящего соединения¶
Простейший вариант использования – обёртывание исходящего TCP-соединения. Порядок действий таков: открыть обычный TCP-сокет, передать его в ssl.wrap_socket(), а затем читать и писать через обёрнутый сокет точно так же, как вы делали бы это с обычным:
import socket
import ssl
addr = socket.getaddrinfo("example.com", 443)[0][-1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(addr)
s = ssl.wrap_socket(sock)
s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
print(s.recv(4096))
s.close()
Обёртывание выполняет TLS-рукопожатие; после этого каждый байт, проходящий через s.send, шифруется на выходе, а каждый байт, полученный из s.recv, был зашифрован на линии. Никакие сертификаты не настраивались, никакой якорь доверия не предоставлялся – TLS просто согласовывает эфемерный сеансовый ключ с тем сервером, который ответит, и использует его.
TLS-рукопожатие, которое выполняет ssl.wrap_socket(). Оно располагается поверх уже открытого TCP-соединения из предыдущего рисунка; как только обе стороны отправят Finished, остальная часть обмена шифруется в обоих направлениях.¶
Предупреждение
Это TLS только с шифрованием, без аутентификации. Камера безопасно общается с чем угодно, что ответило на другом конце TCP-соединения. Если злоумышленник посередине перенаправит соединение на контролируемый им сервер, и этот сервер предъявит любой сертификат, рукопожатие всё равно завершится успешно, и камера в итоге будет безопасно общаться с атакующим. Используйте этот режим только тогда, когда атака посередине не входит в модель угроз – закрытая локальная сеть, среда разработки, общение камеры со службой, работающей на том же оборудовании – а не при выходе в публичный интернет.
Для настоящей аутентификации – когда камера проверяет публичный сервер, камера выступает в роли TLS-сервера или используется взаимный TLS – нужно загрузить сертификаты на устройство. Полное описание приведено в Работа с сертификатами TLS.
То же самое обёртывание работает для входящего TCP-трафика, если выбрать серверный протокол и передать server_side=True в ssl.wrap_socket(). Приведённое выше предупреждение по-прежнему действует: без собственного сертификата камера не может доказать клиенту, кто она такая, и любопытный клиент на большинстве TLS-стеков увидит ошибку рукопожатия «no certificate». Рабочий процесс с сертификатами на стороне продакшена – это то, что позволяет полноценно запускать камеру в роли TLS-сервера.
9.17.3. С asyncio¶
В главе об asyncio была показана asyncio.open_connection() для обычных TCP-клиентов. Этот же вызов принимает именованный аргумент ssl=True, который оборачивает соединение в TLS, опять же без какой-либо настройки сертификатов:
import asyncio
async def main():
reader, writer = await asyncio.open_connection(
"example.com", 443, ssl=True,
)
writer.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
await writer.drain()
print(await reader.read(4096))
writer.close()
await writer.wait_closed()
asyncio.run(main())
Пара reader/writer за TLS-соединением имеет ту же форму, что и для обычного TCP-соединения – различается только настройка. Действует та же оговорка об аутентификации: ssl=True сам по себе даёт только шифрование, но не проверку.
9.17.4. DTLS – TLS поверх UDP¶
TLS, который обсуждался до сих пор, работает поверх TCP. Параллельный протокол для UDP – это DTLS (Datagram TLS), и модуль ssl камеры поддерживает его таким же образом. Если TLS превращает одно TCP-соединение в один зашифрованный поток байтов, то DTLS превращает один UDP-сокет в поток зашифрованных, доставляемых по отдельности датаграмм – так что свойства UDP из UDP – отправь пакет и надейся на лучшее, связанные с потерями, нарушением порядка и отсутствием контроля потока, полностью переносятся, при этом байты внутри каждой датаграммы теперь зашифрованы.
Обёртывание выглядит так же, как в случае с TLS, только с сокетом SOCK_DGRAM и константами протокола DTLS:
import socket
import ssl
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(socket.getaddrinfo("example.com", 4433)[0][-1])
ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
s = ctx.wrap_socket(sock)
s.send(b"ping")
print(s.recv(64))
s.close()
(Вызов connect() на UDP-сокете не открывает соединение – он просто запоминает назначение по умолчанию, чтобы последующим вызовам send() / recv() не приходилось его повторять. DTLS нужно это фиксированное назначение, чтобы выполнить против него своё рукопожатие.)
Рукопожатие имеет ту же форму, что и на диаграмме TLS выше; разница в том, что каждое сообщение рукопожатия само является UDP-датаграммой, и любая из сторон будет повторять отправку при потере.
Примечание
Нарушает ли потеря пакетов шифрование? Нет. Каждый DTLS-пакет несёт порядковый номер, и шифрование использует этот номер для получения разного выхода для каждого пакета – так что один и тот же вход никогда не шифруется в одни и те же байты дважды, и любой пакет можно расшифровать сам по себе, без необходимости получения предыдущего. Потерянные или пришедшие не по порядку пакеты не рассинхронизируют две стороны. (Само рукопожатие – это единственная часть, которая должна дойти надёжно, и DTLS обеспечивает это собственной повторной передачей.)
Действует то же приведённое выше предупреждение о шифровании без сертификатов: DTLS-рукопожатие с партнёром в режиме CERT_NONE шифрует трафик, но не проверяет, кто находится на другой стороне. Полный рабочий процесс DTLS – сертификаты, серверный анти-спуфинговый куки, то, как это та же самая поверхность, что и TLS, за исключением констант протокола – рассматривается вместе с материалом по TLS в Работа с сертификатами TLS.
Версия с asyncio использует тот же шаблон неблокирующего UDP из Сокеты с asyncio. Выполните рукопожатие синхронно в начале, переведите сокет в неблокирующий режим, а затем опрашивайте его внутри корутины:
import asyncio
import socket
import ssl
async def dtls_ping(target_addr, period_ms):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(target_addr)
# Handshake while still blocking, then switch to async polling.
ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
s = ctx.wrap_socket(sock)
s.setblocking(False)
while True:
try:
s.send(b"ping")
except OSError:
pass
await asyncio.sleep_ms(period_ms)
Рукопожатие – единственное место, где эта корутина блокирует цикл событий; после него каждый вызов s.send / s.recv возвращается немедленно (или возбуждает OSError), а await asyncio.sleep_ms позволяет остальной части программы продолжать работу.
9.17.5. Что дальше¶
Всё, что выходит за рамки TLS только с шифрованием – проверка сертификата публичного HTTPS-сервера, запуск камеры в роли аутентифицированного TLS-сервера, взаимный TLS между камерой и бэкендом, выбор ключей и их типов, обработка истечения срока действия сертификатов – находится в Работа с сертификатами TLS. Этот раздел рассказывает, как генерировать самоподписанные сертификаты для локального тестирования, как получить подписанные центром сертификации сертификаты для продакшена, как загрузить их на камеру в правильном формате (DER), как проверить публичный сервер, когда камера выступает клиентом, как думать о защите ключей на устройстве, которое злоумышленник может разобрать, и как спланировать день, когда срок действия сертификата истечёт.
Полный справочник по API ssl – поддерживаемые версии TLS, наборы шифров и параметры контекста – см. в ssl — Модуль SSL/TLS.