Перейти до вмісту

TLS і PKI

Ви відкриваєте банківський сайт у кав’ярні. Ваш трафік проходить через Wi-Fi, яким керує невідомо хто, потім через провайдера, потім через десяток чужих маршрутизаторів. Кожен із них технічно здатен прочитати кожен байт, змінити його або підмінити відповідь. І все ж ви вводите пароль. Захищають вас властивості, які дає TLS, і кожну з них можна зламати окремо:

  • конфіденційність: сторонній не прочитає вміст;
  • цілісність: сторонній не змінить його непоміченим;
  • автентичність: ви говорите саме з тим сервером, який назвали, а не з посередником, що видає себе за нього.

Конфіденційність і цілісність забезпечує симетричне шифрування з автентифікацією. З автентичністю складніше: потрібно якось дізнатися, чиєму відкритому ключу вірити, а спільного секрету в двох незнайомців ще немає. Розв’язок цієї задачі називається PKI (public key infrastructure, інфраструктура відкритих ключів), і від неї залежить більше, ніж від самого шифру.

Передумови. TCP (модуль 11), DNS (модуль 13), HTTP (модуль 14).

Що дає симетричне, що асиметричне шифрування

Section titled “Що дає симетричне, що асиметричне шифрування”

Симетричне шифрування використовує один ключ на обох кінцях. Воно швидке (сучасні процесори мають для цього апаратні інструкції), тому ним шифрують самі дані. Ціна: обидві сторони повинні заздалегідь мати спільний ключ.

Асиметричне використовує пару ключів: відкритий, яким можна перевірити підпис чи зашифрувати, і закритий, яким підписують чи розшифровують. Воно повільніше, зате розв’язує задачу знайомства: відкритий ключ можна оголосити всьому світу.

TLS використовує обидва: асиметричну криптографію на початку зʼєднання, щоб безпечно домовитися про спільний ключ і довести, хто є хто, а симетричну для решти байтів. Сучасні режими шифрування (AEAD, наприклад AES-GCM чи ChaCha20-Poly1305) поєднують конфіденційність і цілісність в одній операції: підроблений чи пошкоджений запис просто не пройде перевірку тегу.

TLS 1.3 описано в RFC 8446. Тут і далі мова про нього, бо саме його сьогодні узгоджують браузери й сервери. Старіші версії відрізняються, і про різницю скажемо нижче.

Рукостискання TLS 1.3: один круговий рейс до початку даних застосункуклієнтсерверClientHelloім’я (SNI), шифри, частка ключа ECDHEServerHelloчастка ключа сервера: ключі вже є в обохCertificate, CertificateVerify, Finishedсертифікат, підпис рукостискання, підтвердженняFinishedклієнт перевірив сертифікат і підтверджуєдані застосунку (HTTP)1 RTTвідкрито для спостерігачазашифровано
Дані застосунку можна слати, щойно клієнт відповів Finished: один круговий рейс на криптографію після рукостискання TCP. Сірі стрілки видно кожному на шляху, акцентні зашифровані.
  1. ClientHello. Клієнт надсилає версії й набори шифрів, які підтримує, ім’я сервера (розширення SNI, про нього нижче), список протоколів застосунку (ALPN, модуль 14) і головне: частку ключового обміну, тобто свій тимчасовий відкритий ключ, наприклад для X25519. Клієнт вгадує групу наперед, тому не чекає відповіді сервера.

  2. ServerHello. Сервер вибирає набір шифрів і відповідає своєю часткою ключового обміну. Тепер обидва мають те, що потрібне для обчислення спільного секрету (обмін Діффі-Гелмана на еліптичних кривих): кожен множить чужий відкритий ключ на свій закритий, і виходить те саме число, якого спостерігач за двома відкритими ключами обчислити не може. З цього секрету обидва виводять ключі. Відтепер усе зашифровано.

  3. Certificate, CertificateVerify, Finished (від сервера). Сервер шле сертифікат, тобто свій довгостроковий відкритий ключ із іменем і підписом видавця. Потім CertificateVerify: підпис усієї розмови до цього моменту, зроблений закритим ключем сертифіката. Це і є автентифікація: лише власник ключа міг підписати саме ці випадкові значення. Наприкінці Finished, код автентифікації всього рукостискання.

  4. Finished (від клієнта). Клієнт перевірив сертифікат і підпис, перевірив код і відповів своїм Finished. Після цього одразу йдуть дані застосунку.

Головна відмінність від TLS 1.2: там ключовий обмін починався другим повідомленням, тому рукостискання коштувало два кругові рейси, а сертифікат летів у відкритому вигляді. У TLS 1.3 клієнт надсилає свою частку ключа в першому повідомленні, і рукостискання займає один круговий рейс; все, що йде після ServerHello, включно із сертифікатом, зашифровано.

TLS 1.3 також позбувся всього, що виявилося слабким: обміну ключем через шифрування RSA (без властивості, описаної нижче), режимів CBC, стиснення, повторної домовленості під час сесії. Залишилося кілька наборів шифрів (AES-GCM та ChaCha20-Poly1305 із SHA-2), і довгого списку конфігурацій, де половина небезпечна, більше нема.

Ключовий обмін тимчасовий (ephemeral): частки ключів існують лише на час сесії й потім знищуються. Довгостроковий ключ сертифіката використовується тільки для підпису, не для виведення ключів. Завдяки цьому TLS має пряму секретність (forward secrecy): якщо сервер згодом скомпрометують і викрадуть його закритий ключ, записаний раніше зашифрований трафік це не розшифрує, хоч би атакувальник зберігав його роками.

Поновлення сесії та 0-RTT

Section titled “Поновлення сесії та 0-RTT”

Сервер може після рукостискання видати клієнтові квиток (session ticket), за яким наступне з’єднання встановлюється без повної перевірки сертифіката. У цьому режимі клієнт може відправити дані вже в першому польоті (0-RTT, early data). Ціна така сама, як у QUIC (модуль 14): такі дані можна відтворити, тому їх дозволяють лише для запитів, повтор яких нешкідливий.

Сертифікат — це підписане твердження «ключ K належить імені N». Формат X.509 старий, але вживають його досі. Подивитися на реальний можна командою openssl x509 -text. Нижче лабораторний сертифікат, виданий для site.test, зі скороченим виводом:

Terminal window
openssl x509 -in site.crt -noout -text
Certificate:
Data:
Version: 3 (0x2)
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = Test Lab, CN = Test Intermediate CA
Validity
Not Before: Sep 30 07:21:09 2026 GMT
Not After : Dec 29 07:21:09 2026 GMT
Subject: CN = site.test
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
ASN1 OID: prime256v1
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
DNS:site.test, DNS:www.site.test
X509v3 Authority Key Identifier: CB:D2:63:13:...
Signature Algorithm: ecdsa-with-SHA256
Signature Value: 30:45:02:21:00:ac:52:...

Що тут читати:

  • Subject і Subject Alternative Name (SAN). Імена, для яких цей ключ дійсний. Сучасні клієнти дивляться лише на SAN, а поле CN у Subject ігнорують. Один сертифікат може перелічувати кілька імен, у тому числі шаблон *.example.com (лише один рівень: a.example.com підходить, a.b.example.com ні).
  • Issuer. Хто підписав.
  • Validity. Проміжок часу дії. Сертифікат поза цим проміжком недійсний. Строки навмисно короткі, бо сертифікат, якому нема як скасувати дію, треба просто змусити швидко застарівати.
  • Basic Constraints CA:FALSE. Цей ключ не має права підписувати інші сертифікати. Без цієї перевірки хто завгодно, отримавши сертифікат для свого сайту, міг би видавати чужі.
  • Key Usage / Extended Key Usage. Для чого ключ призначений: підпис, автентифікація сервера (serverAuth) чи клієнта (clientAuth).
  • Signature. Підпис видавця під усім переліченим.

Сервер сам собі сертифікат не підтвердить: підписати себе може будь-хто. Клієнтові треба знати, чи можна вірити видавцю, і цю задачу розв’язує ієрархія.

Ланцюжок сертифікатів від кореня довіри до сайтусховище довіри системи або браузеракореневий сертифікатсамопідписаний, ключ тримають офлайнуже є в клієнтапроміжний сертифікатвидає сертифікати сайтів щоднясервер надсилаєсертифікат сайтуімʼя у SAN, строк, ключ сайтусервер надсилаєпідписуєпідписує
Кореневий сертифікат сидить у сховищі клієнта наперед. Проміжні й сайтові надсилає сервер, клієнт перевіряє підпис кожної ланки знизу вгору до кореня, якому вірить.

Операційна система чи браузер постачається зі сховищем кореневих сертифікатів (trust store): це порядку сотні записів, які підібрали розробники платформи за суворими вимогами до УЦ. Кореневий ключ УЦ (certification authority, центр сертифікації) надзвичайно цінний, тому його тримають в офлайні й використовують рідко, лише щоб підписати кілька проміжних сертифікатів. Проміжні щодня підписують сертифікати сайтів. Так компрометація одного проміжного ключа обмежена його ланкою, а корінь лишається недоторканним.

Сервер надсилає свій сертифікат разом із проміжними. Кореневого йому слати не треба: клієнт і так має його або не має, і тоді надіслане нічого не змінить. Забутий проміжний сертифікат — найчастіша помилка налаштування: у частини клієнтів (браузерів, що вміють докачати проміжний за посиланням) сайт відкривається, а в інших (curl, застосунки мобільних платформ) ні.

Перевіримо на прикладі лабораторного CA (root.crt, int.crt, site.crt):

Terminal window
openssl verify -CAfile root.crt -untrusted int.crt site.crt
openssl verify -CAfile root.crt site.crt
site.crt: OK
CN = site.test
error 20 at 0 depth lookup: unable to get local issuer certificate
error site.crt: verification failed

Перша команда знає ланцюжок цілком і повертає OK. Друга не має проміжного сертифіката, і ланцюжок не добудовується, це точна модель «сервер забув надіслати проміжний».

Коли сервер надіслав сертифікат, клієнт перевіряє все нижче, і кожен пункт закриває свою атаку:

  1. Ланцюжок. Кожен сертифікат підписаний ключем наступного, а останній належить до сховища довіри. Інакше підмінити сервер міг би хто завгодно з власним сертифікатом.
  2. Обмеження CA. Проміжні мають CA:TRUE; кінцевий не може видавати інші.
  3. Строк дії. Поточний час у межах Not Before — Not After на кожній ланці. Через це годинник із неправильним часом ламає HTTPS.
  4. Ім’я. Ім’я, до якого зверталися (те, що ви ввели в адресний рядок), є серед SAN. Без цього кроку все інше марне: справжній сертифікат evil.test можна показати за bank.test.
  5. Призначення. serverAuth для сервера.
  6. Володіння ключем. Підпис CertificateVerify перевіряється відкритим ключем із сертифіката, тож сервер має закритий ключ, а не просто переслав чужий сертифікат.
  7. Відкликання. Чи не оголошено сертифікат недійсним раніше строку. Цей пункт найслабший, про нього окремо нижче.

Кожен із пунктів можна побачити в openssl s_client. Ось успішне підключення:

Terminal window
echo | openssl s_client -connect 127.0.0.1:9443 -servername site.test \
-CAfile root.crt -verify_hostname site.test -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = site.test
Hash used: SHA256
Signature type: ECDSA
Verification: OK
Server Temp Key: X25519, 253 bits

Server Temp Key: X25519 — це тимчасова частка ключового обміну з прямою секретністю, Signature type: ECDSA — підпис CertificateVerify за ключем сертифіката. А так виглядають типові невдачі:

# інше ім'я: -verify_hostname wrong.test
Verification error: hostname mismatch
# немає кореня в довірених (не вказано -CAfile):
Verification error: unable to get local issuer certificate
# сертифікат прострочений (openssl verify -attime <після Not After>):
error 10 at 0 depth lookup: certificate has expired

s_client за замовчуванням друкує помилку перевірки, але з’єднання все одно доводить до кінця, бо це діагностичний інструмент. Справжній клієнт (браузер, curl, бібліотека) має перервати з’єднання. Код, який вимикає перевірку сертифіката «щоб працювало» (verify=False, curl -k), знімає з рукостискання саме ту частину, заради якої воно існує.

Відкликання: чесно про слабке місце

Section titled “Відкликання: чесно про слабке місце”

Буває, що закритий ключ викрадено, а сертифікат діятиме ще місяць. Його треба скасувати, але клієнти не питають про кожен сертифікат постійно.

  • CRL (certificate revocation list): список серійних номерів, який видає УЦ. Він може бути великий, а клієнти не хочуть завантажувати його перед кожним зʼєднанням.
  • OCSP (online certificate status protocol): клієнт питає УЦ про один сертифікат. Це додає затримку, і сам УЦ дізнається, які сайти ви відвідуєте.
  • OCSP stapling: сервер сам приклеює свіжу підписану відповідь УЦ до рукостискання, тож клієнт нікого не питає. Але клієнт без окремої вимоги не може відрізнити «сервер не прикріпив» від «його ніхто не скасовував».

Головна проблема: коли відповідь про відкликання недоступна, браузери зазвичай мовчки приймають сертифікат (soft-fail). Жорстка відмова зупиняла б пів інтернету щоразу, коли лягає сервер УЦ. Але й атакувальник, який уже перехоплює трафік, без зусиль заблокує запит до OCSP, і клієнт вважатиме, що все гаразд. Відкликання в такому вигляді захищає від випадкового витоку ключа, який ніхто не експлуатує активно, а від цілеспрямованої атаки не захищає. Тому браузери стали вести власні компактні списки відкликаних сертифікатів і перевіряти їх локально, а індустрія рухається до коротших строків дії, за яких відкликання потрібне рідше. Деякі УЦ, зокрема Let’s Encrypt, відмовляються від OCSP на користь CRL.

Одна IP-адреса й один порт 443 обслуговують сотні сайтів. Щоб вибрати правильний сертифікат, сервер має знати ім’я ще до того, як надішле його. Для цього клієнт вписує ім’я в ClientHello — розширення SNI (server name indication). Але ClientHello іде до обміну ключами, і ім’я летить відкритим текстом. Дослід:

Terminal window
tshark -r cap.pcap -d tcp.port==9443,tls -Y tls | cut -c1-110
4 ... TLSv1 Client Hello (SNI=site.test)
6 ... TLSv1.3 Server Hello, Change Cipher Spec, Application Data, Application Data, ...
8 ... TLSv1.3 Change Cipher Spec, Application Data
9 ... TLSv1.3 Application Data

Ім’я в першому повідомленні читається, сертифіката серед повідомлень нема, бо він всередині Application Data (так виглядає вже зашифрований запис). Якщо клієнт не надіслав SNI, сервер видає сертифікат за замовчуванням, і в нашому досліді він не збігається з тим, що очікував клієнт:

$ echo | openssl s_client -connect 127.0.0.1:9443 | grep -E '^subject=|Verification'
subject=CN = other.test
Verification error: self-signed certificate

Автоматична видача: ACME і Let’s Encrypt

Section titled “Автоматична видача: ACME і Let’s Encrypt”

Колись сертифікат купували й ставили руками на рік-два, і закінчення строку було регулярним інцидентом. Сьогодні для звичайних сайтів це автоматика. Протокол ACME (RFC 8555) дозволяє клієнту (certbot, acme.sh, вбудований модуль сервера) отримати сертифікат від УЦ без людини. Let’s Encrypt, безкоштовний УЦ, працює за цим протоколом і видає сертифікати зі строком у 90 днів (на момент написання; галузь поступово скорочує допустимі строки, тож перевірте чинні цифри).

УЦ насамперед має переконатися, що клієнт контролює домен. Для цього в ACME є виклики (challenge):

  • HTTP-01. УЦ просить розмістити певний вміст за адресою http://домен/.well-known/acme-challenge/<токен> і перевіряє його. Найпростіший спосіб, потрібен доступ на порт 80.
  • DNS-01. Треба створити TXT-запис _acme-challenge.домен. Єдиний спосіб отримати шаблонний сертифікат (*.example.com), і не потребує відкритого порту.
  • TLS-ALPN-01. Доведення на порту 443 спеціальним сертифікатом.

Після успішної перевірки клієнт надсилає запит на підпис (CSR, certificate signing request) із своїм відкритим ключем і отримує назад сертифікат. Закритий ключ на жодному етапі не залишає сервера. Строк дії 90 днів налаштований так навмисно: поновлення автоматичне, а недбала видача чи витік ключа обмежені в часі.

Поверх усього цього працює Certificate Transparency: публічні журнали, куди УЦ записує кожен виданий сертифікат. Власник домену бачить, чи не випустив хтось сертифікат на його ім’я без відома (браузери вимагають підтвердження запису в журналі). Це виявляє помилку УЦ постфактум, не запобігає їй.

Зазвичай автентифікується тільки сервер, а клієнт доводить, хто він, іншим способом (пароль, токен). У взаємному TLS (mTLS) сервер теж просить у клієнта сертифікат: він надсилає CertificateRequest, а клієнт відповідає своїм сертифікатом і підписом CertificateVerify.

Такий режим типовий там, де обидві сторони — програми: виклики між мікросервісами, доступ IoT-пристрою до брокера (лабораторна B8). Ключі клієнта тримаються в пристрої, а не в голові людини.

Перевірка в nginx (ssl_verify_client on) і curl:

Terminal window
curl --cacert root.crt https://site.test:9444/
curl --cacert root.crt https://site.test:9444/ --cert cl.crt --key cl.key
<html>... 400 Bad Request ...
<center>No required SSL certificate was sent</center>
hello CN=alice

Без сертифіката сервер відмовляє ще до застосунку. З сертифікатом nginx знає, хто клієнт, і може передати його ім’я ($ssl_client_s_dn) застосунку. Довіра тут будується так само: клієнтський сертифікат перевіряється щодо сховища довірених УЦ, яке задає сам сервер, тому для внутрішніх систем зазвичай заводять власний УЦ.

Що бачить спостерігач у мережі

Section titled “Що бачить спостерігач у мережі”

Тому, хто стоїть на шляху й лише слухає, доступне ось що:

Видно Не видно
IP-адреси клієнта й сервера, порти URL, шлях і параметри запиту
Час, розміри записів, напрямок Заголовки, cookie
Ім’я сайту в SNI Тіло запиту й відповіді
Пропоновані версії й набори шифрів, ALPN Сертифікат сервера (у TLS 1.3)
Скільки разів і як довго ви ходили Відкритий текст жодного вмісту

Тому спостерігач знає, що ви були на bank.example, але не знає, що ви там робили. Проте розміри та часові закономірності пакетів виказують багато: за ними можна робити висновки, яку саме сторінку завантажено, або чи йдеться про відео. Це називають аналізом трафіку (traffic analysis), і TLS від нього не рятує.

Активному посередникові, що втручається в трафік, лишається або підсунути власний сертифікат, який клієнт не прийме (на екрані з’явиться попередження), або змусити клієнта довіряти власному УЦ. Так працюють корпоративні проксі, що розшифровують трафік: на пристрої користувача попередньо встановлено корпоративний кореневий сертифікат. Справжня загроза, яку TLS не усуває, пов’язана саме з цим: якщо УЦ зі сховища видасть шахрайський сертифікат на чуже ім’я, нічого не відрізнить його від справжнього. Відповіддю стали журнали прозорості й швидке вилучення довіри до недобросовісних УЦ.

Усі приклади вище відтворюються локально. Ланцюжок УЦ із трьох рівнів (кореневий, проміжний, сайт) створюється кількома командами openssl:

Terminal window
# кореневий
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout root.key -out root.crt -days 3650 -subj "/O=Test Lab/CN=Test Root CA" \
-addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
# сертифікат сайту: CSR, потім підпис проміжним ключем
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout site.key -out site.csr -subj "/CN=site.test"
openssl x509 -req -in site.csr -CA int.crt -CAkey int.key -CAcreateserial \
-out site.crt -days 90 -extfile site.ext # SAN, CA:FALSE, serverAuth у site.ext
cat site.crt int.crt > chain.pem # це віддає nginx

nginx потрібен файл chain.pem (сайт, потім проміжний) і site.key. Потім:

  • openssl s_client -connect хост:443 -servername ім'я показує ланцюжок, версію, набір шифрів і підсумок перевірки. Додайте -showcerts, щоб побачити кожен сертифікат.
  • openssl x509 -in файл -noout -subject -issuer -dates -ext subjectAltName дає лише головне: кому, хто, коли, для яких імен.
  • openssl verify -CAfile root.crt -untrusted int.crt site.crt перевіряє ланцюжок.
  • tshark -Y tls (або tcpdump -w і потім tshark -r) показує, що видно на дроті. Порівняйте -tls1_2 і -tls1_3 у s_client: у першому Certificate є окремим відкритим повідомленням, у другому вже ні.
  • curl --noproxy '*' для локальних імен: у середовищі з проксі-змінними інакше curl піде через проксі, і ваше site.test там не існує.

Подібний стенд збирається в лабораторній B5.

Типові помилки розуміння

Section titled “Типові помилки розуміння”

«HTTPS означає, що сайт безпечний». Замок каже, що канал до сервера захищено й що сертифікат виданий на це ім’я. Шахрайський сайт теж може мати чинний сертифікат на своє ім’я.

«TLS ховає, на який сайт я ходжу». Ні, спостерігач бачить IP-адресу й ім’я в SNI. Він не бачить лише шляху й вмісту.

«Сертифікат шифрує трафік». Сертифікат лише зв’язує ключ з іменем і підписаний УЦ. Шифрує симетричний ключ, про який сторони домовилися обміном Діффі-Гелмана. Закритий ключ сертифіката використовується для підпису.

«Самопідписаний сертифікат слабший за виданий УЦ». Шифрування в них однакове. Різниця лише в тому, чи має клієнт підстави вірити в ім’я. Для вашого внутрішнього сервісу з власним УЦ у сховищі клієнтів це нормальна конструкція.

«Відкликаний сертифікат браузер точно відхилить». Зазвичай ні: перевірка м’яка (soft-fail), і атакувальник може її заблокувати.

«curl -k лише обходить незручне попередження». Він вимикає перевірку ланцюжка й імені, тобто автентичність: канал лишається шифрованим, але невідомо до кого.

Перевір себе

1. Що саме в TLS 1.3 дає можливість відправляти дані застосунку вже після одного кругового рейсу?
2. Викрадено закритий ключ сервера. Атакувальник має запис усього трафіку за минулий рік. Чи розшифрує він його, якщо використовувався TLS 1.3?
3. Сайт відкривається в браузері, а curl видає «unable to get local issuer certificate». Найімовірніша причина?
4. Чому правильного ланцюжка й чинного строку дії недостатньо, щоб довіряти сертифікату?
5. Чому відкликання сертифікатів вважають слабким місцем PKI?
6. Що спостерігач бачить під час підключення до сайту за TLS 1.3?
7. Для чого mTLS підходить краще, ніж пароль?

B5. Власні DNS і TLS — власний УЦ, сертифікат для nginx, перегляд рукостискання в tshark разом з авторитетною зоною й резолвером із модуля 13. Це в основному те, що розібрано вище, але у вигляді топології просторів імен.

B8. MQTT і IoT-шлюз — TLS між пристроєм і брокером, включно з варіантом, у якому пристрій автентифікується сертифікатом (mTLS).

  • RFC 8446 (TLS 1.3), RFC 5280 (профіль сертифікатів X.509 і CRL), RFC 6960 (OCSP)
  • RFC 8555 (ACME), RFC 6962 (Certificate Transparency)
  • Grigorik I., High Performance Browser Networking, розділ «Transport Layer Security (TLS)»
  • Ristić I., Bulletproof TLS and PKI
  • man openssl-s_client, man openssl-x509, man openssl-verify
  • Документація Let’s Encrypt (letsencrypt.org/docs): види викликів і строки дії