Ориентация главы
Начинающиеся здесь четыре главы занимаются «структурами данных доверия»: как сертификат кодируется (эта глава), как разбирается в представления (глава 79), как проверяется подпись (глава 80), как сцепляется в цепочку доверия (глава 81). Байтовый фундамент всего этого — DER, канонический формат кодирования семейства ASN.1: сертификаты X.509, большинство структурированных полей сообщений TLS, файлы ключей — всё DER. Форма DER едина — тройка TLV (тип, длина, значение); усвоив одну модель «курсор читает слой за слоем», вы получаете ключ к разбору всей сертификатной экосистемы. PEM — текстовая одежда DER: блочный Base64 вида -----BEGIN CERTIFICATE-----, позволяющий двоичности безопасно путешествовать в почте, конфигурационных файлах и переменных окружения. Оба модуля — заимствующие с нулём выделений: ни одного скопированного байта, только представления.
78.1引入
От коллеги приходит файл .pem — «сервисный сертификат». Внутри несколько кусков Base64, начинающихся с -----BEGIN CERTIFICATE----- — как превратить это в то, чем пользуется программа? И наоборот: ваш TLS-сервис должен положить ECDSA-приватный ключ в центр конфигураций, а тот принимает только текст. Оба направления — это кодирование-декодирование PEM.
Глубже: какие байты выходят из Base64 PEM-блока? Это DER-кодированный сертификат. Вы хотите прочитать «кто выдал, до какого дня действует, какой алгоритм у публичного ключа» — придётся разбирать DER. Правила DER записаны в стандартах ASN.1, но инженерно вам нужна одна модель: каждый элемент — тройка «тип-длина-значение», тип решает структуру значения (целое, строка, OID или «constructed-тип, оборачивающий новые элементы»). Разворачивая рекурсивно, прочтёте любой сертификат. Модуль asn1 у XRT сделал эту модель нуль-выделительным курсором — разборщик сертификатов главы 79 и разбор сообщений TLS главы 82 целиком стоят на нём.
78.2概念
TLV: атомарная форма всякого DER-документа
DER-элемент всегда из трёх частей:
Например SEQUENCE с двумя INTEGER: 30 06 02 01 07 02 01 2A — 30 это «UNIVERSAL constructed SEQUENCE», 06 — длина содержимого 6 байтов, далее два 02 01 xx — INTEGER с однобайтовым содержимым. Рекурсивность — это весь DER: сертификат есть большой SEQUENCE с вложенными Issuer, Subject, публичным ключом, расширениями… на каждом слое те же TLV.
«Строгость» DER: почему нельзя разбирать вольно
У ASN.1 есть более вольный брат BER (базовые правила кодирования): он разрешает одни и те же данные кодировать по-разному — ведущие нули у длинных длин, не-кратчайшие целые, неограниченная длина. Вольный формат недружелюбен к передаче (один сертификат может иметь разные байтовые последовательности), поэтому DER ( distinguished правила) предписывает: у каждого значения единственное кодирование. Каждый шаг курсора XRT проходит по строгому режиму DER: отвергаются неограниченная длина и обрыв BER, не-кратчайшая длина, не-кратчайший или переполненный старший ярлык, ошибочная primitive/constructed-форма ходовых типов, не-канонические BOOLEAN/INTEGER/BIT STRING/NULL/OID. Курсорный режим годится высокоскоростным путям с «известной структурой протокола»; для недоверенного самостоятельного DER-документа (скачанный сертификат, файл ключа) вход begins с xrtDerValidate — одноразовой проверки целиком, исключающей «обнаружение выхода за границы на середине чтения».
Модель курсора: нуль выделений, нуль копий, трёхзначный возврат
Четыре объекта-значения составляют всё состояние разбора: xasn1tag (класс + бит constructed + 32-битный номер ярлыка), xdervalue (тип + байтовый диапазон-представление; Raw содержит полный TLV, Value — только содержимое; оба заимствуют ввод), xdercursor (ввод + границы + смещение следующего; копируется по значению и обходит независимо), xderresult (три состояния). Шесть центральных входов:
xrtDerInit: привязывает ввод и строит корневой курсор — запоминает только указатель и длину, не разбирая ни байта.xrtDerRead: берёт следующий элемент по порядку с трёхзначным возвратом —XDER_VALUE(взято),XDER_DONE(нормальный конец, не ошибка),XDER_ERROR(структура незаконна, ставит поточную ошибку).xrtDerExpect: утверждает «следующий обязан быть такого-то типа» и берёт значение — пути протокола с фиксированными ярлыками идут через него (на одну ветвь меньше, чем «прочитал, потом судил тип»).xrtDerEnter: входит внутрь SEQUENCE/SET/явного ярлыка и получает дочерний курсор — без копирования содержимого, слой за слоем.xrtDerDone: утверждает, что курсор потребил ровно всё — один лишний байт уже незаконная структура (защита от хвостового мусора).xrtDerIs: подсматривает без продвижения — разветвление для CHOICE и OPTIONAL-полей.
Два железных правила: неудачный вызов — пустая операция (Read/Expect продвигают курсор и публикуют выходы только при успехе); весь путь с нулём выделений и копий (конвертация значений запускается слоем типовых помощников по надобности).
Слой типовых помощников: последний шаг от представления к числу
xrtDerUInt64/Int64 (представление INTEGER → целое), xrtDerUnsigned, xrtDerBoolean, xrtDerOctets, xrtDerBitString, xrtDerOid (идентификатор объекта) и xrtDerOidEqual (сравнение OID) — курсор даёт «байтовый диапазон»; превращать ли в число и в какое, решает вызывающий. Этот дизайн даёт путям, которым нужна только структура без чисел (обход отпечатка сертификата), нулевую стоимость конвертации.
PEM: блочный протокол текстовой одежды
PEM упаковывает произвольные байты в размеченные текстовые блоки:
-----BEGIN CERTIFICATE-----
MIIB...(64 字符一行的 Base64)
-----END CERTIFICATE-----xrtPemFind(文本, 长度, 标签, 块) (текст, длина, ярлык, блок) локализует в тексте следующий блок с указанным ярлыком — до и после блока допустим пояснительный текст (комментарии в файлах цепочек сертификатов — обычное дело); LF/CRLF/CR все три перевода строки распознаются; начальный и конечный ярлыки обязаны совпадать точно; вложенные границы, отсутствующая конечная граница и незаконный ярлык отвергаются. Возвращённый xpemblock содержит только заимствованные представления (Label/Body/Raw указывают в исходный текст). xrtPemDecodeNew(块, &长度) (блок, &длина) декодирует Base64 тела в владеющий байтовый буфер (xrtFree для освобождения) — вот этот шаг и выделяет; xrtPemEncodeNew(标签, 字节, 长度) (ярлык, байты, длина) обратное — порождает канонический PEM-текст (перевод строки каждые 64 символа). Несколько блоков в файле (приватный ключ + цепочка сертификатов): цикл Find по тому же тексту — курсорная семантика, та же, что у DER.
78.3示例
Первая полная программа: курсор читает DER SEQUENCE
Программа ниже из examples/asn1/der/main.c — из 8-байтового минимального DER-документа читаются два целых; миниатюрная модель разбора сертификата:
*/ #include <stdio.h> #include <xrt.h> int main(void) { */ static const uint8 Document[] = { 0x30, 0x06, 0x02, 0x01, 0x07, 0x02, 0x01, 0x2A }; xdercursor Root; /* 根游标:指向整个文档 */ xdercursor Items; /* 子游标:SEQUENCE 内部 */ xdervalue Value; /* 当前元素的值视图(类型 + 范围) */ uint64 iLeft; uint64 iRight; */ if ( !xrtDerValidate(Document, sizeof(Document)) || !xrtDerInit(&Root, Document, sizeof(Document)) || !xrtDerExpect( &Root, XASN1_UNIVERSAL, (uint32)XASN1_SEQUENCE, true, &Value ) || !xrtDerEnter(&Value, &Items) || /* 依次读取两个 INTEGER 并转换为 uint64(7 与 42)。 */ (xrtDerRead(&Items, &Value) != XDER_VALUE) || !xrtDerUInt64(&Value, &iLeft) || (xrtDerRead(&Items, &Value) != XDER_VALUE) || !xrtDerUInt64(&Value, &iRight) || /* Done 断言恰好消费完:多一个字节都算结构非法。 */ !xrtDerDone(&Items) ) { return 1; } printf("%llu + %llu = %llu\n", (unsigned long long)iLeft, (unsigned long long)iRight, (unsigned long long)(iLeft + iRight)); return 0; }
Что только что произошло. (1) xrtDerValidate сперва проверяет целиком — префиксы длин и вложенные границы одним проходом; для недоверенного ввода это входное действие: при плохой структуре дальше попросту не начнётся. (2) Expect утверждает, что корневой элемент — (UNIVERSAL, SEQUENCE, 构造式): три параметра полностью задают типовой сегмент TLV; затем Enter входит внутрь и получает дочерний курсор, не скопировав из исходного документа ни байта. (3) Два Read берут по xdervalue (тип + представление диапазона), xrtDerUInt64 конвертирует по надобности — 7 и 42. (4) Done утверждает ровно-полное потребление: лишний 0x00 в хвосте ввода обрушит его — «защита от хвостового мусора» — линия безопасности протокольного разбора (разбор сертификатов и сообщений TLS ниже заканчивается именно Done). Любое звено при неудаче идёт в одну и ту же ветку ошибки — цепочечная связка разбора и есть рекомендуемая запись главы.
Вторая полная программа: круговорот PEM — кодирование, поиск, декодирование
Вторая программа из examples/asn1/pem/main.c — 5-байтовая нагрузка упаковывается в PEM-текст и без потерь восстанавливается:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { /* 待封装的 5 字节载荷:01 02 03 04 05(Base64 后为 "AQIDBAU=")。 */ static const uint8 Data[] = { 1, 2, 3, 4, 5 }; xpemblock Block; /* 借用视图:命中的 PEM 块范围 */ bytes pDecoded; /* 拥有式:解码后的字节 */ size_t iDecodedSize; str sText; /* 拥有式:编码后的 PEM 文本 */ /* 编码:标签自定义(证书场景常见 "CERTIFICATE"/"PRIVATE KEY")。 */ sText = xrtPemEncodeNew("XRT DATA", Data, sizeof(Data)); if ( (sText == NULL) || !xrtPemFind(sText, strlen(sText), "XRT DATA", &Block) ) { xrtFree(sText); return 1; } */ pDecoded = xrtPemDecodeNew(&Block, &iDecodedSize); if ( (pDecoded == NULL) || (iDecodedSize != sizeof(Data)) || (memcmp(pDecoded, Data, sizeof(Data)) != 0) ) { xrtFree(pDecoded); xrtFree(sText); return 1; } /* 打印 PEM 文本本身(含结尾换行,故不补 \n)。 */ printf("%s", sText); /* 两个拥有式资源逐一释放。 */ xrtFree(pDecoded); xrtFree(sText); return 0; }
Что только что произошло. (1) xrtPemEncodeNew("XRT DATA", ...) с собственным ярлыком порождает полный PEM-текст (с завершающим переводом строки) — в сертификатной сцене смените ярлык на CERTIFICATE или PRIVATE KEY, и это стандартный файл. (2) xrtPemFind локализует блок с этим ярлыком в сгенерированном тексте — заметьте: он находит заимствованное представление без выделений; в реальной сцене цикл Find идёт по целому куску текста, прочитанному с диска, блок за блоком обрабатывая цепочку сертификатов. (3) xrtPemDecodeNew декодирует Base64 во владеющие байты; тройная проверка (не пусто, длина, побайтовый memcmp) доказывает безlossность круговорота. (4) Два владеющих ресурса (текст и байты) освобождаются xrtFree по одному — Encode/Decode единственные две точки выделения; Find и DER-курсор на всём пути с нулём выделений.
78.4契约
- Курсорная семантика: неудачный вызов — пустая операция и для курсора, и для выходов;
Read/Expectпродвигают только при успехе;XDER_DONE— нормальный конец без ошибки. - Строгий DER: каждое чтение отвергает неограниченную длину, не-кратчайшую запись, переполненный ярлык, ошибочную constructed-форму, не-канонические базовые типы; недоверенный самостоятельный документ сперва через
Validate. - Заимствованный жизненный цикл:
xdervalue/xpemblockвсё заимствуют ввод; память ввода обязана жить, пока все представления не израсходованы (то же правило, что у буферных представлений главы 15). - Разделение конвертации значений: представление и конвертация — два шага (
Readдаёт представление,UInt64и родня конвертируют по надобности) — путям без чисел нулевая стоимость. - Строгие границы PEM: начальный/конечный ярлык — точное совпадение; вложенность, отсутствующая граница, незаконный ярлык — отказ; пояснительный текст вокруг блока допустим; три перевода строки равнозначны.
- Владение:
PemEncodeNew/PemDecodeNewвозвращают владеющее (xrtFree);PemFind/семейство курсора — нуль выделений. - Потоки: все объекты-значения без разделяемого изменяемого состояния; курсоры/представления каждого потока конкурентны произвольно.
- Обрезка:
XRT_MODULE_ASN1_DER(курсор DER) иXRT_MODULE_PEM(зависит от Base64-кодека) обрезаются независимо; PEM не утаскивает DER и наоборот.
Типичные ошибки
Симптом: разбор внешнего сертификата падает с «длина вне границы» только в средней части — несколько слоёв представлений уже опубликованы, и обработке ошибки приходится откатывать кучу состояния; хуже — принять вольный BER за DER, и две машины разбирают один файл по-разному.
Причина: курсор ленив — Init не разбирает ни байта, Read проверяет слой, до которого дошёл. Для ввода с неизвестной структурой «читать и натыкаться на незаконное» значит «половина состояния уже опубликована».
xrtDerInit(&Root, pUntrusted, iSize); /* сразу начинать */ xrtDerRead(&Root, &Value); /* падение только в середине */
if ( !xrtDerValidate(pUntrusted, iSize) ) { return false; /* входная проверка целиком: плохая структура останавливается здесь */ } xrtDerInit(&Root, pUntrusted, iSize);
Симптом: разбор протокола «успешен», но партнёр приложил за законной структурой произвольные байты — следующая позиция разбора сбита, либо в систему внедрены неаудированные данные.
Причина: Read, вернув XDER_DONE, говорит лишь «курсор дошёл до своей границы»; если длина внешнего контейнера не совпадает с фактическим содержимым или в хвосте ввода лишние данные, без утверждения это просачивается.
while ( xrtDerRead(&Items, &Value) == XDER_VALUE ) { handle(&Value); /* цикл кончился и всё: хвостовой мусор никто не спрашивает */ }
while ( xrtDerRead(&Items, &Value) == XDER_VALUE ) { handle(&Value); } if ( !xrtDerDone(&Items) ) { return false; /* потреблено ровно - лишний байт уже незаконен */ }
Симптом: PemFind не находит заведомо существующий блок — ярлык в файле TRUSTED CERTIFICATE или с лишними пробелами по краям, а искали CERTIFICATE.
Причина: совпадение границ PEM точное — -----BEGIN CERTIFICATE----- и -----BEGIN TRUSTED CERTIFICATE----- два разных блока (исторически сосуществуют и другие варианты ярлыков). Вольное «содержится — значит найдено» примет блок приватного ключа за блок сертификата.
/* ярлык по памяти, без сверки с фактическими границами файла */ xrtPemFind(Text, iSize, "CERTIFICATE", &Block); /* в файле на деле -----BEGIN TRUSTED CERTIFICATE-----: не найдётся, либо не тот блок */
/* искать по фактическому ярлыку файла; в многоярлыковом файле пробовать по одному или обойти все блоки */ if ( !xrtPemFind(Text, iSize, "CERTIFICATE", &Block) && !xrtPemFind(Text, iSize, "TRUSTED CERTIFICATE", &Block) ) { return false; /* явная неудача лучше молчаливого не-того блока */ }
Практические задания
Шестнадцатеричные 30 09 02 01 0A 02 04 00 A1 B2 C3 — сперва выведите структуру вручную (SEQUENCE с двумя INTEGER), затем курсорной программой прочтите и напечатайте оба значения (10 и 0xA1B2C3). Затем нарочно замените средний 04 на 05 (поломка длины) и проверьте отказ Validate. Критерии приёмки: ручная выкладка совпадает с выводом программы; опыт с поломкой возвращает неудачу.
Прочитайте PEM-файл с несколькими сертификатами (можно сгенерировать OpenSSL); циклом PemFind + PemDecodeNew декодируйте каждый блок в DER-байты и печатайте построчно «блок N: X байтов, DER-проверка пройдена». Критерии приёмки: файл-цепочка из трёх сертификатов даёт три строки; Validate проходит для каждого блока; декодированные буферы освобождаются по одному без утечек (статистика главы 6).
Реализуйте dump(xdercursor* Cursor, int iDepth): рекурсивно печатать дерево структуры произвольного DER-документа — constructed-типы раскрывать отступом, базовые печатать с типом и длиной (INTEGER заодно со значением). Распечатайте им встроенный сертификат образца inspect главы 79 и сверьте послойно со схемой Certificate из RFC 5280 (tbsCertificate/subjectPublicKeyInfo/extensions). Критерии приёмки: полное раскрытие до листьев; на вольном BER-вводе (рукотворная не-кратчайшая длина) — ошибочный выход, а не падение.
Итоги
| Тема | Кратко |
|---|---|
| Тройка TLV | тип (класс + бит constructed + номер) + длина (кратчайшая запись) + значение (constructed-типы вложенными TLV) |
| DER против BER | DER — единственное кодирование значения; курсор в строгом режиме отвергает неограниченную длину/не-кратчайшее/переполненный ярлык |
| Шесть входов курсора | Init (привязка) / Read (три состояния) / Expect (утверждение типа) / Enter (войти в constructed) / Done (ровно всё прочитано) / Is (подсмотреть) |
| Три состояния | XDER_VALUE взято / XDER_DONE нормальный конец (не ошибка) / XDER_ERROR структура незаконна |
| Неудача — пустая операция | Read/Expect при неудаче не продвигают курсор и не публикуют выходы |
| Разделение конвертации | представление → значение в два шага: UInt64/Int64/Boolean/Oid по надобности |
| Входная проверка | недоверенный самостоятельный DER сперва Validate целиком; курсорный режим — для известной структуры |
| Строение PEM | точные ярлыки BEGIN/END + Base64 с переводом каждые 64 символа; пояснительный текст вокруг блока допустим |
| Три входа PEM | Find (заимствованное представление, нуль выделений) / DecodeNew (владеющие байты) / EncodeNew (владеющий текст) |
| Совместимость переводов | LF / CRLF / CR равнозначны; вложенность и отсутствующая граница — отказ |
| Обрезка | ASN1 и PEM (с зависимостью Base64) независимо; друг друга не утаскивают |
📖 Справочники API в этой главе:asn1 (35 записей)pem (11 записей)codec (26 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки