Ориентация главы
Парсер главы 79 рассказал, что сертификат говорит; два верификатора этой главы отвечают на первые два вопроса «правда или ложь»: проверка подписи — годится ли заявленный алгоритм с публичным ключом издателя для проверки подписи этого сертификата, состоятельна ли она математически (xrtX509CertificateVerify, внизу — протокольно диспетчеризируемый xrtX509SignatureVerify); идентичность сервиса — совпадает ли заявленная сертификатом идентичность с хостом, к которому вы подключаетесь (xrtX509MatchHost/MatchDns по RFC 9525). Третий вопрос «достоин ли этот издатель доверия» требует построения цепочки и хранилища доверия — тема главы 81; здесь проводим границу на уровне понятий. Оба верификатора — заимствующие пути с нулём выделений; бэкенды обрезаются по семействам алгоритмов (RSA/ECDSA/Ed25519 независимо) — прошивка, проверяющая только ECDSA-сертификаты, не втягивает код RSA.
80.1引入
Представьте последовательность проверок в HTTPS-соединении: получен сертификат → разбор (глава 79) → проверка подписи (эта глава) → сопоставление хоста (эта глава) → вдоль Issuer поиск сертификата следующего уровня и поуровневая проверка до якоря доверия (глава 81). Что случится, сделав только первые два шага? Атакующий может самоподписать сертификат: CN и SAN оба bank.example.com, собственной сгенерированной парой ключей подписан сам себе — проверка подписи проходит на 100% (математика безупречна!), идентичность тоже проходит (SAN действительно несёт целевой домен). Недостающее звено — «доверие»: состоятельность подписи доказывает лишь «кто-то с соответствующим приватным ключом подписал это», а подписал ли его признанный вами CA — сама подпись ответить не в состоянии.
А наоборот — только цепочку доверия без проверки подписи? Ещё хуже: каждое звено цепи и есть «проверка подписи нижнего уровня публичным ключом верхнего». Три вопроса — последовательные шлюзы; эта глава реализует первые два и прочерчивает интерфейсную границу третьего. Поняв «какую атаку сдерживает каждый вопрос», вы никогда не напишете классическую уязвимую строку «проверка подписи прошла — accept».
80.2概念
Слоистость проверки подписи: протокольная диспетчеризация + криптобэкенд
Входы проверки в два слоя. Слой удобства xrtX509CertificateVerify(证书, 签发者证书): разбирает схему подписи сертификата и SPKI издателя, вызывает нижний слой и завершает проверку — самый частый «проверить дочерний сертификат родительским» одним шагом. Вариант xrtX509CertificateVerifyKey(证书, 公钥): принимает уже извлечённый или напрямую данный доверенной конфигурацией публичный ключ — при проверке независимого якоря доверия (pinning публичного ключа в духе -pin) не нужно подделывать объект сертификата. Нижний слой xrtX509SignatureVerify(方案, 内容, 签名, 公钥): принимает открытую протокольную модель — универсален для сертификатов, CRL, OCSP и ваших собственных подписанных объектов.
Бэкенды обрезаются по семействам независимо: XRT_FEATURE_X509_VERIFY_RSA (PKCS#1 v1.5 и PSS, все дайджесты SHA-1/224/256/384/512), XRT_FEATURE_X509_VERIFY_ECDSA (строгое DER-декодирование r,s; кривая P-256/P-384 по SPKI; дайджест вводится в базис кривой по bits2int — не косное «P-256 с SHA-256»), XRT_FEATURE_X509_VERIFY_ED25519. Невлитый бэкенд возвращает XERR_UNSUPPORTED — явное «не поддерживаю этот алгоритм», никогда не маскируясь под «подпись не совпала»; так же ведут себя разобранные, но безбэкендные P-521/Ed448. Это различие критично и для отладки, и для политики: несовпадение — вопрос безопасности, неподдержка — вопрос конфигурации.
Что проверяет и что не проверяет: граница Verify
CertificateVerify проверяет только криптографическую подпись TBSCertificate. Чего он заведомо не делает: не сравнивает issuer/subject Name (сопоставление цепочки — ответственность главы 81), не проверяет время, не проверяет BasicConstraints/KeyUsage, не проверяет неизвестные critical-расширения, не ищет отзыв, не судит об якоре доверия. Эта граница позволяет медосмотру корневых сертификатов (самоподписанные проверяют себя) и аудиту CA-материалов (офлайн-проверка целостности подписи каждого сертификата) переиспользовать тот же вход — им нужна математика, не политика.
Идентичность сервиса: правила сопоставления RFC 9525
Сопоставление идентичности отвечает «этот ли сертификат выдан для api.example.test»:
xrtX509MatchHost(证书, 主机名, &呈现身份): сертификатный вход — читает только SubjectAltName DNS-ID и IP-ID, CN не читает вовсе. Возвращает три состояния:X509_VALUE(совпало; опционально выдаёт фактически совпавший SAN),X509_DONE(законная ссылочная идентичность без совпадения — TLS-слой обязан трактовать как неудачу рукопожатия),X509_ERROR(незаконная ссылочная идентичность или структура SAN).xrtX509MatchDns(模式, 主机名): чистая строковая форма DNS-сопоставления — правила wildcard встроены: совпадение ровно с одним крайним левым цельным ярлыком (*.example.testберётapi.example.test, но неa.b.example.test), появление только в левом ярлыке, ASCII-нечувствительность к регистру, нормализация концевой корневой точки (example.test.эквивалентноexample.test). Недействительные presented DNS-ID игнорируются, а не ошибаются.- IP-идентичность: текст IPv4/IPv6 после строгого разбора
xrtNetAddrParseсравнивается с iPAddress SAN как 4/16 байтов точно (квадратные скобки IPv6, привычные в URL, поддержаны; Scope ID отвергается). Заметьте: «адрес соединения, полученный через DNS», всё равно должен сопоставляться с исходным DNS-ID, а не с разложенным в IP.
Ручная реализация этих правил — исторический рассадник уязвимостей (*.example.test совпал с a.b.example.test, wildcard в середине, регистрочувствительное сравнение с пропусками) — встроенный вход истребляет весь класс разом.
Инженерная форма пути проверки
Полная проверка «первых двух вопросов» (минимальное действие TLS-клиента после получения сертификата):
Заметьте «время», затесавшееся сюда — оно ни подпись, ни идентичность, но как независимый пункт проверки всегда исполняется в тот же момент; проверка пути главы 81 оркеструет эти три пункта вместе с построением цепочки и политикой.
80.3示例
Первая полная программа: самопроверка подписи самоподписанного сертификата
Программа ниже из examples/x509/verify/main.c — разбор реального самоподписанного RSA-сертификата и проверка его подписи:
#include <xrt.h> #include "../../../tests/fixtures/x509_legacy_cert.h" #include <stdio.h> */ /* 解析并验证从旧版示例继承的真实自签名 RSA 证书。 */ int main(void) { xx509cert Certificate; if ( !xrtX509Parse( X509_LEGACY_RSA_CERT, sizeof(X509_LEGACY_RSA_CERT), &Certificate ) || !xrtX509CertificateVerify( &Certificate, &Certificate ) ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); return 1; } printf("certificate signature is valid\n"); return 0; }
Что только что произошло. (1) Ввод — байты настоящего сертификата из тестовой оснастки (tests/fixtures/x509_legacy_cert.h) — куда больше игрушечного сертификата inspect, идёт полная проверка разбора. (2) Одна и та же карта передана дважды: CertificateVerify(&Cert, &Cert) — проверяемый и «издатель» одно лицо (определение самоподписи — «сам себе подписал, публичный ключ при себе»). Прошедшая проверка выводит математический факт: подпись действительно создана приватным ключом, соответствующим этому публичному. (3) Ветка неудачи печатает xrtErrorMessage(xrtGetError()) — цепочка причин провала (ошибка подписи xrt.x509, нижняя причина xrt.crypto) прямо для человека; программное суждение — категорийными запросами главы 4. (4) Заметьте: здесь нет никакого суждения о «доверии» — прохождение проверки самоподписанного сертификата нормально (каждый корневой самоподписан); доверять ли ему — дело хранилища доверия главы 81.
Вторая полная программа: сопоставление wildcard-идентичности
Вторая программа из examples/x509/identity/main.c — проверяется, совпадает ли *.example.test с api.example.test:
#include <stdio.h> #include <xrt.h> */ /* 演示零分配 DNS-ID 通配符匹配。 */ int main(void) { xx509result Result = xrtX509MatchDns( XRT_STR_LITERAL("*.example.test"), XRT_STR_LITERAL("api.example.test") ); printf("matched=%s\n", (Result == X509_VALUE) ? "yes" : "no"); return (Result == X509_VALUE) ? 0 : 1; }
Что только что произошло. (1) xrtX509MatchDns — форма чистой функции: шаблон сертификата и ссылочный хост — строковые представления, объект сертификата не нужен; в сертификатной сцене MatchHost внутри зовёт те же правила для каждой DNS-ID SAN. (2) Попадание демонстрирует главную границу wildcard: * глотает и только глотает один крайний левый цельный ярлык — подставьте a.b.example.test, получите X509_DONE (законно, но без совпадения); api.example.org — тот же DONE. (3) Нечувствительность к регистру выполняется автоматически (API.EXAMPLE.TEST тоже попадает) — сравнение на уровне ASCII-ярлыков, без Unicode-фолдинга (U-label обязан сперва перевести в A-label слой IDNA). Прогнать все три варианта — самый быстрый путь к пониманию границы RFC 9525.
80.4契约
- Слоистая обрезка: базовый
x509_verifyне втягивает криптоалгоритмы;XRT_FEATURE_X509_VERIFY_RSA/ECDSA/ED25519включаются по семейству сертификатов; невключённый алгоритм возвращаетXERR_UNSUPPORTED(не маскируясь под несовпадение подписи). - Входы удобства:
CertificateVerify(证书, 签发者证书)иCertificateVerifyKey(证书, 公钥)проверяют только криптоподпись TBSCertificate — без сопоставления имён, времени, ограничений, отзыва, доверия. - Нижние входы:
SignatureVerify(方案, 内容, 签名, 公钥)принимает открытую протокольную модель — универсален для сертификатов/CRL/OCSP/собственных объектов; схема приходит из слоя перевода алгоритмов главы 79. - Правила RSA: PKCS#1 v1.5 и PSS по всем дайджестам; публичный ключ id-RSASSA-PSS проверяется только PSS с согласованными параметрами и солью не короче заявленного ключом нижнего предела; ключ PSS без параметров не ограничивает.
- Правила ECDSA: строгое DER-декодирование r,s; кривая из SPKI; дайджест вводится в базис кривой по bits2int.
- Правила идентичности: RFC 9525 — чтение только DNS-ID/IP-ID из SAN, без CN; wildcard только крайний левый единственный ярлык; ASCII-нечувствительность; IP — точное сравнение, Scope ID отвергается.
- Трёхзначная семантика: у
MatchHostX509_VALUEсовпало /X509_DONEзаконно без совпадения (TLS обязан трактовать как неудачу) /X509_ERRORнезаконная структура. - Форма неудачи: провал проверки возвращает
falseи ставит ошибкуxrt.x509; криптопричина сохранена черезxrtErrorCauseвxrt.crypto; действенный путь — стековое резюме + заимствованные представления, нуль кучевых выделений.
Типичные ошибки
Симптом: в коде успех CertificateVerify сразу accept — атакующий с самоподписанным сертификатом (CN/SAN заполнены целевым доменом) проходит напрямую: подпись математически безупречна.
Причина: ответ проверки подписи — «держатель приватного ключа подписал это», а не «признанный вами CA подписал это». Доверие — суждение цепочки и хранилища (глава 81).
if ( xrtX509CertificateVerify(&Cert, &Issuer) ) { accept_connection(); /* математически верно != доверенно: самоподпись тоже верна */ }
/* подпись + идентичность + (глава 81) цепь до якоря доверия - все три вопроса пройдены, только тогда accept */ if ( xrtX509CertificateVerify(&Cert, &Issuer) && xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost), &Presented) == X509_VALUE && xrtX509ValidAt(&Cert, xrtNow()) && trusted_to_anchor(&Cert /* 第 81 章 */) ) { accept_connection(); }
Симптом: хост не совпал (DONE), вызывающий проверил лишь != X509_ERROR и продолжил — не тот сервис пропущен; либо на ERROR поставили assert с падением — незаконный SAN-ввод превращается в отказ доступности.
Причина: в трёх состояниях DONE — «законно, но без совпадения»: для TLS это провал проверки идентичности, обязателен отказ; ERROR — «структура ввода незаконна»: тоже отказ, но отчётная протокольная ошибка, не падение.
if ( xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost), &Presented) != X509_ERROR ) { accept(); /* DONE тоже вошёл: несовпавшая идентичность пропущена */ }
switch ( xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost), &Presented) ) { case X509_VALUE: accept(); /* единственный пропускающий путь */ break; case X509_DONE: reject("identity mismatch"); /* законно, но не этот хост */ break; default: reject("malformed identity"); /* структура незаконна: доложить и отвергнуть */ break; }
Симптом: соединение со старым сертификатом без SAN (v1/v2 или ранний v3) всё ещё успешно — код при несовпадении SAN откатился сравнивать CN; атакующий проходит проверку с сертификатом, где CN несёт целевой домен, а SAN нет.
Причина: RFC 9525 прямо упраздняет откат на CN. Сертификат без SAN в современном TLS обязан проваливать идентичность — «обратная совместимость» здесь равносильна открытому каналу подделки.
if ( xrtX509MatchHost(&Cert, ...) != X509_VALUE ) { /* откат на CN: CN поддельного сертификата - любая строка */ if ( cn_equals_host(&Cert, sHost) ) { accept(); } }
CertificateVerifyKey 做公钥钉扎,比 CN 更诚实 */ if ( xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost), &Presented) != X509_VALUE ) { reject("no SAN match; CN fallback is forbidden"); }
Практические задания
Постройте для xrtX509MatchDns шесть групп входов: точное попадание, попадание варианта регистра, попадание wildcard по одному ярлыку, промах wildcard по многим ярлыкам, несовпадение суффикса, попадание с концевой корневой точкой. Печатайте трёхзначный результат по группам. Критерий приёмки: шесть результатов совпадают с ручной выкладкой правил RFC 9525; понятно, почему «wildcard по многим ярлыкам» — DONE, а не ERROR.
Реализуйте verify_pinned(证书 DER, 主机名, 钉扎公钥 SPKI) (сертификат DER, хост, pinned SPKI): разбор → CertificateVerifyKey проверяет подпись pinned-ключом → MatchHost сопоставляет хост → ValidAt проверяет время; три вопроса пройдены — true. Подсказка: сцене pinning не нужна цепочка — публичный ключ сам и есть якорь доверия. Критерии приёмки: на собственной паре ключей и самоподписанном сертификате проходят оба пути (смена pinned-ключа обязана валить).
Для пачки сертификатов (RSA-PKCS1, RSA-PSS, ECDSA P-256, ECDSA P-384, Ed25519 — по одному, OpenSSL) напишите инструмент зондирования: разбор алгоритма и типа ключа, попытка CertificateVerify (самоподпис проверяет себя), различимая печать XERR_UNSUPPORTED и несовпадения подписи. Прогоните в двух обрезках — «все бэкенды» и «только ECDSA» — и сверьте выводы. Критерии приёмки: до и после обрезки выводы по поддержанным алгоритмам неизменны, неподдержанные переходят из «valid» в отчётливый UNSUPPORTED — доказательство, что обрезка не даёт молчаливых ложноотрицательных.
Итоги
| Тема | Кратко |
|---|---|
| Модель трёх вопросов | подпись (эта глава) → идентичность (эта глава) → цепь доверия (глава 81); последовательные шлюзы, ни один не лишний |
| Входы удобства | CertificateVerify(证书, 签发者) / VerifyKey(证书, 公钥) — только математическая подпись |
| Нижние входы | SignatureVerify(方案, 内容, 签名, 公钥) — универсален для сертификатов/CRL/OCSP/своих |
| Обрезка бэкендов | XRT_FEATURE_X509_VERIFY_RSA / _ECDSA / _ED25519 независимо; невключён → UNSUPPORTED |
| Правила RSA | v1.5+PSS все дайджесты; у PSS-ключа согласование параметров и нижний предел соли; ключ без параметров не ограничивает |
| Правила ECDSA | строгое DER r,s; кривая из SPKI; дайджест в кривую по bits2int |
| Входы идентичности | MatchHost (сертификат+хост) три состояния / MatchDns (чистые строки) те же правила |
| Wildcard | только крайний левый единственный цельный ярлык; ASCII-нечувствительность; нормализация корневой точки |
| Железное правило трёх состояний | VALUE — единственный пропуск; DONE = провал идентичности, обязателен отказ; ERROR = незаконная структура, доложить и отвергнуть |
| Граница CN | никогда не откатываться на CN; нет SAN — провал идентичности; внутренние системы — pinning публичного ключа |
📖 Справочники API в этой главе:x509 (140 записей)crypto (140 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки