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

Начинающиеся здесь четыре главы занимаются «структурами данных доверия»: как сертификат кодируется (эта глава), как разбирается в представления (глава 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-элемент всегда из трёх частей:

1Тип (Tag)класс (UNIVERSAL/контекст...) + бит constructed + номер ярлыка, от 1 до нескольких байтов
2Длина (Length)число байтов содержимого; канон требует кратчайшей записи - 0x05 это 5, нельзя 0x00 0x05
3Значение (Value)байты содержимого; у constructed-типов (SEQUENCE/SET) значение - вновь вложенная последовательность TLV

Например 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-документа читаются два целых; миниатюрная модель разбора сертификата:

examples/asn1/der/main.c源码 ↗























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/asn1/der/main.c -lws2_32 -liphlpapi7 + 42 = 49

Что только что произошло. (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-текст и без потерь восстанавливается:

examples/asn1/pem/main.c源码 ↗

















 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/asn1/pem/main.c -lws2_32 -liphlpapi-----BEGIN XRT DATA-----AQIDBAU=-----END XRT DATA-----

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

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

Ловушка 1: разбор недоверенного DER без Validate:Ловушка 1: разбор недоверенного DER без Validate

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

Причина: курсор ленив — Init не разбирает ни байта, Read проверяет слой, до которого дошёл. Для ввода с неизвестной структурой «читать и натыкаться на незаконное» значит «половина состояния уже опубликована».

错误写法
xrtDerInit(&Root, pUntrusted, iSize);   /* сразу начинать */
xrtDerRead(&Root, &Value);               /* падение только в середине */
正确写法
if ( !xrtDerValidate(pUntrusted, iSize) ) {
	return false;   /* входная проверка целиком: плохая структура останавливается здесь */
}
xrtDerInit(&Root, pUntrusted, iSize);
Ловушка 2: прочитать и не утвердить Done — проглотить хвостовой мусор:Ловушка 2: прочитать и не утвердить Done — проглотить хвостовой мусор

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

Причина: Read, вернув XDER_DONE, говорит лишь «курсор дошёл до своей границы»; если длина внешнего контейнера не совпадает с фактическим содержимым или в хвосте ввода лишние данные, без утверждения это просачивается.

错误写法
while ( xrtDerRead(&Items, &Value) == XDER_VALUE ) {
	handle(&Value);   /* цикл кончился и всё: хвостовой мусор никто не спрашивает */
}
正确写法
while ( xrtDerRead(&Items, &Value) == XDER_VALUE ) {
	handle(&Value);
}
if ( !xrtDerDone(&Items) ) {
	return false;   /* потреблено ровно - лишний байт уже незаконен */
}
Ловушка 3: PEM-ярлык по наитию — регистр или пробелы не совпали:Ловушка 3: PEM-ярлык по наитию — регистр или пробелы не совпали

Симптом: 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;   /* явная неудача лучше молчаливого не-того блока */
}

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

Базовое: ручная распаковка DER-документа

Шестнадцатеричные 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 против BERDER — единственное кодирование значения; курсор в строгом режиме отвергает неограниченную длину/не-кратчайшее/переполненный ярлык
Шесть входов курсора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 символа; пояснительный текст вокруг блока допустим
Три входа PEMFind (заимствованное представление, нуль выделений) / DecodeNew (владеющие байты) / EncodeNew (владеющий текст)
Совместимость переводовLF / CRLF / CR равнозначны; вложенность и отсутствующая граница — отказ
ОбрезкаASN1 и PEM (с зависимостью Base64) независимо; друг друга не утаскивают

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

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