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

Транспортный слой (глава 121) даёт шифрованный пакетный канал — но откуда берутся ключи? На это отвечает KEX (обмен ключами) — SSH-редакция «рукопожатия TLS» (глава 84), но более ступенчатая. ssh_kex_session компонует KEXINIT обеих сторон, Curve25519, exchange hash SHA-256, проверку подписи хоста Ed25519, вывод ключей A-D и переключение направлений NEWKEYS в детерминированную машину состояний — без socket/Engine/задач/объектов ожидания и без хранения больших буферов KEXINIT. Внизу четыре независимых слоя: сообщения ECDH (кодирование номер сообщения + SSH string), транскрипт SHA-256 (потоковый exchange hash и расширение ключей A-F), примитив Curve25519 (чистая математика, без зависимости от случайности), пара ключей со стойкой случайностью (независимый слой обрезки). Пройдя главу, вы читаете каждое сообщение реального рукопожатия SSH.

124.1引入

Рукопожатие SSH против TLS 1.3 — сходства и различия. Сходство: ECDHE (кривая x25519) + хеш транскрипта + цепочка вывода + переключение Finished/NEWKEYS — та же мысль, что в главе 83: «ключи выводятся из транскрипта; подделка любого сообщения меняет всё». Различие: проверка подписи хоста и доверие к ключу в SSH — два шага: сессия KEX сперва делает криптографическую проверку подписи (действительно ли эта подпись сделана этим ключом хоста), затем шлёт событие VERIFY_HOST_KEY, чтобы приложение вынесло суждение доверия (этот ключ хоста мне знаком? — known_hosts, глава 123); TLS вжимает оба шага в проверку сертификата. Ступенчатость позволяет SSH работать без системы сертификатов — доверие идёт от «при первом соединении вы записали этот ключ» (TOFU) или предварительной раздачи.

У ступенчатости есть и третий выгодоприобретатель: тесты и специализированные устройства. xrtSshKexSessionBeginWithPrivate явно вставляет эфемерный закрытый ключ — детерминированное ядро тестируемо; слой случайного удобства Begin выделен в отдельный модуль (приносит только системный источник стойкой случайности) — стандартные векторные тесты и устройства со своими ключами не тянут зависимость случайности. Это проведение «детерминированное ядро + отделение слоя удобства» на KEX.

124.2概念

KEXINIT и согласование алгоритмов

После установления соединения (за обменом версионными строками) обе стороны шлют друг другу KEXINIT: списки поддерживаемых алгоритмов kex/шифрования/MAC/сжатия/алгоритмов ключа хоста (в порядке приоритета). Правило согласования отличается от TLS — SSH берёт первый в списке клиента, который поддерживает и сервер (приоритет клиента, та же форма, что согласование подпротоколов в главе 93). Модуль ssh_kexinit даёт построение (xrtSshKexInitWrite — запись списков согласования) и разбор сообщения KEXINIT. Объект сессии поглощает оба KEXINIT и фиксирует комбинацию алгоритмов этого раунда.

Сообщения ECDH и Curve25519

1C->S ECDH_INITQ_C (эфемерный открытый ключ клиента, 32Б)
2S->C ECDH_REPLYK_S (blob открытого ключа хоста) || Q_S (эфемерный открытый ключ сервера) || подпись(exchange hash)
3Обе стороныSharedSecret = x25519 скалярное умножение (свой закрытый, открытый противоположной) - сервер отвергает нулевой результат слабого открытого ключа

Слой сообщений (xrtSshEcdhInitWrite/Read, EcdhReplyWrite/Read) обрабатывает только номера сообщений и кодирование SSH string — результат заимствует исходную нагрузку; неверные номера/усечение/хвосты отвергаются. У примитива Curve25519 три входа: xrtSshCurve25519Public (закрытый→открытый), Curve25519Shared (общий секрет, отвергающий нулевой результат слабого открытого ключа — защита от злых открытых ключей главы 76), Curve25519KeyPair (случайная пара — независимая обрезка). Очистка закрытого ключа: по окончании использования транспортом обязателен xrtSecureZero.

Exchange hash и вывод A-F

xrtSshKexHashSha256 пишет транскрипт потоково (не строит промежуточный большой буфер): V_C || V_S || I_C || I_S || K_S || Q_C || Q_S || K — версионные строки, оба KEXINIT, открытый ключ хоста, оба эфемерных открытых ключа, общий секрет; первые семь элементов кодируются SSH string, K — каноничным неотрицательным mpint (деталь RFC 8731: 32-байтовый вывод x25519 интерпретируется напрямую как целое в сетевом порядке, без обращения байтов перед mpint-кодированием — точность уровня буквы контракта). xrtSshKexDeriveSha256 реализует расширение ключей A-F RFC 4253 (начальные IV×2, ключи шифрования C2S/S2C, ключи MAC×2): цепочка HASH(K || H || char || session_id), поддержка вывода за 32 байта; вывод не перекрывает ввод, общий секрет обязан быть ненулевым.

Подпись хоста: проверка и доверие в два шага

Сторона сервера: xrtSshKexSessionExchangeHash берёт дайджест → локальный закрытый ключ/HSM подписывает → blob подписи уходит в EcdhReplyPrepare — функция перепроверяет подпись открытым ключом хоста, ошибочная подпись не попадает на линию (самопроверка сервера). Сторона клиента: после прохождения проверки подписи ECDH_REPLY открытый ключ хоста копируется в хранилище вызывающего и возникает событие XSSH_KEX_EVENT_VERIFY_HOST_KEY — если known_hosts/сертификаты/политика приложения принимают, xrtSshKexSessionHostKeyAccept; если нет — закрыть transport и xrtSshKexSessionFail. Смысл двух шагов: криптографическая проверка (библиотека) отделена от суждения доверия (политика) — политикой может быть TOFU, сверка known_hosts, цепочка сертификатов (разворачивается в главах 119/120).

Транзакции и переключение направлений

Та же дисциплина транзакций, что у транспортного ядра: ECDH_INIT/ECDH_REPLY/NEWKEYS всегда сперва Prepare — после надёжной фиксации пакета транспортным ядром исполняется WriteCommit (отмена — WriteAbort); приём сперва аутентифицирует в ядре, затем ReadPrepare, фиксируя ядро и сессию в том же порядке; ReadAbort прекращает сессию (аутентифицированный ввод не откатывается — сеансный отклик ReadAbort-закрывает-ядро из главы 121). Переключение ключей независимо по направлениям: после фиксации NEWKEYS своей стороны ActivateWrite (переключение направления записи); после аутентификации противоположной — ActivateRead; функции выбирают материал C2S/S2C по роли, а после перехода ядра под управление копии ключей в сессии очищаются (минимальная резидентность — дисциплина главы 77). Четырёхсегментные представления транскрипта действительны внутри раунда KEX — чтобы выйти из жизненного цикла сетевого ввода, сперва точно скопируйте через KexTranscriptMeasure/Write.

124.3示例

Первая полная программа: форма ресурсов объекта сессии

Программа ниже из examples/kex_session — заявление KEX-сессии о нулевой обузе:

extlibs/xssh/examples/kex_session/main.c源码 ↗
#include <xssh.h>
#include <stdio.h>



/* 展示会话对象本身不创建网络、任务或大块固定缓冲。 */
int main(void)
{
	xsshkexsession Session;

	if ( !xrtSshKexSessionInit(&Session, XSSH_ROLE_CLIENT) ) {
		return 1;
	}
	printf("SSH KEX session bytes: %zu\n", sizeof(Session));
	xrtSshKexSessionClear(&Session);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/kex_session/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 KEX 会话结构尺寸的自检结果)

Что только что произошло. (1) xrtSshKexSessionInit(&Session, XSSH_ROLE_CLIENT) строит клиентскую сессию на стеке — то же заявление, что у транспортного ядра главы 121: вывод sizeof и есть документ «не кучевой монстр». (2) Заголовочный комментарий отмечает: «не создаёт ни сети, ни задач, ни больших фиксированных буферов» — всё состояние KEX (результаты согласования, контекст транскрипта, промежуточные величины вывода) живёт в этом стековом объекте. (3) Реальную последовательность драйвера (Begin→оба KEXINIT→круг ECDH→событие проверки хоста→NEWKEYS→оба Activate) собирает верхний клиент — рантайм главы 127 подключает эту машину состояний к socket; образец подтверждает «сами детали предсказуемы».

Вторая полная программа: построение KEXINIT

Вторая программа из examples/kexinit — выписывание списков согласования:

extlibs/xssh/examples/kexinit/main.c源码 ↗
#include <stdio.h>
#include <xssh.h>



/* 使用显式 cookie 构建并解析 KEXINIT,适合测试和自定义随机源。 */
int main(void)
{
	unsigned char arrCookie[XSSH_KEX_COOKIE_SIZE] = { 0u };
	unsigned char arrPayload[512];
	xsshkexinitconfig Config;
	xsshwriter Writer;
	xsshkexinit KexInit;

	if ( !xrtSshKexInitConfigInit(
		&Config,
		XSSH_ROLE_CLIENT,
		true
	) || !xrtSshWriterInit(&Writer, arrPayload, sizeof(arrPayload)) ||
		(xrtSshKexInitWrite(
			&Writer,
			(xbytesview){ arrCookie, sizeof(arrCookie) },
			&Config
		) != XSSH_OK) || (xrtSshKexInitRead(
			(xbytesview){ arrPayload, Writer.Size },
			&KexInit
		) != XSSH_OK) ) {
		return 1;
	}
	printf("kexinit=%zu kex=%.*s\n", Writer.Size,
		(int)KexInit.KexAlgorithms.Size, KexInit.KexAlgorithms.Data);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/kexinit/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 KEXINIT 报文尺寸与所选 kex 算法名的自检结果)

Что только что произошло. (1) xrtSshKexInitWrite кодирует списки алгоритмов (имена через запятую: kex/шифрование/ключ хоста/сжатие/MAC) в сообщение SSH — Writer.Size даёт размер сообщения, печатаемое имя алгоритма приходит из выбора после построения или первого предпочтения. (2) KEXINIT — заявление о личности рукопожатия: порядок списка есть порядок предпочтений (согласование берёт порядок клиента) — настроить его значит настроить алгоритмическую позицию клиента (SSH-соответствие политике главы 85). (3) Семья образцов тестируема послойно: kex_curve25519 (чистый примитив + стандартные векторы), kex_ecdh (слой сообщений), kex_sha256 (векторы транскрипта и вывода), kex_exchange (статический круг четырёх слоёв), семейство *_random (детерминированный контраст слоя случайного удобства) — «на каждый слой свой образец» — приёмочная форма расслоённого дизайна.

124.4契约

  • Детерминированное ядро: сессия не создаёт socket/Engine/задач/объектов ожидания; слой случайного удобства — отдельный модуль (приносит только системный источник стойкой случайности).
  • Правило согласования: порядок списка клиента ∩ поддержка сервера, первый по порядку клиента; KEXINIT независимо кодируется/декодируется ssh_kexinit.
  • Сообщения ECDH: только номера сообщений и string; заимствуют нагрузку; неверный номер/усечение/хвосты отвергаются.
  • Curve25519: Shared отвергает нулевой результат слабого открытого ключа; случайный слой KeyPair обрезается независимо; закрытые ключи по использовании SecureZero.
  • Exchange hash: восемь сегментов транскрипта потоково (V_C..K); K — каноничный неотрицательный mpint (вывод x25519 без обращения, сразу mpint); общий секрет ненулевой.
  • Вывод A-F: цепочка расширения RFC 4253; вывод за 32Б поддерживается; вывод не перекрывает ввод.
  • Два шага проверки: сервер перед Reply самопроверяется; клиент после проверки подписи порождает VERIFY_HOST_KEY — суждение доверия приложению (Accept/Fail).
  • Дисциплина транзакций: Prepare→фиксация ядром→Commit/Abort; ReadAbort прекращает сессию (аутентифицированный ввод не откатывается).
  • Переключение направлений: ActivateWrite/Read независимы; материал выбирается по роли; переход ядра под управление очищает копии ключей сессии.
  • Жизненный цикл транскрипта: четырёхсегментные представления действительны один раунд; для выхода — сперва точная копия (Measure/Write).

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

Ловушка 1: пропустить событие VERIFY_HOST_KEY и сразу Accept:Ловушка 1: пропустить событие VERIFY_HOST_KEY и сразу Accept

Симптом: код ради экономии строк в callback события безусловно зовёт HostKeyAccept — атаки посредника проходят беспрепятственно (любой ключ хоста «доверен»).

Причина: из двух шагов — проверка подписи (криптография) и доверие (политика) — против MITM защищает именно второй; проверка подписи доказывает лишь «ключ соответствует подписи», а не «это тот сервер, к которому вы хотели».

错误写法
case XSSH_KEX_EVENT_VERIFY_HOST_KEY:
	xrtSshKexSessionHostKeyAccept(&Session);  /* безусловное доверие: MITM проходит */
	break;
正确写法
case XSSH_KEX_EVENT_VERIFY_HOST_KEY:
	if ( known_hosts_check(Host) == TRUST ) {   /* суждение главы 123 */
		xrtSshKexSessionHostKeyAccept(&Session);
	} else {
		xrtSshKexSessionFail(&Session, REJECT);
	}
Ловушка 2: представления транскрипта пересекают раунды KEX:Ловушка 2: представления транскрипта пересекают раунды KEX

Симптом: во втором раунде rekey хеш превращается в кашу — представления KEXINIT первого раунда давно недействительны.

Причина: четырёхсегментные представления транскрипта заимствуют сетевой входной буфер, действительны внутри этого раунда KEX; у нового раунда rekey новый транскрипт.

错误写法
/* представление I_C первого раунда сохранено и всё ещё используется при Activate второго */
正确写法
/* нужно сохранить между раундами: сперва точная копия */
xrtSshKexTranscriptMeasure(...);
xrtSshKexTranscriptWrite(...);   /* копия в рабочую область сессии */
Ловушка 3: забыли обнулить эфемерный закрытый ключ:Ловушка 3: забыли обнулить эфемерный закрытый ключ

Симптом: аудит памяти находит остатки закрытого ключа x25519 — нарушение жизненного цикла ключевого материала.

Причина: эфемерный закрытый ключ KEX — чувствительный материал (хоть и короткоживущий); контракт прямо требует «xrtSecureZero обязателен после окончания использования транспортом».

错误写法
/* KEX завершён, массив закрытого ключа естественно покидает область видимости - содержимое всё ещё на стеке */
正确写法
xrtSshKexSessionActivateRead(&Session, &Core);
/* после перехода ядра под управление копии ключей сессии чистятся (автоматически), свой эфемерный ключ обнулите руками: */
xrtSecureZero(Private, sizeof(Private));

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

Базовое: четырёхслойный статический круг

Прогнать образцы kex_curve25519, kex_sha256, kex_exchange и сверить с тест-векторами RFC 7748/8731 (форма вставки закрытых ключей). Критерии приёмки: общий секрет и exchange hash оба попадают в стандартные векторы.

Продвинутое: вставка политики в событие проверки хоста

Детерминированно прокрутить полный раунд KEX (вставленные закрытые ключи), в событии VERIFY_HOST_KEY вставить обе политики — «доверять» и «отклонить» — наблюдая поток NEWKEYS после Accept и закрытие transport после Fail. Критерии приёмки: финальные KexComplete/закрытые состояния обоих путей сходятся с контрактом.

Вызов: цикл rekey

После завершения первого раунда запустить rekey (бюджет rekey главы 121): новый KEXINIT→новый ECDH→оба Activate — три раунда подряд. Критерии приёмки: ключи меняются каждый раунд (сравнение вывода); старые копии ключей очищаются каждый раунд (проверка сканом памяти); транскрипт независим каждый раунд.

Итоги

ТемаКратко
Четырёхслойный разрезсообщения ECDH / транскрипт SHA-256 / примитив Curve25519 / случайная пара ключей (независимая обрезка)
Согласованиепорядок списка клиента ∩ поддержка сервера; KEXINIT независимо кодируется/декодируется
exchange hashвосемь сегментов потоково (V_C..K); K каноничный неотрицательный mpint; вывод x25519 без обращения
Вывод A-Fцепочка расширения RFC 4253; начальные IV×2 + шифрование×2 + MAC×2; за 32Б поддерживается
Два шага проверкибиблиотека проверяет криптографию; приложение судит доверие (событие VERIFY_HOST_KEY)
Самопроверка сервераEcdhReplyPrepare перепроверяет открытым ключом — ошибочная подпись не выходит на линию
Защита от слабых ключейCurve25519Shared отвергает нулевой результат (злые открытые ключи)
ТранзакцииPrepare→фиксация ядром→Commit/Abort; ReadAbort прекращает сессию
Переключение направленийActivateWrite/Read независимо;переход под управление очищает копии; оба живы — тогда завершено
Случайность отделенаBeginWithPrivate детерминирован / Begin — слой случайного удобства отдельным модулем

📖 Справочники API в этой главе:xssh-ssh_kex_session (24 записей)xssh-ssh_kex_exchange (15 записей)xssh-ssh_kexinit (7 записей)

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