Ориентация главы
Глава 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概念
Строение сертификата: подпись, оборачивающая заявление
Отношение трёх составных частей — ядро главы 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 — разбор встроенного минимального сертификата с чтением версии, атрибутов субъекта и типа публичного ключа:
#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; }
Что только что произошло. (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:
#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; }
Что только что произошло. (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).
Типичные ошибки
Симптом: разбор успешен, поля прочитаны — а чуть позже доступ к представлению падает или читает мусор; типично в обёртке «декодировать 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) { оба живут и умирают вместе, вызывающий освобождает/держит их разом */ }
Симптом: в 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 */ }
Симптом: 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: кроме версии/субъекта/ключа напечатайте первый 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 отдельно |
| Курсор Name | NameInit/Read по атрибутам RDN; равенство побайтово по DER; идентичность не читает CN |
| Публичный ключ | xrtX509PublicKey → перечисление типа + материал; тип независимо от влитых бэкендов |
| Перевод алгоритмов | AlgorithmParse+SignatureParse → нормализованные параметры (type/hash/mgf/соль/trailer) |
| Расширения | курсорный обход; SAN/BasicConstraints/NameConstraints/CRL-расширения — специализированные входы |
| Время жизни представлений | привязано к входному DER-буферу; при инкапсуляции буфер и представления живут вместе |
📖 Справочники API в этой главе:x509 (140 записей)asn1 (35 записей)pem (11 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки