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

Событие VERIFY_HOST_KEY главы 122 отдало суждение доверия приложению — эта глава и есть весь инструментарий суждения. Слой формата (ssh_hostkey): кодирование/декодирование родовых открытых ключей/подписей (имя алгоритма + сырые представления параметров — новые алгоритмы не трогают общий префиксный парсер) плюс фиксированный формат ssh-ed25519; слой проверки (ssh_hostkey_ed25519): криптографическая реализация проверки подписи; отпечатки (ssh_fingerprint): SHA-256-отпечаток ключа хоста — форма «люди сравнивают ключи»; хранилище доверия (ssh_known_host/_db/_hash): разбор/сопоставление/запись формата known_hosts, множественные состояния доверия (MATCH/NEW/CHANGED/REVOKED/CERT — изменение есть тревога смены ключа) и хешированные имена хостов (форма приватности). known_hosts — ключевой файл модели доверия SSH — полная реализация TOFU «первый раз записал, дальше сверяешь, изменилось — кричишь».

125.1引入

При первом соединении с новым сервером клиент спрашивает «я собираюсь записать этот ключ хоста — доверять?» — это и есть TOFU (Trust On First Use). Каждое следующее соединение сверяет ключ противоположной стороны с записью: совпал — пропуск; записи нет (новая машина) — вопрос; не совпал — либо сервер сменил ключ (переустановка/ротация), либо кто-то между вами и сервером (посредник). Третий случай — вся причина существования known_hosts: это единственная клиентская линия обороны против «перехвата сетевой позиции» (штучная версия системы сертификатов). MATCH пропускает, NEW (нет записи) спрашивает, CHANGED (запись есть, но ключ другой) громко предупреждает — лишнее по сравнению с PuTTY состояние «изменился» и есть ценность для безопасности.

Дизайн инструментального слоя — как у X.509-представлений главы 79: слой формата без выделений (параметры хранят исходное кодирование — эволюция алгоритмов не трогает общий парсер), слой проверки независим (криптография обрезаема), слой доверия — чисто файловый протокол (ни сети, ни соединений).

125.2概念

Родовой формат: префиксный разбор и расширение алгоритмов

xrtSshPublicKeyRead читает имя алгоритма (string) + последующие поля как сырое представление Parameters — общий префиксный парсер не знает структуры параметров каждого алгоритма, добавление RSA/ECDSA/алгоритмов security-ключей его не меняет. xrtSshSignatureRead строго читает string algorithm || string signature и отвергает хвостовые данные; SignatureWrite пишет прямо в буфер вызывающего. Фиксированный формат Ed25519 (ssh_hostkey_ed25519): открытый ключ = 32-байтовый ключ (в string-обёртке), подпись = 64-байтовое r||s (в string-обёртке) — тройка чтение/запись/проверка.

Отпечатки: форма человеческого сравнения

Ключ хоста — десятки нечитаемых байтов; отпечаток (ssh_fingerprint) сжимает его в короткую строку с префиксом SHA256: в шестнадцатеричной форме (дайджест главы 74 как протокольное применение). Два применения: «подтверждение глазами» при первой записи TOFU (сверка по внешнему каналу — вывод в консоль/проверка администратором) и идентификация ключа в журналах и тревогах (событие «ключ изменился» записывает отпечаток, не полный ключ).

known_hosts: формат и три состояния

1Строка файла[|@]marker [|]hostnames keytype base64-key [comment]
2ЗапросCheck(список имён хоста, порт, представление ключа) -> состояние доверия
3Записьстрока нового хоста дописывается; строка revoked приоритетнее любого совпадения

ssh_known_host — разбор и сопоставление однострочной/внутрипоцессной формы; ssh_known_host_db — файловый уровень (загрузка/запрос/дописывание); ssh_known_host_hash — хешированные имена хостов (|1|salt|hash — защита от утечки имён intranet-хостов самим файлом known_hosts; при сопоставлении пересчёт по salt). Семантика портов: имя хоста с нестандартным портом в форме [host]:port — одна машина на другом порту есть другая личность.

Сборка решения о доверии

Подключение инструментов главы к событию главы 122: перед HostKeyAccept запрос к хранилищу — MATCH пропускает; NEW по политике (интерактивный вопрос/внешняя сверка отпечатка с записью/белый список в конфигурации); CONFLICT отвергает с тревогой. Отпечаток печатается в обоих состояниях NEW/CHANGED — в первом для сверки, во втором для отчёта. Эта сборка имеет стандартную форму в клиентском рантайме главы 127.

125.3示例

Первая полная программа: выписывание и обратное чтение открытого ключа Ed25519

Программа ниже из examples/hostkey — круг слоя формата:

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



/* 构建并读取一个 Ed25519 主机公钥 blob。 */
int main(void)
{
	unsigned char arrPublic[XSSH_ED25519_PUBLIC_SIZE] = { 1u };
	unsigned char arrBlob[64];
	xsshwriter Writer;
	xbytesview PublicKey;

	if ( !xrtSshWriterInit(&Writer, arrBlob, sizeof(arrBlob)) ||
		(xrtSshEd25519PublicKeyWrite(
			&Writer,
			(xbytesview){ arrPublic, sizeof(arrPublic) }
		) != XSSH_OK) || (xrtSshEd25519PublicKeyRead(
			(xbytesview){ arrBlob, Writer.Size },
			&PublicKey
		) != XSSH_OK) ) {
		return 1;
	}
	printf("ed25519-key=%zu blob=%zu\n", PublicKey.Size, Writer.Size);
	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/hostkey/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 ed25519 公钥尺寸与 blob 尺寸的自检结果)

Что только что произошло. (1) xrtSshEd25519PublicKeyWrite кодирует имя алгоритма (ssh-ed25519) + 32-байтовый ключ в стандартный blob — Writer.Size и есть размер blob. (2) xrtSshEd25519PublicKeyRead читает обратно — отношение PublicKey.Size (32) к размеру blob ровно накладные расходы обёртки «string имени алгоритма + string ключа». (3) Совпадение круга = самосогласованность формата — этот blob и есть форма K_S внутри ECDH_REPLY и содержимое base64 в known_hosts: одна кодировка проходит через рукопожатие, проверку и хранение. Семья: hostkey_ed25519 (тройка проверки), key_text (текстовое преобразование), private_key/private_key_pem (загрузка закрытых ключей — форматы OpenSSH/PEM).

Вторая полная программа: трёхстатусный запрос known_hosts

Вторая программа из examples/known_host_db — сердце суждения доверия:

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



/* 对调用方已经读取的 known_hosts 文本执行常见主机信任判定。 */
int main(void)
{
	static const char sSource[] =
		"host.example ssh-ed25519 "
		"AAAAC3NzaC1lZDI1NTE5AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n";
	static const char sKey[] =
		"AAAAC3NzaC1lZDI1NTE5AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
	unsigned char arrKey[64];
	size_t iKeySize = 0u;
	xsshknownhostcheck Check;

	if ( !xrtBase64Decode(
		sKey,
		sizeof(sKey) - 1u,
		arrKey,
		sizeof(arrKey),
		&iKeySize,
		NULL
	) || (xrtSshKnownHostDbCheck(
		XRT_STR_LITERAL(sSource),
		XRT_STR_LITERAL("host.example"),
		22u,
		(xbytesview){ arrKey, iKeySize },
		0u,
		&Check
	) != XSSH_OK) ) {
		return 1;
	}
	printf("trust=%d line=%zu\n", (int)Check.Trust, Check.Entry.LineNumber);
	return Check.Trust == XSSH_KNOWN_HOST_TRUST_MATCH ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/known_host_db/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 trust=MATCH 与记录行号的自检结果)

Что только что произошло. (1) Входы запроса: список имён хоста (одна строка можетхранить несколько хостов), порт, представление ключа — Check.Trust выводит три состояния, Check.Entry.LineNumber указывает на строку попадания (координаты для тревог/удаления). (2) Здесь попадание MATCH (trust=1) + номер строки — форма пути пропуска; CONFLICT/UNKNOWN покрыты тест-векторами (образцы known_host/known_host_hash проверяют соответственно однострочное сопоставление и сопоставление хешированных имён). (3) Ветви вызывающего по состояниям — материальное воплощение обработки события главы 122: MATCH→Accept, NEW→политика, CHANGED→Fail+тревога (с отпечатком и номером строки).

Против сертификатной системы TLS (проектное наблюдение)

X.509 глав 80/81 и known_hosts этой главы решают одну задачу — «кому принадлежит этот открытый ключ» — но формы почти противоположны. Сертификаты: иерархическое доверие (корень→промежуточный→лист), поручительство третьей стороны, проверка есть построение цепи (PathBuild) + политика (раздельный слой Verify); known_hosts: плоское доверие (по одной записи на хост), самоличное поручительство (TOFU или внешний канал), проверка есть точечное сопоставление (три состояния Check). Цена и выгода у каждой свои: сертификатная система тяжела в развёртывании (операция CA/отзыв/обновление), но масштабируется (один доверенный корень — миллион сайтов); known_hosts — ноль развёртывания, но каждый клиент ведёт своё. Инженерный выбор не взаимоисключающий — у SSH тоже есть форма сертификатов (XSSH_KNOWN_HOST_TRUST_CERT_AUTHORITY — состояние доверия, зарезервированное как выход для записей центров сертификации), а у TLS во intranet есть pinning (глава 85). Читатель, понимающий обе формы, выбирает инструмент по сценарию: открытый интернет HTTPS — сертификаты; внутренняя автоматизация SSH — known_hosts; высокая чувствительность — наслоение обоих с закреплением отпечатков.

125.4契约

  • Слой формата без выделений: представления параметров в исходной кодировке; общий префиксный парсер не меняется с эволюцией алгоритмов; чтение через заимствование, запись напрямую.
  • Строгость подписи: algorithm || signature двойной string; хвосты отвергаются; Ed25519 фиксированно 32/64 байта.
  • Отпечатки: форма SHA-256; для человеческой сверки и идентификации в журналах — не замена проверки (сопоставление всё равно по ключу).
  • Семантика состояний доверия: MATCH/NEW/CHANGED/REVOKED/CERT; CHANGED — событие безопасности (смена ключа/ротация или MITM — по умолчанию отказ + тревога).
  • Приоритет revoked: попадание строки @revoked — отказ, даже если есть другая совпадающая строка.
  • Хешированные имена хостов: форма |1|salt|hash; сопоставление пересчётом по salt; для сценариев приватности (утечка файла не раскрывает имена).
  • Идентичность порта: форма [host]:port — нестандартный порт есть независимая личность.
  • Координаты строки: результат запроса несёт номер строки — адресация для тревог/удаления/аудита.
  • Файловый протокольный слой: слой known_hosts не касается сети/соединений/генерации ключей — только файлы и сопоставление.

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

Ловушка 1: CONFLICT обработан как UNKNOWN:Ловушка 1: CONFLICT обработан как UNKNOWN

Симптом: при смене ключа клиент спрашивает «записать новый ключ?» — пользователь по привычке жмёт Enter, атака посредника завершает перехват.

Причина: UNKNOWN (первый раз) и CONFLICT (записан, но другой) — совершенно разные уровни безопасности: первое — нормальный поток, второе по умолчанию обязано отказывать — тревога и ручное расследование (сервер действительно сменил ключ? подтвердить по внешнему каналу, затем чистить старую запись).

错误写法
if ( Check.Trust != XSSH_KNOWN_HOST_TRUST_MATCH ) {
	prompt_and_save(Host, Key);   /* конфликт как первая запись: дыра перехвата */
}
正确写法
switch ( Check.Trust ) {
case XSSH_KNOWN_HOST_TRUST_MATCH:
	accept(); break;
case XSSH_KNOWN_HOST_TRUST_NEW:
	if ( confirm_tofu(fingerprint) ) { save_and_accept(); }
	break;
default:  /* CONFLICT */
	alert_key_changed(Host, fingerprint, Check.Entry.LineNumber);
	reject();   /* отказ по умолчанию - старую запись чистить только после ручного подтверждения */
}
Ловушка 2: сравнение отпечатком вместо ключа:Ловушка 2: сравнение отпечатком вместо ключа

Симптом: пропуск по совпадению первых 8 байтов отпечатка — поверхность коллизий разрастается, а разнобой форм кодирования отпечатков (base64/hex/с префиксом или без) ведёт к ошибочным вердиктам.

Причина: отпечаток — форма отображения; программное сопоставление идёт по байтам ключа (параметр представления ключа у Check) — точное совпадение. Отпечаток — только в человеко-машинный интерфейс и журналы.

错误写法
if ( strncmp(fp_text, LineFp, 8) == 0 ) { accept(); }  /* коллизия префикса + разнобой кодирования */
正确写法
/* внутри библиотеки сравнение по ключу */
xrtSshKnownHostDbCheck(Db, Hosts, Port, KeyView, &Check);
if ( Check.Trust == XSSH_KNOWN_HOST_TRUST_MATCH ) { accept(); }
Ловушка 3: known_hosts открытым текстом хранит имена intranet-хостов:Ловушка 3: known_hosts открытым текстом хранит имена intranet-хостов

Симптом: аудит соответствия не проходит — кража ноутбука раскрывает всю топологию intranet.

Причина: стандартный known_hosts хранит имена хостов открытым текстом; хешированная форма (known_host_hash) создана ровно для этого — на стороне сопоставления незаметно (пересчёт по salt), на стороне файла нечитаемо.

错误写法
save_line("buildserver.internal.corp,10.1.2.3 ssh-ed25519 AAAA...");
正确写法
/* запись в хешированной форме - утечка файла покажет только salt и hash */
save_hashed_line(Host, Key);   /* |1|salt|hash ssh-ed25519 AAAA... */

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

Базовое: матрица трёх состояний

Построить три строки known_hosts (хосты: совпадение/чужой ключ/без записи) и выполнить запросы тем же ключом + другим ключом — покрыть всю комбинацию MATCH/NEW/CHANGED. Критерии приёмки: шесть результатов запросов согласуются с семантикой состояний; номер строки CHANGED указывает на существующую запись.

Продвинутое: фрагмент интерактивного TOFU-клиента

Реализовать verify_host(主机, 密钥, 库) (хост, ключ, хранилище): запрос → ветвление по состояниям (MATCH молчаливый пропуск / NEW печать отпечатка с вопросом / CHANGED тревога и отказ) — подключить к событию главы 122. Критерии приёмки: три пути ведут себя верно; вывод отпечатка содержит префикс алгоритма (форма SHA256:).

Вызов: процесс ротации ключей

Сымитировать смену ключа сервером: старое хранилище CHANGED → тревога → имитация внешнего подтверждения → удалить старую строку (по номеру) → записать новую → переподключение MATCH. Всё в форме хешированных имён хостов. Критерии приёмки: после ротации пропуск; до подтверждения всегда отказ; в файле всё время нет имён хостов открытым текстом.

Итоги

ТемаКратко
Слой форматаимя алгоритма + сырые представления параметров; общий парсер не трогается эволюцией алгоритмов; без выделений
Ed25519открытый ключ 32Б / подпись 64Б (в string); тройка чтение/запись/проверка
Отпечатокформа отображения SHA-256; для людей и журналов — программы сравнивают по ключу
Состояния доверияMATCH пропускает / NEW спрашивает (TOFU) / CHANGED тревога и отказ / REVOKED сразу отказ
revokedпопадание @-строки — отказ; приоритетнее совпадений
Хешированные имена\1\salt\hash; сопоставление пересчётом по salt; защищает топологию при утечке файла
Идентичность порта[host]:port — нестандартный порт есть отдельная личность
Координаты строкирезультат несёт LineNumber — адресация тревог/удаления/аудита
Стык с главой 118событие VERIFY_HOST_KEY + запрос хранилища этой главы = полное суждение доверия

📖 Справочники API в этой главе:xssh-ssh_hostkey (7 записей)xssh-ssh_known_host_db (3 записей)xssh-ssh_fingerprint (2 записей)

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