Ориентация главы

В главе 81 вы нажали одну функцию — рукопожатие пробежало само; эта глава открывает чёрный ящик. В рукопожатии TLS 1.3 на деле лишь четыре корневых сообщения: ClientHello, ServerHello, (EncryptedExtensions, Certificate, CertificateVerify,) Finished плюс клиентский Finished — для каждого разобрано «кто послал, что несёт, что проверяет», со сквозной цепью расписания ключей (дайджест transcript → handshake secret → двусторонние traffic secret → secret приложения — всё на HKDF; примитивы глав 74/77 сходятся здесь). Затем три инженерных ответвления: HelloRetryRequest (сервер просит сменить группу), версионный и suite-negociación (двухстек 1.2/1.3), KeyUpdate (смена ключа длинного соединения). Два образца главы — обмен ключами и кодек сертификатных сообщений — два независимых входа, разводящие два центральных механизма рукопожатия под отдельные тесты; скелет сессии главы 77, полностью развёрнутый на протокольный слой.

84.1引入

При отладке провала TLS слова «рукопожатие зависло» несут ноль информации. Группа ClientHello не поддерживается (цикл HelloRetryRequest)? Провалилась проверка сертификатов после ServerHello? Не состоялась подпись CertificateVerify? Или дайджест Finished не совпал (посредник поменял байты transcript)? Каждому классу провала — своя обработка, а не знающий последовательности может лишь перезапускать наудачу.

Ключ к пониманию последовательности — одно наблюдение: каждый шаг рукопожатия TLS 1.3 накапливает transcript — обе стороны кормят каждую посланную-принятую рукопожатийную запись в один и тот же бегущий дайджест; ключи деривируются из transcript, Finished проверяет transcript, билеты восстановления привязаны к transcript. Поменяйте один байт любой записи — все последующие ключи станут другими; вот и механизм «рукопожатие защищено от подделки», не магия, а хеш-цепь. С этим взглядом читайте четыре записи: каждое поле отвечает на «почему оно обязано быть здесь».

84.2概念

Диаграмма последовательности четырёх корневых сообщений

1ClientHello (открытым текстом)высшая поддерживаемая версия + случайное + ID сессии + список suites + список групп + список подписных схем + key share (публичный ключ предпочтительной группы) + SNI/ALPN/PSK
2ServerHello (открытым текстом)выбранная версия + suite + случайное + эхо ID сессии + выбранный key share - обе стороны выводят здесь handshake secret
3[начало шифртекста сервера] EncryptedExtensionsподтверждение SNI, выбор ALPN, дополнение группового negociación
4Certificateцепочка лист + промежуточные (позаписно, каждая может нести расширение OCSP)
5CertificateVerifyподпись «ролевой префикс + дайджест transcript» приватным ключом сертификата - само доказательство идентичности
6FinishedHMAC над «transcript до включения этой записи» - само доказательство защищённости от подделки
7[клиент проверяет цепочку и дайджест] клиентский Finished (защищён пишущим ключом сервера)после двустороннего подтверждения - переключение на ключи приложения

Три линии чтения. Граница открытый текст/шифртекст — после ServerHello: EE/Cert/CV/Finished сервера все шифрованы, подслушиватель видит лишь длины сертификатов, не содержимое; что проверяет каждая запись: EE — согласованность negociación, CV — идентичность (проверки глав 80/81 срабатывают здесь), Finished — глобальную целостность; точки смены ключей две — после ServerHello обе стороны порознь деривируют ключи рукопожатия (с EE начинается шифрование), после клиентского Finished — переключение на ключи приложения.

Расписание ключей: одна HKDF-цепь

1ECDHEkey share из ClientHello и ServerHello -> общий секрет (ECDH главы 76)

Сверьте с образцом сессии главы 77: тот же скелет «ECDH → HKDF → AEAD»; TLS сделал из него многоступенчатую лестницу — каждая ступень Extract/Expand привязывает к ключу дайджест transcript на данный момент, поэтому ключ запирает не только секрет, но и «каждую нашу фразу». У направлений приёма и передачи свои secret (клиент пишет / сервер читает одним, в обратную сторону — другим) — односторонняя смена ключа (KeyUpdate) отсюда и возможна. Временный приватный ключ клиента стирается немедленно после успешного переключения ключей рукопожатия — точка приземления прямой секретности.

HelloRetryRequest: сервер говорит «смените группу и приходите снова»

Когда предпочтительную группу клиента (скажем x25519) сервер не поддерживает, он отвечает HRR с требованием сменить группу и пере послать ClientHello. Клиент и сервер XRT поддерживают одиночный HRR — новый ClientHello выходит с назначенной сервером группой (скажем P-256) и несёт метку повтора h2. Приложению всё прозрачно; диагностике важно: «два ClientHello» в журнале соединения — не переподключение, а HRR.

Версионный и suite-negociación

Правила negociación двухстека (1.2/1.3): offered-список партнёра ∩ локальная поддержка → высшая версия; suite дополнительно проходит тройной фильтр — порядок offered партнёра + локальная поддержка + совместимость с типом идентичности (идентичность RSA исключает чисто ECDSA-наборы). Локальный массив политики (глава 85) и есть входная база этих двух шагов; сервер исполняет их на каждом соединении. xrtTlsVersionSelect/CipherSelect выносят negociación в отдельно тестируемые чистые функции — образец главы демонстрирует весь поток. Требования стороны TLS 1.2: EMS (extended master secret) и пустая начальная привязка RFC 5746 обязательны, повторный negociación не предоставляется — известные слабости старого протокола закрыты прямым инженерным решением.

KeyUpdate: ритм смены ключа длинного соединения

Когда поток данных приложения достигает определённого объёма, слой записей меняет ключ автоматически (прозрачно для приложения); можно и явно — xrtTlsClientKeyUpdate(方向). Правила: сообщение KeyUpdate занимает запись целиком; update_not_requested меняет ключ только читающего направления; update_requested сперва ставит ответ в очередь старым пишущим ключом и лишь потом переключает — любое AGAIN-прерывание не порождает полуобновлённого состояния (старый ключ, номера последовательности и очередь сохранены как были; после стекания выхода — повтор). Порядок epoch уже поставленных данных гарантирует проволочная очередь — дисциплина «nonce-счётчик» главы 75, автоматизированная на протокольном слое.

Сертификатные сообщения: позаписная модель

Certificate у TLS 1.3 организован позаписно (каждая = DER-сертификат + опциональные расширения вроде статуса OCSP), в 1.2 — голый список; xrtTlsCertificateEncode/Parse/Entries обрабатывает обе версии единообразно (версию даёт первый параметр). Записи — DER-представления; разбор уходит xrtX509Parse главы 79. Машина состояний клиента строга к расширениям записей: непросмотренное позаписное расширение отвергается напрямую.

84.3示例

Первая полная программа: двусторонний замкнутый круг обмена ключами под управлением метаданных

Программа ниже из examples/tls/key_exchange/main.c — независимый юнит-тест центрального механизма рукопожатия (генерация и деривация key share):

examples/tls/key_exchange/main.c源码 ↗
#include <xrt.h>

#include <stdio.h>
#include <string.h>



/* 选择当前构建中第一个可用命名组。 */
static const xtlsgroupinfo* exampleGroup(void)
{
	static const uint16 Groups[] = {
		XTLS_GROUP_X25519,
		XTLS_GROUP_SECP256R1,
		XTLS_GROUP_X448,
		XTLS_GROUP_SECP384R1
	};

	for ( size_t i = 0; i < (sizeof(Groups) / sizeof(Groups[0])); i++ ) {
		if ( xrtTlsGroupAvailable(Groups[i]) ) {
			return xrtTlsGroupInfo(Groups[i]);
		}
	}
	return NULL;
}






















 */


/* 演示调用方按元数据提供缓冲,并完成双向共享秘密校验。 */
int main(void)
{
	const xtlsgroupinfo* pInfo = exampleGroup();
	uint8 ClientPrivate[56];
	uint8 ClientPublic[97];
	uint8 ServerPrivate[56];
	uint8 ServerPublic[97];
	uint8 ClientShared[56];
	uint8 ServerShared[56];

	if ( pInfo == NULL ) {
		return 1;
	}
	if ( !xrtTlsKeyShareGenerate(
		pInfo->Group,
		ClientPrivate, sizeof(ClientPrivate),
		ClientPublic, sizeof(ClientPublic)
	) || !xrtTlsKeyShareGenerate(
		pInfo->Group,
		ServerPrivate, sizeof(ServerPrivate),
		ServerPublic, sizeof(ServerPublic)
	) || !xrtTlsKeyShareDerive(
		pInfo->Group,
		(xbytesview) { ClientPrivate, pInfo->PrivateSize },
		(xbytesview) { ServerPublic, pInfo->PublicSize },
		ClientShared, sizeof(ClientShared)
	) || !xrtTlsKeyShareDerive(
		pInfo->Group,
		(xbytesview) { ServerPrivate, pInfo->PrivateSize },
		(xbytesview) { ClientPublic, pInfo->PublicSize },
		ServerShared, sizeof(ServerShared)
	) ) {
		return 1;
	}
	if ( memcmp(ClientShared, ServerShared, pInfo->SharedSize) != 0 ) {
		return 1;
	}
	printf("group=%u private=%u public=%u shared=%u\n",
		(unsigned)pInfo->Group,
		(unsigned)pInfo->PrivateSize,
		(unsigned)pInfo->PublicSize,
		(unsigned)pInfo->SharedSize);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/key_exchange/main.c -lws2_32 -liphlpapigroup=29 private=32 public=32 shared=32

Что только что произошло. (1) exampleGroup в порядке x25519 → P-256 → x448 → P-384 ищет группу, доступную в текущей сборке (xrtTlsGroupAvailable отражает обрезку и возможности бэкендов) — group=29 это TLS-номер группы x25519. (2) xtlsgroupinfo даёт точные размеры трёх буферов — приватный 32/публичный 32/общий 32 (у x25519 все 32; у P-384 было бы 48/97/48): буферы открываются по метаданным, без угадывания констант — один и тот же код бегает любую группу. (3) После двустороннего Derive memcmp равен — проверка симметрии ECDH (глава 76) в TLS-групповой обёртке. Это вся математика расширения key share в ClientHello/ServerHello: Generate дважды, обмен публичными ключами, Derive даёт общий секрет ECDHE — первое звено цепи расписания ключей.

Вторая полная программа: кодирование сообщения Certificate и обход записей

Вторая программа из examples/tls/messages/main.c — полный круговорот «цепочка сертификатов → тело сообщения → курсор записей»:

examples/tls/messages/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xrt.h>






















 */


/* 编码并遍历一个两项 TLS 1.3 Certificate 正文。 */
int main(void)
{
	static const uint8 Leaf[] = { 0x30, 0x01, 0x01 };
	static const uint8 Issuer[] = { 0x30, 0x01, 0x02 };
	xtlscertificateentry Chain[2];
	uint8 Body[64];
	xtlscertificatemessage Message;
	xtlscertificatecursor Cursor;
	xtlscertificateentry Entry;
	xtlsitemresult Result;
	size_t iBodySize;
	size_t iCount = 0;

	memset(Chain, 0, sizeof(Chain));
	Chain[0].Data = (xbytesview) { Leaf, sizeof(Leaf) };
	Chain[1].Data = (xbytesview) { Issuer, sizeof(Issuer) };
	iBodySize = xrtTlsCertificateSize(
		XTLS_VERSION_13, (xbytesview) { NULL, 0 }, Chain, 2u
	);
	if ( (iBodySize == 0) || !xrtTlsCertificateEncode(
		XTLS_VERSION_13, (xbytesview) { NULL, 0 }, Chain, 2u,
		Body, sizeof(Body)
	) || !xrtTlsCertificateParse(
		XTLS_VERSION_13, (xbytesview) { Body, iBodySize }, &Message
	) || !xrtTlsCertificateEntries(&Message, &Cursor) ) {
		return 1;
	}
	while ( (Result = xrtTlsCertificatesRead(
		&Cursor, &Entry
	)) == XTLS_ITEM_VALUE ) {
		printf("certificate[%zu]: %zu bytes\n", iCount, Entry.Data.Size);
		iCount++;
	}
	return (Result == XTLS_ITEM_DONE) && (iCount == 2u) ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/messages/main.c -lws2_32 -liphlpapicertificate[0]: 3 bytescertificate[1]: 3 bytes

Что только что произошло. (1) Кодирование в два прохода: CertificateSize сперва меряет длину тела (request_context пустое представление — направление клиента; у серверного направления контекст CV непуст), CertificateEncode пишет — двухэлементная цепь (лист + издатель, здесь 3-байтовые игрушечные DER) кодируется в формат записей TLS 1.3. (2) CertificateParse даёт представление сообщения, Entries строит курсор, Read трёхзначно перебирает — тот же словарь, что у курсоров DER/PEM (глава 78). (3) Каждое Entry.Data — DER-представление; настоящий разбор уходит в главу 78, два слоя каждый своим делом: TLS-слой ведает структурой записей, слой X.509 — семантикой сертификата. Прокси, анализаторы трафика и журналы прозрачности сертификатов обрабатывают проволочные сертификаты именно этой парой входов.

84.4契约

  • Охват протоколов: двухстек TLS 1.2/1.3; для 1.2 обязательны EMS и пустая начальная привязка, повторного negociación нет; Ed448 не публикуется; P-521/SHA-512 не входят в offer 1.3.
  • Transcript необходим: все записи рукопожатия входят в дайджест; деривация ключей и Finished привязаны к transcript; объект подписи CertificateVerify — «ролевой префикс + дайджест до включения CV».
  • Атомарность смены ключей: провал любого шага — FAILED, полугруппы ключей не публикуется; временный приватный ключ клиента стирается немедленно после успешного переключения (прямая секретность).
  • HRR: клиент и сервер поддерживают одиночный; повторный ClientHello меняет группу и несёт метку; приложению прозрачно.
  • Правила negociación: версия — высшая из пересечения; suite через тройной фильтр offered-порядок + локальная поддержка + совместимость идентичности; массив политики — база (глава 85).
  • KeyUpdate: запись целиком; два режима запроса; ответ старым пишущим ключом; AGAIN без полуобновления; порядок epoch гарантирует очередь.
  • Записи сертификата: 1.3 позаписно (с расширениями), 1.2 голый список — единый вход Encode/Parse; записи DER-представления уходят на слой X.509; непросмотренное расширение записи — отказ.
  • Отображение ошибок: провал — XTLS_STATE_FAILED со структурной ошибкой; XTLS_AGAIN — лишь «нужно больше входа/места под вывод».

Типичные ошибки

Ловушка 1: HRR принят за повтор при провале соединения:Ловушка 1: HRR принят за повтор при провале соединения

Симптом: мониторинг видит два ClientHello у одного соединения, ложно распознаёт «клиент бесконечно переподключается» — шторм тревог; либо самописный прокси рвёт связь, приняв HRR за ошибку.

Причина: HelloRetryRequest — нормальная внутрипротокольная ветка: сервер просит сменить группу и повторить. 1.3 это разрешает (XRT поддерживает раз); превышение одного раза — уже аномалия.

错误写法
/* слой захвата/журнала: увидели второй ClientHello - тревога */
if ( hello_count(client) > 1 ) {
	alert("client stuck in reconnect loop");
}
正确写法
   会话状态机已把它折叠成正常握手——日志层看状态而不是数包 */
if ( tls_state(session) == XTLS_STATE_FAILED ) {
	alert(failure_reason(session));
}
Ловушка 2: самовольная «помощь» протоколу в оптимизации transcript:Ловушка 2: самовольная «помощь» протоколу в оптимизации transcript

Симптом: самописный промежуточный слой (прокси, журнал, ускоритель) поменял границы записей рукопожатия — слил или расщепил — и после этого Finished вечно проваливает проверку.

Причина: объект дайджеста transcript — записи рукопожатия, а не записи-передачи, но целостность сообщения зависит от побайтной верности слоя записей; всякая «оптимизация» проводных байтов (перегруппировка, перекодирование) меняет transcript, и HMAC Finished расходится — ровко та подделка, которую протокол и ищет.

错误写法
/* промежуточный слой «привёл в порядок» границы записей и переслал */
merge_small_records(&Inbound);   /* transcript изменился */
正确写法
/* прозрачно пересылать исходные байты; для разбора - слой разбора записей только на чтение */
/* (только-читающий вход слоя записей главы 87 и приготовлен для таких инструментов) */
Ловушка 3: сунуть 1.2-специальную схему в политику 1.3:Ловушка 3: сунуть 1.2-специальную схему в политику 1.3

Симптом: xrtTlsPolicyValid отвергает конфигурацию — «список подписей содержит 1.2-специальную схему»; либо наоборот, чистому развёртыванию 1.2 всунули suite только-1.3.

Причина: валидация политики гарантирует самосогласованность каждого списка с включёнными версиями: каждый suite соответствует включённой версии, непустой список подписей пригоден хотя бы для одной включённой версии — PKCS#1-схема 1.2 в чисто 1.3-политику не пройдёт. В этом ценность слоя политики: ошибки конфигурации перехватываются на старте.

错误写法
static const xtlsversion Versions[] = { XTLS_VERSION_13 };
Policy.Versions = Versions;
Policy.VersionCount = 1;
/* SignatureCount не изменён: остальное умолчальное семейство (включая 1.2-специальные) -> Valid отказывает */
正确写法
static const xtlsversion Versions[] = { XTLS_VERSION_13 };
static const xtlssignature Sigs13[] = { /* только схемы 1.3 */ };
Policy.Versions = Versions;
Policy.VersionCount = 1;
Policy.Signatures = Sigs13;
Policy.SignatureCount = /* согласовано с массивом */;
if ( !xrtTlsPolicyValid(&Policy) ) { /* ошибка конфигурации на этапе старта */ }

Практические задания

Базовое: два групповых обмена ключами

Поменяйте порядок групп в exampleGroup на P-256-приоритет и перегоните key_exchange — пронаблюдайте смену номера группы и трёх размеров (публичный ключ 97 байтов). Затем закомментируйте все группы и проверьте путь отказа Available. Критерии приёмки: обе группы замыкают круг успешно; изменения размеров согласованы с документацией xtlsgroupinfo.

Продвинутое: версионное сличение сертификатных сообщений

Закодируйте одну и ту же цепочку записей через xrtTlsCertificateEncode/Parse сперва с XTLS_VERSION_12, затем с XTLS_VERSION_13, сдампите оба тела в шестнадцатеричном виде и сравните структурную разницу (у 1.2 нет request_context и расширений записей). Критерии приёмки: оба тела обходятся Parse+Entries своей версии; вы можете указать поля, добавленные в 1.3.

Вызов: наблюдатель последовательности рукопожатия

Слоем разбора записей (глава 87) или сырым сокетом захватите одно полное рукопожатие (локальный dial до любого сайта) и разметьте построчно по диаграмме главы: тип каждой записи, границу открытый текст/шифртекст, последовательность типов рукопожатийных сообщений. Критерии приёмки: размеченная последовательность полностью соответствует диаграмме главы (включая возможный одиночный HRR); вы можете указать, какое содержимое после ServerHello вам уже не видно (начался шифртекст).

Итоги

ТемаКратко
Четыре сообщенияClientHello / ServerHello / (EE+Cert+CV) / Finished + клиентский Finished
Граница шифрапосле ServerHello сообщения сервера шифрованы; подслушивателю видны лишь длины
Transcriptбегущий дайджест всех записей рукопожатия; ключи и Finished привязаны к нему
Расписание ключейECDHE → цепь Extract (Early/Handshake/Master) → двусторонние traffic secret → key/IV
Независимость направленийу приёма и передачи свои secret; отсюда односторонний KeyUpdate
Прямая секретностьвременный приватный ключ клиента стирается после смены ключей рукопожатия
HRRзапрос сервера сменить группу; одиночный; новый ClientHello с меткой; приложению прозрачно
Negociaciónверсия — высшая из пересечения; suite через тройной фильтр offered+локально+совместимость идентичности
KeyUpdateзапись целиком; ответ старым пишущим ключом; AGAIN без полуобновления; порядок epoch гарантирован
Записи сертификата1.3 позаписно / 1.2 голый список — единый вход; записи DER-представления уходят на слой X.509
Границы 1.2EMS обязателен, без повторного negociación; 1.2-специальные схемы не входят в чисто 1.3-политику

📖 Справочники API в этой главе:tls (201 записей)crypto (140 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки