Network Programming

TLS/SSL — криптографічний захист мережевих з'єднань

Академічне вивчення протоколу TLS — від симетричної та асиметричної криптографії до X.509 сертифікатів, PKI, TLS Handshake 1.2/1.3, Record Layer, атак BEAST/POODLE/HEARTBLEED та практичної реалізації в .NET через SslStream і HttpClient.

TLS/SSL — криптографічний захист мережевих з'єднань

Чому відкрита мережа — це ворожа територія

Уявіть, що кожен лист, який ви відправляєте поштою, написаний на прозорій листівці. Листоноша, сусід, будь-хто на пошті — всі можуть прочитати його без жодних зусиль. Саме так виглядає передача даних через мережу без шифрування.

Інтернет — це децентралізована мережа з тисячами проміжних вузлів: маршрутизаторів, комутаторів, проксі-серверів. Пакети TCP, що несуть ваш HTTP-запит, можуть пройти через десятки таких вузлів перш ніж досягнути сервера. Кожен із цих вузлів технічно здатний:

Перехопити дані. Оператор будь-якого проміжного вузла може зчитувати вміст нешифрованих TCP-пакетів у відкритому вигляді. Ваш пароль, номер кредитної картки, медичні записи — все це видно як звичайний текст.

Підмінити відповідь. Зловмисник між клієнтом і сервером (атака «людина посередині», Man-in-the-Middle, MitM) може не просто читати, а й змінювати дані на льоту. Ви думаєте, що завантажуєте оновлення програми — насправді отримуєте виконуваний файл із шкідливим кодом.

Видати себе за сервер. Без механізму перевірки справжності нічого не заважає зловмисникові підняти фейковий сервер, що відповідає на запити до bank.example.com. Клієнт не матиме жодного способу відрізнити справжній сервер від підробки.

Без TLS (HTTP):

Клієнт                    Маршрутизатор ISP           Сервер
  |                              |                       |
  |---[GET /login HTTP/1.1  ]--->|                       |
  |---[Authorization: Basic ]--->|                       |
  |---[ dXNlcjpwYXNzd29yZA= ]--->|---------------------> |
  |                              |                       |
  |                         Видно всім!
  |                    user:password (base64)
З TLS (HTTPS):

Клієнт                    Маршрутизатор ISP           Сервер
  |                              |                       |
  |---[TLS Record: ÿ§2Ø...  ]--->|                       |
  |---[TLS Record: ×9∆Ψ...  ]--->|---------------------> |
  |                              |                       |
  |                      Виглядає як сміття.
  |                  Ключ є лише у клієнта і сервера.
Ключова ідея розділу: TLS (Transport Layer Security) вирішує три фундаментальні проблеми мережевої безпеки одночасно — конфіденційність (дані не може прочитати третя сторона), цілісність (дані не можна непомітно змінити) та автентичність (сервер є саме тим, за кого себе видає). Жодна з цих властивостей окремо не є достатньою — лише всі три разом.

Коротка, але насичена подіями історія

Від Netscape до IETF: народження SSL

Протокол SSL (Secure Sockets Layer) розробила компанія Netscape Communications на початку 1990-х років для свого браузера Netscape Navigator. Ціль була конкретною: зробити можливим безпечні покупки в інтернеті. Без шифрування комерційна революція в мережі була приречена.

SSL 1.0 (1994) — ніколи не публікувався. Під час внутрішнього аудиту в самій Netscape були виявлені критичні вразливості. Версія була відкинута до будь-якого публічного використання.

SSL 2.0 (1995) — перша публічна версія. Вже у 1996 році дослідник Вагнер (David Wagner) разом із колегами опублікували роботу, що виявила кілька серйозних криптографічних слабкостей. SSL 2.0 офіційно визнано небезпечним у RFC 6176 (2011) і заборонено до використання.

SSL 3.0 (1996) — повне переписування. Спроектований разом із незалежними криптографами, SSL 3.0 став надійною основою. Але у 2014 році атака POODLE (Padding Oracle On Downgraded Legacy Encryption) зробила його небезпечним. RFC 7568 (2015) офіційно забороняє SSL 3.0.

У 1999 році IETF взяла SSL під свій контроль і перейменувала його на TLS (Transport Layer Security). Зміна назви підкреслює зміну статусу — з фірмового продукту Netscape до відкритого міжнародного стандарту.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

robust "SSL/TLS" as protocol

@0
protocol is "SSL 1.0\n(1994)"

@1
protocol is "SSL 2.0\n(1995)"

@3
protocol is "SSL 3.0\n(1996)"

@7
protocol is "TLS 1.0\n(1999)"

@12
protocol is "TLS 1.1\n(2006)"

@15
protocol is "TLS 1.2\n(2008)"

@25
protocol is "TLS 1.3\n(2018)"

@enduml
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "SSL 1.0 (1994)" #fce4ec {
    note as n1
      Ніколи не опублікований.
      Критичні вразливості виявлено
      до релізу всередині Netscape.
    end note
}

rectangle "SSL 2.0 (1995)" #ffcdd2 {
    note as n2
      Перша публічна версія.
      1996: атака Вагнера.
      RFC 6176 (2011): заборонено.
    end note
}

rectangle "SSL 3.0 (1996)" #ffe082 {
    note as n3
      Повне переписування.
      Надійна основа на 18 років.
      2014: атака POODLE.
      RFC 7568 (2015): заборонено.
    end note
}

rectangle "TLS 1.0 (RFC 2246, 1999)" #fff9c4 {
    note as n4
      IETF бере протокол під контроль.
      Перейменовано з SSL 3.0.
      2021: RFC 8996 — депрекований.
    end note
}

rectangle "TLS 1.1 (RFC 4346, 2006)" #f3e5f5 {
    note as n5
      Виправлено атаку CBC.
      Явний IV для CBC-режиму.
      2021: RFC 8996 — депрекований.
    end note
}

rectangle "TLS 1.2 (RFC 5246, 2008)" #e3f2fd {
    note as n6
      Підтримка SHA-256 та GCM.
      Гнучке узгодження алгоритмів.
      Досі широко підтримується.
    end note
}

rectangle "TLS 1.3 (RFC 8446, 2018)" #e8f5e9 {
    note as n7
      Революційне спрощення.
      1-RTT Handshake (і 0-RTT).
      Видалено застарілі алгоритми.
      Forward Secrecy обов'язкова.
      Шифрування з першого байту.
    end note
}

"SSL 1.0 (1994)" -down-> "SSL 2.0 (1995)"
"SSL 2.0 (1995)" -down-> "SSL 3.0 (1996)"
"SSL 3.0 (1996)" -down-> "TLS 1.0 (RFC 2246, 1999)" : IETF стандартизація
"TLS 1.0 (RFC 2246, 1999)" -down-> "TLS 1.1 (RFC 4346, 2006)"
"TLS 1.1 (RFC 4346, 2006)" -down-> "TLS 1.2 (RFC 5246, 2008)"
"TLS 1.2 (RFC 5246, 2008)" -down-> "TLS 1.3 (RFC 8446, 2018)"

@enduml
Станом на 2024 рік IETF рекомендує лише TLS 1.2 та TLS 1.3. Версії TLS 1.0 та TLS 1.1 офіційно депреційовано у RFC 8996 (2021). SSL 3.0 і нижче — заборонені. Якщо ваш застосунок досі підтримує TLS 1.0 або TLS 1.1, це є порушенням вимог PCI DSS та GDPR.

Криптографічні основи: мова, якою говорить TLS

Перш ніж занурюватись у деталі протоколу, необхідно опанувати криптографічну абетку. TLS — це не один алгоритм, а оркестр різних криптографічних примітивів, кожен з яких вирішує свою конкретну задачу.

Симетрична криптографія: швидкість ціною спільного секрету

У симетричній криптографії той самий ключ використовується і для шифрування, і для розшифрування. Це ефективно та швидко — сучасні процесори мають апаратне прискорення (AES-NI), що дозволяє шифрувати гігабайти даних за секунду.

Симетричне шифрування:

  Відкритий текст          Зашифрований текст
  "Hello, World!"  ──[KEY]──►  "Ω∆≤≥¥§ÿ..."
                                   │
                               [той самий KEY]
                                   │
  "Hello, World!"  ◄──────────────┘

Класичний приклад — AES (Advanced Encryption Standard):

AES оперує блоками по 128 біт і підтримує ключі довжиною 128, 192 або 256 біт. Але просте блочне шифрування (AES-ECB) є вразливим: однакові блоки відкритого тексту дають однакові блоки шифртексту, що дозволяє виявити патерни.

AES-ECB (небезпечний — зберігає структуру):

Блок 1: "АЛІСА ВІДПРАВЛЯ"  ──► [AES-ECB] ──► "Ω∆≤3K7#..."
Блок 2: "Є БОБУ 100 ДОЛАРІ"──► [AES-ECB] ──► "ΨΠ∑9L2@..."
Блок 3: "АЛІСА ВІДПРАВЛЯ"  ──► [AES-ECB] ──► "Ω∆≤3K7#..."  ← Однаковий! Витік інформації.

Тому TLS використовує режими роботи блочних шифрів:

AES-CBC (Cipher Block Chaining)
TLS 1.2
Кожен блок XOR-ується з попереднім зашифрованим блоком перед шифруванням. Перший блок XOR-ується з випадковим Initialization Vector (IV). Однакові блоки відкритого тексту дають різні блоки шифртексту. Вразливий до атак на основі оракула заповнення (Padding Oracle), якщо реалізований неправильно (саме звідси — атаки BEAST і POODLE).
AES-GCM (Galois/Counter Mode)
TLS 1.2/1.3, рекомендований
Поєднує CTR-режим шифрування з автентифікацією Galois MAC. AEAD (Authenticated Encryption with Associated Data) — шифрує та автентифікує одночасно. Паралелізується (на відміну від CBC), апаратно прискорений, позбавлений вразливостей padding oracle. TLS 1.3 дозволяє лише AEAD-режими.
ChaCha20-Poly1305
TLS 1.2/1.3, альтернатива AES-GCM
Потоковий шифр ChaCha20 + MAC Poly1305. Розроблений Деніелом Бернштейном. Ефективний на пристроях без апаратного прискорення AES (мобільні ARM без AES-NI). Google обрав його як основний шифр у Chrome для Android.

Фундаментальна проблема симетричної криптографії: якщо Аліса і Боб хочуть спілкуватись зашифровано, їм спочатку потрібно узгодити спільний ключ. Але якщо передати ключ у відкритій мережі — його перехоплять. Це замкнене коло: щоб передати ключ безпечно — потрібен ключ.


Асиметрична криптографія: відкриті та закриті ключі

Асиметрична криптографія (public-key cryptography) вирішує парадокс розподілу ключів геніально простим способом: у кожного учасника є два пов'язаних ключі — відкритий (public key) та закритий (private key). Математичний зв'язок між ними такий: те, що зашифровано відкритим ключем, можна розшифрувати лише відповідним закритим ключем, і навпаки.

Асиметричне шифрування (для обміну даними):

      Аліса                              Боб
  [публічний ключ Боба]            [приватний ключ Боба]
  [приватний ключ Аліси]           [публічний ключ Аліси]
         │                                  │
         │   "Привіт, Боб!"                 │
         │ ──[encrypt(Боб.pub)]──►          │
         │   "ΩΨ∆≤K7#@..."                  │
         │                   ──[decrypt(Боб.priv)]──►
         │                                  │
         │                          "Привіт, Боб!"
Цифровий підпис (для автентифікації):

      Аліса                              Боб
  [приватний ключ Аліси]           [публічний ключ Аліси]
         │                                  │
         │   Документ D                     │
         │   sign(hash(D), Аліса.priv)      │
         │ ──[Документ D + Підпис S]──►     │
         │                                  │
         │              verify(hash(D), S, Аліса.pub) == true?
         │                           ↑
         │              Якщо так — Аліса точно підписала,
         │              і документ не змінювався.

RSA (Rivest–Shamir–Adleman) — найстаріший і найвідоміший алгоритм асиметричного шифрування. Безпека RSA ґрунтується на обчислювальній складності факторизації великих чисел: добуток двох великих простих чисел легко обчислити, але відновити ці числа із добутку — практично неможливо за розумний час.

RSA — математична основа (спрощено):

1. Беремо два великих простих числа: p = 61, q = 53
2. n = p × q = 3233  (модуль, частина публічного ключа)
3. φ(n) = (p-1)(q-1) = 3120  (функція Ейлера)
4. Обираємо e таке, що gcd(e, φ(n)) = 1:  e = 17
5. Знаходимо d таке, що (d × e) mod φ(n) = 1:  d = 2753

Публічний ключ:  (e=17, n=3233)  ← можна поширювати
Приватний ключ:  (d=2753, n=3233) ← тримати в таємниці

Шифрування:  C = M^e mod n  →  42^17 mod 3233 = 2557
Дешифрування: M = C^d mod n  →  2557^2753 mod 3233 = 42  ✓

На практиці: n має бути 2048+ біт (≈617 десяткових цифр).
Факторизація 2048-бітного числа неможлива за розумний час
навіть для найпотужніших суперкомп'ютерів.

ECDSA та ECDH — криптографія еліптичних кривих: Сучасніша альтернатива RSA. Той самий рівень безпеки досягається з значно меншими ключами: 256-бітний ключ ECDSA приблизно еквівалентний 3072-бітному ключу RSA за надійністю. Математична основа — операції на еліптичних кривих у скінченних полях.

Порівняння розмірів ключів для еквівалентного рівня безпеки:

Рівень безпеки │ RSA/DH      │ ECC
─────────────────────────────────────
80 біт         │ 1024 біт    │ 160 біт
112 біт        │ 2048 біт    │ 224 біт
128 біт        │ 3072 біт    │ 256 біт  ← P-256, найпоширеніший
192 біт        │ 7680 біт    │ 384 біт
256 біт        │ 15360 біт   │ 521 біт
Асиметрична криптографія є принципово повільнішою за симетричну — на 100–10000 разів. Тому TLS не шифрує дані асиметрично. Натомість асиметрична криптографія використовується лише для Handshake — для безпечного узгодження симетричного сесійного ключа. Після цього всі дані шифруються швидкою симетричною криптографією.

Алгоритм Діффі–Гелмана: як узгодити ключ через відкритий канал

У 1976 році Вітфілд Діффі та Мартін Гелман опублікували революційну роботу, що вирішила задачу безпечного обміну ключами через відкритий канал. Ідея проста, але геніальна: два учасники можуть узгодити спільний секрет, жодного разу не передаючи його через мережу.

Найпростіший спосіб зрозуміти цей алгоритм — аналогія з фарбами:

Аналогія з фарбами (Діффі–Гелман):

1. Аліса і Боб публічно домовляються про
   спільну "базову" фарбу: ЖОВТА.
   Вороги це бачать — і це нормально.

2. Аліса додає свою таємну фарбу (СИНЯ) →
   отримує ЗЕЛЕНУ, яку надсилає Бобу.

3. Боб додає свою таємну фарбу (ЧЕРВОНА) →
   отримує ПОМАРАНЧЕВУ, яку надсилає Алісі.

4. Аліса отримує ПОМАРАНЧЕВУ Боба, додає
   свою СИНЮ → КОРИЧНЕВА (спільний секрет!)

5. Боб отримує ЗЕЛЕНУ Аліси, додає
   свою ЧЕРВОНУ → КОРИЧНЕВА (той самий секрет!)

Ворог бачить: ЖОВТУ, ЗЕЛЕНУ, ПОМАРАНЧЕВУ.
Відновити КОРИЧНЕВУ без знання таємних фарб —
обчислювально неможливо (задача дискретного логарифму).
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Аліса" as alice #e3f2fd
participant "Публічний канал\n(підслуховує ворог)" as channel #fce4ec
participant "Боб" as bob #e8f5e9

note over alice, bob
  Публічні параметри (відомі всім):
  g = 2 (генератор), p = 23 (просте число)
end note

alice -> alice : Обирає секрет a = 6\nОбчислює A = g^a mod p\n= 2^6 mod 23 = 18
bob -> bob : Обирає секрет b = 15\nОбчислює B = g^b mod p\n= 2^15 mod 23 = 19

alice -> channel : A = 18 (публічно відправляє)
channel -> bob : A = 18

bob -> channel : B = 19 (публічно відправляє)
channel -> alice : B = 19

alice -> alice : Спільний секрет:\nS = B^a mod p\n= 19^6 mod 23 = **2**
bob -> bob : Спільний секрет:\nS = A^b mod p\n= 18^15 mod 23 = **2**

note over alice, bob #e8f5e9
  Спільний секрет S = 2 отримано обома сторонами.
  Ворог бачить лише: g=2, p=23, A=18, B=19.
  Відновити S без знання a або b = задача дискретного логарифму.
  На практиці p — 2048-бітне число, що робить задачу нерозв'язною.
end note

@enduml

ECDH (Elliptic Curve Diffie-Hellman) — версія алгоритму на еліптичних кривих. Використовується в TLS 1.3. Замість модульних потенцій — скалярне множення точок на кривій, що забезпечує той самий рівень безпеки при значно менших ключах.

ECDH на кривій P-256:

1. Публічно: крива P-256, точка G (генератор)
2. Аліса: обирає секрет a, обчислює A = a·G (точка на кривій)
3. Боб:   обирає секрет b, обчислює B = b·G (точка на кривій)
4. Аліса → Боб: A;   Боб → Аліса: B
5. Аліса: S = a·B = a·b·G
6. Боб:   S = b·A = b·a·G  → той самий S ✓

Ворог знає G, A, B, але не може обчислити a або b
(задача дискретного логарифму на еліптичних кривих).
Ephemeral DH (DHE/ECDHE): У TLS для кожної нової сесії генеруються нові тимчасові (ephemeral) DH-ключі. Навіть якщо приватний ключ сервера буде скомпрометований у майбутньому, зловмисник не зможе розшифрувати минулі сесії — для цього потрібні ефемерні ключі, що вже знищені. Ця властивість називається Perfect Forward Secrecy (PFS) і є обов'язковою у TLS 1.3.

Хеш-функції та HMAC: цілісність без секрету

Криптографічна хеш-функція перетворює дані довільного розміру на рядок фіксованого розміру (дайджест) із такими властивостями:

Властивості криптографічних хеш-функцій:

1. Детермінованість:
   SHA256("Hello") = "185f8db3..."  ← завжди однаково

2. Лавинний ефект:
   SHA256("Hello") = "185f8db3..."
   SHA256("Hello!") = "334d0162..."  ← кардинально інший!

3. Незворотність (Pre-image resistance):
   "185f8db3..." → ? → неможливо відновити "Hello"
   (обчислювально неможливо, не теоретично)

4. Стійкість до колізій (Collision resistance):
   Неможливо знайти два різних входи з однаковим хешем.
   SHA256(X) = SHA256(Y), де X ≠ Y → практично неможливо

5. Ефективність:
   Обчислення хешу — дуже швидка операція.

SHA-256 та SHA-384 — основні хеш-функції в TLS 1.2/1.3. SHA-1 офіційно вилучено через практичні атаки колізій (Google, 2017).

HMAC (Hash-based Message Authentication Code) — механізм автентифікації повідомлень на основі хеш-функції та секретного ключа:

HMAC-SHA256(key, message):

ipad = 0x36 repeated 64 times
opad = 0x5C repeated 64 times

HMAC = SHA256( (key XOR opad) || SHA256( (key XOR ipad) || message ) )

Властивості:
- Без знання key неможливо обчислити правильний HMAC
- Зміна будь-якого байту message дасть інший HMAC
- Використовується для перевірки цілісності TLS записів

Криптографічний алфавіт TLS: Cipher Suite

Перш ніж перейти до Handshake, введемо поняття Cipher Suite (набір шифрів) — це ідентифікатор, що повністю описує комбінацію алгоритмів, що використовуються у TLS-сесії. Кожен Cipher Suite кодує чотири компоненти:

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
│    │     │        │    │   │    │
│    │     │        │    │   │    └── Хеш-функція (для PRF/HMAC)
│    │     │        │    │   └─────── Режим шифрування
│    │     │        │    └─────────── Розмір ключа (біт)
│    │     │        └──────────────── Симетричний алгоритм
│    │     └───────────────────────── Алгоритм автентифікації
│    └─────────────────────────────── Алгоритм обміну ключами
└──────────────────────────────────── Протокол
Cipher SuiteОбмін ключамиАвтентифікаціяШифруванняMAC
TLS_AES_256_GCM_SHA384ECDHEз сертифікатуAES-256-GCMВбудований (AEAD)
TLS_CHACHA20_POLY1305_SHA256ECDHEз сертифікатуChaCha20Poly1305 (AEAD)
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256ECDHERSAAES-128-GCMSHA-256
TLS_RSA_WITH_AES_256_CBC_SHARSARSAAES-256-CBCSHA-1
У TLS 1.3 залишилось лише 5 стандартних Cipher Suite — всі вони є AEAD. Небезпечні алгоритми (RC4, 3DES, MD5, SHA-1, RSA-обмін ключами) офіційно видалені. Якщо ваш сервер пропонує Cipher Suite з NULL, EXPORT, anon, RC4 або 3DES — це серйозна вразливість.

Перша частина охоплює криптографічний фундамент TLS. Далі розглянемо сертифікати X.509, інфраструктуру відкритих ключів (PKI), ланцюжок довіри та як браузер перевіряє, що bank.example.com — справді ваш банк, а не зловмисник.


Сертифікати X.509 та інфраструктура відкритих ключів (PKI)

Парадокс відкритого ключа: кому довіряти?

Асиметрична криптографія вирішила задачу обміну ключами, але породила нову. Припустімо, Аліса хоче відправити зашифроване повідомлення Бобу. Вона отримує «відкритий ключ Боба» з якогось сайту. Але звідки вона знає, що це справді ключ Боба? Що ніхто не підмінив його своїм ключем на шляху?

Атака підміни ключа (MitM на відкритому ключі):

Аліса               Зловмисник Єва               Боб
   |                      |                        |
   | "Дай мені твій       |                        |
   |  відкритий ключ" ──► |                        |
   |                      | ──► Боб.pub_key ──────►|
   |                      | ◄── Боб.pub_key ────── |
   |                      |                        |
   |    [Єва.pub_key] ◄── |  (підміна!)            |
   |                      |                        |
   | Шифрує своїм ключем  |                        |
   | Єви, думаючи що це   |                        |
   | ключ Боба ──────────►|                        |
   |              Єва розшифровує,                 |
   |              читає, перешифровує              |
   |              ключем Боба і передає ►          |

Ця атака відома як Man-in-the-Middle (MitM). Без додаткового механізму перевірки справжності ключа асиметрична криптографія не захищає від неї.

Рішення: потрібна довірена третя сторона, яка засвідчить зв'язок між відкритим ключем та ідентичністю його власника. Саме цю роль виконує Центр Сертифікації (Certificate Authority, CA), а документ, що засвідчує зв'язок — цифровий сертифікат X.509.

Ключова ідея: Сертифікат X.509 — це, по суті, відповідь на питання «Хто запевняє, що цей відкритий ключ належить саме цьому домену?». Відповідь: «Центр Сертифікації, якому довіряє ваш браузер/ОС».

Анатомія сертифіката X.509

Стандарт X.509 визначений ITU-T і є частиною стандарту X.500 для служб каталогів. Версія X.509v3 (RFC 5280) — поточний стандарт для сертифікатів в Інтернеті.

Сертифікат — це бінарний документ у форматі DER (Distinguished Encoding Rules, підмножина ASN.1), часто закодований у Base64 у форматі PEM (Privacy Enhanced Mail). Структура:

Сертифікат X.509v3 (спрощено):

┌─────────────────────────────────────────────┐
│  TBSCertificate (To Be Signed Certificate)  │
│  ┌──────────────────────────────────────┐   │
│  │ Version: 3                           │   │
│  │ Serial Number: 0x0F:A3:2B:...        │   │
│  │ Signature Algorithm: sha256WithRSA   │   │
│  │                                      │   │
│  │ Issuer (Видавець):                   │   │
│  │   C=US, O=DigiCert Inc,              │   │
│  │   CN=DigiCert TLS RSA SHA256 2020 CA1│   │
│  │                                      │   │
│  │ Validity:                            │   │
│  │   Not Before: 2024-01-15 00:00:00    │   │
│  │   Not After:  2025-02-14 23:59:59    │   │
│  │                                      │   │
│  │ Subject (Власник):                   │   │
│  │   C=US, ST=California, L=Menlo Park  │   │
│  │   O=Example Corp, CN=www.example.com │   │
│  │                                      │   │
│  │ Subject Public Key Info:             │   │
│  │   Algorithm: rsaEncryption           │   │
│  │   Public Key: (2048 bit)             │   │
│  │   30 82 01 0a 02 82 01 01 00 ...     │   │
│  │                                      │   │
│  │ Extensions (v3):                     │   │
│  │   Subject Alt Names: DNS:example.com │   │
│  │                       DNS:*.example.com│  │
│  │   Key Usage: Digital Signature,      │   │
│  │              Key Encipherment        │   │
│  │   Extended Key Usage: serverAuth     │   │
│  │   Basic Constraints: CA:false        │   │
│  │   CRL Distribution Points: http://.. │   │
│  │   OCSP: http://ocsp.digicert.com     │   │
│  └──────────────────────────────────────┘   │
│                                             │
│  Signature Algorithm: sha256WithRSAEncryption│
│  Signature Value:                           │
│    (цифровий підпис CA, ~256 байт RSA)      │
│    3d 4a f2 b1 ... 9e 0c 7f               │
└─────────────────────────────────────────────┘
Version
integer (1/2/3)
Версія стандарту X.509. Версія 3 (значення 2 у DER через відлік від нуля) — єдина актуальна. Додала розширення (Extensions), без яких неможливі SAN, Key Usage та інші критично важливі поля.
Serial Number
великий integer
Унікальний номер сертифіката, призначений CA. Використовується у списках відкликання (CRL). Починаючи з 2016 року (RFC 5280 errata + Baseline Requirements) — повинен містити мінімум 64 біти ентропії для запобігання передбачуваності.
Issuer / Subject
Distinguished Name (DN)
Ієрархічний ідентифікатор у форматі X.500. Компоненти: CN (Common Name), O (Organization), OU (Organizational Unit), C (Country), ST (State), L (Locality). У серверних сертифікатах Subject.CN колись вказував на домен, але тепер застарів — для доменів використовується лише Subject Alternative Names (SAN).
Subject Alternative Names (SAN)
розширення X.509v3, критичне
Список доменів, IP-адрес, email або URI, для яких дійсний сертифікат. Замінив Subject.CN для перевірки домену (RFC 2818). Приклад: DNS:example.com, DNS:*.example.com, IP:93.184.216.34. Wildcard (*) охоплює лише один рівень: *.example.comwww.example.com ✓, але sub.www.example.com ✗.
Key Usage / Extended Key Usage
розширення X.509v3
Key Usage обмежує криптографічне використання ключа: digitalSignature, keyEncipherment, keyCertSign (для CA). Extended Key Usage вказує призначення: serverAuth (TLS-сервер), clientAuth (TLS-клієнт), codeSigning, emailProtection. Сервер без serverAuth в EKU буде відхилений браузером.
Basic Constraints
розширення X.509v3, критичне
CA:true — сертифікат може підписувати інші сертифікати (є CA). CA:false — кінцевий (leaf) сертифікат, не може підписувати. PathLen обмежує глибину ланцюжка. Критично важливе розширення: якщо CA:false, браузер не дозволить використати сертифікат для підпису інших.
Signature Value
байтовий рядок
Цифровий підпис CA, обчислений над полем TBSCertificate (все вище підпису). Верифікується відкритим ключем Issuer (CA). Зміна будь-якого поля сертифіката одразу робить підпис невалідним — сертифікат стає підробленим і буде відхилений.

Подивимось на реальний сертифікат. Команда openssl x509 -text парсить DER/PEM та виводить усі поля у читабельному вигляді:

# Отримати та розібрати сертифікат github.com
echo | openssl s_client -connect github.com:443 2>/dev/null | openssl x509 -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            17:67:44:75:96:48:b3:8e:6b:de:a9:92:42:10:d7:bb
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C=US, O=DigiCert, Inc., CN=DigiCert TLS Hybrid ECC SHA384 2020 CA1
        Validity
            Not Before: Feb 15 00:00:00 2024 GMT
            Not After : Mar 15 23:59:59 2025 GMT
        Subject: C=US, ST=California, L=San Francisco,
                 O=GitHub, Inc., CN=github.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub: 04:fa:2d:...
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:github.com, DNS:www.github.com
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            Authority Information Access:
                OCSP - URI:http://ocsp.digicert.com
                CA Issuers - URI:http://cacerts.digicert.com/...

Ланцюжок довіри: від Root CA до вашого сертифіката

Жоден CA не підписує сертифікати кінцевих сервісів своїм кореневим (Root) CA сертифікатом безпосередньо. Чому? Приватний ключ Root CA — найцінніший актив у PKI. Компрометація Root CA означає компрометацію всіх сертифікатів, виданих цим CA.

Тому в реальному світі використовується ієрархія сертифікатів із трьох рівнів:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Root CA (Кореневий ЦС)" as root #e8f5e9 {
    note as rn
      DigiCert Global Root CA
      Самопідписаний (self-signed)
      Термін дії: 20-25 років
      Приватний ключ: зберігається офлайн,
      у HSM (Hardware Security Module),
      у фізично захищеному бункері.
      Підписує ЛИШЕ сертифікати
      проміжних CA.
    end note
}

rectangle "Intermediate CA (Проміжний ЦС)" as inter #e3f2fd {
    note as in_
      DigiCert TLS Hybrid ECC SHA384 2020 CA1
      Підписаний Root CA.
      Термін дії: 5-10 років.
      Приватний ключ: зберігається в HSM,
      але онлайн (для видачі сертифікатів).
      Підписує кінцеві (leaf) сертифікати.
      Якщо компрометований — відкликається
      без шкоди для Root CA.
    end note
}

rectangle "Leaf Certificate (Кінцевий сертифікат)" as leaf #fff3e0 {
    note as ln
      github.com
      Підписаний Intermediate CA.
      Термін дії: ≤ 397 днів (з 2020 р.)
      Приватний ключ: на веб-сервері.
      Видається власнику домену після
      перевірки (DV/OV/EV validation).
    end note
}

root -down-> inter : підписує
inter -down-> leaf : підписує

note right of root #e8f5e9
  Вбудований у браузер/ОС.
  ~150 Root CA у Firefox,
  ~130 у Chrome/Windows.
  Не потребує передачі по мережі.
end note

note right of inter #e3f2fd
  Надсилається разом із
  leaf сертифікатом під час
  TLS Handshake.
end note

note right of leaf #fff3e0
  Надсилається разом із
  intermediate сертифікатом під час
  TLS Handshake.
end note

@enduml

Як клієнт перевіряє ланцюжок довіри:

Отримання сертифікатів із TLS Handshake

Під час TLS Handshake сервер надсилає Certificate message, що містить свій leaf-сертифікат та один або кілька проміжних сертифікатів (але не Root CA — він вже є у клієнта). Якщо сервер не надсилає проміжний CA — більшість клієнтів відхилить з'єднання, хоча деякі можуть спробувати завантажити його через AIA (Authority Information Access).

Побудова ланцюжка

Клієнт будує ланцюжок від leaf-сертифіката до кореневого: Leaf → Intermediate → Root. Кожен наступний рівень є Issuer (видавцем) попереднього.

Верифікація підписів

Для кожної пари (сертифікат, його видавець) клієнт перевіряє:

  • Підпис сертифіката верифікується відкритим ключем видавця
  • Видавець має Basic Constraints: CA:true
  • Поточний час знаходиться між Not Before і Not After

Перевірка прив'язки до Root

Ланцюжок повинен завершуватись сертифікатом, що є в системному сховищі довірених кореневих CA. Windows: Certificate Store. macOS: Keychain. Linux: /etc/ssl/certs/. Firefox: власне сховище, незалежне від ОС.

Перевірка відкликання

Чи не відкликано сертифікат? Два механізми:

  • CRL (Certificate Revocation List): Завантажити список відкликаних серійних номерів з URL у CRL Distribution Points. Може бути мегабайтним.
  • OCSP (Online Certificate Status Protocol): Запит до OCSP Responder із серійним номером сертифіката. Відповідь: good, revoked, або unknown.

Перевірка домену

Домен у запиті (github.com) точно збігається з SAN сертифіката? Якщо так — перевірка пройдена. Wildcard-сертифікат *.github.com покриває api.github.com, але не github.com і не sub.api.github.com.

Повний шлях перевірки (chain validation):

Браузер                    Мережа                    Сервер
   │                          │                         │
   │◄─────────────────────────┤ Certificate (leaf)      │
   │◄─────────────────────────┤ + Intermediate CA       │
   │                          │                         │
   │ [1] Знайти Root CA у сховищі ОС                   │
   │     Root CA = DigiCert Global Root CA              │
   │                                                    │
   │ [2] verify(Intermediate.signature, Root.pubkey)    │
   │     → OK ✓                                         │
   │                                                    │
   │ [3] verify(Leaf.signature, Intermediate.pubkey)    │
   │     → OK ✓                                         │
   │                                                    │
   │ [4] Leaf.NotBefore ≤ now ≤ Leaf.NotAfter           │
   │     → OK ✓                                         │
   │                                                    │
   │ [5] OCSP запит: чи відкликано Leaf?                │
   │ ────────────────────────────────────────────────►  │
   │ ◄────────────────────────────────── status: good ✓ │
   │                                                    │
   │ [6] "github.com" ∈ Leaf.SAN?                       │
   │     DNS:github.com ✓                               │
   │                                                    │
   │ TLS Handshake продовжується. З'єднання довірене.  │

Типи валідації та рівні довіри сертифікатів

Не всі сертифікати однаково «довірені» з точки зору ідентифікації власника. CA Браузерний форум (CA/Browser Forum) визначає три рівні:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "DV — Domain Validation\n(Перевірка домену)" as dv #e3f2fd {
    note as dvn
      Перевіряється: чи контролює заявник домен?
      Метод: HTTP challenge (файл на .well-known/),
             DNS challenge (TXT-запис),
             Email challenge.

      Видається: за хвилини (Let's Encrypt — секунди).
      Вартість: безкоштовно (Let's Encrypt) або ~10$/рік.

      Що підтверджує: лише контроль над доменом.
      НЕ підтверджує: хто є власником, чи є він
                      легальною організацією.

      Відображення у браузері:
      🔒 (замочок) без назви організації.

      Приклад: більшість сайтів у 2024 р.
    end note
}

rectangle "OV — Organization Validation\n(Перевірка організації)" as ov #fff3e0 {
    note as ovn
      Перевіряється: + юридичне існування організації.
      CA перевіряє: реєстр компаній, телефон,
                    юридичну адресу.

      Видається: 1-3 дні.
      Вартість: ~100-300$/рік.

      Що підтверджує: домен + реальна організація.
      Subject.O містить назву компанії.

      Відображення у браузері:
      🔒 (замочок) без назви в адресному рядку.
      (Деталі — у Certificate Viewer)

      Приклад: корпоративні сайти, API.
    end note
}

rectangle "EV — Extended Validation\n(Розширена перевірка)" as ev #e8f5e9 {
    note as evn
      Перевіряється: + жорстка ручна перевірка CA/B Forum.
      CA перевіряє: юридична особа, фізична адреса,
                    право власності на домен,
                    телефонна перевірка,
                    аудит документів.

      Видається: 1-2 тижні.
      Вартість: ~300-1000$/рік.

      Що підтверджує: домен + верифікована компанія.

      Відображення у браузері:
      🔒 + "GitHub, Inc." (Chrome до 2019 р.)
      З 2019 р. Chrome/Firefox прибрали EV badge
      через дослідження про відсутність впливу
      на сприйняття безпеки користувачами.

      Приклад: банки, платіжні системи.
    end note
}

dv -right-> ov : вища довіра
ov -right-> ev : вища довіра

@enduml
Let's Encrypt (заснований у 2014, ISRG) виконав революцію в PKI: безкоштовні DV-сертифікати з автоматичним оновленням через протокол ACME (RFC 8555). До 2014 року DV-сертифікат коштував $10-50/рік. Let's Encrypt видає близько 400 мільйонів активних сертифікатів, що більше ніж усі комерційні CA разом. HTTPS став доступним для будь-якого сайту.

Certificate Transparency: публічний аудит видачі сертифікатів

Уявіть ситуацію: CA DigiCert за помилкою (або зловмисно) видає сертифікат для google.com якійсь третій особі. Ця особа може здійснити MitM-атаку на мільярди користувачів. Саме так трапилось у 2011 р. із нідерландським CA DigiNotar — він видав фальшиві сертифікати для Google та Yahoo іранським хакерам.

Certificate Transparency (CT, RFC 9162) — відповідь на цю загрозу, запроваджена Google у 2013 р. та обов'язкова у Chrome з 2018 р.

Ідея: кожен публічний сертифікат повинен бути записаний у публічний, незмінний журнал (CT Log) до моменту видачі. Будь-хто може перевірити журнал і виявити несанкціоновану видачу сертифіката для свого домену.

Certificate Transparency workflow:

CA                    CT Log (Merkle Tree)              Браузер
 │                          │                               │
 │ Надсилає pre-certificate │                               │
 │─────────────────────────►│                               │
 │                          │ Додає до append-only журналу  │
 │◄─────────────────────────│ Повертає SCT (signed timestamp│
 │  SCT (Signed             │ + підпис Log) ─────────────►  │
 │  Certificate Timestamp)  │                               │
 │                          │                               │
 │ Вбудовує SCT у сертифікат│                               │
 │                          │                               │
 │──────────────────────────────────────────────────────►   │
 │             Сертифікат із SCT                             │
 │                          │                               │
 │                          │ Браузер перевіряє SCT        │
 │                          │ Немає SCT → ❌ не довіряти   │
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Certificate\nAuthority (CA)" as ca #e3f2fd
participant "CT Log Server\n(Google, Cloudflare,\nDigiCert...)" as log #fff3e0
participant "Власник домену\n(моніторинг)" as owner #e8f5e9
participant "Браузер\n(Chrome, Firefox)" as browser #f3e5f5

== Видача сертифіката ==
ca -> log : Submit pre-certificate
log -> log : Додати до Merkle Tree\n(append-only, незмінний)
log --> ca : SCT (Signed Certificate Timestamp)\n= Log.sign(timestamp, cert_hash)

ca -> ca : Вбудувати SCT у фінальний сертифікат
ca --> owner : Сертифікат з SCT

== Моніторинг (постійно) ==
log -> owner : Certificate appeared in log:\ncert for "yourdomain.com"
note right of owner
  Якщо власник не замовляв
  такий сертифікат — негайно
  відкликати та розслідувати!
end note

== TLS Handshake ==
owner -> browser : Certificate (з SCT всередині)
browser -> browser : Перевірити SCT підпис\nвідомих CT Log серверів
alt SCT відсутній або невалідний
    browser --> owner : ❌ ERR_CERTIFICATE_TRANSPARENCY_REQUIRED
else SCT валідний
    browser --> owner : ✅ З'єднання встановлено
end

@enduml

Відкликання сертифікатів: CRL та OCSP

Що відбувається, якщо приватний ключ сервера скомпрометовано ще до закінчення дії сертифіката? Наприклад, зловмисник отримав доступ до сервера і скопіював server.key. Сертифікат формально ще дійсний — але довіряти йому не можна.

Рішення: відкликання (revocation) — CA публікує інформацію про те, що сертифікат більше не є довіреним, незважаючи на технічну дійсність.

OCSP fail-open: За замовчуванням, якщо OCSP Responder недоступний, більшість браузерів продовжують з'єднання (fail-open), а не блокують його (fail-closed). Це компроміс між безпекою та доступністю. Єдиний механізм fail-closed — OCSP Must-Staple (розширення сертифіката, що вимагає обов'язкову OCSP Stapling відповідь від сервера).

Pinning та HPKP: довіра понад PKI

HTTP Public Key Pinning (HPKP, RFC 7469) — механізм, що дозволяв сайту «закріпити» конкретні відкриті ключі у браузері. Навіть якби хтось отримав сертифікат від іншого CA — браузер відхилив би його, бо він не збігається із закріпленим ключем.

HPKP був депрецовано у Chrome у 2017 р. та Firefox у 2018 р. Причина — катастрофічна небезпека: сайт, що помилково закріпив ключі і потім замінив їх, стає недоступним для всіх відвідувачів до закінчення терміну дії pin. Кілька великих сайтів пережили такі інциденти.

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

// Certificate pinning у HttpClient (.NET)
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback =
    (message, cert, chain, errors) =>
    {
        // SHA-256 fingerprint очікуваного сертифіката
        const string expectedPin =
            "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";

        // Обчислити fingerprint отриманого сертифіката
        var actualPin = Convert.ToBase64String(
            SHA256.HashData(cert!.RawData));
        var actual = $"sha256/{actualPin}";

        return actual == expectedPin;
    };
Certificate Pinning ускладнює оновлення сертифікатів та налагодження (неможливо використовувати корпоративний TLS-інспектор). Оновлення застосунку при зміні сертифіката стає критично важливим. Використовуйте public key pinning замість certificate pinning — ключ живе довше за сертифікат.

Друга частина охоплює архітектуру PKI від сертифіката X.509 до ланцюжків довіри та механізмів відкликання. Далі — серце протоколу: TLS Handshake у версіях 1.2 та 1.3.


TLS Handshake: від першого байту до зашифрованих даних

Загальна картина: що відбувається до першого HTTP-запиту

Коли ви вводите https://github.com і натискаєте Enter, браузер не надсилає HTTP GET одразу. Спочатку відбувається TLS Handshake — протокол узгодження, що встановлює захищений канал. Лише після його успішного завершення перший байт HTTP-запиту вирушає у мережу.

Handshake вирішує чотири задачі одночасно:

Задачі TLS Handshake:

1. УЗГОДЖЕННЯ ВЕРСІЇ ТА АЛГОРИТМІВ
   Клієнт і сервер домовляються:
   - Яку версію TLS використовувати (1.2 чи 1.3)?
   - Який Cipher Suite? (AES-256-GCM? ChaCha20?)
   - Які параметри обміну ключами?

2. АВТЕНТИФІКАЦІЯ СЕРВЕРА
   Сервер доводить свою ідентичність:
   - Надсилає сертифікат X.509
   - Клієнт перевіряє ланцюжок довіри (PKI)
   - Клієнт перевіряє, що CN/SAN збігається з доменом

3. ОБМІН КЛЮЧАМИ
   Клієнт і сервер встановлюють спільний секрет
   (Pre-Master Secret → Master Secret → Session Keys)
   через асиметричну криптографію або DH,
   жодного разу не передаючи секрет у відкритому вигляді.

4. ПІДТВЕРДЖЕННЯ
   Обидві сторони доводять, що вони мають
   однаковий сесійний ключ (Finished message).
   Від цього моменту всі дані шифруються.

TLS 1.2 і TLS 1.3 вирішують ці задачі принципово по-різному. Розглянемо обидва варіанти.


TLS 1.2 Handshake: класичний підхід

TLS 1.2 Handshake вимагає 2 round-trips (2-RTT) до початку передачі даних застосунку. Тобто між SYN TCP та першим байтом HTTP-відповіді проходить 3 round-trips: 1 для TCP + 2 для TLS.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Клієнт\n(браузер)" as client #e3f2fd
participant "Сервер" as server #e8f5e9

== TCP 3-way handshake (1 RTT) ==
client -> server : SYN
server --> client : SYN-ACK
client -> server : ACK
note over client, server : TCP з'єднання встановлено

== TLS Handshake (2 RTT) ==

group RTT 1: ClientHello → ServerHelloDone
client -> server : **ClientHello**\nVersion: TLS 1.2\nRandom: 28 випадкових байт\nSession ID: (порожній або для resume)\nCipher Suites: [список підтримуваних]\nExtensions: SNI, supported_groups...

server --> client : **ServerHello**\nVersion: TLS 1.2\nRandom: 28 випадкових байт\nSession ID: abc123\nCipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

server --> client : **Certificate**\n[Leaf cert + Intermediate CA cert]

server --> client : **ServerKeyExchange**\n(тільки для DHE/ECDHE)\nECDH публічний ключ сервера\n+ підпис приватним ключем RSA/ECDSA

server --> client : **ServerHelloDone**\n(сервер завершив своє слово)
end

note right of client
  Клієнт перевіряє:
  1. Ланцюжок довіри сертифіката
  2. Підпис ServerKeyExchange
  3. Домен у SAN сертифіката
  Генерує свій ECDH ключ.
  Обчислює Pre-Master Secret.
end note

group RTT 2: ClientKeyExchange → Finished
client -> server : **ClientKeyExchange**\nECDH публічний ключ клієнта

note over client, server
  Обидві сторони незалежно обчислюють:
  Pre-Master Secret = ECDH(client_priv, server_pub)
  Master Secret = PRF(pre_master, "master secret",
                      ClientRandom + ServerRandom)
  Session Keys = PRF(master_secret, "key expansion",
                     ServerRandom + ClientRandom)
  → client_write_key, server_write_key,
    client_write_IV, server_write_IV
end note

client -> server : **ChangeCipherSpec**\n(повідомлення: "переходжу на шифрування")

client -> server : **Finished** (зашифрований!)\nverify_data = PRF(master_secret, "client finished",\n                  Hash(всі попередні HS повідомлення))

server --> client : **ChangeCipherSpec**

server --> client : **Finished** (зашифрований!)\nverify_data = PRF(master_secret, "server finished",\n                  Hash(всі попередні HS повідомлення))
end

== Application Data ==
client -> server : HTTP GET /index.html (зашифрований)
server --> client : HTTP 200 OK + body (зашифрований)

@enduml

Розберемо кожне повідомлення детально.

ClientHello: клієнт відкриває переговори

ClientHello — перше повідомлення TLS, що надсилається після встановлення TCP-з'єднання. Це «меню» можливостей клієнта:

ClientHello (розбір у hex + пояснення):

Record Header:
  16          → Content Type: Handshake (0x16)
  03 01       → Legacy Record Version: TLS 1.0 (для сумісності!)
  00 f1       → Record Length: 241 байт

Handshake Header:
  01          → Handshake Type: ClientHello (1)
  00 00 ed    → Length: 237 байт

ClientHello тіло:
  03 03       → Client Version: TLS 1.2
  [28 bytes]  → ClientRandom:
                  4a 3f 2b 8c d1 e5 ... (timestamp + random)

  00          → Session ID Length: 0 (нова сесія)

  00 2a       → Cipher Suites Length: 42 (21 suite × 2 байти)
  13 01       →   TLS_AES_128_GCM_SHA256          (TLS 1.3)
  13 02       →   TLS_AES_256_GCM_SHA384          (TLS 1.3)
  13 03       →   TLS_CHACHA20_POLY1305_SHA256     (TLS 1.3)
  c0 2b       →   TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  c0 2f       →   TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  c0 2c       →   TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  ... (ще 15 наборів)
  00 ff       →   TLS_EMPTY_RENEGOTIATION_INFO_SCSV (захист)

  01          → Compression Methods Length: 1
  00          →   null (стиснення заборонено з TLS 1.3)

  Extensions:
  00 00       →   server_name (SNI): "github.com"
  00 0d       →   signature_algorithms: sha256+rsa, sha384+ecdsa...
  00 0a       →   supported_groups: x25519, P-256, P-384
  00 23       →   session_ticket: (порожній)
  00 10       →   application_layer_protocol_negotiation (ALPN):
                    h2, http/1.1
SNI (Server Name Indication) — критично важливе розширення. Воно дозволяє одному IP-серверу обслуговувати кілька доменів із різними сертифікатами. Клієнт вказує у server_name extension, до якого домену підключається, ще до отримання сертифіката. Без SNI неможливо, наприклад, розмістити alice.example.com і bob.example.com на одному сервері з різними сертифікатами.Важливо: SNI надсилається у відкритому вигляді до встановлення шифрування. Тобто ISP або зловмисник може бачити, до якого домену ви підключаєтесь — навіть для HTTPS. Рішення: Encrypted Client Hello (ECH), що шифрує SNI. Наразі (2026) ECH реалізовано у Chrome та Firefox.

ServerHello та ServerKeyExchange: відповідь сервера

Сервер обирає один набір шифрів з переліку клієнта і відповідає:

ServerHello:
  Version:      TLS 1.2
  ServerRandom: [28 bytes] ← 28 нових випадкових байт
  Session ID:   abc123...  ← для можливого resume
  Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

ServerKeyExchange (для ECDHE):
  Curve:        named_curve: secp256r1 (P-256)
  ECPoint:      04 a3 f2 ... ← публічний ключ ECDH сервера (65 байт)
  Signature:    [RSA підпис приватним ключем сервера над:
                  ClientRandom + ServerRandom + ECPoint]

  Навіщо підпис?
  Без нього зловмисник міг би підмінити ECPoint власним ключем.
  Підпис прив'язує ECDH-ключ до сертифіката сервера.

Master Secret та деривація ключів

Після отримання ServerKeyExchange та відправки ClientKeyExchange обидві сторони мають однаковий Pre-Master Secret. З нього через PRF (Pseudorandom Function) деривуються всі сесійні ключі:

Деривація ключів у TLS 1.2:

Pre-Master Secret (48 байт):
  = ECDH(client_ephemeral_priv, server_ephemeral_pub)
  = 3f a2 b1 ...  (48 байт)

Master Secret (48 байт):
  = PRF(pre_master_secret,
        label: "master secret",
        seed:  ClientRandom + ServerRandom)
  = 9e 4c 2d ...

Key Material (деривується з Master Secret):
  = PRF(master_secret,
        label: "key expansion",
        seed:  ServerRandom + ClientRandom)

Розбивається на:
  client_write_MAC_key  → HMAC-ключ для даних від клієнта
  server_write_MAC_key  → HMAC-ключ для даних від сервера
  client_write_key      → AES-ключ для шифрування від клієнта
  server_write_key      → AES-ключ для шифрування від сервера
  client_write_IV       → IV для AES-GCM від клієнта
  server_write_IV       → IV для AES-GCM від сервера
ClientRandom та ServerRandom у деривації ключів виконують важливу функцію: навіть якщо два з'єднання мають однаковий Pre-Master Secret (теоретично), сесійні ключі будуть різними, бо Random-и унікальні для кожної сесії. Це захищає від атак повторного відтворення (replay attacks).

Finished: взаємна верифікація

Finished — перше зашифроване повідомлення Handshake. Воно містить хеш усіх попередніх Handshake-повідомлень, обчислений через PRF:

client Finished:
  verify_data = PRF(master_secret,
                    "client finished",
                    SHA256(всі HS повідомлення від ClientHello))

server Finished:
  verify_data = PRF(master_secret,
                    "server finished",
                    SHA256(всі HS повідомлення від ClientHello))

Якщо зловмисник модифікував будь-яке повідомлення під час Handshake — хеші не збіжаться, і з'єднання буде негайно закрите. Finished забезпечує цілісність всього Handshake ретроспективно.


TLS 1.3 Handshake: революційне спрощення

TLS 1.3 (RFC 8446, 2018) — результат чотирьох років роботи IETF та аналізу десятків атак на TLS 1.2. Мета: видалити все застаріле, спростити до мінімуму, зашифрувати якнайбільше.

Ключова різниця у порівнянні з TLS 1.2:

TLS 1.2: 2-RTT до початку даних
  TCP SYN ─►
           ◄─ TCP SYN-ACK
  TCP ACK ─►
  ClientHello ─►                              (RTT 1 початок)
               ◄─ ServerHello
               ◄─ Certificate
               ◄─ ServerKeyExchange
               ◄─ ServerHelloDone            (RTT 1 кінець)
  ClientKeyExchange ─►
  ChangeCipherSpec ─►
  Finished ─►                                 (RTT 2 початок)
               ◄─ ChangeCipherSpec
               ◄─ Finished                   (RTT 2 кінець)
  HTTP GET ─►

TLS 1.3: 1-RTT до початку даних
  TCP SYN ─►
           ◄─ TCP SYN-ACK
  TCP ACK ─►
  ClientHello ─►                              (RTT 1 початок)
  (з KeyShare: ECDH публічний ключ вже тут!)
               ◄─ ServerHello + KeyShare
               ◄─ EncryptedExtensions       ← вже зашифровано!
               ◄─ Certificate               ← вже зашифровано!
               ◄─ CertificateVerify         ← вже зашифровано!
               ◄─ Finished                  ← вже зашифровано!
  Finished ─►                                (RTT 1 кінець)
  HTTP GET ─►  ← одразу після Finished!
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Клієнт" as client #e3f2fd
participant "Сервер" as server #e8f5e9

== TCP 3-way handshake ==
client -> server : SYN
server --> client : SYN-ACK
client -> server : ACK

== TLS 1.3 Handshake (1 RTT) ==

group RTT 1 (єдиний round-trip)
client -> server : **ClientHello**\nlegacy_version: 0x0303 (TLS 1.2 — для сумісності)\nsupported_versions: TLS 1.3\nKeyShare: x25519 публічний ключ клієнта\nsupported_groups: x25519, P-256\nsignature_algorithms: ecdsa_secp256r1_sha256...\nSNI: "github.com"

note right of server
  Сервер отримав KeyShare від клієнта.
  Генерує свій ECDH ключ.
  Вже може обчислити Handshake Secret
  і зашифрувати подальші повідомлення!
end note

server --> client : **ServerHello**\nlegacy_version: 0x0303\nsupported_versions: TLS 1.3\nKeyShare: x25519 публічний ключ сервера\nCipher Suite: TLS_AES_256_GCM_SHA384

note over client, server #fff3e0
  Обидві сторони тепер мають:
  (EC)DHE Secret = x25519(client_priv, server_pub)
  Handshake Secret = HKDF-Extract(DHE_Secret)
  → handshake_traffic_keys (для шифрування HS)
end note

server --> client : **EncryptedExtensions** 🔒\n(SNI підтвердження, ALPN результат...)

server --> client : **Certificate** 🔒\n[Leaf cert + Intermediate CA]

server --> client : **CertificateVerify** 🔒\nsignature = sign(priv_key,\n  "TLS 1.3, server CertificateVerify" ||\n  Hash(весь Handshake transcript))

server --> client : **Finished** 🔒\nMAC(finish_key, Hash(весь Handshake transcript))

note right of client
  Клієнт перевіряє:
  1. Finished MAC сервера ✓
  2. Підпис у CertificateVerify ✓
  3. Сертифікат та ланцюжок довіри ✓
  4. Домен у SAN ✓
  Обчислює Application Traffic Keys.
end note

client -> server : **Finished** 🔒\nMAC(finish_key, Hash(весь Handshake transcript))
end

note over client, server #e8f5e9
  Application Secret = HKDF-Expand(Master Secret)
  client_application_traffic_key → шифрує HTTP запити
  server_application_traffic_key → шифрує HTTP відповіді
end note

== Application Data (зашифровано) ==
client -> server : HTTP GET / 🔒
server --> client : HTTP 200 OK 🔒

@enduml

Що змінилось у TLS 1.3 принципово


0-RTT Early Data: найшвидший TLS

TLS 1.3 пропонує ще один режим — 0-RTT (Zero Round Trip Time), що дозволяє надіслати дані застосунку в першому пакеті, разом із ClientHello, ще до завершення Handshake.

Можливо це завдяки Pre-Shared Key (PSK) — секрету від попередньої сесії, що зберігається клієнтом та сервером після першого з'єднання:

Перше з'єднання (1-RTT):
  Звичайний TLS 1.3 Handshake.
  Наприкінці сервер надсилає:
    NewSessionTicket (зашифрований):
      ticket: [зашифрований PSK для сервера]
      ticket_lifetime: 7200 (2 години)

  Клієнт зберігає ticket та PSK.

Повторне з'єднання (0-RTT):

  ClientHello ─►
  + early_data_indication extension
  + PSK identity (посилання на ticket)
  HTTP GET / ─►  ← 0-RTT Early Data!
                 ← сервер отримує запит до завершення HS!

               ◄─ ServerHello
               ◄─ EncryptedExtensions
               ◄─ Finished (прийнято 0-RTT)
  Finished ─►
               ◄─ HTTP 200 OK
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Клієнт" as client #e3f2fd
participant "Сервер" as server #e8f5e9

== Перша сесія (звичайний 1-RTT) ==
client -> server : ClientHello
server --> client : ServerHello + ... + Finished
client -> server : Finished

server --> client : **NewSessionTicket** 🔒\nPSK = [session resumption secret]\nticket = encrypt(PSK, server_key)\nlifetime = 7200s

note right of client
  Зберігає ticket та PSK
  для наступного з'єднання.
end note

== Наступна сесія (0-RTT) ==
note over client, server : Нове TCP з'єднання

client -> server : **ClientHello**\n+ psk_key_exchange_modes\n+ pre_shared_key: ticket\n+ early_data_indication: ✓

client -> server : **0-RTT Early Data** 🔒\nHTTP GET /dashboard\n(зашифровано ключем від PSK)

note right of server
  Сервер декодує ticket → PSK.
  Може обробити запит до
  завершення Handshake!
end note

server --> client : **ServerHello**\n+ pre_shared_key (підтверджено)

server --> client : **EncryptedExtensions** 🔒\nearly_data: accepted ✓

server --> client : **Finished** 🔒

client -> server : **EndOfEarlyData** 🔒
client -> server : **Finished** 🔒

server --> client : **HTTP 200 OK** 🔒\n(відповідь на 0-RTT запит)

@enduml
0-RTT має фундаментальне обмеження: вразливість до Replay-атак. Зловмисник може перехопити пакет із 0-RTT Early Data та надіслати його повторно. Для ідемпотентних операцій (GET-запити) це прийнятно. Для операцій зі станом (POST, платежі) — небезпечно без додаткових заходів на рівні застосунку (nonce, timestamp). IETF вказує у RFC 8446: «0-RTT data is not forward secret, and there are no guarantees of non-replay between connections».

Session Resumption у TLS 1.2: Session ID та Session Ticket

До появи 0-RTT у TLS 1.3, TLS 1.2 мав два механізми відновлення сесії, що дозволяли уникнути повного Handshake.

Session ID: Сервер зберігає стан сесії (Master Secret та параметри) у внутрішньому кеші та надає клієнту короткий Session ID. При повторному підключенні клієнт надсилає Session ID у ClientHello. Якщо сервер знаходить відповідний запис у кеші — Handshake скорочується до 1-RTT.

Session Resumption через Session ID (TLS 1.2, 1-RTT):

ClientHello (Session ID: abc123) ─►
                                  ◄─ ServerHello (Session ID: abc123)
                                  ◄─ ChangeCipherSpec
                                  ◄─ Finished
ChangeCipherSpec ─►
Finished ─►
HTTP GET ─►

Проблема Session ID: Сервер повинен зберігати стан кожної сесії. При мільйонах клієнтів — це величезне навантаження на пам'ять. І якщо клієнт підключиться до іншого сервера у кластері — Session ID буде невідомим.

Session Ticket (RFC 5077): Сервер шифрує стан сесії та відправляє його клієнту у вигляді непрозорого Session Ticket. При відновленні клієнт повертає ticket, сервер розшифровує його власним ключем і відновлює стан. Сервер не зберігає нічого — стан на стороні клієнта.

Session Ticket у TLS 1.2 є попередником PSK у TLS 1.3. Але між ними є важлива різниця: у TLS 1.2 відновлена сесія успадковує Master Secret від початкової сесії. Якщо початкова сесія використовувала RSA Key Exchange (без Forward Secrecy), то всі відновлені сесії теж не мають PFS. У TLS 1.3 кожна відновлена сесія (0-RTT або 1-RTT з PSK) завжди виконує ECDHE, що гарантує PFS.

Взаємна автентифікація (mTLS): клієнт теж доводить ідентичність

У стандартному TLS лише сервер автентифікується (надає сертифікат). Клієнт залишається анонімним з точки зору TLS (хоча може автентифікуватись пізніше через HTTP, OAuth тощо).

У mTLS (mutual TLS) обидві сторони надають сертифікати:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Клієнт\n(з клієнтським сертифікатом)" as client #e3f2fd
participant "Сервер\n(вимагає клієнтський сертифікат)" as server #e8f5e9

client -> server : ClientHello

server --> client : ServerHello
server --> client : Certificate (серверний)
server --> client : **CertificateRequest** ← нове повідомлення!\nacceptable_certificate_types: RSA, ECDSA\nacceptable_CAs: [список довірених CA]
server --> client : ServerHelloDone

note right of client
  Клієнт вибирає свій сертифікат,
  що підходить під вимоги сервера.
  Якщо немає підходящого —
  надсилає порожній Certificate.
end note

client -> server : **Certificate** (клієнтський)\n[Leaf cert клієнта + intermediate]

client -> server : ClientKeyExchange

client -> server : **CertificateVerify**\nsignature = sign(client_priv_key,\n  Hash(всі Handshake повідомлення))

client -> server : ChangeCipherSpec
client -> server : Finished

note right of server
  Сервер перевіряє:
  1. Ланцюжок довіри клієнтського сертифіката
  2. Підпис у CertificateVerify
  3. Чи є CN/SAN у whitelist?
  Якщо ні → alert: certificate_unknown → закрити з'єднання
end note

server --> client : ChangeCipherSpec
server --> client : Finished

@enduml

mTLS широко використовується у:

  • Service Mesh (Istio, Linkerd) — автентифікація між мікросервісами
  • Zero Trust архітектурах — кожен клієнт має свій сертифікат, IP-адреса не є достатнім доказом ідентичності
  • API Gateway — клієнти (партнери, мобільні застосунки) підключаються з клієнтськими сертифікатами замість API-ключів
  • IoT пристрої — кожен пристрій має унікальний сертифікат, що дозволяє точно ідентифікувати його
// mTLS клієнт у .NET
var cert = X509Certificate2.CreateFromPemFile("client.crt", "client.key");

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
// При підключенні клієнт автоматично надає сертифікат,
// якщо сервер надіслав CertificateRequest

var client = new HttpClient(handler);
var response = await client.GetAsync("https://api.internal/data");

Wireshark: Handshake наочно

Найкращий спосіб закріпити розуміння Handshake — переглянути реальний трафік. Wireshark дозволяє бачити кожне повідомлення TLS:

# Захопити TLS трафік до github.com (фільтр у Wireshark)
tls.handshake and ip.addr == 140.82.121.3

# Або через tshark (CLI Wireshark):
tshark -i en0 \
  -f "host github.com and port 443" \
  -Y "tls.handshake" \
  -V 2>/dev/null | grep -A3 "Handshake Protocol"
Frame 4: TLSv1.3 Record Layer: Handshake Protocol: Client Hello
    Content Type: Handshake (22)
    Version: TLS 1.0 (0x0301)  ← legacy compatibility!
    Handshake Protocol: Client Hello
        Version: TLS 1.2 (0x0303)
        Random: 4a3f2b8c...
        Extensions Length: 508
        Extension: server_name (len=16)
            Server Name: github.com
        Extension: supported_versions (len=7)
            Supported Version: TLS 1.3 (0x0304)  ← справжня версія
        Extension: key_share (len=71)
            Key Share Entry: Group: x25519, Key Exchange length: 32

Frame 6: TLSv1.3 Record Layer: Handshake Protocol: Server Hello
    Handshake Protocol: Server Hello
        Version: TLS 1.2 (0x0303)  ← legacy compatibility!
        Extension: supported_versions
            Supported Version: TLS 1.3 (0x0304)  ← обрано TLS 1.3
        Extension: key_share
            Key Share Entry: Group: x25519, Key Exchange length: 32

Frame 7: TLSv1.3 Record Layer: Handshake Protocol: (Encrypted)
    Content Type: Application Data (23)  ← шифрований HS!
    [Decrypted TLS: Certificate, CertificateVerify, Finished]
Зверніть увагу на legacy compatibility: ClientHello вказує Version: TLS 1.0 (0x0301) у Record Header та Version: TLS 1.2 (0x0303) у тілі, а справжня версія TLS 1.3 (0x0304) передається через supported_versions extension. Це навмисний дизайн для обходу middlebox-ів (проміжних пристроїв), що помилково відхиляли невідомі версії TLS. Аналогічно, ServerHello у TLS 1.3 виглядає як звичайний TLS 1.2 ServerHello для несумісних middlebox-ів.

Третя частина охоплює TLS Handshake — від класичного 2-RTT у TLS 1.2 до оптимізованого 1-RTT та 0-RTT у TLS 1.3, мTLS та механізми відновлення сесій. Далі — TLS Record Layer та каталог реальних атак.


TLS Record Layer: як дані шифруються після Handshake

Концептуальна модель: TLS як обгортка

Після завершення Handshake TLS стає прозорою трубою для даних застосунку. HTTP, SMTP, WebSocket — будь-який протокол прикладного рівня передає свої байти через TLS, не знаючи нічого про деталі шифрування. Це архітектурна елегантність: TLS реалізує безпеку на транспортному рівні, не вимагаючи від застосунку жодних змін.

Але «прозора труба» — це спрощення. Насправді TLS не просто шифрує потік байтів: він розбиває його на записи (records) і обробляє кожен запис окремо.

Місце TLS Record Layer у стеку:

┌─────────────────────────────────┐
│   Застосунок (HTTP, SMTP...)    │  "GET /index.html HTTP/1.1\r\n..."
└─────────────────┬───────────────┘
                  │ передає байти
┌─────────────────▼───────────────┐
│      TLS Record Protocol        │  розбиває → шифрує → передає
│  ┌──────────┐  ┌──────────────┐ │
│  │Handshake │  │Alert Protocol│ │  (субпротоколи TLS)
│  └──────────┘  └──────────────┘ │
└─────────────────┬───────────────┘
                  │ TLS Records (бінарний потік)
┌─────────────────▼───────────────┐
│         TCP (потік байтів)      │
└─────────────────────────────────┘

TLS Record Protocol є мультиплексором: він обслуговує кілька субпротоколів одночасно, використовуючи поле Content Type для розрізнення:

Content TypeЗначенняПризначення
change_cipher_spec20Сигнал переходу на нові ключі (legacy, TLS 1.2)
alert21Повідомлення про помилки та закриття
handshake22Повідомлення Handshake-протоколу
application_data23Дані застосунку (HTTP, тощо)

Структура TLS Record

Кожен TLS Record — це самодостатній блок із заголовком та payload:

TLS Record (5-байтовий заголовок + payload):

┌──────────────────────────────────────────────┐
│ Content Type    │  1 байт  │ 0x17 = app data  │
│ Legacy Version  │  2 байти │ 0x03 0x03         │
│ Length          │  2 байти │ до 16384 байт     │
├──────────────────────────────────────────────┤
│                                              │
│  Encrypted Payload                           │
│  (Ciphertext + Authentication Tag)           │
│                                              │
└──────────────────────────────────────────────┘

Максимальний розмір plaintext payload: 2^14 = 16 384 байти
(з розширенням max_fragment_length: до 2^16 − 1)

Розглянемо реальний TLS 1.3 Record із зашифрованими даними застосунку у hex:

17 03 03 00 45  ← заголовок (Content Type: 23, Version: TLS 1.2 legacy, Length: 69)

Encrypted payload (69 байт):
  a3 f2 b1 4c 8e 2d 71 09 ...  ← Ciphertext (AES-GCM)
  ...
  6f 2a 11 bc 9e 44 d3 7c      ← Authentication Tag (16 байт, GCM)
                                   (Полі1305: теж 16 байт)

Примітка: у TLS 1.3 Content Type у заголовку завжди 0x17
(application_data), навіть для Handshake повідомлень після
ServerHello. Справжній тип зашифровано всередині payload.

AES-GCM шифрування запису: покроково

Для конкретності розглянемо, як шифрується TLS Record із AES-256-GCM — найпоширенішим шифром у сучасному TLS.

GCM (Galois/Counter Mode) — режим AEAD (Authenticated Encryption with Associated Data). «Authenticated» означає: шифрування та автентифікація виконуються одночасно, одним алгоритмом. Це принципово важливо: автентифікація охоплює і заголовок запису (Associated Data), що запобігає атакам на метадані.

Шифрування одного TLS Record (AES-256-GCM):

Входи:
  key        = client_write_key      (32 байти, AES-256)
  nonce      = client_write_IV XOR sequence_number
               ← sequence_number інкрементується для кожного record!
               ← 0, 1, 2, 3... (захист від replay в межах сесії)
  plaintext  = дані застосунку + TLS inner content type (1 байт)
  aad        = Record Header (Content Type + Version + Length)
               ← автентифікується, але НЕ шифрується

Алгоритм:
  1. CTR режим: ciphertext = plaintext XOR AES-CTR(key, nonce)
  2. GHASH: authentication_tag = GHASH(aad || ciphertext)
     (128-бітний поліноміальний MAC над полем GF(2^128))

Вихід:
  TLS Record = Header || Ciphertext || Authentication Tag (16 байт)
Чому nonce = IV XOR sequence_number?

Проблема: AES-GCM ВИМАГАЄ унікального nonce для кожного
шифрування з одним ключем. Якщо nonce повторюється —
повна катастрофа: зловмисник може відновити ключ.

Рішення у TLS 1.3:
  implicit_nonce (12 байт) = client_write_IV XOR record_seq_num

  sequence_number = 0: nonce = IV XOR 0x000000000000000000000000
  sequence_number = 1: nonce = IV XOR 0x000000000000000000000001
  sequence_number = 2: nonce = IV XOR 0x000000000000000000000002
  ...

Кожен запис — унікальний nonce. Гарантовано, бо
sequence_number ніколи не повторюється в рамках сесії.
Послідовний nonce також забезпечує захист від replay-атак в межах однієї TLS-сесії: якщо зловмисник перехопить і повторно надішле Record #42, отримувач відхилить його, бо sequence_number вже пройшов це значення.

TLS Alert Protocol: мова помилок

TLS Alert Protocol — механізм сигналізації про помилки та стани з'єднання. Кожен Alert — це двобайтовий TLS Record (Content Type: 21):

Alert Record:
  Level       (1 байт): warning (1) або fatal (2)
  Description (1 байт): код помилки

Приклади:
  02 00  → fatal: close_notify       — коректне закриття з'єднання
  02 02  → fatal: unexpected_message — неочікуване повідомлення
  02 14  → fatal: bad_record_mac     — невалідний MAC (некоректний ключ?)
  02 28  → fatal: handshake_failure  — не вдалось узгодити параметри
  02 2a  → fatal: bad_certificate    — проблема з сертифікатом
  02 2c  → fatal: certificate_revoked
  02 2f  → fatal: certificate_expired
  02 30  → fatal: unknown_ca         — невідомий кореневий CA
  02 46  → fatal: inappropriate_fallback — SCSV downgrade detection
  02 70  → fatal: no_application_protocol — ALPN не узгоджено
  01 00  → warning: close_notify     — graceful close (TLS 1.3: завжди fatal!)
У TLS 1.3 всі Alerts є fatal — сесія завжди закривається після Alert. Рівень warning збережено лише для close_notify для зворотної сумісності, але фактично після нього теж закривається з'єднання. Це усунуло цілий клас атак, що використовували warning alerts для маніпуляцій зі станом з'єднання.

Key Update: ротація ключів без перезапуску

TLS 1.3 вводить механізм Key Update — оновлення ключів шифрування без нового Handshake. Це важливо для довготривалих з'єднань:

Проблема: чому треба оновлювати ключі?

AES-GCM має обмеження: при одному ключі безпечно
зашифрувати не більше ~2^32 записів (≈4 мільярди).
Після цього ймовірність nonce-колізії стає неприйнятною.

Для HTTPS-сесій це теоретична проблема.
Але для довгострокових gRPC-стрімів, VPN-тунелів
або файлових передач — цілком реальна.
Key Update handshake (TLS 1.3):

Клієнт                                        Сервер
   │                                             │
   │ KeyUpdate (update_requested: true) 🔒 ─────►│
   │   ← просить сервер теж оновити ключ        │
   │                                             │
   │          нові ключі (HKDF-Expand від       │
   │          поточного application_secret)     │
   │                                             │
   │◄────────────────── KeyUpdate (not_requested)│
   │                                             │
   │   Обидві сторони переходять на нові ключі. │
   │   Старі ключі знищуються.                  │

Реальні атаки на TLS: від теорії до практики

Найкращий спосіб зрозуміти, чому TLS 1.3 виглядає саме так — вивчити атаки, що зламали попередні версії. Кожна вразливість нижче стала уроком, що знайшов відображення у дизайні TLS 1.3.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

@startmindmap
* Атаки на TLS/SSL
** На протокол
*** BEAST (2011)\nCBC + IV передбачення
*** POODLE (2014)\nSSL 3.0 + CBC padding
*** DROWN (2016)\nSSLv2 cross-protocol
*** LOGJAM (2015)\nDHE downgrade 512-bit
*** FREAK (2015)\nExport RSA downgrade
** На реалізацію
*** Heartbleed (2014)\nOpenSSL buffer over-read
*** CCS Injection (2014)\nOpenSSL timing
*** BERserk (2014)\nRSA signature forgery
** На PKI
*** DigiNotar (2011)\nCompromised CA
*** Mis-issued certs\n(Symantec 2017)
** На конфігурацію
*** CRIME (2012)\nTLS Compression
*** BREACH (2013)\nHTTP Compression
*** SWEET32 (2016)\n3DES birthday attack
@endmindmap

@enduml

BEAST (2011): коли IV стає передбачуваним

Browser Exploit Against SSL/TLS — атака Дуонга (Thai Duong) та Різо (Juliano Rizzo), представлена на ekoparty 2011.

Вразливий компонент: TLS 1.0 + AES-CBC.

Суть проблеми: У TLS 1.0 IV для кожного наступного запису не є випадковим — ним є останній блок шифртексту попереднього запису. Це відоме як «IV chaining» або «implicit IV».

TLS 1.0 CBC IV Chaining (вразливість):

Запис N:
  IV_N = останній блок ciphertext(N-1)  ← передбачуваний!
  Ciphertext_block_1 = AES(key, IV_N XOR plaintext_block_1)

Атака (спрощено):
  Зловмисник знає IV для наступного запису (він відкритий у мережі).
  Зловмисник може вибирати, які дані клієнт відправляє.
  (Наприклад, через JavaScript у браузері)

  Вгадка G такого, що: AES(key, IV XOR G) == ciphertext_block_1?
  → Підбір побайтово через browser oracle.
  → Атака відновлює cookie/session token.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Зловмисник\n(JS у браузері жертви)" as attacker #fce4ec
participant "Браузер жертви" as victim #fff3e0
participant "TLS 1.0 Сервер" as server #e8f5e9

note over attacker, server
  Мета: відновити значення секретного cookie C.
end note

attacker -> victim : Виконати JS: надіслати запит R1
victim -> server : TLS Record 1: [header | C | padding]
note right of attacker : Спостерігає IV_2 = last_block(Ciphertext_1)

attacker -> victim : Виконати JS: надіслати запит R2\nтакий, що перший блок = IV_2 XOR guess
victim -> server : TLS Record 2: [block = AES(key, IV_2 XOR guess)]

note right of attacker
  Якщо ciphertext_block(R2) == ciphertext_block_1(R1)
  → guess правильний → відновлено один байт C.
  Повторити 16 разів для всього блоку.
end note

@enduml

Виправлення в TLS 1.1/1.2: Явний випадковий IV для кожного запису. Виправлення в TLS 1.3: CBC повністю видалено.


POODLE (2014): оракул заповнення на старому протоколі

Padding Oracle On Downgraded Legacy Encryption — атака Меллера (Bodo Möller), Дуонга та Козловського з Google Security Team.

Вразлива конфігурація: SSL 3.0 підтримується як fallback.

Суть: SSL 3.0 використовує CBC з padding, де останній байт padding вказує на довжину, але решта байтів padding не перевіряється. Це padding oracle: сервер мимоволі повідомляє, чи правильний padding, через різні response коди (успіх vs MAC error).

SSL 3.0 CBC Padding (вразлива схема):

Block = [data...][pad_bytes...][pad_length_byte]

Перевірка: лише останній байт == кількість padding байтів.
Інші padding байти можуть бути будь-якими.

Оракул: якщо сервер повертає "MAC error" — padding OK.
         якщо "decryption error" — padding не OK.
         (або вимірювання часу відповіді)

Атака: маніпулюючи зашифрованим блоком та використовуючи
оракул, зловмисник може відновити plaintext побайтово.
1 байт = 256 спроб у середньому.
Для 16-байтового блоку = ~4096 HTTPS запитів.

Ключова умова: зловмисник може змусити клієнт перемкнутись на SSL 3.0 через downgrade fallback. Якщо TLS-з'єднання «не вдається» (підроблений мережевий збій), браузер пробує SSL 3.0.

Виправлення: вимкнути SSL 3.0 повністю. TLS Fallback SCSV (RFC 7507) — клієнт вставляє спеціальний псевдо-cipher-suite TLS_FALLBACK_SCSV у ClientHello при downgrade, сервер відхиляє підключення якщо версія нижча за підтримувану.

TLS_FALLBACK_SCSV (захист від downgrade):

Клієнт підтримує TLS 1.2, але через помилку намагається TLS 1.1:
  ClientHello:
    Version: TLS 1.1
    CipherSuites: [..., TLS_FALLBACK_SCSV (0x56,0x00)]

Сервер підтримує TLS 1.2:
  Бачить SCSV + TLS 1.1 < TLS 1.2 → fatal alert: inappropriate_fallback
  Зловмисник не може штучно понизити версію протоколу.

Heartbleed (CVE-2014-0160): кров з серця OpenSSL

Heartbleed — не атака на протокол TLS, а вразливість реалізації у бібліотеці OpenSSL. Але вона стала однією з найруйнівніших вразливостей в історії інтернету: понад 500 000 серверів були вразливі на момент публікації (квітень 2014).

Компонент: TLS Heartbeat Extension (RFC 6520) — механізм keepalive для перевірки живучості з'єднання.

Принцип роботи Heartbeat (нормальний):

Клієнт → Сервер: HeartbeatRequest
  type:    request (1)
  length:  5
  payload: "HELLO"  (5 байт)

Сервер → Клієнт: HeartbeatResponse
  type:    response (2)
  length:  5
  payload: "HELLO"  (скопійовано з запиту)

Вразливість (Heartbleed):

/* Вразливий код OpenSSL (спрощено): */
void tls1_process_heartbeat(SSL *s) {
    unsigned char *p = &s->s3->rrec.data[0], *pl;
    unsigned short hbtype;
    unsigned int payload;

    hbtype = *p++;             /* тип: request/response */
    n2s(p, payload);           /* довжина payload з пакету */
    pl = p;                    /* вказівник на payload */

    /* ПОМИЛКА: не перевіряється, чи payload <= реальний розмір даних! */

    unsigned char *buffer = OPENSSL_malloc(1 + 2 + payload + padding);
    memcpy(bp, pl, payload);   /* копіюємо payload байт з пам'яті */
    /* Якщо payload=65535, але реальних даних 5 — читаємо 65530 зайвих байт! */
}

Атака:

Зловмисник → Сервер: HeartbeatRequest
  type:    request
  length:  65535   ← заявлена довжина
  payload: "HELLO" ← лише 5 байт реальних даних

Сервер → Зловмисник: HeartbeatResponse
  payload: "HELLO" + [65530 байт з heap-пам'яті сервера]
                         ↑
             Тут можуть бути:
             - приватні ключі TLS
             - паролі користувачів
             - session tokens
             - інші секрети з пам'яті
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Зловмисник" as attacker #fce4ec
participant "OpenSSL Server\n(вразлива версія)" as server #e8f5e9

note over attacker, server #fce4ec
  CVE-2014-0160: OpenSSL 1.0.1 — 1.0.1f
  (березень 2012 — квітень 2014)
end note

attacker -> server : HeartbeatRequest\ntype=1, length=65535\npayload="A" (1 байт)

note right of server
  Код: memcpy(response, payload, 65535)
  Реально в буфері: 1 байт.
  Читає 65534 зайвих байтів з heap!
end note

server --> attacker : HeartbeatResponse\n"A" + [65534 байт сирої пам'яті]

note right of attacker
  Повторити ~1000 разів для
  повного зливу heap.
  Без автентифікації.
  Без слідів у логах.
end note

attacker -> attacker : Аналізувати дамп:\n- PEM private key headers\n- "password=", "token="\n- HTTP cookie headers

@enduml

Масштаб катастрофи: Атака не залишала слідів у логах. Не вимагала автентифікації. Виконувалась через легітимне TLS-з'єднання. Могла витягувати 64 КБ пам'яті за запит, необмежену кількість разів.

Виправлення: OpenSSL 1.0.1g (7 квітня 2014) — додано перевірку: if (1 + 2 + payload + padding > s->s3->rrec.num_data) return 0;

Heartbleed показав: навіть бездоганний криптографічний дизайн TLS не захищає від вразливостей реалізації. Безпека TLS-системи — це ланцюг: криптографія + реалізація (OpenSSL, BoringSSL, SChannel) + конфігурація + операційна система.

CRIME та BREACH: стиснення як оракул

CRIME (Compression Ratio Info-leak Made Easy, 2012) — атака на TLS-стиснення (DEFLATE як TLS extension).

Принцип: Алгоритми стиснення (LZ77) замінюють повторювані рядки посиланнями. Якщо payload містить рядок, що вже зустрічався — розмір зашифрованого запису стає меншим. Зловмисник може спостерігати розмір записів навіть без розшифрування.

CRIME — атака через розмір:

Секрет у cookie: "sessionid=abc123secret"

Спроба 1: Зловмисник змушує браузер надіслати:
  ?q=sessionid=a  → cookie + query мають спільний "sessionid=a"
  Compressed size: МАЛИЙ  ← стиснення спрацювало!

Спроба 2:
  ?q=sessionid=b  → no match
  Compressed size: БІЛЬШИЙ

→ "a" правильний! Побайтово відновлюємо весь sessionid.

Виправлення: Заборонити TLS-стиснення. У TLS 1.3 стиснення на рівні Record Layer видалено повністю.

BREACH (2013) — аналогічна атака, але на HTTP-стиснення (gzip у HTTP response body). Оскільки HTTP-стиснення не є частиною TLS — виправити на рівні TLS неможливо. Заходи: рандомізація padding у HTTP відповідях, відключення Gzip для відповідей із секретами.


SWEET32 (2016): day birthday у 3DES

Атака: Блонто (Karthikeyan Bhargavan) та Лейні (Gaëtan Leurent) з INRIA.

Проблема: 3DES використовує 64-бітний розмір блоку. За парадоксом днів народження, при шифруванні $2^{32}$ блоків (~32 ГБ) виникає приблизно 50% ймовірність колізії двох блоків шифртексту. Якщо два блоки шифртексту однакові — зловмисник знає, що відповідні блоки plaintext теж однакові (XOR з однаковим keystream).

Birthday bound для 64-бітного блоку:

Кількість блоків для колізії з ймовірністю 50%:
  N = 2^(n/2) = 2^32 ≈ 4 мільярди блоків × 8 байт = 32 ГБ

Сучасне HTTPS з'єднання передає 32 ГБ?
  HTTP keep-alive + TLS session reuse → так, за кілька годин!
  Особливо актуально для bulk-transfer або streaming.

3DES у TLS → TLS_RSA_WITH_3DES_EDE_CBC_SHA (найпоширеніший)

Виправлення: Заборонити 3DES у TLS. Рекомендація: обмежити тривалість сесії або кількість записів при використанні будь-якого шифру зі слабким birthday bound.


LOGJAM та FREAK: атаки зниження до export-grade

FREAK (2015): Федеральний уряд США у 1990-х вимагав, щоб американські продукти на експорт підтримували ослаблену export-grade криптографію (RSA 512 біт, замість 2048). Ця вимога зникла у 2000 р., але export cipher suites залишились у коді багатьох реалізацій.

LOGJAM (2015): Аналогічна атака, але на DHE export (512-бітний DH). Зловмисник типу MitM може «переконати» клієнт перемкнутись на 512-бітний DHE, який легко зламати за ~7 хвилин на звичайному ноутбуці.

Логіка downgrade атаки (спрощено):

  Клієнт (підтримує TLS 1.2 + DHE 2048):
    ClientHello: [..., TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, ...]

  Зловмисник між клієнтом і сервером:
    Підмінює ClientHello, залишаючи лише export suites.

  Сервер (підтримує export для legacy):
    ServerHello: TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA ✓
    DHE: 512-бітні параметри

  Зловмисник: зламує 512-бітний DH за хвилини.
  Розшифровує і підмінює трафік.

Виправлення: Вилучити всі EXPORT та NULL cipher suites. TLS 1.3 видалив їх повністю.


Порівняльна таблиця атак

АтакаРікВразлива конфігураціяЩо компрометуєтьсяВиправлення в TLS 1.3
BEAST2011TLS 1.0 + CBCSession cookiesCBC видалено
CRIME2012TLS CompressionSession tokensСтиснення видалено
BREACH2013HTTP CompressionSecrets in bodyНе в TLS (застосунок)
Heartbleed2014OpenSSL реалізаціяПриватні ключіНе в TLS (патч OpenSSL)
POODLE2014SSL 3.0 fallbackSession cookiesSSL 3.0/downgrade заборонено
FREAK2015Export RSA 512-bitSession ключіExport видалено
LOGJAM2015Export DHE 512-bitSession ключіDHE < 1024 заборонено
DROWN2016SSLv2 на тому ж ключіRSA decryptSSLv2 заборонено
SWEET3220163DESБлоки plaintext3DES видалено
Спільна риса більшості атак — downgrade: зловмисник змушує сторони використовувати застарілий, слабший протокол або алгоритм. TLS 1.3 вирішує це радикально: просто видаляє всі застарілі опції. Неможливо бути атакованим через алгоритм, якого немає.

Четверта частина охоплює TLS Record Layer та реальні атаки — від BEAST до Heartbleed. Далі — практика: TLS у .NET.


TLS у .NET: від SslStream до Kestrel

Архітектура TLS у .NET

.NET надає TLS на кількох рівнях абстракції. Розуміння ієрархії допомагає обрати правильний інструмент для конкретної задачі:

Рівні TLS-абстракції у .NET:

┌─────────────────────────────────────────────┐
│  HttpClient / IHttpClientFactory            │  ← найвищий рівень
│  (HTTPS автоматично через HttpClientHandler)│
├─────────────────────────────────────────────┤
│  ASP.NET Core Kestrel                        │  ← для серверів
│  (ssl_protocols, ssl_ciphers, cert config)  │
├─────────────────────────────────────────────┤
│  SslStream                                  │  ← прямий доступ до TLS
│  (поверх будь-якого Stream: TCP, Pipe...)   │
├─────────────────────────────────────────────┤
│  System.Security.Cryptography.X509Certs     │  ← сертифікати
│  System.Net.Security.SslClientAuthOptions   │  ← налаштування
├─────────────────────────────────────────────┤
│  SChannel (Windows) / OpenSSL (Linux/macOS) │  ← нативний TLS-стек ОС
└─────────────────────────────────────────────┘

.NET не реалізує TLS самостійно — він делегує до нативного TLS-стеку ОС:

  • Windows: SChannel (Security Support Provider Interface)
  • Linux: OpenSSL (через libssl)
  • macOS: Secure Transport (Apple) або OpenSSL

Це означає: оновлення безпеки TLS (нові Cipher Suites, виправлення вразливостей) надходять через оновлення ОС, а не через оновлення .NET.


SslStream: найнижчий рівень API

System.Net.Security.SslStream — прямий wrapper над нативним TLS. Він обгортає будь-який Stream (зазвичай NetworkStream від TcpClient) і додає шифрування. Використовується коли потрібен повний контроль: нестандартні протоколи, custom certificate validation, raw TLS поверх TCP.

TLS-клієнт на SslStream

using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;
using System.Text;

// Підключення до HTTPS сервера вручну через SslStream
var tcpClient = new TcpClient();
await tcpClient.ConnectAsync("github.com", 443);

// Обгортаємо NetworkStream у SslStream
// leaveInnerStreamOpen: false — закрити TcpClient разом із SslStream
var sslStream = new SslStream(
    innerStream: tcpClient.GetStream(),
    leaveInnerStreamOpen: false,
    userCertificateValidationCallback: ValidateServerCertificate);

// Виконати TLS Handshake (клієнтська сторона)
var clientOptions = new SslClientAuthenticationOptions
{
    TargetHost = "github.com",          // SNI + перевірка CN/SAN
    EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
    CertificateRevocationCheckMode = X509RevocationMode.Online,
    // ClientCertificates — для mTLS (необов'язково)
};

await sslStream.AuthenticateAsClientAsync(clientOptions);

Console.WriteLine($"Protocol:    {sslStream.SslProtocol}");
Console.WriteLine($"Cipher:      {sslStream.NegotiatedCipherSuite}");
Console.WriteLine($"KeyExchange: {sslStream.KeyExchangeAlgorithm}");
Console.WriteLine($"Cert:        {sslStream.RemoteCertificate?.Subject}");

// Тепер SslStream — звичайний Stream для читання/запису
// Всі дані автоматично шифруються/дешифруються
var request = "GET / HTTP/1.1\r\nHost: github.com\r\nConnection: close\r\n\r\n";
await sslStream.WriteAsync(Encoding.ASCII.GetBytes(request));

using var reader = new StreamReader(sslStream);
var firstLine = await reader.ReadLineAsync();
Console.WriteLine($"Response: {firstLine}");  // HTTP/1.1 200 OK

// Коректне закриття TLS-з'єднання (відправляє close_notify alert)
await sslStream.ShutdownAsync();

// Валідатор сертифіката (можна повністю замінити стандартну поведінку)
static bool ValidateServerCertificate(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // У продакшені: повертати sslPolicyErrors == SslPolicyErrors.None
    // Тут — логування для навчального прикладу

    if (sslPolicyErrors != SslPolicyErrors.None)
    {
        Console.Error.WriteLine($"TLS Error: {sslPolicyErrors}");
        // RemoteCertificateChainErrors — детальні помилки ланцюжка
        if (chain != null)
        {
            foreach (var status in chain.ChainStatus)
                Console.Error.WriteLine($"  Chain: {status.StatusInformation}");
        }
        return false;  // відхилити з'єднання
    }
    return true;
}

TLS-сервер на SslStream

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

// Завантажити сертифікат сервера з PEM файлів
// (або з Windows Certificate Store: X509Store)
var serverCert = X509Certificate2.CreateFromPemFile(
    certPemFilePath: "server.crt",
    keyPemFilePath:  "server.key");

var listener = new TcpListener(IPAddress.Any, 8443);
listener.Start();
Console.WriteLine("TLS сервер слухає на :8443");

while (true)
{
    var tcpClient = await listener.AcceptTcpClientAsync();

    // Обробляємо кожне з'єднання в окремій Task
    _ = Task.Run(() => HandleClientAsync(tcpClient, serverCert));
}

async Task HandleClientAsync(TcpClient client, X509Certificate2 cert)
{
    await using var sslStream = new SslStream(
        client.GetStream(),
        leaveInnerStreamOpen: false);

    try
    {
        // TLS Handshake — серверна сторона
        var serverOptions = new SslServerAuthenticationOptions
        {
            ServerCertificate = cert,
            EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
            ClientCertificateRequired = false,    // true для mTLS
            CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
            // AllowRenegotiation = false — вимкнути renegotiation (безпечніше)
        };

        await sslStream.AuthenticateAsServerAsync(serverOptions);

        Console.WriteLine($"[{client.Client.RemoteEndPoint}] "
            + $"TLS {sslStream.SslProtocol}, {sslStream.NegotiatedCipherSuite}");

        // Читати HTTP запит
        using var reader = new StreamReader(sslStream, leaveOpen: true);
        var requestLine = await reader.ReadLineAsync();
        Console.WriteLine($"Request: {requestLine}");

        // Відповісти HTTP/1.1
        var response =
            "HTTP/1.1 200 OK\r\n" +
            "Content-Type: text/plain\r\n" +
            "Content-Length: 13\r\n" +
            "Connection: close\r\n\r\n" +
            "Hello, TLS!\r\n";

        await sslStream.WriteAsync(Encoding.UTF8.GetBytes(response));
        await sslStream.ShutdownAsync();
    }
    catch (AuthenticationException ex)
    {
        // Handshake провалився (невалідний сертифікат, версія, тощо)
        Console.Error.WriteLine($"TLS Handshake failed: {ex.Message}");
    }
}

HttpClient та HTTPS: найпоширеніший сценарій

Для більшості застосунків прямий SslStream надлишковий — достатньо HttpClient. Він автоматично виконує TLS Handshake, перевіряє сертифікати та керує пулом з'єднань.

Базове використання

// HttpClient автоматично використовує HTTPS для https:// URL
// TLS налаштовується через HttpClientHandler (або SocketsHttpHandler)
using var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync("https://api.github.com/");

Тонке налаштування TLS через SocketsHttpHandler

var handler = new SocketsHttpHandler
{
    // Пул з'єднань — одне TLS-з'єднання перевикористовується
    // для багатьох HTTP-запитів (HTTP/1.1 Keep-Alive, HTTP/2)
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),

    SslOptions = new SslClientAuthenticationOptions
    {
        // Дозволити лише сучасні версії TLS
        EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,

        // Власна логіка валідації сертифіката
        RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
        {
            // Приклад: ігнорувати помилки у dev-середовищі
            if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
                == "Development")
            {
                return true;  // НЕБЕЗПЕЧНО У ПРОДАКШЕНІ!
            }
            return errors == SslPolicyErrors.None;
        },

        // Для mTLS: клієнтський сертифікат
        ClientCertificates = new X509CertificateCollection
        {
            X509Certificate2.CreateFromPemFile("client.crt", "client.key")
        },
    }
};

using var httpClient = new HttpClient(handler);

IHttpClientFactory: правильна реєстрація у DI

// Program.cs / DI реєстрація
builder.Services.AddHttpClient("SecureApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    SslOptions = new SslClientAuthenticationOptions
    {
        EnabledSslProtocols = SslProtocols.Tls13,
    }
});

// Використання в сервісі
public class ApiService(IHttpClientFactory factory)
{
    public async Task<string> GetDataAsync()
    {
        // IHttpClientFactory керує пулом — не створює нові handler-и щоразу
        var client = factory.CreateClient("SecureApi");
        return await client.GetStringAsync("/data");
    }
}
Ніколи не створюйте new HttpClient() у кожному запиті — це виснажує порти (socket exhaustion). IHttpClientFactory або довгоживучий static HttpClient — правильні підходи. Крім того, HttpClientHandler не є thread-safe; SocketsHttpHandler — є.

Kestrel: налаштування TLS для ASP.NET Core сервера

Kestrel — вбудований веб-сервер ASP.NET Core. Він підтримує HTTPS безпосередньо (або через reverse proxy як Nginx).

Базове налаштування через appsettings.json

{
    "Kestrel": {
        "Endpoints": {
            "Http": {
                "Url": "http://localhost:5000"
            },
            "Https": {
                "Url": "https://localhost:5001",
                "Certificate": {
                    "Path": "certs/server.pfx",
                    "Password": "cert-password"
                }
            }
        }
    }
}

Програмне налаштування TLS (детальний контроль)

// Program.cs
builder.WebHost.ConfigureKestrel(options =>
{
    // HTTP — лише для redirect на HTTPS (або для health checks)
    options.ListenAnyIP(5000);

    // HTTPS з повним контролем TLS
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            // Сертифікат: з файлу, зі сховища або через Let's Encrypt
            httpsOptions.ServerCertificate =
                X509Certificate2.CreateFromPemFile("server.crt", "server.key");

            // Дозволені версії TLS
            httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;

            // Cipher Suites (Windows: через CNG policy; Linux: через OpenSSL)
            // На Linux можна задати через DOTNET_SYSTEM_NET_SECURITY_*
            // env variables або через SslStreamCertificateContext

            // Для mTLS: вимагати клієнтський сертифікат
            httpsOptions.ClientCertificateMode =
                ClientCertificateMode.RequireCertificate;

            httpsOptions.ClientCertificateValidation =
                (cert, chain, errors) =>
                {
                    // Перевірити, що CN клієнтського сертифіката
                    // є у whitelist дозволених сервісів
                    return cert.Subject.Contains("CN=trusted-service");
                };
        });
    });
});

HTTP Strict Transport Security (HSTS)

HSTS (RFC 6797) — заголовок відповіді, що змушує браузер завжди використовувати HTTPS для цього домену протягом зазначеного часу, навіть якщо користувач введе http://:

// Program.cs
var app = builder.Build();

// Redirect HTTP → HTTPS
app.UseHttpsRedirection();

// HSTS — лише у продакшені!
// У розробці HSTS може заблокувати localhost на HTTP
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
    // Відправляє: Strict-Transport-Security: max-age=31536000; includeSubDomains
}
// Налаштування HSTS
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);   // 1 рік
    options.IncludeSubDomains = true;           // і субдомени
    options.Preload = true;                     // для HSTS preload list
    // options.ExcludedHosts.Add("api.example.com"); // виключення
});
HSTS в дії:

Перший запит (HTTP):
  Client → http://example.com/
  Server ← 301 Moved Permanently → https://example.com/
           Strict-Transport-Security: max-age=31536000

Всі наступні запити (протягом 1 року):
  Браузер: бачить "http://example.com/" → автоматично змінює на HTTPS
  Жодного HTTP запиту не надсилається взагалі.
  MitM зловмисник не може перехопити initial HTTP запит.
HSTS Preload List — браузери Chrome, Firefox, Safari мають вбудований список доменів, що завжди повинні завантажуватись через HTTPS. Навіть перший запит іде одразу по HTTPS. Домен можна додати через hstspreload.org (безповоротньо — видалення займає місяці).

Генерація та управління сертифікатами у .NET

Самопідписаний сертифікат для розробки

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// Генерація самопідписаного сертифіката для localhost
// (не для продакшену!)
static X509Certificate2 CreateSelfSignedCertificate(string subjectName)
{
    using var rsa = RSA.Create(keySizeInBits: 2048);

    var request = new CertificateRequest(
        subjectName: $"CN={subjectName}",
        key: rsa,
        hashAlgorithm: HashAlgorithmName.SHA256,
        RSASignaturePadding.Pkcs1);

    // Розширення
    request.CertificateExtensions.Add(
        new X509BasicConstraintsExtension(
            certificateAuthority: false,
            hasPathLengthConstraint: false,
            pathLengthConstraint: 0,
            critical: true));

    request.CertificateExtensions.Add(
        new X509KeyUsageExtension(
            X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
            critical: true));

    request.CertificateExtensions.Add(
        new X509EnhancedKeyUsageExtension(
            new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, // serverAuth
            critical: false));

    // Subject Alternative Names (SAN)
    var sanBuilder = new SubjectAlternativeNameBuilder();
    sanBuilder.AddDnsName("localhost");
    sanBuilder.AddDnsName(subjectName);
    sanBuilder.AddIpAddress(System.Net.IPAddress.Loopback);
    request.CertificateExtensions.Add(sanBuilder.Build());

    // Самопідписаний (без CA)
    var cert = request.CreateSelfSigned(
        notBefore: DateTimeOffset.UtcNow.AddMinutes(-5),
        notAfter:  DateTimeOffset.UtcNow.AddYears(1));

    // Повернути з приватним ключем (для сервера)
    return new X509Certificate2(
        cert.Export(X509ContentType.Pfx),
        password: (string?)null,
        X509KeyStorageFlags.Exportable);
}

dotnet dev-certs: стандарт для розробки

.NET SDK має вбудований інструмент для dev-сертифікатів:

# Створити та встановити dev-сертифікат (робить його довіреним в ОС)
dotnet dev-certs https --trust

# Експортувати у PEM для nginx/docker
dotnet dev-certs https --export-path ./certs/dev.pem --format Pem

# Перевірити статус
dotnet dev-certs https --check --trust

# Очистити та перестворити
dotnet dev-certs https --clean
dotnet dev-certs https --trust

Завантаження сертифікатів із різних джерел

// З PEM файлів (Linux/macOS стандарт, Let's Encrypt)
var cert1 = X509Certificate2.CreateFromPemFile("cert.pem", "key.pem");

// З PFX/PKCS#12 файлу (Windows стандарт)
var cert2 = new X509Certificate2("cert.pfx", "password",
    X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);

// З Windows Certificate Store
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates
    .Find(X509FindType.FindBySubjectName, "example.com", validOnly: true);
var cert3 = certs.Count > 0 ? certs[0] : throw new Exception("Cert not found");

// Із змінної середовища (Kubernetes Secrets, Docker Secrets)
var certBase64 = Environment.GetEnvironmentVariable("TLS_CERT_BASE64")!;
var keyBase64  = Environment.GetEnvironmentVariable("TLS_KEY_BASE64")!;
var certPem = Encoding.UTF8.GetString(Convert.FromBase64String(certBase64));
var keyPem  = Encoding.UTF8.GetString(Convert.FromBase64String(keyBase64));
var cert4 = X509Certificate2.CreateFromPem(certPem, keyPem);

Перевірка TLS конфігурації сервера

Перед виходом у продакшен варто перевірити конфігурацію TLS інструментами:

OpenSSL s_client: ручна перевірка

# Базова перевірка TLS з'єднання
openssl s_client -connect example.com:443 -servername example.com

# Примусово тільки TLS 1.3
openssl s_client -connect example.com:443 -tls1_3

# Перевірити підтримку TLS 1.0 (має бути відхилено)
openssl s_client -connect example.com:443 -tls1   # має повернути помилку

# Перевірити OCSP Stapling
openssl s_client -connect example.com:443 -status 2>/dev/null | \
  grep -A10 "OCSP Response"

# Переглянути повний ланцюжок сертифікатів
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
  openssl x509 -text -noout

# Виміряти час Handshake (TLS 1.3 vs TLS 1.2)
time openssl s_client -connect example.com:443 -tls1_3 < /dev/null
time openssl s_client -connect example.com:443 -tls1_2 < /dev/null

nmap: сканування Cipher Suites

# Перелік підтримуваних Cipher Suites
nmap --script ssl-enum-ciphers -p 443 example.com

# Приклад виводу:
# PORT    STATE SERVICE
# 443/tcp open  https
# | ssl-enum-ciphers:
# |   TLSv1.2:
# |     ciphers:
# |       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
# |       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
# |     compressors:
# |       NULL
# |   TLSv1.3:
# |     ciphers:
# |       TLS_AKE_WITH_AES_256_GCM_SHA384 - A
# |     cipher preference: server
# |_  least strength: A

SSL Labs API: автоматизована перевірка

// Програмна перевірка через Qualys SSL Labs API
using var http = new HttpClient();

var endpoint = "https://api.ssllabs.com/api/v3/analyze";
var url = $"{endpoint}?host=example.com&publish=off&ignoreMismatch=off";

var result = await http.GetFromJsonAsync<SslLabsResult>(url);
Console.WriteLine($"Grade: {result?.Endpoints?[0]?.Grade}");
// A+ — відмінно, A — добре, B — є проблеми, F — критичні вразливості

Типові помилки конфігурації TLS у .NET


Налаштування TLS на рівні ОС та середовища

Деякі параметри TLS у .NET керуються через змінні середовища або системні налаштування:

# Вимкнути TLS 1.0 та 1.1 глобально для всього .NET процесу
# (якщо не задано явно через SslProtocols)
export DOTNET_SYSTEM_NET_SECURITY_TLSPROTOCOL=Tls12,Tls13

# OpenSSL (Linux): шлях до CA certificates bundle
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs/

# Вимкнути перевірку відкликання (не рекомендовано)
export DOTNET_SYSTEM_NET_SECURITY_NOOCSPCHECK=1

# Діагностика TLS (verbose logging)
export DOTNET_SYSTEM_NET_SECURITY_LOGBROWSERAUTHENTICATIONERRORS=1
// Программне налаштування глобальних TLS параметрів
// (впливає на весь AppDomain — використовувати з обережністю)
System.Net.ServicePointManager.SecurityProtocol =
    System.Net.SecurityProtocolType.Tls12 |
    System.Net.SecurityProtocolType.Tls13;
// Примітка: ServicePointManager застарів у .NET Core.
// Для .NET Core/5+ використовуйте SocketsHttpHandler.SslOptions.

Підсумок: чеклист безпечного TLS у .NET

✅ Протокол

  • Дозволено лише TLS 1.2 та TLS 1.3
  • TLS 1.3 пріоритетний (швидший, безпечніший)
  • SSL 3.0, TLS 1.0, TLS 1.1 — заборонені

✅ Сертифікат

  • RSA 2048+ або ECDSA P-256+
  • Термін дії ≤ 397 днів
  • SAN містить усі необхідні домени
  • OCSP Stapling увімкнено
  • CT SCT вбудовано

✅ Cipher Suites

  • AES-256-GCM або ChaCha20-Poly1305
  • ECDHE для обміну ключами (PFS)
  • Заборонені: NULL, EXPORT, RC4, 3DES, MD5, SHA-1

✅ HTTP заголовки

  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Content-Security-Policy: upgrade-insecure-requests
  • X-Content-Type-Options: nosniff
  • HSTS Preload для публічних сервісів

TLS — це не просто «увімкнути HTTPS». Це система із взаємозалежних компонентів: криптографічні примітиви, сертифікати та PKI, протокол Handshake, шифрування записів та операційна конфігурація. Розуміння кожного рівня дозволяє не лише правильно налаштувати захист, але й діагностувати проблеми: від ERR_CERTIFICATE_TRANSPARENCY_REQUIRED до handshake_failure у Wireshark.

Найважливіший урок з еволюції SSL → TLS 1.3: безпека досягається виключенням, а не включенням. Кожна нова версія протоколу ставала безпечнішою переважно через видалення застарілих механізмів, а не через додавання нових. Правило просте: чим менше опцій — тим менше поверхні для атаки.

Copyright © 2026