14.4.5. Проверка публичного сервера (камера в роли клиента)¶
Всё, что было сказано на предыдущей странице о клиенте, который «уже имеет корневой сертификат», верно для браузеров, телефонов и ПК – но это не так для камеры. Модуль ssl в MicroPython поставляется без встроенного хранилища доверия: только что прошитая камера не доверяет вообще ни одному CA, а режим по умолчанию (ssl.CERT_NONE) ничего не проверяет и полностью открыт для атаки «человек посередине». Поэтому, когда камера выступает в роли клиента, подключающегося к публичному TLS-серверу (HTTPS API, брокер MQTT, …), и вы хотите, чтобы она действительно проверяла этот сервер, вы должны сами предоставить якорь доверия.
Механика та же, что и в примере с самоподписанным клиентом на странице Самоподписанные сертификаты; единственное отличие в том, что загружаемый файл – это настоящий сертификат CA, а не собственный сертификат партнёра:
Получите сертификат 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), выпущенный промежуточным CAE8. Запись 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 меняет промежуточный сертификат – гораздо чаще, чем корневой (см. компромисс ниже).Преобразуйте его в DER, точно так же, как и раньше:
openssl x509 -in isrgrootx1.pem -outform DER -out ca.derСкопируйте
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, относитесь к нему как к любому другому сертификату на устройстве.