Ориентация главы
Глава 80 закончилась на «проверка подписи прошла ≠ доверенно» — эта глава добавляет недостающий третий вопрос. Три компонента выходят по порядку: путь сертификации (эстафета подписей «листовой сертификат → промежуточный CA → якорь доверия»; xrtX509PathValidate строго проверяет каждое звено, xrtX509PathBuild автоматически находит цепочку из неупорядоченных кандидатов); хранилище доверия (владеющее множество якорей — три формы загрузки: явное добавление, PEM-файл, системные корни; семейство xrtX509StoreCreate/Add/AddPem/AddSystem); проверка отзыва по CRL (двухслойный дизайн: xrtX509CrlValidate проверяет раз — xrtX509CrlCheck легко спрашивает N раз). Вместе три части составляют полную клиентскую проверку PKI — проверка сертификатов TLS-клиентом главы 82 и есть прямая комбинация этих компонентов. Глава заодно прочерчивает то, чем этот слой не занимается: сетевую выборку, кеширование, политику мягкой неудачи — это верх; API политики сохраняет однозначный результат, оставляя выбор правил выборки и отказа вам.
81.1引入
Вернитесь к атакующему главы 80: он самоподписывает сертификат, CN/SAN несут целевой домен, подпись собственной парой — проверка подписи проходит, идентичность проходит. Ему не хватает ровно одного: его публичного ключа нет в ваших якорях доверия. Якорь (anchor) — корневой сертификат, заранее признанный и локально сохранённый; ОС и браузеры предустанавливают больше сотни корней CA, а интранет вашей компании мог добавить и свой. Суть проверки: можно ли от сертификата перед глазами,вдоль линии «кто чью подпись ставил», дойти до какого-то локального якоря.
Цепочки реального мира почти всегда с промежуточными слоями: 网站证书 ← 中间CA ← 根证书 (сертификат сайта ← промежуточный CA ← корень). При рукопожатии TLS партнёр присылает лист и промежуточные вместе (порядок не гарантирован); корень не присылает — корень у вас локально. Итак, полный алгоритм проверки: разобрать все сертификаты → от листа автоматически построить цепочку до некоторого якоря → проверить подпись и ограничения каждого звена (действительно ли этот промежуточный CA вправе подписывать сертификаты? не превышен ли потолок глубины пути? разрешают ли ограничения имён?) → всё пройдено — доверяемо. И этого мало: всесторонне законный сертификат мог быть отозван издателем (приватный ключ утёк, домен перепродан) — последний вопрос берёт CRL (список отозванных) издателя и ищет серийный номер. Три образца главы соответствуют трём шагам «построить и проверить цепь, загрузить якоря, спросить отзыв».
81.2概念
Путь сертификации: эстафета подписей и её правила
Путь сертификации — массив сертификатов «цель впереди, промежуточные CA приближаются к якорю». Якорь доверия — вход проверки, в путь не входит — xrtX509Anchor(&根证书, &锚) извлекает из доверенного сертификата Subject, публичный ключ и опциональные NameConstraints; повторное появление того же якорного сертификата в пути отвергается (защита от «сунуть присланный партнёром корень в путь под видом якоря»).
xrtX509PathValidate(路径, 数量, &锚, &配置) проверяет каждое звено в точно заданное конфигурацией время; список отказов: повторные сертификаты, разрыв цепочки издателей, провал подписи, необработанные critical-расширения; промежуточный CA обязан иметь непустой Subject и critical BasicConstraints(cA=TRUE); при наличии KeyUsage тот обязан разрешать keyCertSign, а при pathLenConstraint KeyUsage обязателен явно; NameConstraints пересекаются послойно и применяются к цели и ко всем не-self-issued промежуточным под ней; ExtendedKeyUsage промежуточного CA, если присутствует, обязан разрешать запрошенное назначение. Время проверки задаётся вызывающим явно (config.Time) — тесты воспроизводимы, кеш определим, повтор проверяем; в этом размежевание с дизайном «неявно взять текущее время». X509_PATH_REQUIRE_KEY_USAGE/PURPOSE могут потребовать у целевого сертификата явного объявления назначения.
PathBuild: автоматический поиск цепочки из кучи сертификатов
xrtX509PathValidate требует уже выстроенного пути; xrtX509PathBuild(目标, &候选源, &配置, 输出路径, 容量, &结果) принимает неупорядоченных кандидатов и ищет автоматически: на каждом уровне сперва пытается напрямую завершиться в якорь, затем по порядку кандидатов фильтрует Issuer/Subject и AKI/SKI (xrtX509IssuerMatch), детерминированный поиск в глубину с возвратом; найденный полный путь всегда отдаётся тому же строгому PathValidate — у построителя цепи нет люфта послаблений. Выходной путь никогда не содержит якорного сертификата; нехватка ёмкости возвращает XERR_RANGE, отсутствие пути XERR_NOT_FOUND, причина неудачи сохраняется в цепочке X509_ERROR_PATH_BUILD. При ёмкости ≤16 поиск без кучевых выделений. Это стандартная поза TLS-клиента после получения цепочки партнёра.
Хранилище доверия: дом якорей
xx509store — владеющее множество якорей доверия; три формы загрузки комбинируются по надобности:
- По одной:
xrtX509StoreAdd(库, DER, 长度)копирует и строго разбирает — в хранилище хранится ровно один DER на запись, представления заимствуют его; полностью идентичный повторный сертификат возвращаетX509_DONE(идемпотентно). - PEM-текст:
xrtX509StoreAddPemобходит все блоки текста и импортирует только блоки с ярлыкомCERTIFICATE— транзакционно: неудача любого блока откатывает всё, добавленное в этот раз. - Файл:
xrtX509StoreAddFile(отдельная обрезка) автоматически распознаёт одиночный DER или много-блочный PEM. - Системные корни:
xrtX509StoreSystem()одним движением даёт независимый снимок — Windows перечисляет логическое хранилище ROOT, macOS читает keychain, Unix читаетSSL_CERT_FILE/DIRили ходовой bundle. Снимок независим от системы (последующие системные изменения не трогают построенное хранилище), настроек платформы не меняет, частичные результаты молча не принимает (неудача перечисления — откат целиком). Пр default-база доверия HTTPS-клиента и есть он.
xrtX509StoreSource(库, 外部中间证书数组, 数量, &候选源) комбинирует «якоря внутри хранилища + присланные партнёром промежуточные» во вход PathBuild — хранилище источник якорей, материал рукопожатия кандидат, каждый на своём месте. Одно важное правило инвалидации: продолжение записи может сдвинуть внутренние структуры, обнуляя существующие указатели и уже построенный xx509pathsource — загрузили всё, потом строите источник; построив источник, больше не пишете. Само хранилище не решает за вас «стоит ли доверять»: явное добавление и есть присвоение статуса якоря.
CRL: проверить раз, спросить N раз
Проверка отзыва двухслойна — под две частоты употребления. Слой тяжёлой проверки xrtX509CrlValidate(CRL, 签发者证书, 配置, &有效视图): проверяет подпись CRL, совпадение Issuer, окно времени, назначение cRLSign издателя, AKI, CRL Number, unknown critical — выдаёт заимствованное представление xx509crlvalid (CRL и издатель обязаны оставаться живы). Слой лёгких запросов xrtX509CrlCheck(&有效视图, 证书, &结论): лишь спрашивает, числится ли серийный номер в списке отзыва — «проверить раз, спрашивать многократно» и есть его проектная сцена (CRL одного CA проверили раз — опрашиваем целую пачку сертификатов). Для одноразового пути есть комбинированный вход xrtX509CrlStatus.
Три состояния запроса надо понимать точно: X509_ERROR — ошибка входа/протокола; X509_DONE — этот CRL неприменим к сертификату (или delta-CRL без новых записей) — это не «не отозван»; X509_VALUE единственный публикует вердикт (X509_REVOCATION_GOOD/REVOKED и др.). Есть тонкий подводный камень: у reason-segmented CRL GOOD доказывает лишь неотозванность причин, покрытых CoveredReasons — несколько сегментированных CRL следует агрегировать поочерёдно xrtX509RevocationUpdate; окончательный вердикт есть лишь при полном покрытии множества или подтверждении отзыва любым фрагментом. Комбинация complete/delta — через xrtX509CrlSetInit/Check. Чем этот слой не занимается: сетевой выборкой CRL, кешированием, мягкой неудачей (пропускать ли при недоступности CRL) — решения верхнего уровня; API политики сохраняет однозначный результат именно затем, чтобы право выбора осталось за вами.
Сборка: форма полной клиентской проверки
Глава 82 покажет TLS-клиента, инкапсулировавшего эти шаги в конфигурацию контекста — но поведенческая граница каждого шага определена именно здесь.
81.3示例
Первая полная программа: автоматическое построение цепочки из неупорядоченных кандидатов
Программа ниже из examples/x509/path_build/main.c — три сертификата (лист/промежуточный/корень) разбираются, и из «кучи кандидатов + одного якоря» автоматически строится и проверяется путь:
#include <xrt.h> #include "../../../tests/fixtures/x509_path_legacy.h" #include <stdio.h> #include <string.h> */ /* 从无序候选证书中构建并验证一条到独立信任锚的认证路径。 */ int main(void) { xx509cert Leaf; xx509cert Intermediate; xx509cert Root; xx509anchor Anchor; xx509pathsource Source; xx509pathconfig Config; xx509pathresult Result; const xx509cert* Issuers[2]; const xx509cert* Path[3]; if ( !xrtX509Parse( X509_PATH_LEAF, sizeof(X509_PATH_LEAF), &Leaf ) || !xrtX509Parse( X509_PATH_INTERMEDIATE, sizeof(X509_PATH_INTERMEDIATE), &Intermediate ) || !xrtX509Parse( X509_PATH_ROOT, sizeof(X509_PATH_ROOT), &Root ) || !xrtX509Anchor(&Root, &Anchor) ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); return 1; } memset(&Config, 0, sizeof(Config)); Config.Time = Leaf.NotBefore; Issuers[0] = &Root; Issuers[1] = &Intermediate; Source.Issuers = Issuers; Source.IssuerCount = 2u; Source.Anchors = &Anchor; Source.AnchorCount = 1u; if ( !xrtX509PathBuild( &Leaf, &Source, &Config, Path, 3u, &Result ) ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); return 1; } printf("certificate path contains %zu certificate(s)\n", Result.Count); return 0; }
Что только что произошло. (1) Три сертификата каждый xrtX509Parse в представление; только корень переведён xrtX509Anchor в якорь — якорь происходит из локального суждения (здесь задан вручную); это точка впрыска «доверия». (2) Источник кандидатов xx509pathsource перечисляет и корень, и промежуточный как Issuers, якорь отдельно — заметьте: корень и в кандидатах, и в якорях; Build на каждом уровне сперва пробует завершение в якорь, затем фильтрует кандидатов, роли не смешиваются. (3) PathBuild выводит 2 — путь содержит лист и промежуточный CA, якорь не считается; выходной путь уже прошёл все поуровневые проверки (подпись, время явно задано Config.Time = Leaf.NotBefore, BasicConstraints, KeyUsage). (4) Выходной буфер Path предоставляет вызывающий; ёмкость 3 заодно ограничивает максимальную глубину — при построении цепи из недоверенного материала заниженная ёмкость есть защитная поза (при большом пуле кандидатов это ограничение глубины/ширины). Сверьте разделение труда: образец examples/x509/path демонстрирует прямую форму — вызывающий сам выстраивает цепь и зовёт PathValidate; оба входа делят один строгий верификатор.
Вторая полная программа: загрузка системного хранилища доверия одним движением
Вторая программа из examples/x509/store_system/main.c — одна строка даёт независимый снимок якорей текущей платформы:
#include <xrt.h> #include <stdio.h> */ /* 创建当前平台的独立系统信任库快照。 */ int main(void) { xx509store* pStore = xrtX509StoreSystem(); if ( pStore == NULL ) { return 1; } printf("system anchors=%llu\n", (unsigned long long)xrtX509StoreCount(pStore)); xrtX509StoreFree(pStore); return 0; }
Что только что произошло. (1) xrtX509StoreSystem() возвращает независимый снимок — число якорей меняется с количеством корней платформы (здесь 33; у вас, скорее всего, другое); снимок не пользуется глобальным кешем, и последующие системные добавления/удаления корней не трогают построенное хранилище. (2) После пересчёта StoreFree освобождает — снимок владеющий объект, та же форма «одно выделение, возврат целиком», что у снapshot интерфейсов главы 71. (3) Настоящее употребление — StoreSource, превращающий его в источник построения цепи: default-база доверия HTTPS-клиента и есть комбинация этих двух функций. Хотите сузить поверхность доверия (верить только конкретным CA) — берите StoreCreate + Add/AddFile с явной загрузкой якорей: хранилище не решает за вас, кому верить; добавили — значит доверились.
Третья полная программа: лёгкий путь запроса отзыва
Третья программа из examples/x509/crl_policy/main.c — рукотворно собранное проверенное представление с фокусом на семантике запросов CrlCheck:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 展示复用已验证 CRL 视图查询多张证书的轻量路径。 */ int main(void) { static const uint8 Name[] = { 0x30, 0x0E, 0x31, 0x0C, 0x30, 0x0A, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x03, 0x43, 0x41, 0x31 }; static const uint8 Entries[] = { 0x30, 0x14, 0x30, 0x12, 0x02, 0x01, 0x2A, 0x17, 0x0D, 0x32, 0x36, 0x30, 0x34, 0x30, 0x38, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5A }; static const uint8 Serial[] = { 0x2A }; xx509cert Issuer; xx509cert Certificate; xx509crl Crl; xx509crlvalid Valid; xx509revocation Revocation; xx509result Result; /* 实际程序应由 xrtX509CrlValidate 产生 Valid;这里聚焦查询接口。 */ memset(&Issuer, 0, sizeof(Issuer)); memset(&Crl, 0, sizeof(Crl)); memset(&Valid, 0, sizeof(Valid)); Issuer.Subject = (xbytesview) { Name, sizeof(Name) }; Crl.Issuer = Issuer.Subject; Crl.Version = X509_CRL_VERSION_1; Crl.Revoked = (xbytesview) { Entries, sizeof(Entries) }; Valid.Crl = &Crl; Valid.Issuer = &Issuer; memset(&Certificate, 0, sizeof(Certificate)); Certificate.Version = X509_VERSION_3; Certificate.Issuer = Issuer.Subject; Certificate.Serial = (xbytesview) { Serial, sizeof(Serial) }; Result = xrtX509CrlCheck(&Valid, &Certificate, &Revocation); if ( Result != X509_VALUE ) { return 1; } printf("revocation state: %d\n", (int)Revocation.State); return Revocation.State == X509_REVOCATION_REVOKED ? 0 : 1; }
Что только что произошло. (1) xx509crlvalid в настоящей программе обязан порождаться только xrtX509CrlValidate — здесь он собран руками, чтобы сфокусироваться на интерфейсе запроса (представление CRL с записями отзыва, издатель с Subject). (2) В записях отзыва лежит серийный номер 0x2A; Serial проверяемого сертификата ровно 0x2A, Issuer совпадает — CrlCheck возвращает X509_VALUE и публикует State=1 (X509_REVOCATION_REVOKED). (3) Серийный номер на всём пути — представление байтов произвольной точности (семантика Serial-как-есть главы 79 выровнена на стороне CRL — точное байтовое совпадение, без усекающей конвертации). Парный образец examples/x509/crl демонстрирует CrlParse + курсорный обход записей: у пустого списка Read сразу DONE — «структура целиком пройдена до конца» и «прервано ошибкой» два разных финала, в коде их различайте.
81.4契约
- Семантика якоря: якорь — вход проверки, в путь не входит;
xrtX509Anchorизвлекает Subject/публичный ключ/ограничения; появление якорного сертификата в пути — отказ; сертификаты партнёра всегда только кандидаты, не якоря. - Время проверки:
config.Timeзадаётся явно; проверка исследует каждый сертификат в данный момент — воспроизводимо, кешируемо, проверяемо на повтор. - Правила пути: промежуточному CA нужны непустой Subject + critical BasicConstraints(cA=TRUE); KeyUsage/pathLen/NameConstraints/EKU проверяются по звеньям; длина считает только не-self-issued промежуточные CA; NameConstraints у самой цели отвергаются.
- Границы построителя: не читает файлы, системные хранилища, сеть, AIA и не расширяет кандидатов бесконечно; полный путь обязан пройти PathValidate; ёмкость ≤16 — ноль кучевых выделений;
XERR_NOT_FOUNDнет пути,XERR_RANGEнехватка ёмкости, цепочка причин вX509_ERROR_PATH_BUILD. - Транзакционность хранилища: AddPem/AddFile/AddSystem — неудача любого блока откатывает всё; повторный сертификат идемпотентен (DONE); на каждую запись один DER, представления заимствуют внутрибиблиотечное хранение.
- Правило инвалидации: запись может сдвинуть структуры — сперва вся загрузка, потом указатели/источник; после построения — только чтение, конкурентно допустимо; запись и освобождение не конкурентны с чтением.
- Двухслойность CRL: Validate (тяжёлый) даёт заимствованное представление
xx509crlvalid(CRL и издатель обязаны жить); Check (лёгкий) опрашивает многие; одноразово — CrlStatus; конфигурация строга по умолчанию (требует nextUpdate/CRL Number/AKI/KeyUsage). - Три состояния отзыва: ERROR плохой вход / DONE неприменимо (не «не отозван») / VALUE единственный с вердиктом; GOOD сегментированного CRL ограничен CoveredReasons, агрегатор нескольких —
xrtX509RevocationUpdate. - Принадлежность политики: сетевая выборка, кеширование, мягкая/жёсткая неудача, комбинирование OCSP — верхнему слою; этот слой сохраняет однозначный результат.
- Обрезка: store_file, store_system, crl_policy — независимые макросы возможностей; слой пути — независимые бэкенды RSA/ECDSA/Ed25519.
Типичные ошибки
Симптом: посредник строит себе всю цепь «корень + промежуточный + лист» и шлёт клиенту; клиент берёт вершину цепи за якорь — проверка проходит; доверие целиком назначено партнёром.
Причина: статус якоря доверия приходит только из локальной загрузки (системное хранилище, конфигурационный файл, явный Add). Каждый присланный сертификат — кандидатный материал на проверку, включая тот самоподписанный, что похож на корень. Дизайн PathBuild и разводит их: якорь передаётся отдельно и никогда не входит в путь.
/* последнюю карту материала рукопожатия прямо в якоря */ xrtX509Anchor(&PeerChain[n - 1], &Anchor); xrtX509PathValidate(Path, n, &Anchor, &Config); /* партнёр контролирует содержимое цепи = партнёр контролирует доверие */
/* якорь только из локального хранилища; вся цепь партнёра уходит в Build как кандидаты */ xx509pathsource Source; xrtX509StoreSource(pLocalStore, Issuers, n, &Source); if ( !xrtX509PathBuild(&Leaf, &Source, &Config, Path, 4u, &Result) ) { reject(); /* не дойти до локального якоря: не доверяем */ }
Симптом: сертификат мог быть отозван, но код пропустил его из-за «CrlCheck вернул не ERROR» — DONE (этот CRL неприменим к сертификату) и GOOD (спрошено, не отозван) смешались.
Причина: в трёх состояниях DONE — «вердикта нет»: несовпадение издателя CRL с издателем сертификата, delta-CRL без записей — всё возвращает его. Принять «не нашлось» за «всё в порядке» значит обнулить проверку отзыва.
if ( xrtX509CrlCheck(&Valid, &Cert, &Status) != X509_ERROR ) { accept(); /* DONE тоже вошёл: неприменимость принята за неотозванность */ }
switch ( xrtX509CrlCheck(&Valid, &Cert, &Status) ) { case X509_VALUE: if ( Status.State == X509_REVOCATION_REVOKED ) { reject("revoked"); } else { accept(); /* проверено и не отозвано - единственный пропуск */ } break; default: строгая - отказ, мягкая - записать и пропустить */ handle_no_verdict(); break; }
Симптом: изредка висячие представления или перепутанные результаты построения цепи — сперва StoreSource построил источник, а midway ещё StoreAdd добавил сертификат.
Причина: внутри хранилище — массивное хранение; запись может сдвинуть сертификаты и якорные структуры — существующие указатели представлений и уже построенный xx509pathsource инвалидируются. Это стандартная ловушка комбинации «владеющее множество + заимствованные представления» (то же явление разобрано в главе 18).
xrtX509StoreSource(pStore, Issuers, n, &Source); xrtX509StoreAdd(pStore, NewDer, iSize); /* может сдвинуть внутренние структуры */ xrtX509PathBuild(&Leaf, &Source, ...); /* Source повис */
/* Фаза загрузки: все Add/AddPem/AddFile/AddSystem завершены */ load_all_anchors(pStore); /* Фаза использования: брать указатели, строить источник, строить цепь - после этого не писать */ xrtX509StoreSource(pStore, Issuers, n, &Source); xrtX509PathBuild(&Leaf, &Source, &Config, Path, 4u, &Result);
Практические задания
Постройте OpenSSL собственную цепь «корень → промежуточный → лист» (по одному) и прогоните обе дороги — PathValidate (путь выстроен вами) и PathBuild (неупорядоченные кандидаты). Критерии приёмки: выводы совпадают; нарочно перепутанный порядок кандидатов не мешает Build найти ту же цепь; смена якоря на неустановленный локально корень валит обе дороги.
StoreCreate + AddFile — загрузите только ваш собственный корень и прогоните для листового сертификата полную проверку «построение цепи + MatchHost + ValidAt». Затем сравните результат той же карты под снимком StoreSystem. Критерии приёмки: в суженном хранилище проверка проходит (ваш корень в нём есть); цепь с нев_loaded корнем немедленно валится; вы можете назвать сцены развёртывания, подходящие каждому из двух хранилищ.
Дана группа сертификатов и файл CRL их издателя: CrlValidate один раз, цикл CrlCheck с выводом вердикта по каждой (три класса REVOKED/GOOD/неприменимо); для «неприменимо» доложите причину (несовпадение издателя и пр.). Затем оберните цикл запросов хронометром и сравните траты «CrlStatus с перепроверкой на каждую» и «проверили раз — спрашиваем N раз». Критерии приёмки: три класса сходятся с инструментом openssl crl; разница времён двух режимов объяснима (амортизация стоимости проверки).
Итоги
| Тема | Кратко |
|---|---|
| Пазл трёх вопросов | подпись (79) + идентичность (79) + цепь до локального якоря (эта глава) + отзыв (эта глава) = полная проверка PKI |
| Модель пути | лист → промежуточные → …; якорь — вход проверки, в путь не входит; якорь в пути — отказ |
| PathValidate | цепь выстроена вызывающим: время явно, по звеньям подпись/BasicConstraints/KeyUsage/NameConstraints |
| PathBuild | автоматическое построение из неупорядоченных кандидатов: сперва якорь, фильтры AKI/SKI, возвраты; полный путь обязан пройти строгую проверку |
| Семантика результатов | XERR_NOT_FOUND нет пути / XERR_RANGE нехватка ёмкости / цепочка причин X509_ERROR_PATH_BUILD |
| Хранилище доверия | Add/AddPem/AddFile/AddSystem транзакционно; на запись один DER; явное добавление = присвоение доверия |
| Системный снимок | StoreSystem независим: неудача перечисления платформы откатывает всё; настройки системы не меняются |
| Правило инвалидации | сперва вся загрузка, потом источник; запись инвалидирует указатели/источник; после построения — только чтение, конкурентно |
| Два слоя CRL | Validate проверяет раз (подпись/время/назначение) → crlvalid; Check легко опрашивает N серийных номеров |
| Три состояния отзыва | ERROR плохо / DONE неприменимо (не GOOD) / VALUE есть вердикт; сегментированные CRL — агрегатором |
| Принадлежность политики | выборка/кеш/мягкая неудача — верхнему слою; этот слой сохраняет однозначный результат для ваших решений |
📖 Справочники API в этой главе:x509 (140 записей)crypto (140 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки