Ориентация главы
Событие 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: формат и три состояния
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 — круг слоя формата:
#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; }
Что только что произошло. (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 — сердце суждения доверия:
#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; }
Что только что произошло. (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 не касается сети/соединений/генерации ключей — только файлы и сопоставление.
Типичные ошибки
Симптом: при смене ключа клиент спрашивает «записать новый ключ?» — пользователь по привычке жмёт 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(); /* отказ по умолчанию - старую запись чистить только после ручного подтверждения */ }
Симптом: пропуск по совпадению первых 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(); }
Симптом: аудит соответствия не проходит — кража ноутбука раскрывает всю топологию 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 указывает на существующую запись.
Реализовать 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки