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

Всё, что было сказано на предыдущей странице о клиенте, который «уже имеет корневой сертификат», верно для браузеров, телефонов и ПК – но это не так для камеры. Модуль ssl в MicroPython поставляется без встроенного хранилища доверия: только что прошитая камера не доверяет вообще ни одному 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 своими промежуточными CA серии 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 (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 чаще.

  • Сам конечный сертификат (закрепление сертификата, pinning). Самый строгий вариант, но конечный сертификат меняется при каждом продлении – примерно каждые 90 дней для Let’s Encrypt – поэтому это имеет смысл только тогда, когда вы также контролируете сервер и можете синхронно отправить новый закреплённый сертификат на каждую камеру. Именно это и делает пример с самоподписанным клиентом.

Примечание

Метод ssl.SSLContext.load_verify_locations() принимает один сертификат CA в кодировке DER, поэтому камера доверяет ровно одному якорю за раз. Чтобы обращаться к серверам под разными CA, используйте отдельный ssl.SSLContext для каждого якоря. И поскольку сам этот сертификат со временем тоже истечёт или будет заменён CA, относитесь к нему как к любому другому сертификату на устройстве.