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 Cam поставляється без вбудованого сховища сертифікатів: щойно прошита камера не довіряє жодному центру сертифікації, не має власного серверного сертифіката, а стандартний режим перевірки (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 просто погоджує ефемерний сесійний ключ із тим сервером, що відповість, і використовує його.

A diagram with two columns labelled "client" and "server". A dashed horizontal line near the top is labelled "TCP connection already open". Below it, three arrows show the TLS handshake: "ClientHello" from client to server, "ServerHello + certificate + key share" back, and "Finished" forward again. A second dashed horizontal line below is labelled "TLS session open -- everything after this is encrypted". Two thick bidirectional arrows below it carry "encrypted data".

TLS-рукостискання, яке виконує ssl.wrap_socket(). Воно розміщується поверх вже відкритого TCP-з’єднання з попереднього рисунка; як тільки обидві сторони надіслали Finished, решта розмови шифрується в обох напрямках.

Попередження

Це лише шифрування, а не автентифікований TLS. Камера безпечно спілкується з тим, хто відповів на іншому кінці TCP-з’єднання. Якщо атака «людина посередині» перенаправляє з’єднання на підконтрольний сервер і той сервер пред’являє будь-який сертифікат, рукостискання все одно успішне, і камера в підсумку безпечно спілкується зі зловмисником. Використовуйте цей режим лише тоді, коли атака «людина посередині» не входить до моделі загроз – закрита локальна мережа, середовище розробки, камера спілкується з сервісом на тому ж обладнанні – не при виході в публічний інтернет.

Для справжньої автентифікації – коли камера перевіряє публічний сервер, виступає TLS-сервером або здійснюється взаємний TLS – потрібно завантажити сертифікати на пристрій. Повний опис наведено в Робота з TLS-сертифікатами.

Те саме огортання працює для вхідного TCP-трафіку: потрібно вибрати серверний протокол і передати server_side=True до ssl.wrap_socket(). Попередження вище все ще діє: без власного сертифіката камера не може довести клієнту, хто вона є, і допитливий клієнт побачить помилку рукостискання «немає сертифіката» на більшості TLS-стеків. Робочий процес із сертифікатом з боку продакшена – це те, що дозволяє корисно запускати камеру як 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-робочий процес – сертифікати, anti-spoofing cookie на стороні сервера, чим він схожий на 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-сертифікатами. Цей розділ охоплює: як генерувати самопідписані сертифікати для локального тестування, як отримати сертифікати, підписані CA, для продакшена, як завантажити їх на камеру у правильному форматі (DER), як перевіряти публічний сервер, коли камера є клієнтом, як думати про захист ключів на пристрої, який зловмисник може розібрати, і як планувати на той день, коли сертифікат закінчиться.

Для повного довідника API ssl – підтримувані версії TLS, набори шифрів і параметри контексту – дивіться ssl — модуль SSL/TLS.