14.4.5. Перевірка публічного сервера (камера як клієнт)

Усе сказане на попередній сторінці про клієнта, який «вже має кореневий сертифікат», справедливо для браузерів, телефонів і ПК – але не для камери. MicroPython’s ssl не містить вбудованого сховища довіри: щойно прошита камера не довіряє жодному CA, а налаштування за замовчуванням (ssl.CERT_NONE) нічого не перевіряє та повністю відкрита для атаки типу «людина посередині». Тому якщо камера є клієнтом, який підключається до публічного TLS-сервера (HTTPS API, MQTT-брокер тощо) і ви хочете справжньої перевірки цього сервера, потрібно самостійно надати якір довіри.

Механіка та ж, що й у прикладі з самопідписаним клієнтом на сторінці Самопідписані сертифікати; єдина відмінність — файл, який ви завантажуєте, є справжнім сертифікатом CA, а не власним сертифікатом вузла:

  1. Отримайте сертифікат CA, який є якорем ланцюжка сервера. «Якір» означає сертифікат на вершині (або поблизу вершини) ланцюжка сервера, який ви обираєте як початкову точку довіри. TLS-сервер надсилає свій листовий та зазвичай проміжний сертифікат(и); кореневий він ніколи не надсилає. Якір довіри ви повинні отримати самостійно і незалежно від сервера – просто довіряти тому, що сервер надсилає, зводить нанівець весь сенс перевірки.

    Спочатку з’ясуйте, який CA фактично видав сертифікат сервера. Наприклад, для openmv.io

    openssl s_client -connect openmv.io:443 -showcerts < /dev/null
    

    Блок Certificate chain містить кожен сертифікат з його суб’єктом (s:) та видавцем (i:); нові версії OpenSSL також виводять рядки a: (тип ключа) та v: (термін дії), які тут можна ігнорувати:

    Certificate chain
     0 s:CN=openmv.io
       i:C=US, O=Let's Encrypt, CN=E8
     1 s:C=US, O=Let's Encrypt, CN=E8
       i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
    

    Запис 0 — це листовий сертифікат (openmv.io), виданий проміжним CA E8. Запис 1 — це цей проміжний CA, виданий кореневим ISRG Root X1. Видавець (i:) найвищого запису називає корінь – тут ISRG Root X1. (Проміжний – E8, а не R10 / R11, які ви могли бачити в інших місцях, тому що openmv.io використовує сертифікат ECDSA; Let’s Encrypt підписує листові сертифікати ECDSA своїми проміжними сертифікатами серії E, а листові RSA – серії R. Обидва ведуть до ISRG Root X1.)

    OpenSSL також виводить рядки depth= і може повідомити про корінь із Verification: OK. Це відбувається лише тому, що ваш ПК вже довіряє ISRG Root X1 – сервер його не надсилав (сервер ніколи не надсилає свій корінь), і камера, не маючи сховища довіри, його теж не матиме. Саме тому ви повинні надати його самостійно.

    Завантажте цей кореневий сертифікат з опублікованих кореневих сертифікатів самого CA. Let’s Encrypt каталогізує всі свої на сторінці сертифікатів Let’s Encrypt; прямий файл для ISRG Root X1 — isrgrootx1.pem (вони також пропонують його у вигляді isrgrootx1.der). Інші CA публікують свої на схожій сторінці «кореневих сертифікатів» / «сховища»; канонічний публічний набір — Mozilla CA program (CCADB). Підтвердіть завантажений файл, порівнявши його відбиток із значенням, опублікованим CA (додайте -inform DER, якщо завантажили .der):

    openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256
    

    Якщо ви не хочете відстежувати кореневий сертифікат, ви можете натомість скопіювати проміжний безпосередньо з виводу -showcerts (другий блок -----BEGIN CERTIFICATE-----), довіряти йому та прийняти, що вам доведеться оновлювати його щоразу, коли CA ротує проміжний – що відбувається набагато частіше, ніж ротація кореневого (детальніше про компроміс нижче).

  2. Конвертуйте в DER, точно як раніше:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Скопіюйте ca.der на камеру (файлова система або ROMFS) та завантажте як якір довіри:

    import socket
    import ssl
    import ntptime
    
    ntptime.settime()                  # validity check needs the clock
    
    addr = socket.getaddrinfo("api.example.com", 443)[0][-1]
    sock = socket.socket()
    sock.connect(addr)
    
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.verify_mode = ssl.CERT_REQUIRED
    ctx.load_verify_locations(cafile="ca.der")
    ssock = ctx.wrap_socket(sock, server_hostname="api.example.com")
    

    server_hostname тут є обов’язковим: він керує SNI і є ім’ям, яке перевіряється проти subjectAltName сертифіката сервера.

Порада

Скорочення для поширеного випадку. Let’s Encrypt є найбільш широко використовуваним публічним CA, і обидва його RSA та ECDSA сертифікати наразі ведуть до ISRG Root X1 (як показано в прикладі з openmv.io вище). Якщо сервери, до яких підключається ваша камера, використовують Let’s Encrypt, ви можете пропустити перевірку: просто покладіть isrgrootx1.der на камеру та викличте load_verify_locations для нього.

Це не робить TLS робочим для кожного сайту. Сервер, сертифікат якого видано іншим CA (DigiCert, Google Trust Services, Amazon, Sectigo тощо), все одно не пройде перевірку, і оскільки камера довіряє одному DER-сертифікату на ssl.SSLContext, ви не можете зібрати всі кореневі сертифікати, як це робить браузер. У разі сумнівів визначте фактичний CA сервера, як показано вище, та довіряйте цьому кореневому сертифікату.

Вибір сертифіката для довіри є компромісом:

  • Кореневий (рекомендовано). Довгостроковий – часто десятиліттями – тому ca.der рідко змінюється. Вимагає від сервера надсилати свій проміжний сертифікат, щоб mbedTLS міг побудувати шлях листовий → проміжний → ваш довірений корінь; практично кожен правильно налаштований публічний сервер це робить.

  • Проміжний. Також працює і продовжує працювати, навіть якщо сервер не надсилає проміжний, але проміжні ротуються набагато частіше, ніж кореневі, тому вам доведеться частіше оновлювати ca.der.

  • Сам листовий сертифікат (прив’язка сертифіката). Найбільш обмежений, але листовий сертифікат змінюється при кожному поновленні – приблизно кожні 90 днів для Let’s Encrypt – тому це має сенс лише тоді, коли ви також контролюєте сервер і можете одночасно надіслати новий прив’язаний сертифікат на кожну камеру. Саме це й робить приклад із самопідписаним клієнтом.

Примітка

ssl.SSLContext.load_verify_locations() приймає один DER-кодований сертифікат CA, тому камера довіряє рівно одному якорю одночасно. Щоб підключатися до серверів з різними CA, використовуйте окремий ssl.SSLContext для кожного якоря. І оскільки цей сертифікат з часом може закінчитися або бути ротований CA, ставтесь до нього як до будь-якого іншого сертифіката на пристрої.