Перейти к содержимому
ecdsa.com

Иллюстрированный разбор

Постквантовое соединение TLS 1.3 в иллюстрациях

Это одно настоящее рукопожатие TLS 1.3, перехваченное в сети и разобранное байт за байтом. Стороны согласуют ключ с помощью группы X25519MLKEM768 — гибрида постквантового ML-KEM-768 и классического X25519, поэтому сеанс остаётся защищённым даже от злоумышленника, который записывает его сегодня, а эллиптические кривые взломает позже. Каждое число ниже пересчитано по перехваченным байтам и сверено с тем, что OpenSSL записал в журнал; ни одно значение не взято для примера.

Одноразовые ключи этого сеанса

Рукопожатие можно воспроизвести, только если известны его секреты, поэтому в этой демонстрации они опубликованы. Каждое значение здесь — одноразовый секрет этого единственного сеанса: оно сгенерировано специально для перехвата и больше никогда не используется, поэтому публикация этих значений ничего не раскрывает о других соединениях. Именно они позволяют странице и вам пересчитать каждый шаг ниже.

seed d ‖ z для генерации ключа ML-KEM (64 байта)
84745c7adf177dfb94e15c7cdef6f8006897b295bdafaadd090ef050c004b212921eb10b6b41e8f0221e0f7f164aadef44f4cba1139edd522c9f0860db6b0cde
закрытый ключ X25519 клиента (гибридный key share)
784edfa4400a57540a7cbd71957c8ab182ae801a5c519755abe69f553b9e8c5d
закрытый ключ X25519 клиента (отдельный key share)
d81df8305a5f24f9077b3d669885d0d7b93ec55fe405ccde814bac62e778f656
случайность m для инкапсуляции ML-KEM (32 байта)
4888d955ca1924fb19e233b6d516249f84e945e1d8345e9816b7fbaa2c3632b7
закрытый ключ X25519 сервера
c81c64cbae9dc5549313f148f09f00da5f8fde45be679697e02204a781796145
закрытый скаляр сертификата сервера (P-256)
11aa8fac7486c821d6c57baa03c3a3e119a635f33aaa2043bba12d7f20a40a99

Гибридный key share по частям

Вся цена перехода на постквантовую криптографию сосредоточена в двух полях. Группа X25519MLKEM768 конкатенирует значение ML-KEM-768 и значение X25519, причём в этой группе ML-KEM идёт первым (RFC 10024, § 4): клиент предлагает оба значения в одном key share, сервер отвечает обоими, а две 32-байтовые половины общего секрета соединяются так же.

Key share клиента — 1 216 байт

ML-KEM-768 · 1 184 байта
X25519 · 32 байта

Key share сервера — 1 120 байт

ML-KEM-768 · 1 088 байт
X25519 · 32 байта

Общий секрет — 64 байта

ML-KEM-768 · 32 байта
X25519 · 32 байта

Чего стоит постквантовая защита в трафике

Те же клиент и сервер провели второе рукопожатие с обычным X25519 при прочих равных условиях. В каждом направлении гибридный ключевой материал занимает на килобайт с лишним больше, и это единственная реальная цена: из-за него ClientHello уже не помещается в один пакет.

ПоказательX25519MLKEM768X25519Разница
ClientHello (вся запись)1 483 байта261 байт+1 222 байта
Расширение key_share (клиент)1 262 байта42 байта+1 220 байт
Расширение supported_groups22 байта20 байт+2 байта
ServerHello (вся запись)1 215 байт127 байт+1 088 байт
Расширение key_share (сервер)1 128 байт40 байт+1 088 байт
Всего отправил клиент1 620 байт398 байт+1 222 байта
Всего отправил сервер2 326 байт1 236 байт+1 090 байт
TCP-сегменты с ClientHello2 пакета1 пакет+1 пакет
Клиент → серверЗапись 1 · 1 483 байта в сети

ClientHello

Клиент открывает соединение. Он предлагает TLS 1.3 и 3 набора шифров AES/ChaCha, перечисляет поддерживаемые группы для обмена ключами, начиная с X25519MLKEM768, и отправляет по key share для каждой из двух предпочтительных групп, в том числе гибридный key share длиной 1216 байт. Сообщение передаётся в открытом виде: секретов в нём пока нет.

Передано по сети · 1 483 байта

  • Эти 1 478 байт совпадают с тем, что OpenSSL вывел для этой записи с параметром -msg.
Как это вычисляетсяKey share клиента

Ещё до первого сообщения клиент генерирует эфемерные ключи, которые предложит. Для гибридной группы это пара ключей ML-KEM-768 и пара ключей X25519, открытые ключи которых конкатенируются в один key share длиной 1216 байт; для отдельной группы X25519 генерируется свой, независимый ключ.

Определение
d, z     ←$ B^32                                   FIPS 203 §7.1
(ek, dk) = ML-KEM-768.KeyGen_internal(d, z)        FIPS 203 §6.1
x_c      ←$ B^32,   X_c = X25519(x_c, 9)           RFC 7748 §5
share    = ek ‖ X_c                                1184 + 32 = 1216   RFC 10024 §4.1

y_c      ←$ B^32,   Y_c = X25519(y_c, 9)           key_share: x25519 (0x001d)
Значения из этого сеанса
d (32 байта) — разворачивается в секретный вектор
84745c7adf177dfb94e15c7cdef6f8006897b295bdafaadd090ef050c004b212
z (32 байта) — секрет для неявного отклонения
921eb10b6b41e8f0221e0f7f164aadef44f4cba1139edd522c9f0860db6b0cde
SHA-256 ключа инкапсуляции (1184 байта)
4a2903bee32a7a18d3294d93a4d24d6ecc8c07bc098bab31899689f87a572be8
закрытый ключ X25519 клиента (гибридный key share)
784edfa4400a57540a7cbd71957c8ab182ae801a5c519755abe69f553b9e8c5d
открытый ключ X25519 клиента (гибридный key share)
09e0802926363ed192b02f5261520f2056ba7af9a0acd1eee5bbe50181e0cf29
закрытый ключ X25519 клиента (отдельный key share)
d81df8305a5f24f9077b3d669885d0d7b93ec55fe405ccde814bac62e778f656
открытый ключ X25519 клиента (отдельный key share)
67aab4deeec206fa89034014d772bdd47cedd7930610e81f634871565424f14d
Что это доказывает
  • Генерация ключа ML-KEM-768 из seed воспроизводит ключ инкапсуляции из key share клиента.
  • Все коэффициенты ключа инкапсуляции (их 768) меньше q = 3329 (наибольший — 3326), поэтому сервер принимает ключ (FIPS 203, § 7.2).
  • Закрытый ключ X25519 даёт то же открытое значение, что и в гибридном key share клиента.
  • Отдельный key share X25519 использует собственный ключ, не тот, что в гибридном key share.
Воспроизведите сами
printf '%s' 3054020100300b06096086480165030404020442804084745c7adf177dfb94e15c7cdef6f8006897b295bdafaadd090ef050c004b212921eb10b6b41e8f0221e0f7f164aadef44f4cba1139edd522c9f0860db6b0cde | unhex > client-mlkem.der
openssl pkey -inform DER -in client-mlkem.der -pubout -outform DER | tail -c 1184 | openssl dgst -sha256 -binary | tohex

выводит 4a2903bee32a7a18d3294d93a4d24d6ecc8c07bc098bab31899689f87a572be8

printf '%s' 302e020100300506032b656e04220420784edfa4400a57540a7cbd71957c8ab182ae801a5c519755abe69f553b9e8c5d | unhex > x25519.der
openssl pkey -inform DER -in x25519.der -pubout -outform DER | tail -c 32 | tohex

выводит 09e0802926363ed192b02f5261520f2056ba7af9a0acd1eee5bbe50181e0cf29

Сервер → клиентЗапись 2 · 1 215 байт в сети

ServerHello

Сервер выбирает TLS 1.3, набор шифров TLS_AES_128_GCM_SHA256 и группу X25519MLKEM768 и возвращает собственный гибридный key share длиной 1120 байт. Это последнее сообщение в открытом виде: дальше обе стороны могут вычислить ключи рукопожатия, и всё последующее зашифровано.

Передано по сети · 1 215 байт

  • Эти 1 210 байт совпадают с тем, что OpenSSL вывел для этой записи с параметром -msg.
  • Утилита s_client из OpenSSL сообщила согласованную группу: X25519MLKEM768.
  • Утилита s_client из OpenSSL сообщила шифр: TLS_AES_128_GCM_SHA256.
Как это вычисляетсяKey share сервера

ML-KEM относится к KEM, а не к протоколам Диффи — Хеллмана, поэтому у сервера нет парного закрытого ключа, который можно было бы умножить. Сервер инкапсулирует новый секрет на ключе ML-KEM клиента, получая шифртекст и постквантовый общий секрет, и генерирует собственный ключ X25519. Key share сервера состоит из шифртекста, за которым следует открытое значение X25519 сервера.

Определение
m      ←$ B^32
(K, c) = ML-KEM-768.Encaps_internal(ek, m)         FIPS 203 §6.2
x_s    ←$ B^32,   X_s = X25519(x_s, 9)             RFC 7748 §5
share  = c ‖ X_s                                   1088 + 32 = 1120   RFC 10024 §4.2
Значения из этого сеанса
случайность m для инкапсуляции ML-KEM (32 байта)
4888d955ca1924fb19e233b6d516249f84e945e1d8345e9816b7fbaa2c3632b7
общий секрет ML-KEM K (32 байта)
dad26963f7510c64b7c7d9b80731d6b573abf15999f5bed3076173e3f8c0cbdb
SHA-256 шифртекста ML-KEM (1088 байт)
30d5f4e26e8548b8796088ef0d56d307cab52802ab03e3cea966cb1391dbfc2a
закрытый ключ X25519 сервера
c81c64cbae9dc5549313f148f09f00da5f8fde45be679697e02204a781796145
открытый ключ X25519 сервера
339fdc7e19b200cb062df86bdf0e022c949abacd7db8dbdae0a92a6d9ba8d530
Что это доказывает
  • Инкапсуляция на ключе клиента с записанной случайностью воспроизводит шифртекст сервера.
  • Закрытый ключ X25519 сервера даёт то же открытое значение, что и в его key share.
Как это вычисляетсяОбщий секрет

Теперь обе стороны могут получить одни и те же 64 байта. Клиент декапсулирует шифртекст своим закрытым ключом ML-KEM, обе стороны завершают обмен X25519, и два 32-байтовых результата конкатенируются (сначала ML-KEM). Чтобы восстановить общий секрет, злоумышленнику придётся взломать обе половины — в этом и весь смысл гибрида.

Определение
K  = ML-KEM-768.Decaps(dk, c)                      FIPS 203 §7.3
Z  = X25519(x_c, X_s) = X25519(x_s, X_c)           RFC 7748 §6.1
ss = K ‖ Z                                         32 + 32 = 64   RFC 10024 §4.3
Значения из этого сеанса
общий секрет ML-KEM K (32 байта)
dad26963f7510c64b7c7d9b80731d6b573abf15999f5bed3076173e3f8c0cbdb
общий секрет X25519 Z (32 байта)
568c3055b3ac3516f0d780bbe022beb336ade7e7bae81752f2de6df09ebdc428
общий секрет = K ‖ Z (64 байта)
dad26963f7510c64b7c7d9b80731d6b573abf15999f5bed3076173e3f8c0cbdb568c3055b3ac3516f0d780bbe022beb336ade7e7bae81752f2de6df09ebdc428
Что это доказывает
  • Декапсуляция на стороне клиента и инкапсуляция на стороне сервера дают один и тот же секрет ML-KEM.
  • Обе стороны вычисляют один и тот же секрет X25519.
  • Секрет X25519 не состоит из одних нулей, как и требует RFC 9846, § 7.4.2.
  • Первые 32 байта общего секрета — секрет ML-KEM: такой порядок задаёт RFC 10024, § 4.3.
Воспроизведите сами
printf '%s' 3054020100300b06096086480165030404020442804084745c7adf177dfb94e15c7cdef6f8006897b295bdafaadd090ef050c004b212921eb10b6b41e8f0221e0f7f164aadef44f4cba1139edd522c9f0860db6b0cde | unhex > client-mlkem.der
unhex > ciphertext.bin <<'EOF'
5db0e57cb763af0d1678753192830257df14de0eb4dd0d9a2d7ee2a7c94f4aab
2526fd0957b0c1025efff1763488c3bff22007fdd19b33a0e5605fc2b50ed3aa
adee4643eaa746cc65882d54a49ce4cb208226f6c732dc39981d81de9b788740
f6361120dc130f814bf5dd41d4df8682468e8ec8e2b3d5056cf245df0e0febe7
cf7423f725533dc39210093d57134c0e86a716d6d62694c9548082a5cd9352f2
fb8254397df2921d3a48702df9f622a7a539ba6110bec4736b6d15e38c558105
d6954e5fe1aec5b5ae065222fe1df4b1639e001695dcfba2d0aa09d14f9fee31
201383e58a16709631cb7b182ffbae8dc4e1345afd1763e55bb0714e92470c53
805f05ace3ce45402563cfd08db5429ffe5eeb033a7695530ee01ab7c5a179b1
1e3cd17082f457a158861fda8a9c08e7987dc5e18a48955ddba38766e14e09c3
b6319dbb5cb7c6c8e4aad9d40c2ec2b69e8ab793f742765d2af2644afc87cc1f
add8e0dc76f9725dfb9063f116ef8b02c7dc949160e22d716cfe1bed9a7eccae
e26ebc9615fd2c6e53a93bf97db63bbcae24bff5082a83dd2128fec9297cf449
2811d09363a21e98ac4869512ede9300c3fe5f049d7348404c56c37e1329fbbe
90fa854ef8b4de86c67bd16c4263f5eb99a50939a80086c9a1c0d038343d8c9e
d8b9300a6556d950492925628fb281cd077c82681f75e7aae144617adf613f49
11ff6e8f4246039f2ace861f2b2ec1d0f555e3e1a7fce9c80c6c2a8a062d0a64
7be31fc4b42c8776cd4b6517c4aa939a398b110c052e30a8eb746c476ee0dfc8
8866fbd8fdc816d20add8dbfeabcac6e0dac0f41a4e4822ab1f51b54e4bc178f
4eb7c07670704e425fd99205b8efd2f3fbccb200760127b1188fab5d7be84b06
87d779bfbe63e3b1db13f34ab9205d9f06312046e77737a05c070f1b808b5c80
9aa9e9945c52e4bf46d5b328030b42dbfba641cc0a39e6ace09361371d25fb3e
e78507a6fadfc2b6cd4a06851488b001d8b40a1ceb647b7d9debcbff6c375a13
73540d114e9bca1377b83192209bdb70824784b23da425bc7c8cca3ba0845d14
45e732d4e2327348f3a1d1f2499753bd5786ef80ef9a47c7293eedeef64d21ef
4ec47e847303fb963deff2f873efc8f2cfef263bdc40c34cb9aa80584e1ffd03
efe811cd6e12e4e6cdbda8a1ddc2df5f8f86462311425bae494f087f28524721
43e8bf6f53fb68f6063961b07dd34b0a4ebb4aba6b0f989f5955c4fe10088cae
6f7531a6b699f9e9dd34b2fc3ae102409bc97775b7bdc1e0028042e7c27fb8ed
c43c611b3ffd91de6a08a44700dd9b2d241488710e7cfabf974db0fd8a8f9b68
fee885b69d2cac23f6531af4f69c6f58e0d1ce620c47578c0bf4d3f3435df6ba
67556efaa65ec7bb518c76e840747c8454d0538f36134ee8beb0bd24a616a42d
fc826926ff9608def9b1c433d6e9ad8fcf1e11543a9595795f69c6630b60f581
156d98ce7e53124ceb06ce7e6e8054c6849aa2039b96cde7f9eac19b036b44a7
EOF
openssl pkeyutl -decap -inkey client-mlkem.der -keyform DER -in ciphertext.bin -secret mlkem-secret.bin
tohex < mlkem-secret.bin

выводит dad26963f7510c64b7c7d9b80731d6b573abf15999f5bed3076173e3f8c0cbdb

printf '%s' 302e020100300506032b656e04220420784edfa4400a57540a7cbd71957c8ab182ae801a5c519755abe69f553b9e8c5d | unhex > own.der
printf '%s' 302a300506032b656e032100339fdc7e19b200cb062df86bdf0e022c949abacd7db8dbdae0a92a6d9ba8d530 | unhex > peer.der
openssl pkeyutl -derive -inkey own.der -keyform DER -peerkey peer.der -peerform DER | tohex

выводит 568c3055b3ac3516f0d780bbe022beb336ade7e7bae81752f2de6df09ebdc428

Как это вычисляетсяСекреты трафика рукопожатия

Общий секрет поступает в расписание ключей TLS 1.3 (RFC 9846, § 7.1) — цепочку шагов HKDF-Extract и Derive-Secret. Без предварительно согласованного ключа ранний секрет равен HKDF-Extract от одних нулей; после подмешивания общего секрета получается секрет рукопожатия, а из него по транскрипту ClientHello…ServerHello выводятся секреты трафика рукопожатия клиента и сервера.

Определение
early_secret     = HKDF-Extract(salt = 0, IKM = 0)            0 = 00 × 32
derived          = Derive-Secret(early_secret, "derived", "")
handshake_secret = HKDF-Extract(salt = derived, IKM = K ‖ Z)
c_hs_traffic     = Derive-Secret(handshake_secret, "c hs traffic", ClientHello ‖ ServerHello)
s_hs_traffic     = Derive-Secret(handshake_secret, "s hs traffic", ClientHello ‖ ServerHello)

Derive-Secret(S, label, M)        = HKDF-Expand-Label(S, label, SHA-256(M), 32)
HKDF-Expand-Label(S, label, C, n) = HKDF-Expand(S, HkdfLabel, n)
HkdfLabel = uint16(n) ‖ uint8(6 + |label|) ‖ "tls13 " ‖ label ‖ uint8(|C|) ‖ C
Значения из этого сеанса
early_secret = HKDF-Extract(0, 0)
33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a
SHA-256("") — хеш пустого контекста
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
производный секрет
6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba
handshake_secret
06b849eff15c46a4401e154110a2e63a14f7333237260156f7f1b67ca3305198
SHA-256(ClientHello ‖ ServerHello)
0da9cb1015db6852c07fbdc2389ad83ee8b1898a147932620753ce82d3c0709c
HkdfLabel для «c hs traffic»
002012746c73313320632068732074726166666963200da9cb1015db6852c07fbdc2389ad83ee8b1898a147932620753ce82d3c0709c
client_handshake_traffic_secret
8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623
server_handshake_traffic_secret
46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47f
Что это доказывает
  • Совпадает со строкой CLIENT_HANDSHAKE_TRAFFIC_SECRET в обоих файлах SSLKEYLOGFILE.
  • Совпадает со строкой SERVER_HANDSHAKE_TRAFFIC_SECRET в обоих файлах SSLKEYLOGFILE.
Воспроизведите сами
openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXTRACT_ONLY \
  -kdfopt hexsalt:0000000000000000000000000000000000000000000000000000000000000000 \
  -kdfopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 \
  HKDF | tohex

выводит 33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a

openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
  -kdfopt hexkey:33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a \
  -kdfopt 'prefix:tls13 ' -kdfopt 'label:derived' \
  -kdfopt hexdata:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 \
  TLS13-KDF | tohex

выводит 6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba

openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXTRACT_ONLY \
  -kdfopt hexsalt:6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba \
  -kdfopt hexkey:dad26963f7510c64b7c7d9b80731d6b573abf15999f5bed3076173e3f8c0cbdb568c3055b3ac3516f0d780bbe022beb336ade7e7bae81752f2de6df09ebdc428 \
  HKDF | tohex

выводит 06b849eff15c46a4401e154110a2e63a14f7333237260156f7f1b67ca3305198

unhex <<'EOF' | openssl dgst -sha256 -binary | tohex
010005c2030398fd30aaaf1617a1d20cc42c00c775d798dbaba001b1ae95ca6b
039f1e1fbbd7202bfbccd3b1cde003314afdea3e30508dd784674df104782760
5aeaca811f310d00061302130313010100057300000013001100000e70712d74
6c732e6578616d706c65000b000403000102000a0012001011ec001d0017001e
0018001901000101002300000016000000170000000d002a0028090509060904
04030503060308070808081a081b081c0809080a080b08040805080604010501
0601002b0003020304002d00020101003304ea04e811ec04c064180ac1469cd6
5bc990ebc8e4b7a5bf643e3a709d3f543a187c20c7aa082a02aecd74cb814452
7f8606b7a846953ca9f657ae6a7c7463f22ae0e44589139450d20b0311c7ca93
4a8898abde41c22c20b0367875bb8191d086a6a4141a6e4630d2588642d040c2
a73f8c82c8d6776a196c39f9066e9807a7b62b7745c12b6b04c2c2a0b235f889
8e16a1db48bdea02b66744b2e844b78a98a6796a10d2ab03143943f7d32286fc
8e78047854d555956c7db02079cbac0289cc60e345ceac998f7e4157b646c870
72627627076649b7b0ac7cc60294abe1229b42c4e8f9041d4b76268234550079
81ea36d3f62fc52382eec77f3796513b91a9a56ab1675466486475ebc287ac08
91185b37d1b36b31735a5d647b02481f0c1937dab04e3c956e15e69f89e83fb8
db43a428ab9d97a41e077e91a7b6ba26a350255c20b7a9791283aa5a181b8560
1b7971d993bcb698cd4dab1a92e8657b4329fad22705876904e2bd6ef5469cf8
a9c5674178d2b94d9065491b8fa530018bf15e0a1a17fefc67be14b07a755a80
5b650319025fc72c39b93141051ada437d154712647624b893be82f48ed0632d
a6317df11c56c14835b6d42b5a405f7eca9770675710b545e4f21cec18b139d7
b91a578a0416641f291950e5493cd170f7c8a4273a13bef81215bba72dc214d8
958db49a2004487aef97a672454758dc82f78704762c5264d596d9d38ce4589d
0f549710383924b16365596e2d6100cbc54e2eec665c572d13508b693c61445b
a7ea4b9a6a053c0c89ad955b43e8c035cb7130f6e85063d20e6f8652f592c069
02bedd0209ea14c79f9a902ee62a5a0b5f295779f1946a49b782bac2781b4922
354ca674a99b51d999e784659890acb2569dcc34aadcc51e52a637fea85dfc02
214d796fc64b424034cfa1e40cb114733e0c66fd4c990174c4dfe02d92e8b002
cc75d7597fb6ab5c27824c738c2bb3a239e2726ed9bc8cdecc944c992c9d3044
05493ff537a8d04632b3d3c0dca8ab98428cdd9184455455e7294d772c126e08
aa925c92c2660985b811ca041b7997646d909fe1671c83cccfbf2c49c7086587
d6beeb65a9596406bc95a88d7aa09ec58cc6c1b37366cd4d7c9ed49c90903995
f098412f1abfe1516ab63866b1a86d5f939691bb4dcff85c0cd62ada4c553c10
129e33b14f45a9794990f1501710c485b4c19370e1ca7d4326b092b12280316b
c76267957d5f48210a5baccb81a5d1fc985a77c8dc156fef1188d83616104623
1fe9352a16c8f2262946f3ca9015cb27e26e07052d19d7565e9623e1182cc2f0
67854708c921bd9f52852b810505b577146b31a2acb909440cfb2730e0ea520c
828b2b30c1d3c115700504a518ab05bac15c4a6fd634977748c6b0283b01784f
23179277199d1f399d66d5c2a3636d905a05b625a8e69686b43b166f635510e4
2dba6996bf10bdb0f2afea9ca9e9e8b1c41b012ed735d7646998d051f7124989
c29f3da18334ec22b5a47eea9c13fa9ac1d4e7bf9c1899abd96831f323bcd6ab
2d4caf33e86afeb475f7db256fd56da6c1367ce33a91260443e22573221f4295
3a4517046a2a951b9b41afdca2d156b5cb2c876ab032102392b02d7f437802dd
17ef543d6612ef4856f5558c2dddea2dde270fdab179bdb25309e0802926363e
d192b02f5261520f2056ba7af9a0acd1eee5bbe50181e0cf29001d002067aab4
deeec206fa89034014d772bdd47cedd7930610e81f634871565424f14d001b00
050400010003020004b60303498497776391725d59356de785c94cedb9c5def8
704f1426dfa548c1b589437f202bfbccd3b1cde003314afdea3e30508dd78467
4df1047827605aeaca811f310d130100046e002b000203040033046411ec0460
5db0e57cb763af0d1678753192830257df14de0eb4dd0d9a2d7ee2a7c94f4aab
2526fd0957b0c1025efff1763488c3bff22007fdd19b33a0e5605fc2b50ed3aa
adee4643eaa746cc65882d54a49ce4cb208226f6c732dc39981d81de9b788740
f6361120dc130f814bf5dd41d4df8682468e8ec8e2b3d5056cf245df0e0febe7
cf7423f725533dc39210093d57134c0e86a716d6d62694c9548082a5cd9352f2
fb8254397df2921d3a48702df9f622a7a539ba6110bec4736b6d15e38c558105
d6954e5fe1aec5b5ae065222fe1df4b1639e001695dcfba2d0aa09d14f9fee31
201383e58a16709631cb7b182ffbae8dc4e1345afd1763e55bb0714e92470c53
805f05ace3ce45402563cfd08db5429ffe5eeb033a7695530ee01ab7c5a179b1
1e3cd17082f457a158861fda8a9c08e7987dc5e18a48955ddba38766e14e09c3
b6319dbb5cb7c6c8e4aad9d40c2ec2b69e8ab793f742765d2af2644afc87cc1f
add8e0dc76f9725dfb9063f116ef8b02c7dc949160e22d716cfe1bed9a7eccae
e26ebc9615fd2c6e53a93bf97db63bbcae24bff5082a83dd2128fec9297cf449
2811d09363a21e98ac4869512ede9300c3fe5f049d7348404c56c37e1329fbbe
90fa854ef8b4de86c67bd16c4263f5eb99a50939a80086c9a1c0d038343d8c9e
d8b9300a6556d950492925628fb281cd077c82681f75e7aae144617adf613f49
11ff6e8f4246039f2ace861f2b2ec1d0f555e3e1a7fce9c80c6c2a8a062d0a64
7be31fc4b42c8776cd4b6517c4aa939a398b110c052e30a8eb746c476ee0dfc8
8866fbd8fdc816d20add8dbfeabcac6e0dac0f41a4e4822ab1f51b54e4bc178f
4eb7c07670704e425fd99205b8efd2f3fbccb200760127b1188fab5d7be84b06
87d779bfbe63e3b1db13f34ab9205d9f06312046e77737a05c070f1b808b5c80
9aa9e9945c52e4bf46d5b328030b42dbfba641cc0a39e6ace09361371d25fb3e
e78507a6fadfc2b6cd4a06851488b001d8b40a1ceb647b7d9debcbff6c375a13
73540d114e9bca1377b83192209bdb70824784b23da425bc7c8cca3ba0845d14
45e732d4e2327348f3a1d1f2499753bd5786ef80ef9a47c7293eedeef64d21ef
4ec47e847303fb963deff2f873efc8f2cfef263bdc40c34cb9aa80584e1ffd03
efe811cd6e12e4e6cdbda8a1ddc2df5f8f86462311425bae494f087f28524721
43e8bf6f53fb68f6063961b07dd34b0a4ebb4aba6b0f989f5955c4fe10088cae
6f7531a6b699f9e9dd34b2fc3ae102409bc97775b7bdc1e0028042e7c27fb8ed
c43c611b3ffd91de6a08a44700dd9b2d241488710e7cfabf974db0fd8a8f9b68
fee885b69d2cac23f6531af4f69c6f58e0d1ce620c47578c0bf4d3f3435df6ba
67556efaa65ec7bb518c76e840747c8454d0538f36134ee8beb0bd24a616a42d
fc826926ff9608def9b1c433d6e9ad8fcf1e11543a9595795f69c6630b60f581
156d98ce7e53124ceb06ce7e6e8054c6849aa2039b96cde7f9eac19b036b44a7
339fdc7e19b200cb062df86bdf0e022c949abacd7db8dbdae0a92a6d9ba8d530
EOF

выводит 0da9cb1015db6852c07fbdc2389ad83ee8b1898a147932620753ce82d3c0709c

openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
  -kdfopt hexkey:06b849eff15c46a4401e154110a2e63a14f7333237260156f7f1b67ca3305198 \
  -kdfopt 'prefix:tls13 ' -kdfopt 'label:c hs traffic' \
  -kdfopt hexdata:0da9cb1015db6852c07fbdc2389ad83ee8b1898a147932620753ce82d3c0709c \
  TLS13-KDF | tohex

выводит 8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623

openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
  -kdfopt hexkey:06b849eff15c46a4401e154110a2e63a14f7333237260156f7f1b67ca3305198 \
  -kdfopt 'prefix:tls13 ' -kdfopt 'label:s hs traffic' \
  -kdfopt hexdata:0da9cb1015db6852c07fbdc2389ad83ee8b1898a147932620753ce82d3c0709c \
  TLS13-KDF | tohex

выводит 46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47f

Как это вычисляетсяКлючи записей рукопожатия

Каждый секрет трафика с помощью HKDF-Expand-Label разворачивается в ключ AES-128-GCM и 12-байтовый write_iv. Ими расшифровываются все записи рукопожатия начиная с EncryptedExtensions; nonce каждой записи — это write_iv, объединённый операцией XOR с порядковым номером записи.

Определение
write_key = HKDF-Expand-Label(traffic_secret, "key", "", 16)     AES-128-GCM   RFC 9846 §7.3
write_iv  = HKDF-Expand-Label(traffic_secret, "iv",  "", 12)
nonce     = write_iv XOR uint96(seq)                             RFC 9846 §5.3
Значения из этого сеанса
HkdfLabel для «key»
001009746c733133206b657900
серверный ключ рукопожатия (AES-128)
5ed29e03b3495fd20354ed3f2421640a
серверный write_iv рукопожатия
e465385b2d22f07ffc6a4079
клиентский ключ рукопожатия (AES-128)
96fa01469d090b2a8f4ef6b9fa5e0784
клиентский write_iv рукопожатия
9f45d2e464131048897a81c6
Что это доказывает
    Воспроизведите сами
    openssl kdf -binary -keylen 16 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47f \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:key' \
      TLS13-KDF | tohex

    выводит 5ed29e03b3495fd20354ed3f2421640a

    openssl kdf -binary -keylen 12 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47f \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:iv' \
      TLS13-KDF | tohex

    выводит e465385b2d22f07ffc6a4079

    openssl kdf -binary -keylen 16 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623 \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:key' \
      TLS13-KDF | tohex

    выводит 96fa01469d090b2a8f4ef6b9fa5e0784

    openssl kdf -binary -keylen 12 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623 \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:iv' \
      TLS13-KDF | tohex

    выводит 9f45d2e464131048897a81c6

    Сервер → клиентЗапись 3 · 6 байт в сети

    ChangeCipherSpec

    Один байт 0x01, который ничего не делает. TLS 1.3 сохраняет его только для того, чтобы рукопожатие выглядело как TLS 1.2 для промежуточных устройств, которые иначе разорвали бы соединение (RFC 9846, приложение E.4).

    Передано по сети · 6 байт

    • Этот 1 байт совпадает с тем, что OpenSSL вывел для этой записи с параметром -msg.
    Сервер → клиентЗапись 4 · 28 байт в сети

    EncryptedExtensions

    Первое зашифрованное сообщение: расширения сервера, которые не нужны для установки ключей. Здесь их нет, поэтому сообщение пустое — но оно уже защищено серверным ключом рукопожатия, а это доказывает, что расписание ключей работает.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика рукопожатия с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    5ed29e03b3495fd20354ed3f2421640a
    Вектор инициализацииwrite_iv
    e465385b2d22f07ffc6a4079
    Nonce для порядкового номера 0nonce
    e465385b2d22f07ffc6a4079
    Дополнительные данные: заголовок записиadditional_data
    1703030017
    Тег аутентификацииtag
    55c0c9b55b240c731db30884422ef691
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 0.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 6 байт совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 28 байт

    Расшифрованное содержимое · 7 байт

    Сервер → клиентЗапись 5 · 524 байта в сети

    Certificate

    Цепочка сертификатов сервера (здесь это один сертификат ECDSA P-256), зашифрованная ключом рукопожатия. Страница разбирает её вплоть до двух координат открытого ключа и проверяет подпись, которую поставил на сертификате демонстрационный CA.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика рукопожатия с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    5ed29e03b3495fd20354ed3f2421640a
    Вектор инициализацииwrite_iv
    e465385b2d22f07ffc6a4079
    Nonce для порядкового номера 1nonce
    e465385b2d22f07ffc6a4078
    Дополнительные данные: заголовок записиadditional_data
    1703030207
    Тег аутентификацииtag
    01e58b85a9daa69fa78913be25313b55
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 1.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 502 байта совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 524 байта

    Расшифрованное содержимое · 503 байта

    Как это вычисляетсяЦепочка сертификатов

    Подлинность сервера держится на обычной цепочке подписей. Демонстрационный CA подписал байты tbsCertificate сертификата сервера; строка OpenSSL «Verification: OK» означает именно проверку этой подписи открытым ключом CA, а также проверку имени и срока действия.

    Определение
    e     = SHA-256(tbsCertificate)
    valid = ECDSA-P256-Verify(Q_CA, e, (r, s))         ecdsa-with-SHA256   RFC 5758 §3.2
    Значения из этого сеанса
    открытый ключ демонстрационного CA (несжатая точка P-256)
    04136596262dfe32c00a117a326530235df0416dcfb8300ac1e76b4ceaa7c3417ab65dbcce2950dc2616c49bea9c842dcc4f7d2fe1a2157adc6e4900e72b78cf64
    SHA-256(tbsCertificate)
    68ec291c24ed1fc272a0439a31cd0d28307258ca08f3cc67bde7fa115516e293
    значение подписи r
    5f1c8dec01971b89bab6c887166550128ee211099c2ecbe17c0efd5d1c2d5e71
    значение подписи s
    323909f5ef7bb5a0743701eccfbe4ffbc7fdfefbd12d33733c05541d8d87c86b
    Что это доказывает
    • Подпись сертификата проходит проверку ключом CA (O=ecdsa.com, CN=Illustrated PQ TLS Demo CA).
    • Сертификат CA самоподписанный: его подписал сам O=ecdsa.com, CN=Illustrated PQ TLS Demo CA.
    • Сертификат в сообщении Certificate байт в байт совпадает с тем, что был записан при перехвате.
    • Расширение subjectAltName сертификата охватывает имя pq-tls.example.
    • Время перехвата 2026-10-20T11:32:29Z попадает в срок действия сертификата.
    • Утилита s_client из OpenSSL вывела «Verification: OK» и проверенное имя удалённой стороны.
    Воспроизведите сами
    cat > ca.pem <<'EOF'
    -----BEGIN CERTIFICATE-----
    MIIBtjCCAVygAwIBAgIUPfq2HpUguo3lvTcCMSTFcL+tV1swCgYIKoZIzj0EAwIw
    OTESMBAGA1UECgwJZWNkc2EuY29tMSMwIQYDVQQDDBpJbGx1c3RyYXRlZCBQUSBU
    TFMgRGVtbyBDQTAeFw0yNjEwMjAxMTMyMjlaFw0yNzEwMjAxMTMyMjlaMDkxEjAQ
    BgNVBAoMCWVjZHNhLmNvbTEjMCEGA1UEAwwaSWxsdXN0cmF0ZWQgUFEgVExTIERl
    bW8gQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQTZZYmLf4ywAoRejJlMCNd
    8EFtz7gwCsHna0zqp8NBerZdvM4pUNwmFsSb6pyELcxPfS/hohV63G5JAOcreM9k
    o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU
    1K3Yjw1jXxL5EZzxdBQ4jybTtAcwCgYIKoZIzj0EAwIDSAAwRQIgYKgqZkMtJeI9
    K8K7qZ2tdO5RPkRq7VYFUSdJZZxMNKACIQDs0idPxo5cjXaIzPUz3GNZeZGlQ0s9
    vjbIEmIss3iqJw==
    -----END CERTIFICATE-----
    EOF
    cat > server.pem <<'EOF'
    -----BEGIN CERTIFICATE-----
    MIIB5TCCAYygAwIBAgIUGfvagR77OCskKbwCCGme9dcUymUwCgYIKoZIzj0EAwIw
    OTESMBAGA1UECgwJZWNkc2EuY29tMSMwIQYDVQQDDBpJbGx1c3RyYXRlZCBQUSBU
    TFMgRGVtbyBDQTAeFw0yNjEwMjAxMTMyMjlaFw0yNjEyMDYxMTMyMjlaMBkxFzAV
    BgNVBAMMDnBxLXRscy5leGFtcGxlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
    vkyKek8iRLRJMhTwPtyzIi6agRiN4WF05FHlTfFE91h7RW5dsCsVIaxyYsvRhUQ/
    dZJrO8Vm3mzr9OWSBf4rz6OBkTCBjjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE
    AwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAZBgNVHREEEjAQgg5wcS10bHMuZXhh
    bXBsZTAdBgNVHQ4EFgQUQsmdGy/ZbmpLy3vpYLmOhLcQs/4wHwYDVR0jBBgwFoAU
    1K3Yjw1jXxL5EZzxdBQ4jybTtAcwCgYIKoZIzj0EAwIDRwAwRAIgXxyN7AGXG4m6
    tsiHFmVQEo7iEQmcLsvhfA79XRwtXnECIDI5CfXve7WgdDcB7M++T/vH/f770S0z
    czwFVB2Nh8hr
    -----END CERTIFICATE-----
    EOF
    openssl verify -attime 1792495949 -CAfile ca.pem -verify_hostname pq-tls.example server.pem

    выводит server.pem: OK

    Сервер → клиентЗапись 6 · 102 байта в сети

    CertificateVerify

    Сервер подписывает весь накопленный транскрипт рукопожатия закрытым ключом своего сертификата и тем самым доказывает, что владеет этим ключом. Подпись проверяется открытым ключом из сообщения Certificate.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика рукопожатия с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    5ed29e03b3495fd20354ed3f2421640a
    Вектор инициализацииwrite_iv
    e465385b2d22f07ffc6a4079
    Nonce для порядкового номера 2nonce
    e465385b2d22f07ffc6a407b
    Дополнительные данные: заголовок записиadditional_data
    1703030061
    Тег аутентификацииtag
    a7d50cfc2454b60756d83834baef0ca7
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 2.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 80 байт совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 102 байта

    Расшифрованное содержимое · 81 байт

    Как это вычисляетсяПодпись CertificateVerify

    Действительного сертификата недостаточно: сервер должен доказать, что владеет соответствующим закрытым ключом, причём прямо сейчас и для этого транскрипта. Сервер подписывает фиксированную строку контекста вместе с хешем всех сообщений до Certificate включительно, а клиент проверяет подпись только что полученным открытым ключом.

    Определение
    content = 0x20 × 64 ‖ "TLS 1.3, server CertificateVerify" ‖ 0x00 ‖ SHA-256(ClientHello … Certificate)
    valid   = ECDSA-P256-Verify(Q_server, SHA-256(content), (r, s))      ecdsa_secp256r1_sha256
    Значения из этого сеанса
    SHA-256(ClientHello … Certificate)
    af76fc7ac4f19eb2af3f584874eb0cd0429a06a15cf60faefa739b3fa4eb8aad
    подписываемые данные (130 Б)
    20202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020544c5320312e332c2073657276657220436572746966696361746556657269667900af76fc7ac4f19eb2af3f584874eb0cd0429a06a15cf60faefa739b3fa4eb8aad
    открытый ключ сертификата сервера (точка P-256)
    04be4c8a7a4f2244b4493214f03edcb3222e9a81188de16174e451e54df144f7587b456e5db02b1521ac7262cbd185443f75926b3bc566de6cebf4e59205fe2bcf
    значение подписи r
    e2a43fd0b309c60cd461d2e842bf89926c1d271a3f6e79ee5776cb8cc4f8ddbc
    значение подписи s
    a9667486294b49473c7bc82723f305694cdd7e8f591b587e2b75b909da995827
    Что это доказывает
    • Схема ecdsa_secp256r1_sha256 входит в число предложенных клиентом в signature_algorithms.
    • Подпись CertificateVerify над подписываемыми данными проходит проверку открытым ключом сертификата.
    • Опубликованный демонстрационный ключ подписи — это закрытый ключ сертификата.
    • Утилита s_client из OpenSSL сообщила тип подписи удалённой стороны: ecdsa_secp256r1_sha256.
    Воспроизведите сами
    unhex > server.der <<'EOF'
    308201e53082018ca003020102021419fbda811efb382b2429bc0208699ef5d7
    14ca65300a06082a8648ce3d040302303931123010060355040a0c0965636473
    612e636f6d3123302106035504030c1a496c6c75737472617465642050512054
    4c532044656d6f204341301e170d3236313032303131333232395a170d323631
    3230363131333232395a30193117301506035504030c0e70712d746c732e6578
    616d706c653059301306072a8648ce3d020106082a8648ce3d03010703420004
    be4c8a7a4f2244b4493214f03edcb3222e9a81188de16174e451e54df144f758
    7b456e5db02b1521ac7262cbd185443f75926b3bc566de6cebf4e59205fe2bcf
    a3819130818e300c0603551d130101ff04023000300e0603551d0f0101ff0404
    0302078030130603551d25040c300a06082b0601050507030130190603551d11
    04123010820e70712d746c732e6578616d706c65301d0603551d0e0416041442
    c99d1b2fd96e6a4bcb7be960b98e84b710b3fe301f0603551d23041830168014
    d4add88f0d635f12f9119cf17414388f26d3b407300a06082a8648ce3d040302
    034700304402205f1c8dec01971b89bab6c887166550128ee211099c2ecbe17c
    0efd5d1c2d5e710220323909f5ef7bb5a0743701eccfbe4ffbc7fdfefbd12d33
    733c05541d8d87c86b
    EOF
    openssl x509 -inform DER -in server.der -pubkey -noout > server-pub.pem
    { printf '%64s' ''; printf 'TLS 1.3, server CertificateVerify\000'; printf '%s' af76fc7ac4f19eb2af3f584874eb0cd0429a06a15cf60faefa739b3fa4eb8aad | unhex; } > signed.bin
    printf '%s' 3046022100e2a43fd0b309c60cd461d2e842bf89926c1d271a3f6e79ee5776cb8cc4f8ddbc022100a9667486294b49473c7bc82723f305694cdd7e8f591b587e2b75b909da995827 | unhex > signature.der
    openssl dgst -sha256 -verify server-pub.pem -signature signature.der signed.bin

    выводит Verified OK

    printf '%s' 3031020101042011aa8fac7486c821d6c57baa03c3a3e119a635f33aaa2043bba12d7f20a40a99a00a06082a8648ce3d030107 | unhex > server-key.der
    openssl ec -inform DER -in server-key.der -pubout -outform DER 2>/dev/null | tail -c 65 | tohex

    выводит 04be4c8a7a4f2244b4493214f03edcb3222e9a81188de16174e451e54df144f7587b456e5db02b1521ac7262cbd185443f75926b3bc566de6cebf4e59205fe2bcf

    Сервер → клиентЗапись 7 · 58 байт в сети

    Finished (сервер)

    HMAC от всего транскрипта рукопожатия, вычисленный на секрете, который может быть только у того, кто знает ключи рукопожатия. Это сообщение завершает серверную половину рукопожатия: как только клиент его проверит, сервер считается аутентифицированным, а ключи — подтверждёнными.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика рукопожатия с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    5ed29e03b3495fd20354ed3f2421640a
    Вектор инициализацииwrite_iv
    e465385b2d22f07ffc6a4079
    Nonce для порядкового номера 3nonce
    e465385b2d22f07ffc6a407a
    Дополнительные данные: заголовок записиadditional_data
    1703030035
    Тег аутентификацииtag
    19198cc6e0426e75d2b219994f1a0467
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 3.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 36 байт совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 58 байт

    Расшифрованное содержимое · 37 байт

    Как это вычисляетсяFinished сервера

    Finished сервера — это HMAC от всего транскрипта на текущий момент, вычисленный на ключе, который выведен из секрета трафика рукопожатия. Создать его может только сторона, вычислившая те же ключи, поэтому его проверка подтверждает и ключи сервера, и то, что транскрипт никто не подменил.

    Определение
    finished_key = HKDF-Expand-Label(s_hs_traffic, "finished", "", 32)
    verify_data  = HMAC-SHA256(finished_key, SHA-256(ClientHello … CertificateVerify))
    Значения из этого сеанса
    finished_key сервера
    0506725331e4fe72ae23212deef946f34556d38825410fb427bf071ee67951a1
    SHA-256(ClientHello … CertificateVerify)
    9e49537a2a313824d7825cbef2384d2a12ae91c8e09dcf3ac42fcd2393dae658
    verify_data = HMAC(finished_key, транскрипт)
    37187ea7e2a1e27e9d42e736edaac65dccc6dc9d03e7b862019dac0212000280
    Что это доказывает
    • Пересчитанное значение verify_data совпадает со значением в сообщении Finished.
    Воспроизведите сами
    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47f \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:finished' \
      TLS13-KDF | tohex

    выводит 0506725331e4fe72ae23212deef946f34556d38825410fb427bf071ee67951a1

    printf '%s' 9e49537a2a313824d7825cbef2384d2a12ae91c8e09dcf3ac42fcd2393dae658 | unhex | openssl mac -binary -digest SHA256 -macopt hexkey:0506725331e4fe72ae23212deef946f34556d38825410fb427bf071ee67951a1 HMAC | tohex

    выводит 37187ea7e2a1e27e9d42e736edaac65dccc6dc9d03e7b862019dac0212000280

    Как это вычисляетсяСекреты трафика приложения

    Когда серверная половина рукопожатия завершена, расписание ключей переходит к основному секрету и по транскрипту до Finished сервера включительно выводит секреты трафика приложения, а также секрет экспортёра. Ключи, полученные из секретов трафика приложения, защищают все данные приложения; OpenSSL записал в журнал все три секрета, и страница сверяет с ними свои значения.

    Определение
    derived         = Derive-Secret(handshake_secret, "derived", "")
    main_secret     = HKDF-Extract(salt = derived, IKM = 0)          RFC 8446: master_secret
    c_ap_traffic    = Derive-Secret(main_secret, "c ap traffic", ClientHello … server Finished)
    s_ap_traffic    = Derive-Secret(main_secret, "s ap traffic", ClientHello … server Finished)
    exporter_secret = Derive-Secret(main_secret, "exp master",   ClientHello … server Finished)
    
    write_key = HKDF-Expand-Label(c_ap_traffic | s_ap_traffic, "key", "", 16)
    write_iv  = HKDF-Expand-Label(c_ap_traffic | s_ap_traffic, "iv",  "", 12)
    Значения из этого сеанса
    производный секрет
    cf180a268bf009d2b86962078fe3a21b7946acf2bf393b3a1672276547e88a81
    main_secret (master_secret в RFC 8446)
    535beb1d564ea9c45dce195f453307900f8548a29f3889d55aea446547724e1b
    SHA-256(ClientHello … Finished сервера)
    5a0c617c785aacf18760a5272a8424b31be9504bf266939c6d0c7a008ca49adb
    client_application_traffic_secret_0
    822f761f7f0218c1566132cf545c5f7b632ec3c9e5c454254c1ab051f2a92dec
    server_application_traffic_secret_0
    556780cd0561751359abc293e490bbdff13dee06fe016160961add756502e56d
    exporter_master_secret
    fdf77404b2ef159adf31a636a7efb2a693ed0491d0362d0eb270ad93bd96592e
    серверный ключ трафика приложения (AES-128)
    f2c6df22d8075b76928d37545f335a64
    серверный write_iv трафика приложения
    d3bd0df90c48d9fbb108cffb
    клиентский ключ трафика приложения (AES-128)
    58391c840aa46afae3552c38ad15b95b
    клиентский write_iv трафика приложения
    68020e323a5ac9a9f0a4d49c
    Что это доказывает
    • Совпадает со строкой CLIENT_TRAFFIC_SECRET_0 в обоих файлах SSLKEYLOGFILE.
    • Совпадает со строкой SERVER_TRAFFIC_SECRET_0 в обоих файлах SSLKEYLOGFILE.
    • Совпадает со строкой EXPORTER_SECRET в обоих файлах SSLKEYLOGFILE.
    Воспроизведите сами
    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:06b849eff15c46a4401e154110a2e63a14f7333237260156f7f1b67ca3305198 \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:derived' \
      -kdfopt hexdata:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 \
      TLS13-KDF | tohex

    выводит cf180a268bf009d2b86962078fe3a21b7946acf2bf393b3a1672276547e88a81

    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXTRACT_ONLY \
      -kdfopt hexsalt:cf180a268bf009d2b86962078fe3a21b7946acf2bf393b3a1672276547e88a81 \
      -kdfopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 \
      HKDF | tohex

    выводит 535beb1d564ea9c45dce195f453307900f8548a29f3889d55aea446547724e1b

    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:535beb1d564ea9c45dce195f453307900f8548a29f3889d55aea446547724e1b \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:c ap traffic' \
      -kdfopt hexdata:5a0c617c785aacf18760a5272a8424b31be9504bf266939c6d0c7a008ca49adb \
      TLS13-KDF | tohex

    выводит 822f761f7f0218c1566132cf545c5f7b632ec3c9e5c454254c1ab051f2a92dec

    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:535beb1d564ea9c45dce195f453307900f8548a29f3889d55aea446547724e1b \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:s ap traffic' \
      -kdfopt hexdata:5a0c617c785aacf18760a5272a8424b31be9504bf266939c6d0c7a008ca49adb \
      TLS13-KDF | tohex

    выводит 556780cd0561751359abc293e490bbdff13dee06fe016160961add756502e56d

    Клиент → серверЗапись 8 · 6 байт в сети

    ChangeCipherSpec

    Такая же фиктивная запись от клиента и по той же причине: ради промежуточных устройств.

    Передано по сети · 6 байт

    • Этот 1 байт совпадает с тем, что OpenSSL вывел для этой записи с параметром -msg.
    Клиент → серверЗапись 9 · 58 байт в сети

    Finished (клиент)

    HMAC клиента от транскрипта до Finished сервера включительно. Это сообщение завершает рукопожатие: теперь обе стороны аутентифицированы и переходят на ключи трафика приложения.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе клиентского трафика рукопожатия с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    96fa01469d090b2a8f4ef6b9fa5e0784
    Вектор инициализацииwrite_iv
    9f45d2e464131048897a81c6
    Nonce для порядкового номера 0nonce
    9f45d2e464131048897a81c6
    Дополнительные данные: заголовок записиadditional_data
    1703030035
    Тег аутентификацииtag
    f3a8eac5bf26a9ac3aaca3a4afebd656
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 0.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 36 байт совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 58 байт

    Расшифрованное содержимое · 37 байт

    Как это вычисляетсяFinished клиента

    Клиент отвечает собственным Finished — HMAC от транскрипта до Finished сервера включительно на ключе из клиентского секрета трафика рукопожатия. Как только сервер его проверит, рукопожатие считается завершённым и аутентифицированным в обоих направлениях.

    Определение
    finished_key = HKDF-Expand-Label(c_hs_traffic, "finished", "", 32)
    verify_data  = HMAC-SHA256(finished_key, SHA-256(ClientHello … server Finished))
    Значения из этого сеанса
    finished_key клиента
    780c2e56af642c22fc8ae057276cd0d8bde149879a68ccb28064b4cf6b677a73
    SHA-256(ClientHello … Finished сервера)
    5a0c617c785aacf18760a5272a8424b31be9504bf266939c6d0c7a008ca49adb
    verify_data = HMAC(finished_key, транскрипт)
    e1c250d75d6df181b0df677983ec692a146f428d982a0b4281cca28607ac7fb1
    Что это доказывает
    • Пересчитанное значение verify_data совпадает со значением в сообщении Finished.
    Воспроизведите сами
    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623 \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:finished' \
      TLS13-KDF | tohex

    выводит 780c2e56af642c22fc8ae057276cd0d8bde149879a68ccb28064b4cf6b677a73

    printf '%s' 5a0c617c785aacf18760a5272a8424b31be9504bf266939c6d0c7a008ca49adb | unhex | openssl mac -binary -digest SHA256 -macopt hexkey:780c2e56af642c22fc8ae057276cd0d8bde149879a68ccb28064b4cf6b677a73 HMAC | tohex

    выводит e1c250d75d6df181b0df677983ec692a146f428d982a0b4281cca28607ac7fb1

    Сервер → клиентЗапись 10 · 239 байт в сети

    NewSessionTicket

    Подарок от сервера после рукопожатия: билет, который клиент может предъявить в следующий раз, чтобы возобновить сеанс без полного обмена ключами. Предварительно согласованный ключ (PSK), который стоит за билетом, выводится из секрета возобновления и nonce билета.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика приложения с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    f2c6df22d8075b76928d37545f335a64
    Вектор инициализацииwrite_iv
    d3bd0df90c48d9fbb108cffb
    Nonce для порядкового номера 0nonce
    d3bd0df90c48d9fbb108cffb
    Дополнительные данные: заголовок записиadditional_data
    17030300ea
    Тег аутентификацииtag
    da58bb9fa9790e3f945cd811785d9ab3
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 0.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 217 байт совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 239 байт

    Расшифрованное содержимое · 218 байт

    Как это вычисляетсяПредварительно согласованный ключ для возобновления

    Без секрета, на который указывает билет, сам билет бесполезен. Этот предварительно согласованный ключ выводится из секрета возобновления (который, в свою очередь, получен из транскрипта до Finished клиента включительно) и nonce билета. OpenSSL вывел и PSK, и билет; страница заново вычисляет PSK и сверяет оба значения.

    Определение
    resumption_secret = Derive-Secret(main_secret, "res master", ClientHello … client Finished)
    PSK               = HKDF-Expand-Label(resumption_secret, "resumption", ticket_nonce, 32)
    Значения из этого сеанса
    SHA-256(ClientHello … Finished клиента)
    b014275674636844d99285ea448e9c45e92acb5bc9bf249fa8d94b7796eb1211
    resumption_master_secret
    d013100d5c7e78c4d128a445969ce4ed1ddc3cd63a7f63bddcd82728d5129f8a
    ticket_nonce из NewSessionTicket
    0000000000000000
    PSK для возобновления
    d1039d8e602a12c21ed17aa291499d5f1ef30eda117144c3591d5fccd74f7c0d
    Что это доказывает
    • Пересчитанный PSK для возобновления совпадает с тем, что вывел s_client.
    • 192 байта билета совпадают с билетом, который вывел s_client.
    Воспроизведите сами
    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:535beb1d564ea9c45dce195f453307900f8548a29f3889d55aea446547724e1b \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:res master' \
      -kdfopt hexdata:b014275674636844d99285ea448e9c45e92acb5bc9bf249fa8d94b7796eb1211 \
      TLS13-KDF | tohex

    выводит d013100d5c7e78c4d128a445969ce4ed1ddc3cd63a7f63bddcd82728d5129f8a

    openssl kdf -binary -keylen 32 -kdfopt digest:SHA256 -kdfopt mode:EXPAND_ONLY \
      -kdfopt hexkey:d013100d5c7e78c4d128a445969ce4ed1ddc3cd63a7f63bddcd82728d5129f8a \
      -kdfopt 'prefix:tls13 ' -kdfopt 'label:resumption' \
      -kdfopt hexdata:0000000000000000 \
      TLS13-KDF | tohex

    выводит d1039d8e602a12c21ed17aa291499d5f1ef30eda117144c3591d5fccd74f7c0d

    Клиент → серверЗапись 11 · 49 байт в сети

    Данные приложения (запрос)

    Первая настоящая полезная нагрузка, зашифрованная клиентским ключом трафика приложения: однострочный HTTP-запрос. С этого момента тип содержимого записи скрыт под шифрованием.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе клиентского трафика приложения с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    58391c840aa46afae3552c38ad15b95b
    Вектор инициализацииwrite_iv
    68020e323a5ac9a9f0a4d49c
    Nonce для порядкового номера 0nonce
    68020e323a5ac9a9f0a4d49c
    Дополнительные данные: заголовок записиadditional_data
    170303002c
    Тег аутентификацииtag
    44f8869fbc72bc4a095024412310f30e
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 0.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные тип содержимого и байты совпадают с трассировкой OpenSSL -msg.

    Передано по сети · 49 байт

    Расшифрованное содержимое · 28 байт

    Сервер → клиентЗапись 12 · 130 байт в сети

    Данные приложения (ответ)

    Ответ сервера, зашифрованный его ключом трафика приложения: HTTP-ответ с файлом, который запросил клиент.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика приложения с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    f2c6df22d8075b76928d37545f335a64
    Вектор инициализацииwrite_iv
    d3bd0df90c48d9fbb108cffb
    Nonce для порядкового номера 1nonce
    d3bd0df90c48d9fbb108cffa
    Дополнительные данные: заголовок записиadditional_data
    170303007d
    Тег аутентификацииtag
    152ee3122b64d6188a96491377f3ba2d
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 1.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные тип содержимого и байты совпадают с трассировкой OpenSSL -msg.

    Передано по сети · 130 байт

    Расшифрованное содержимое · 109 байт

    Сервер → клиентЗапись 13 · 24 байта в сети

    Оповещение (close_notify)

    Сервер сообщает о штатном закрытии соединения. Оповещения тоже шифруются, так что даже закрытие соединения остаётся конфиденциальным.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе серверного трафика приложения с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    f2c6df22d8075b76928d37545f335a64
    Вектор инициализацииwrite_iv
    d3bd0df90c48d9fbb108cffb
    Nonce для порядкового номера 2nonce
    d3bd0df90c48d9fbb108cff9
    Дополнительные данные: заголовок записиadditional_data
    1703030013
    Тег аутентификацииtag
    d1a180f9a2947d57978b590f4ea3faa8
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 2.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 2 байта совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 24 байта

    Расшифрованное содержимое · 3 байта

    Клиент → серверЗапись 14 · 24 байта в сети

    Оповещение (close_notify)

    Клиент подтверждает закрытие, и соединение завершается.

    Защита записи

    Это запись типа application_data с зашифрованными данными. Она расшифровывается алгоритмом AES-128-GCM на ключе клиентского трафика приложения с nonce этой записи, приведённым ниже; 16-байтовый тег аутентификации проверяется по 5-байтовому заголовку записи как по дополнительным данным (RFC 9846, § 5.2).

    Ключ AES-128-GCMwrite_key
    58391c840aa46afae3552c38ad15b95b
    Вектор инициализацииwrite_iv
    68020e323a5ac9a9f0a4d49c
    Nonce для порядкового номера 1nonce
    68020e323a5ac9a9f0a4d49d
    Дополнительные данные: заголовок записиadditional_data
    1703030013
    Тег аутентификацииtag
    daf6ae55a253a373f23464e913920d47
    • Тег GCM прошёл проверку: запись без ошибок расшифровывается при порядковом номере 1.
    • Повторное шифрование расшифрованного открытого текста в точности воспроизводит перехваченную запись.
    • Расшифрованные 2 байта совпадают с тем, что OpenSSL вывел для этого сообщения с параметром -msg.

    Передано по сети · 24 байта

    Расшифрованное содержимое · 3 байта

    Сверка с журналом ключей OpenSSL

    OpenSSL записал SSLKEYLOGFILE на каждой стороне — в формате, который Wireshark читает, чтобы расшифровать перехваченный трафик. Каждый из 5 секретов в нём — один из секретов трафика, вычисленных выше, поэтому их полное совпадение доказывает, что расписание ключей на этой странице — то самое, по которому работало реальное соединение.

    Метка SSLKEYLOGFILEСекрет (hex)Вычислен на этой странице как
    SERVER_HANDSHAKE_TRAFFIC_SECRET46925f99fa426e5dc105f5eaccfca048e1e89a93370b07cb9ab447518eb8f47fserver_handshake_traffic_secret
    EXPORTER_SECRETfdf77404b2ef159adf31a636a7efb2a693ed0491d0362d0eb270ad93bd96592eexporter_master_secret
    SERVER_TRAFFIC_SECRET_0556780cd0561751359abc293e490bbdff13dee06fe016160961add756502e56dserver_application_traffic_secret_0
    CLIENT_HANDSHAKE_TRAFFIC_SECRET8b0beea15604895fd4408c32b82684d347fa5e2422644c1d93978db70666f623client_handshake_traffic_secret
    CLIENT_TRAFFIC_SECRET_0822f761f7f0218c1566132cf545c5f7b632ec3c9e5c454254c1ab051f2a92decclient_application_traffic_secret_0

    Как был сделан перехват

    Рукопожатие настоящее, а не имитация: два процесса OpenSSL общаются по TLS 1.3 через loopback-интерфейс внутри одного контейнера с зафиксированным образом, а пакеты записываются прямо с сетевого интерфейса. При повторном перехвате получается новый сеанс с той же структурой.

    Образ контейнера
    node:24.22.0-trixie@sha256:ff054b356ab93ed540518ca23cfcd9840e5256dd04db5a8d8afc43f4ecc7c9f8
    OpenSSL
    OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026)
    Дата перехвата
    2026-10-20T11:32:29Z
    Сервер
    openssl s_server -accept 4433 -naccept 1 -WWW -cert server.crt -key server.key -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256 -groups X25519MLKEM768 -num_tickets 1 -keylogfile server-keylog.txt
    Клиент
    openssl s_client -connect 127.0.0.1:4433 -servername pq-tls.example -verify_hostname pq-tls.example -CAfile ca.crt -verify_return_error -tls1_3 -keylogfile client-keylog.txt -ign_eof -msg -msgfile client-msg.txt

    Каждая команда самодостаточна, если не считать этих двух вспомогательных функций; вставьте их первыми (нужны OpenSSL 3.5, а также Perl и od, которые есть и в Linux, и в macOS):

    unhex() { perl -0777 -ne 's/\s+//g; print pack("H*", $_)'; }
    tohex() { od -An -v -tx1 | tr -d ' \n'; echo; }

    Спецификации

    Гибридный обмен ключами описан в RFC 10024 (X25519MLKEM768): он объединяет ML-KEM-768 из NIST FIPS 203 с X25519 из RFC 7748; гибридная конструкция и порядок частей в key share соответствуют RFC 9954; всё остальное — TLS 1.3 по RFC 9846 (он заменяет RFC 8446), а трассировка расписания ключей сверена с RFC 8448. Кодовые значения и названия алгоритмов взяты из реестров TLS в IANA и из реестров идентификаторов объектов.

    Формат этой страницы — настоящее соединение, в котором объяснён каждый байт, — позаимствован у проекта The Illustrated TLS 1.3 Connection Майкла Дрисколла (лицензия MIT). Перехват, объяснения и код здесь наши собственные; эта страница дополняет оригинал постквантовым обменом ключами.