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

Глава 78 научила читать TLV; эта глава читает этим самым важный DER-документ — сертификат X.509. Сертификат — поручительство CA некоторому публичному ключу: «этот ключ принадлежит субъекту по имени X, издатель по имени Y ручается на таком-то отрезке времени». xrtX509Parse за один шаг превращает DER в представление xx509cert: версия, серийный номер, издатель, субъект, срок действия, публичный ключ, расширения, подпись — всё заимствует исходный DER, нуль копий, нуль выделений. Глава разъясняет каждое поле представления, курсор RDN у Name, способ обхода расширений и слой перевода «OID алгоритма подписи → нормализованные параметры»; встроенный потребительский чек-лист парсера (структура, каноничность кодирования, порядок полей) также выписан полностью. Проверка (состоялась ли подпись) — дело главы 80; эта глава отвечает только за точное чтение того, что сертификат сказал.

79.1引入

Что первым делом делает TLS-клиент, получив серверный сертификат? Не проверку подписи — разбор. Вашему коду надо ответить на вереницу вопросов: кто выдал этот сертификат (искать следующий уровень)? как зовут субъекта (сопоставление идентичности)? до какого дня действует (проверка просрочки)? какой тип у публичного ключа (выбор бэкенда проверки)? какие расширения несёт (искать SAN, искать адреса отзыва)? Каждый вопрос соответствует одному-двум полям представления xx509cert.

Парсер сам — линия обороны: сертификат с незаконным форматом (перепутан порядок полей, длина вне границ, не-каноническое кодирование, повторный OID расширения) отвергается уже на шаге Parse — не придётся падать при проверке подписи. Синтаксическое дерево сертификата по RFC 5280 насчитывает десятки узлов; XRT их все интернализировал: вы получаете полностью проверенное представление, а не голый TLV для самостоятельного блуждания.

79.2概念

Строение сертификата: подпись, оборачивающая заявление

1tbsCertificateверсия/серийный номер/алгоритм подписи/издатель/срок действия/субъект/публичный ключ/расширения - заверяемое заявление
2signatureAlgorithmалгоритм подписи, фактически употреблённый издателем (AlgorithmIdentifier)
3signatureValueподпись над DER tbsCertificate (BIT STRING)

Отношение трёх составных частей — ядро главы 80: signatureValue есть подпись над всей DER-последовательностью байтов tbsCertificate — при проверке берётся именно этот оригинал. xx509cert сохраняет полный сертификат и DER TBS (Raw/Tbs), оба AlgorithmIdentifier — у TBS и у внешнего слоя (требуется их совпадение; исторически встречались сертификаты с различными — пункт разбора с отказом) и содержимое подписи без unused-бит.

Вся семья представлений: что читается из сертификата

Факты, доступные после успеха xrtX509Parse(DER, 长度, &证书视图):

ПолеСодержимоеПримечание
Versionнатуральный номер v1/v2/v3отсутствие поля означает v1 (значение перечисления 1), не 0
Serialполное содержимое октетов серийного номеразаимствованное представление; знаковый бит положительности сохранён, в величину не переводится — потребитель принимает нулевые/отрицательные/сверхдлинные исторические значения
Issuer/Subjectполный DER издателя/субъектаструктура Name, см. следующий раздел
NotBefore/NotAfterсрок действия, уже в xtimeзамкнутый интервал; xrtX509ValidAt(&cert, xrtNow()) проверяет просрочку одним шагом
SPKIполный DER SubjectPublicKeyInfoалгоритм публичного ключа + материал, см. раздел ниже
Extensionsпоследовательность расширений (опционально, v3)курсорный обход, см. далее
Signatureсодержимое подписи (BIT STRING без смещения бит)для проверки главой 79

Два контракта стоят отдельной записи: срок действия — замкнутый интервал, и при законных параметрах «просрочен» возвращает лишь false, а не ошибку исполнения (просрочка — нормальное бизнес-состояние, не поломка разбора); Serial хранится байтами как есть — RFC 5280 требует от CA положительных чисел не длиннее 20 байтов, но потребитель обязан корректно обходиться с нулевыми и отрицательными серийными номерами, реально живущими в корневых хранилищах; ограничения издателя — дело инструментов на стороне CA.

Name: субъект — не строка, а последовательность атрибутов

Issuer/Subject в сертификате — не «одно имя», а последовательность RDN (относительных различимых имён) — группа атрибутов вроде C=CN, O=Example, CN=api.example.test. Курсор xrtX509NameInit/Read выдаёт атрибут за атрибутом: OID (идентифицирует: страна/организация/общее имя) + представление значения. Равенство двух Name — побайтовое сравнение DER (не строковое): в проверке цепочки главы 81 «Issuer дочернего == Subject родительского» и есть точное байтовое совпадение. Важная граница: современное суждение об идентичности не смотрит на CN (Common Name) — RFC 9525 прямо признаёт только SubjectAltName (глава 80); CN — для отображения.

Публичный ключ и алгоритм подписи: слой перевода OID

Все алгоритмы в сертификате идентифицируются OID (строка чисел вроде 1.2.840.113549.1.1.10). XRT не заставляет вас вести таблицу соответствий:

  • xrtX509PublicKey(&cert, &公钥视图): разбирает SPKI — выдаёт перечисление Type (RSA, RSA-PSS, EC, Ed25519, Ed448 и два семейства кривых x25519/x448, независимо от влитых сейчас бэкендов) плюс параметры алгоритма и представление материала ключа.
  • xrtX509AlgorithmParse(DER 视图, &算法) + xrtX509SignatureParse(&算法, &规范签名): переводит AlgorithmIdentifier в нормализованную структуру параметров — тип подписи (скажем RSA-PSS), дайджест содержимого, дайджест MGF1, длину соли, trailer — вход проверки главы 80 ест эту структуру напрямую. Отсутствующие параметры дополняются умолчаниями RFC; незаконные комбинации ошибаются уже на слое разбора.

Расширения: плагинная система сертификата

Зона расширений v3-сертификата — последовательность пар: OID + бит critical + значение DER. Парсер проверяет повторные OID и законность структуры; обход идёт расширительным курсором построчно. Ходовые расширения: SAN (субъектные альтернативные имена — настоящий источник DNS/IP-идентичности), BasicConstraints (CA ли это, потолок глубины пути — центральный вход проверки цепочки), KeyUsage (битовая карта назначений ключа), AKI/SKI (идентификаторы ключей издателя/субъекта — индексы поиска в цепочке), точки раздачи CRL (вход проверки отзыва главы 81). Значение каждого расширения — снова собственная DER-структура; XRT даёт специализированные входы разбора для SAN, BasicConstraints, NameConstraints и CRL-расширений (разделы docs/api/x509.md), индекс — в шпаргалке главы.

Чек-лист парсера: потребительская строгость

Входы, отвергаемые Parse: ошибочное число и порядок полей, явная версия по умолчанию, не-канонический INTEGER серийного номера, пустой Issuer, время не в реальном календаре, NotAfter раньше NotBefore, испорченная структура SPKI, противоречие опциональных полей и версии (у v1-сертификата поля v3), незаконный DER тела расширения, повторный OID расширения, несовпадение алгоритмов подписи TBS и внешнего слоя, незаконный BIT STRING подписи, пустой Subject без непустого critical SAN. Этот список — «потребительская строгость»: все законные варианты, существующие в реальном мире (нулевой серийный номер, сверхдлинный серийный номер), принимаются, всякая структурная двусмысленность отвергается — разбор возможен = структура однозначна, а суждение о подлинности остаётся слою проверки.

79.3示例

Первая полная программа: нуль-выделительный разбор и обход субъекта

Программа ниже из examples/x509/inspect/main.c — разбор встроенного минимального сертификата с чтением версии, атрибутов субъекта и типа публичного ключа:

examples/x509/inspect/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>




















 */


/* 演示零分配解析证书,并遍历其 Subject Name。 */
int main(void)
{
	static const uint8 Certificate[] = {
		0x30, 0x68, 0x30, 0x5C, 0x02, 0x01, 0x01, 0x30, 0x04, 0x06, 0x02, 0x2A,
		0x03, 0x30, 0x11, 0x31, 0x0F, 0x30, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x03,
		0x0C, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x30, 0x1E, 0x17, 0x0D,
		0x32, 0x35, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
		0x5A, 0x17, 0x0D, 0x32, 0x36, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30,
		0x30, 0x30, 0x30, 0x5A, 0x30, 0x12, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03,
		0x55, 0x04, 0x03, 0x0C, 0x07, 0x53, 0x75, 0x62, 0x6A, 0x65, 0x63, 0x74,
		0x30, 0x0A, 0x30, 0x04, 0x06, 0x02, 0x2A, 0x03, 0x03, 0x02, 0x00, 0x4B,
		0x30, 0x04, 0x06, 0x02, 0x2A, 0x03, 0x03, 0x02, 0x00, 0x53
	};
	xx509cert Cert;
	xx509namecursor Cursor;
	xx509nameattr Attribute;
	xx509pubkey PublicKey;

	if ( !xrtX509Parse(Certificate, sizeof(Certificate), &Cert) ||
		!xrtX509NameInit(Cert.Subject, &Cursor) ||
		(xrtX509NameRead(&Cursor, &Attribute) != X509_VALUE) ||
		!xrtX509PublicKey(&Cert, &PublicKey) ) {
		return 1;
	}
	printf(
		"version=%d subject=%.*s key-type=%d\n",
		(int)Cert.Version,
		(int)Attribute.Value.Size, (const char*)Attribute.Value.Data,
		(int)PublicKey.Type
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/x509/inspect/main.c -lws2_32 -liphlpapiversion=1 subject=Subject key-type=0

Что только что произошло. (1) Ввод — 104-байтовый рукотворный минимальный сертификат (массив прямо встроен) — xrtX509Parse одним шагом проходит все структурные проверки и выдаёт представление; кучевых выделений нет, все указатели внутри Cert целятся в массив. (2) Курсор xrtX509NameInit + Read берёт первый атрибут Subject — представление значения Subject (CN этого сертификата); обход RDN изоморфен DER-курсору главы 78, только атрибутная семантика уже переведена. (3) xrtX509PublicKey даёт Type=0 (X509_KEY_UNKNOWN — OID алгоритма публичного ключа этой игрушки не входит в известное множество); у настоящего сертификата будет значение перечисления RSA/EC/ED25519. (4) Печать и возврат — представления заимствуют массив, ни одного вызова освобождения; в реальной сцене, когда ввод это «DER-буфер, декодированный из PEM», время жизни представлений привязано к тому буферу (см. ловушку 1).

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

Вторая программа из examples/x509/signature/main.c — из отдельного AlgorithmIdentifier читается полный набор параметров RSA-PSS:

examples/x509/signature/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>





















 */


/* 演示从独立 AlgorithmIdentifier 读取完整 RSA-PSS 参数。 */
int main(void)
{
	static const uint8 AlgorithmDer[] = {
		0x30, 0x0D,
		0x06, 0x09, 0x2A, 0x86, 0x48, 0x86,
		0xF7, 0x0D, 0x01, 0x01, 0x0A,
		0x30, 0x00
	};
	xx509algorithm Algorithm;
	xx509signature Signature;

	if ( !xrtX509AlgorithmParse(
		(xbytesview) { AlgorithmDer, sizeof(AlgorithmDer) }, &Algorithm
	) || (xrtX509SignatureParse(
		&Algorithm, &Signature
	) != X509_VALUE) ) {
		return 1;
	}
	printf(
		"type=%d hash=%d mgf-hash=%d salt=%zu trailer=%u\n",
		(int)Signature.Type, (int)Signature.Hash,
		(int)Signature.MaskHash, Signature.SaltSize,
		(unsigned)Signature.Trailer
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/x509/signature/main.c -lws2_32 -liphlpapitype=2 hash=1 mgf-hash=1 salt=20 trailer=1

Что только что произошло. (1) Ввод 15 байтов: SEQUENCE с OID 1.2.840.113549.1.1.10 (id-RSASSA-PSS) и пустым SEQUENCE параметров. (2) Перевод в два шага: AlgorithmParse распознаёт OID и даёт идентификатор алгоритма; SignatureParse заполняет нормализованную структуру параметрами (включая доставку умолчаний) — type=2 это перечисление RSA-PSS, hash=1 семейство SHA-256, MGF1 с тем же хешем, соль 20 байтов (умолчание RFC 4055), trailer 0xBC. (3) Вызывающий получает параметры, напрямую гонящие криптомодуль главы 76 — без собственной таблицы OID и без заботы об умолчаниях; в этом смысл «слоя перевода OID». В сертификатной сцене эти два шага интернализированы CertificateVerify (глава 80), но самостоятельные входы так же годятся для CRL, OCSP и произвольных подписанных объектов.

79.4契约

  • Представления без копий: все поля xx509cert заимствуют ввод DER; память ввода обязана жить до конца использования представлений; ни одного выделения — ни одного освобождения.
  • Структура и есть проверка: разбор прошёл = порядок полей/кодирование/согласованность все законны (потребительский список); структурная двусмысленность отвергается в Parse, домен ошибок xrt.x509.
  • Срок действия: форма xtime, замкнутый интервал; при законных параметрах xrtX509ValidAt за просрочку/недействие возвращает лишь false, не ошибку.
  • Семантика Serial: каноническое содержимое INTEGER сохраняется как есть (со знаковым битом); ограничения на стороне CA (положительное, ≤20 байтов) исполняются отдельными инструментами издателя/комплаенса.
  • Равенство Name: побайтовое сравнение DER (xrtX509NameEqual), не строковая эквивалентность; суждение об идентичности не читает CN (правило SAN главы 80).
  • Перевод алгоритмов: AlgorithmIdentifier → нормализованные параметры на слое разбора; умолчания по RFC; незаконные комбинации — ошибка.
  • Тип публичного ключа: xx509keytype независимо от влитых бэкендов проверки — разбор не нуждается в криптоалгоритмах; проверка (глава 80) обрезается по надобности.
  • Расширения: повторный OID и незаконная структура отвергаются при разборе; для SAN/BasicConstraints/NameConstraints/CRL-расширений есть специализированные входы (docs/api/x509.md).

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

Ловушка 1: время жизни представлений следует за DER-буфером:Ловушка 1: время жизни представлений следует за DER-буфером

Симптом: разбор успешен, поля прочитаны — а чуть позже доступ к представлению падает или читает мусор; типично в обёртке «декодировать PEM внутри функции, вернуть представление сертификата».

Причина: xx509cert — лишь набор указателей в DER. DER-буфер (будь то продукт декодирования PEM или прочитанное содержимое файла), будучи освобождён, оставляет все представления висячими.

错误写法
xx509cert* load_cert(const char* sPemText) {
	bytes pDer = ...;                    /* 本地 DER 缓冲 */
	xx509cert* pCert = (xx509cert*)xrtMalloc(sizeof(*pCert));
	xrtX509Parse(pDer, iSize, pCert);
	return pCert;   /* время жизни pDer кончилось: все возвращённые представления висят */
}
正确写法
/* кто держит DER, тот держит представления - буфер и представления в одной области видимости */
typedef struct { bytes pDer; size_t iSize; xx509cert Cert; } loadedcert;
bool load_cert(const char* sPemText, loadedcert* pOut) {
	
	   оба живут и умирают вместе, вызывающий освобождает/держит их разом */
}
Ловушка 2: Common Name в роли идентичности сервиса:Ловушка 2: Common Name в роли идентичности сервиса

Симптом: в SAN сертификата есть api.example.test, а CN — Example Ltd; код,.matching по CN, либо не совпадает (хороший случай), либо обманывается CN злорадного сертификата (плохой): атакующий самоподписывает сертификат, вписав в CN целевой домен.

Причина: RFC 9525 упразднил откат на CN — идентичность сервиса смотрит только на DNS-ID/IP-ID из SubjectAltName. CN — отображаемое имя для человека, атакующий вправе писать что угодно.

错误写法
/* обойти Subject, найти атрибут CN, сравнить значение с хостом */
if ( strcmp(cn_value, sHost) == 0 ) { accept(); }
/* CN всецело контролируется автором сертификата: это не проверка идентичности */
正确写法
/* сопоставление идентичности по SAN: xrtX509MatchHost (глава 80) читает только DNS-ID/IP-ID */
xx509genname Presented;
if ( xrtX509MatchHost(&Cert, XRT_STR_LITERAL(sHost),
		&Presented) != X509_VALUE ) {
	reject();   /* SAN не попал: отказ, без всякого отката на CN */
}
Ловушка 3: Version как битовая маска или строковое сравнение:Ловушка 3: Version как битовая маска или строковое сравнение

Симптом: Version == 3 всегда ложь (у v3 значение перечисления 3, но сравнили строкой/битами), либо для v1-сертификата обходят пустое представление Extensions и продолжают.

Причина: версия — натуральное перечисление (v1=1, v2=2, v3=3), причём отсутствие поля и есть v1 — старые сертификаты не имеют явного байта версии. Расширения существуют только в v3; для v1/v2 перед обходом расширений сперва проверьте версию.

错误写法
if ( Cert.Version & 3 ) {          /* битовая проверка: семантика в корне неверна */
	xrtX509ExtensionInit(&Cert, &Cursor);   /* сертификат v1: расширений нет */
	while ( /* обход */ );
}
正确写法
if ( Cert.Version >= X509_VERSION_3 ) {
	/* v3: расширения есть, безопасный обход */
} else {
	
	   такие сертификаты в современном TLS и следует отвергать */
}

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

Базовое: все факты сертификата inspect

Расширьте образец inspect: кроме версии/субъекта/ключа напечатайте первый RDN Issuer, годы NotBefore/NotAfter, результат xrtX509ValidAt(&Cert, xrtNow()). Критерии приёмки: совпадение с ручной выкладкой DER (по схеме RFC 5280); вывод о сроке игрушечного сертификата (вероятно «просрочен») объясним.

Продвинутое: браузер серийных номеров и алгоритмов

Напишите функцию dump_cert(loadedcert* pC): печать серийного номера (HEX, побайтово), результата перевода алгоритма подписи (type/hash/соль), имени перечисления типа ключа. Сгенерируйте OpenSSL три разных сертификата (RSA-PSS, ECDSA P-256, Ed25519), прогоните каждый и сверьте с openssl x509 -text. Критерии приёмки: параметры алгоритмов трёх сертификатов совпадают с показанным OpenSSL; серийные номера побайтово равны (включая сохранение ведущих нулей).

Вызов: инструмент инвентаризации расширений

Реализуйте «медосмотр сертификата»: обход всех расширений с выводом OID, бита critical и смыслового резюме распознанных (список DNS у SAN, флаг CA и глубина у BasicConstraints, битовая карта KeyUsage, URL точек раздачи CRL); нераспознанные помечайте unknown. Проверьте им сертификат реального сайта. Критерии приёмки: резюме распознанных совпадает с openssl x509 -text; critical и нераспознанные расширения — выделенное предупреждение (глава 81 объяснит, почему это должно валить проверку).

Итоги

ТемаКратко
СтроениеCertificate = tbsCertificate + signatureAlgorithm + signatureValue (подпись над оригиналом TBS)
Разбор за шагxrtX509Parse(DER, 长度, &视图); нуль выделений и освобождений; структурная проверка встроена
Поля представленияRaw/Tbs/Version/Serial/Issuer/Subject/NotBefore/NotAfter/SPKI/Extensions/Signature
Версиянатуральный номер (v1=1 умолчание/v2=2/v3=3); расширения только в v3
Срок действияxtime замкнутый интервал; xrtX509ValidAt(&cert, xrtNow()) одним шагом; просрочка ≠ ошибка
Serialбайты содержимого как есть (знаковый бит и сверхдлина сохранены); ограничения CA отдельно
Курсор NameNameInit/Read по атрибутам RDN; равенство побайтово по DER; идентичность не читает CN
Публичный ключxrtX509PublicKey → перечисление типа + материал; тип независимо от влитых бэкендов
Перевод алгоритмовAlgorithmParse+SignatureParse → нормализованные параметры (type/hash/mgf/соль/trailer)
Расширениякурсорный обход; SAN/BasicConstraints/NameConstraints/CRL-расширения — специализированные входы
Время жизни представленийпривязано к входному DER-буферу; при инкапсуляции буфер и представления живут вместе

📖 Справочники API в этой главе:x509 (140 записей)asn1 (35 записей)pem (11 записей)

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