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

Парсер главы 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-клиента после получения сертификата):

1РазборParse выдаёт представления (законность структуры гарантирует парсер)
2ПодписьCertificateVerify(сертификат, сертификат издателя) - математически верно?
3ИдентичностьMatchHost(сертификат, ожидаемый хост) == X509_VALUE - имя совпало?
4ВремяValidAt(сертификат, now) - не просрочен?
5(глава 81) Цепьвдоль Issuer дойти до якоря доверия + проверка отзыва

Заметьте «время», затесавшееся сюда — оно ни подпись, ни идентичность, но как независимый пункт проверки всегда исполняется в тот же момент; проверка пути главы 81 оркеструет эти три пункта вместе с построением цепочки и политикой.

80.3示例

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

Программа ниже из examples/x509/verify/main.c — разбор реального самоподписанного RSA-сертификата и проверка его подписи:

examples/x509/verify/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -I . -include xrt.h impl.c examples/x509/verify/main.c -lws2_32 -liphlpapicertificate signature is valid

Что только что произошло. (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:

examples/x509/identity/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/x509/identity/main.c -lws2_32 -liphlpapimatched=yes

Что только что произошло. (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 отвергается.
  • Трёхзначная семантика: у MatchHost X509_VALUE совпало / X509_DONE законно без совпадения (TLS обязан трактовать как неудачу) / X509_ERROR незаконная структура.
  • Форма неудачи: провал проверки возвращает false и ставит ошибку xrt.x509; криптопричина сохранена через xrtErrorCause в xrt.crypto; действенный путь — стековое резюме + заимствованные представления, нуль кучевых выделений.

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

Ловушка 1: «проверка подписи прошла — доверяем»:Ловушка 1: «проверка подписи прошла — доверяем»

Симптом: в коде успех 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();
}
Ловушка 2: X509_DONE как успех (или как падение):Ловушка 2: X509_DONE как успех (или как падение)

Симптом: хост не совпал (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;
}
Ловушка 3: откат сопоставления идентичности на CN:Ловушка 3: откат сопоставления идентичности на CN

Симптом: соединение со старым сертификатом без 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.

Продвинутое: верификатор pinning-а публичного ключа

Реализуйте 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
Правила RSAv1.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 записей)

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