Руководство по выживанию — ASN.1

Это руководство по выживанию в ASN.1 (ITU X.680) и DER (Distinguished Encoding Rules, ITU X.690), которые применяются в сертификатах X.509 (SSL) и других технологиях Инфраструктуры открытых ключей (Public Key Infrastructure, PKIX). В сети полно материалов по обеим темам, но, как нам показалось, большинство из них довольно путаные и часто неполные, в том смысле, что нам не удалось найти там ответы на наши конкретные вопросы. Мы не собирались получать учёную степень в ASN.1 или DER, а просто хотели написать кодировщик для интерпретации файлов .der/.p12/.cer, для чего нам нужно было разобраться в сути ASN.1 и методов кодирования. Мы долго блуждали по тупикам (в этом нам нет равных) и собирали знания буквально по крупицам. В результате появился этот документ, поскольку второй раз пройти этот путь у нас уже не хватит сил. Если он покажется Вам полезным — здорово, если же нет —  добавьте его в свой список запутанных и/или неполных материалов по ASN.1.

Этот документ не охватывает все аспекты ASN.1, но он должен позволить Вам разобраться в большинстве определений ASN.1 и декодировать их из DER. В этом документе для иллюстрации ключевых элементов, охватывающих большую часть синтаксиса ASN.1, использованы рабочие примеры определений ASN.1, взятые из RFC 5912 (и других). В них используется стандарт ASN.1 2002 года (X.680 07/2002), свободно распространяемый ITU. Каждый пример затем продемонстрирован в закодированной в DER форме. DER определён в стандарте X.690 (07/2002), опять же, свободно распространяемом ITU. Если Вам необходимо детально разобраться в ASN.1, в частности, для составления модулей и определений ASN.1, существует множество книг и веб-статей различной степени полезности. Лучше всего будет начать с книги Olivier Dubuisson "ASN.1" (издания от 5 июня 2000 г.), или с книги John Larmouth "ASN.1 Complete" (издания 1999 г.). Обе они доступны бесплатно для личного пользования. В книгах используются стандарты, вышедшие до 2002 года, а большинство современных RFC (в том числе RFC 5912) основаны на стандартах 2002 года, поэтому там может не быть части какого-нибудь важного материала.

Плохие новости: Мы, конечно, неудачно пошутили, что использовали только X.680. Вам также может понадобиться прочитать и переварить стандарты X.681, X.682 и X.683. Но если Вы хотите остаться в здравом уме, держитесь подальше от X.691.

Действительно плохие новости: Простого пути для понимания этого материала, к сожалению, не существует. Вам придётся накопить невероятно много знаний, прежде чем над всем этим забрезжит хоть искорка смысла. Так что препояшьте чресла свои, и в путь.

<Бесплатная консультация> ASN.1 — довольно сложный и беспорядочный материал. Сначала нас преследовало чувство, что мы утонем в деталях. Обновление стандарта ASN.1 от 2002 года (используется во всех рабочих примерах, кроме одного) добавлено значительно больше возможностей к и без того сложному синтаксису. В обновлённых RFC (начиная с RFC 5912) в полной мере использованы эти новые возможности для создания более жестких и менее двусмысленных определений, которые так нравятся программам анализаторам/кодировщикам ASN.1. С другой стороны, их гораздо сложнее читать новичкам. В более старых RFC (до RFC 5912) обычно использовались стандарты ASN.1 1988 и 1993 годов (и даже X.208), и они, как правило, значительно легче читаются людьми со скромными интеллектуальными возможностями. Кодировка DER будет идентична независимо от используемого в исходных данных стандарта ASN.1 (1988, 1993 или 2002 годов), для иллюстрации этого в рабочем примере для ContentInfo мы использовали стандарты 1988 и 2002 годов. Ирония в том, что современным читателям могут понравиться исходные данные ASN.1 стандартов до 2002 года, а современные анализаторы/кодировщики ASN.1 могут в них просто задохнуться.</Бесплатная консультация>

Содержание:

  1. Обзор ASN.1
    1. Определения из источника ASN.1
    2. Комментарии ASN.1
    3. ASN.1 с высоты птичьего полёта — синтаксис и терминология
    4. Резюме по синтаксису ASN.1
  2. Рабочие примеры ASN.1
    1. ASN.1 и DER для подмножества X.509
    2. ASN.1 и DER для ContentInfo — ASN.1 версии 1988 года
    3. ASN.1 и DER для расширения сертификата X.509 V3 SubjectAltName
    4. ASN.1 и DER для ContentInfo — ASN.1 версии 2002 года
    5. ASN.1 и DER для сертификата X.509 (полностью)
  3. Зарезервированные/ключевые слова ASN.1
  4. Универсальные типы и теги ASN.1
  5. Обзор DER (схема кодирования TLV)
    1. DER-кодирование типов/тегов
    2. DER-кодирование длины
    3. DER-кодирование универсальных типов
  6. Изменения страницы

Обзор ASN.1

Хотя ASN.1 традиционно ассоциируется с сертификатами X.509 (SSL), он широко используется во всех формах коммуникаций. ASN.1 представляет собой реализацию агностического метода для определения структуры данных, которая может однозначно (недвусмысленно) передаваться между гетерогенными системами с использованием различных схем кодирования (например, BER, CER, DER, PER и других).

Начнём, пожалуй, со знакомства с ASN.1. Вот фрагмент ASN.1 сертификата X.509 из раздела 14 RFC 5912 (PKIX1Implicit-2009):

  --
  -- Начало структуры сертификата
  --

  Certificate  ::=  SIGNED{TBSCertificate}

  TBSCertificate  ::=  SEQUENCE  {
      version         [0]  Version DEFAULT v1,
      serialNumber         CertificateSerialNumber,
      signature            AlgorithmIdentifier{SIGNATURE-ALGORITHM,
                                {SignatureAlgorithms}},
      issuer               Name,
      validity             Validity,
      subject              Name,
      subjectPublicKeyInfo SubjectPublicKeyInfo,
      ... ,
      [[2:               -- При наличии такого расширения, version ДОЛЖНА быть v2
      issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
      subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL
      ]],
      [[3:               -- При наличии такого расширения, version ДОЛЖНА быть v3
      extensions      [3]  Extensions{{CertExtensions}} OPTIONAL
      ]], ... }

  Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }

  CertificateSerialNumber  ::=  INTEGER

  Validity ::= SEQUENCE {
      notBefore      Time,
      notAfter       Time  }

  Time ::= CHOICE {
      utcTime        UTCTime,
      generalTime    GeneralizedTime }

  UniqueIdentifier  ::=  BIT STRING

  SubjectPublicKeyInfo  ::=  SEQUENCE  {
      algorithm            AlgorithmIdentifier{PUBLIC-KEY,
                               {PublicKeyAlgorithms}},
      subjectPublicKey     BIT STRING  }

Он иллюстрирует большинство ключевых свойств ASN.1. На этом этапе не так важно, чтобы Вы разобрались во всём, что здесь написано, но на паре основополагающих вещей стоит остановиться.

Определения из источника ASN.1

Вы всегда можете декодировать любой закодированный в DER поток, не имея доступа к источнику ASN.1, и получить "голые" результирующие данные, в том смысле, что данные индивидуальных типов можно отделить и отобразить в формате, указанном их типом, например, целочисленные значения или строки.

Однако, в большинстве случаев эти результаты будут довольно бессмысленными. Мы можем только гадать, что же означает это декодированное целое число, хотя некоторые интеллектуальные предположения и распознавание мета-шаблонов могут в этом помочь. Такой режим работы, когда декодировщик не имеет доступа к источнику ASN.1, иногда называется безсхемным. Хотя термин "схема" не присущ ASN.1 (ближайшим с точки зрения терминологии ASN.1 понятием будет, скорее всего, коллекция конструкций), он часто используется для ссылки на отрывки или фрагменты из модуля ASN.1, определяющие конкретный объект. Таким образом, если у декодировщика есть доступ к такому фрагменту ASN.1, то часто говорят, что он "знает схему", если нет — то он "безсхемный".

В общем случае, единственный способ проверить достоверность DER (а не просто декодировать) — это иметь доступ к источнику ASN.1 либо в полном объёме, либо к частям/фрагментам, относящимся к декодируемой записи ("знать схему").

Комментарии ASN.1

Поскольку в данном документе используется подробное комментирование в определениях ASN.1 с целью разъяснения содержимого приводимых фрагментов, целесообразно начать с описания формата комментариев. Есть две формы комментариев в ASN.1. Однострочные комментарии начинаются с -- (двух минусов) и могут либо ограничиваться той же последовательностью --, либо продолжаться до окончания строки. Многострочные комментарии используют классическую последовательность в стиле C: /* */. Примеры:

-- это однострочный комментарий, занимающий всю строку
-- это тоже однострочный комментарий, занимающий всю строку (но ограниченный) --
blah -- комментарий, присутствующий в строке с выражением ASN.1
blah -- однострочный комментарий (ограниченный) в середине строки с выражением ASN.1 -- blah
/* многострочный комментарий, начинающийся на этой строке
(широко не используется, но удобно)
и заканчивающийся на этой строке */

Какой значительный шаг вперёд в понимании! Это, конечно, так, но... это лишь один шаг. А их осталось ещё не менее 7 234.

ASN.1 с высоты птичьего полёта — синтаксис и терминология

Как и многие стандарты ITU, ASN.1 начинался с достойной цели достижения максимальной гибкости. В результате, на наш взгляд, такой благородный порыв привел к тому, что некий важный материал похоронен в неком разделе x.y.z, и чтобы до него добраться, придётся прочитать спецификацию целиком (тонны материала), что для таких лентяев, как мы, просто нереально. Мы же хотим взглянуть на стандарт с высоты птичьего полёта, чтобы понять, что же происходит, опускаясь до деталей лишь тогда, когда это действительно необходимо.

Итак, мы начинали данное руководство по выживанию с попытки определить основные структуры и правила ASN.1 (синтаксис, если Вам так больше нравится), используя терминологию, не всегда применяемую в стандартах серии X.680 (или, скорее, лишь иногда используемую в разделах x.y.z), пытаясь сделать её более (на наш взгляд) понятной (обычно в скобках мы приводили и оригинальную терминологию). Может быть, у нас не всё получилось, как хотелось. Судить Вам, поскольку мы то это понимаем (по крайней мере, на момент написания этой статьи у нас было понимание).

Если Вы патологически не любите разнообразный вводный материал, можете сразу переходить к кратким итогам по синтаксису или к рабочим примерам.

Примечание: Для поддержания единообразия мы, в основном, использовали, возможно, не слишком удачный термин элемент, когда речь шла об неких определениях, которые могут состоять из нескольких частей, и термин пункт при указании на каждую из частей такого элемента. Также мы часто использовали термин блок для обозначения материала (ещё один технический термин, который любим использовать) внутри фигурных скобок {}. Ни один из используемых нами терминов не фигурирует в спецификациях ITU. Просто в тот момент, когда мы разбирались с этим материалом, они показались нам хорошей идеей (в большей степени отражающей суть, как мы её понимаем).

Источник ASN.1 определяется в модулях ASN.1. Модули могут импортировать материал из других модулей, поэтому источник ASN.1 может довольно быстро стать по-настоящему запутанным.

ASN.1 очень, подчёркиваем, очень чувствителен к регистру.

Назначения (определения/конструкции)

Ключевой элемент ASN.1 — назначение (assignment) (это можно назвать также определением или конструкцией (production), что является официальным термином ASN.1) с использованием оператора ::= (лексический пункт назначения, определяется в разделах 5.2 и 11.16 стандарта X.680). Итак, всё, что содержит символы ::=, является назначением, и этот термин мы используем повсеместно в этом документе. Если Вам больше нравятся термины определение или конструкция, придётся мысленно подставлять их самостоятельно. Пример назначения из приведённого выше источника ASN.1:

CertificateSerialNumber  ::=  INTEGER

Типы (пользовательские и универсальные)

Почти везде в ASN.1 используется общий термин тип. У всех типов есть Имя типа (TypeName) (в терминологии X.680 это называется указателем типа (typereference)). Имя типа всегда начинается с Заглавной буквы (некоторые из них полностью пишутся ЗАГЛАВНЫМИ БУКВАМИ). В предыдущем примере в левой части выражения указано имя того, что мы называем пользовательским типом (UserType) (поскольку он определяется пользователем). Его имя типа — CertificateSerialName, и ему присваивается один универсальный тип (UniversalType) INTEGER (универсальные типы также можно назвать встроенными типами (BuiltinTypes), если Вам так больше нравится). (Этот тип является пользовательским, поскольку его имя начинается с заглавной буквы, и его нет в списке зарезервированных/ключевых слов). У всех имён типов (универсальных или пользовательских) глобальная область видимости, то есть на них можно ссылаться откуда угодно в модуле ASN.1.

Имена многих универсальных типов полностью состоят из заглавных букв, хотя имена некоторых из них, в частности, строковых типов, составляют исключение, однако все они начинаются с заглавной буквы. Например, VisibleString является универсальным типом, и потому подчиняется основному для имён типов правилу (его имя начинается с заглавной буквы). Исходя из приведённого выше определения, мы можем использовать тип CertificateSerialNumber где угодно в нашем модуле ASN.1 и знать, что ему назначен универсальный тип INTEGER. Полный список универсальных типов.

Как и в большинстве других языков, в ASN.1 есть список зарезервированных/ключевых слов. В случае ASN.1 он включает в себя универсальные типы (имена могут начинаться с заглавной буквы или полностью состоять из заглавных букв), а также дополнительный набор КЛЮЧЕВЫХ СЛОВ, имена которых полностью состоят из заглавных букв.

Примитивы и составные типы

Некоторые универсальные типы просты, состоят из одного пункта и называются примитивами (Primitives) (как, например, INTEGER), а некоторые из них более сложные, могут состоять из одного или нескольких элементов, и называются составными типами (Constructed). Один из самых распространенных составных универсальных типов — SEQUENCE, обычно определяется примерно так (снова фрагмент первоначального примера):

Validity ::= SEQUENCE {

 -- notBefore и notAfter представляют собой элементы в последовательности (SEQUENCE).
 -- Все элементы в SEQUENCE, как правило, ограничены фигурными скобками {}.

      notBefore      Time,
      notAfter       Time  }

-- Примечание: Имя Time в этом примере начинается с заглавной буквы и не присутствует в списке
-- универсальных типов или зарезервированных/ключевых слов, поэтому можно предположить, что это
-- пользовательский тип (позже мы убедимся, что это так).

-- ASN.1 - язык свободного формата, и приведённое выше назначение SEQUENCE
-- может быть записано и так:

Validity ::= SEQUENCE  notBefore Time, notAfter Time

-- что, вероятно, менее ясно, нежели первая версия

Обратите внимание, что имена notBefore и notAfter в этом примере НЕ начинаются с заглавной буквы. Такое отсутствие заглавной буквы чрезвычайно важно, оно подводит нас к следующему блоку материала. Сердце бьётся всё чаще и чаще.

Идентификаторы

Существует два случая, когда имеют место имена, начинающиеся с буквы в нижнем регистре. Первый из них — внутри фигурных скобок (внутри блока, если Вам так больше нравится), используемых с некоторыми составными универсальными типами (например, SEQUENCE или SET) или зарезервированным/ключевым словом CHOICE. В этом случае он формально называется идентификатор (identifier) и имеет область видимости только внутри соответствующей структуры (внутри фигурных скобок блока). Идентификатор определяется следующим образом:

-- формат, используемый внутри фигурных скобок {}
{
-- В идентификаторах не используется символ назначения.
-- Если Вам так нравится, можете называть их молчаливыми назначениями.

identifier-name(WHITESPACE)Type-name[,]

-- Здесь:
-- имя идентификатора (identifier-name) начинается с буквы в нижнем регистре;
-- (WHITESPACE) представляет собой один или несколько символов пробела/VT/HT/LF/FF/CR,
-- это означает, что такое определение может занимать одну или несколько строк;
-- [,] каждый пункт, кроме последнего, заканчивается запятой;
-- имя типа (Type-name) может быть именем либо универсального, либо пользовательского типа.
...
}
-- Примеры:
{
-- формат с пользовательским типом
validity  Validity,

-- формат с универсальным типом
number INTEGER,
...
}

Примечание: При использовании символа & в определении CLASS (его мы рассмотрим далее) идентификатор, по-видимому, обладает областью видимости вне пределов структуры.

Ссылки на значение

Второй случай — это ссылка на значение (valueReference) с глобальной областью видимости. У ссылки на значение есть имя значения (valueName), и ей присваивается определённое значение именованного типа, а не просто тип или типы. Вот пример использования двух ссылок на значение (обе взяты из RFC 5912):

-- Взято со страницы 87 RFC 5912 (в самом RFC допущена опечатка,
-- мы приводим корректную версию).
-- Здесь просто одна ссылка на значение присваивается другой (псевдоним).
-- В результате разрешения правой ссылки на значение id-at-clearance-rfc821
-- получится значение и тип.

   id-at-clearance ::= id-at-clearance-rfc3281

-- а у id-at-clearance-rfc3281 есть и значение, и тип:

id-at-clearance-rfc3281              OBJECT IDENTIFIER ::= {
       joint-iso-ccitt(2) ds(5) module(1) selected-attribute-types(5)
       clearance (55) }

-- Здесь ссылке на значение присваивается явное значение именованного типа.
-- В данном случае тип - это универсальный тип OBJECT IDENTIFIER.
-- Позже мы детальнее разберём тип OBJECT IDENTIFIER.

-- А эта ссылка на значение из рабочего примера SubjectAltName:

   id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

-- Здесь id-ce из конструкции {id-ce 17} также представляет собой
-- ссылку на значение со следующим назначением (значения и типа):

id-ce OBJECT IDENTIFIER  ::=  { joint-iso-ccitt(2) ds(5) 29 }

-- Как здесь показано, можно представить числовые значения в виде именованного числа
-- (namedNumber), либо используя функционально идентичный числовой формат:

id-ce OBJECT IDENTIFIER  ::=  { 2 5 29 }

-- Таким образом, полное значение id-ce-subjectAltName - 2 5 29 17,
-- или в более привычном представлении OID - 2.5.29.17.

Тип-классы

Наш последний (слава Богу!) пример — это то, что в стандарте (X.681) называется информационным объектом (Information Object), а мы обозначаем это термином тип-класс (ClassType) и сейчас постараемся объяснить почему. Имя типа тип-класса должно состоять из всех заглавных букв (согласно разделу 7.1 X.681). Вот пример назначения тип-класса из RFC 5912 для класса EXTENSION (далее мы разберём его в деталях):

-- EXTENSION представляет собой тип информационного объекта (тип-класс), при его назначении
-- всегда используется зарезервированное/ключевое слово CLASS.
-- Именно поэтому мы используем термин тип-класс.

 EXTENSION ::= CLASS {

 -- Не обращайте внимание на эту белиберду, мы разберём её позже.
      &id  OBJECT IDENTIFIER UNIQUE,
      &ExtnType,
      &Critical    BOOLEAN DEFAULT {TRUE | FALSE }
  } WITH SYNTAX {
      SYNTAX &ExtnType IDENTIFIED BY &id
      [CRITICALITY &Critical]
  }

-- На этот класс можно будет сослаться в назначении, наподобие приведённого ниже
-- (опять же, позднее мы рассмотрим его в деталях).
-- Вот форма ссылки на значение:

   ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }

-- А вот форма пользовательского типа, ссылающегося на мифический класс под названием THING:

StupidType THING ::= {
 ... -- нечто внутри назначения, ссылающееся на назначение класса THING.
}

Резюме по синтаксису ASN.1

В предыдущем разделе был приведён поверхностный обзор синтаксиса с большим количеством деталей и нюансов, которые еще предстоит разобрать подробнее. Вместо того, чтобы делать это абстрактно, в следующем разделе мы возьмём несколько хорошо известных объектов и разберём их на составляющие (а заодно и закодируем их в DER). Однако, взглянув на определение или модуль ASN.1 с высоты птичьего полёта, мы можем сделать несколько общих замечаний о том, что же там происходит:

  1. Если какое-либо имя начинается с заглавной буквы, то оно определяет либо пользовательский тип, либо универсальный тип, либо тип-класс, и, наконец, это может быть зарезервированное/ключевое слово. Мы помогали, как могли!

  2. Если какое-либо имя состоит из всех заглавных букв, то оно определяет либо универсальный тип, либо тип-класс, либо может быть зарезервированным/ключевым словом, но никогда не будет пользовательским типом. Ну хоть чуточку попроще.

  3. Если какое-либо имя состоит из всех заглавных букв и не входит в список универсальных типов и зарезервированных/ключевых слов, то это тип-класс, и если Вы хотите узнать о нём побольше, поищите его определение в модуле ASN.1. Наконец хоть какая-то определённость!

  4. Если какое-либо имя начинается с заглавной буквы и заканчивается на 'String', то, скорее всего (!), это универсальный тип со свойствами строки, например, PrintableString. Но это не непреложное правило. Тут можно попасть впросак, поэтому сверяйтесь со списком универсальных типов. Подвохи на каждом шагу.

  5. Если имя в правой части определения начинается с маленькой буквы, то это ссылка на значение или идентификатор, в зависимости от того, находится ли оно внутри или снаружи фигурных скобок. Запомните хорошенько.

Необычайно полезное резюме, не правда ли?

Наверх

Рабочие примеры ASN.1

В примерах этого раздела мы возьмём фрагменты ASN.1 с определениями хорошо известных и интересных объектов, разъясним их уникальные характеристики, а затем продемонстрируем, как они кодируются в DER.

  1. ASN.1 и DER для подмножества X.509
  2. ASN.1 и DER для ContentInfo — ASN.1 версии 1988 года
  3. ASN.1 и DER для расширения сертификата X.509 V3 SubjectAltName
  4. ASN.1 и DER для ContentInfo — ASN.1 версии 2002 года
  5. ASN.1 и DER для сертификата X.509 (полностью)

Наверх

ASN.1 и DER для подмножества X.509

В качестве аккуратного (!) вступления мы возьмём часть источника ASN.1 стандарта X.509 из нашего первого примера (раздел 14 RFC 5912 (PKIX1Implicit-2009)). Рассмотрим только два элемента CertificateSerialNumber и Validity вместе с их определениями:

  -- Это стандартное назначение (конструкция), некоторые
  -- элементы которого были опущены ради простоты примера.
  TBSCertificate  ::=  SEQUENCE  {

    ....   -- часть элементов опущена для упрощения примера

    serialNumber         CertificateSerialNumber,

    ....   -- часть элементов опущена для упрощения примера

    validity             Validity,

    ....   -- часть элементов опущена для упрощения примера
    }

CertificateSerialNumber  ::=  INTEGER

  Validity ::= SEQUENCE {
      notBefore      Time,
      notAfter       Time  }

  Time ::= CHOICE {
      utcTime        UTCTime,
      generalTime    GeneralizedTime }

Итак, соберём волю в кулачок, и за дело. Начинается всё самое страшное.

В самом начале у нас идёт конструкция (в терминологии ASN.1, в этом документе в качестве более предпочтительного мы используем термин назначение). Назначение может быть либо полностью определено в рамках одной строки, либо заключено в фигурные скобки { }, если его определение занимает более одной строки (мы называем это блоком, чтобы было более понятно, хотя такой термин не используется в ASN.1). Пункт (если более строго, то, согласно разделу 5.2 стандарта X.680, лексический пункт, или, если угодно, токен) слева от символа назначения представляет собой тип, название его начинается с заглавной буквы и не входит в список зарезервированных/ключевых слов (включающий в себя универсальные типы). Такие типы мы называем пользовательскими (поскольку их определил составитель модуля ASN.1 (пользователь)). Пункт в правой части назначения также представляет собой тип, в данном случае это SEQUENCE (один из универсальных типов), определяющий упорядоченную последовательность элементов. Итак, наше первое назначение (определение типа, если Вам так более понятно) выглядит следующим образом:


/* TBSCertificate - это определяемый нами новый пользовательский тип (его имя начинается
 с заглавной буквы и не входит в список зарезервированных/ключевых слов).
 Его определение занимает несколько строк (ограничено фигурными скобками {}) и состоит из
 универсального типа SEQUENCE, определяющего упорядоченный список элементов
 (блок SEQUENCE, если Вам так понятней), который мы рассмотрим далее
 (и да, это также пример нечасто используемого в ASN.1 многострочного типа комментирования).
 */

TBSCertificate  ::=  SEQUENCE  {
    ....  -- эти точки указывают, что здесь присутствует один или несколько элементов,
          -- но для простоты мы их пока опустили
    }

А теперь давайте вернём несколько пропущенных нами элементов из определения SEQUENCE нашего рабочего примера:

TBSCertificate  ::=  SEQUENCE  {

    ....   -- часть элементов опущена для упрощения примера

    serialNumber         CertificateSerialNumber,

    ....   -- часть элементов опущена для упрощения примера

    validity             Validity,

    ....   -- часть элементов опущена для упрощения примера
    }

Последовательность SEQUENCE состоит из упорядоченного списка элементов различных типов, в нашем примере таких два (serialNumber и validity). В левой части каждого пункта последовательности SEQUENCE находится идентификатор (произвольное, но чаще всего осмысленное имя, не входящее в список зарезервированных/ключевых слов и начинающееся с маленькой буквы (раздел 11.3 стандарта X.680)). В нашем примере это serialNumber и validity. В правой части в обоих случаях у нас находится пользовательский тип (имя начинается с заглавной буквы и не входит в список зарезервированных/ключевых слов, именно поэтому в нашей терминологии это называется пользовательским типом).

Существует некое общепринятое соглашение, согласно которому имена в левой части назначения совпадают с именами в правой его части, только последние начинаются с заглавной буквы (поскольку это имена пользовательских типов). Но это всего лишь соглашение, и, как показано в примере выше, оно часто нарушается (serialNumber). Область видимости идентификатора локальная относительно SEQUENCE (или окружающего его блока). Все имена пользовательских типов имеют глобальную область видимости (внутри модуля ASN.1, а при использовании импорта (IMPORTS) этого модуля и гораздо большую).

Итак, теперь мы знаем, что если имя представляет собой пользовательский тип, то у него должно быть назначение (определение типа), как это показано в приведённом ниже примере источника ASN.1:


-- Это стандартное назначение (конструкция) сертификата X.509, некоторые
-- элементы которого были опущены ради простоты примера.

  TBSCertificate  ::=  SEQUENCE  {

    ....   -- часть элементов опущена для упрощения примера

    serialNumber         CertificateSerialNumber,

    ....   -- часть элементов опущена для упрощения примера

    validity             Validity,

    ....   -- часть элементов опущена для упрощения примера
    }

CertificateSerialNumber  ::=  INTEGER

  Validity ::= SEQUENCE {
      notBefore      Time,
      notAfter       Time  }

  Time ::= CHOICE {
      utcTime        UTCTime,
      generalTime    GeneralizedTime }

Строка, начинающаяся с CertificateSerialNumber, является назначением пользовательского типа соответствующего идентификатору serialNumber внутри SEQUENCE-назначения для типа TBSCertificate (TBSCertificate, если Вам интересно, изначально назывался ToBesSignedCertificate).

-- Однострочное назначение (или определение типа) пользовательскому типу типа INTEGER
-- INTEGER - это один из универсальных типов

CertificateSerialNumber  ::=  INTEGER

-- У CertificateSerialNumber глобальная область видимости, таким образом
-- любой ссылке на это имя типа будет назначен тип INTEGER.
-- Если нам не нужна глобальная область видимости,
-- мы можем сделать что-то наподобие такого:

TBSCertificate ::= SEQUENCE { -- начало SEQUENCE
  ... -- что-то опущено
  serialNumber   INTEGER,
  ... -- что-то ещё опущено
  }  -- завершение SEQUENCE

Тип Validity немного сложнее, в его назначении используется другая последовательность SEQUENCE (содержащая два идентификатора notBefore и notAfter, у каждого из которых пользовательский тип Time).

Validity ::= SEQUENCE {

    -- Данная последовательность SEQUENCE состоит из двух элементов, каждый из которых
    -- использует пользовательский тип с именем типа Time.

    notBefore      Time,
    notAfter       Time  }

    -- Примечание: закрывающаяся фигурная скобка может находиться
    -- как на той же строке, так и на новой строке,
    -- ASN.1 - свободный формат (в основном).

Назначение (определение типа) для пользовательского типа Time состоит из зарезервированного/ключевого слова CHOICE (ещё одна многострочная структура), которое говорит о том, что только один элемент из предложенных вариантов CHOICE может присутствовать в DER-кодировке, но это может быть любой элемент из представленных в блоке CHOICE. Формат элементов внутри блока CHOICE (термин "блок" не используется в стандарте X.680) тот же, что и для типа SEQUENCE:

Time ::= CHOICE {

-- Каждый из двух элементов типа Validity (notBefore, notAfter) может состоять
-- только из одного универсального типа UTCTime или GeneralizedTime.
-- Так, notBefore может использовать UCTTime, а notAfter может использовать GeneralizedTime
-- (или наоборот), либо оба могут использовать один и тот же универсальный тип.

      utcTime        UTCTime,
      generalTime    GeneralizedTime }

На этом пока остановимся. Теперь сварим кофейку и попробуем закодировать всё это в DER. По окончании этого действа потребуется некоторое время на реабилитацию от душевных потрясений.

Наверх

DER-кодирование подмножества X.509

Чтобы пример DER-кодирования получился простым, предположим, что TBSCertificate состоит только из тех двух пунктов, которые мы уже рассмотрели. Таким образом, источник ASN.1, который мы собираемся кодировать, выглядит так:

-- Это стандартное назначение (конструкция), некоторые
-- элементы которого были опущены ради простоты примера.

  TBSCertificate  ::=  SEQUENCE  {
    -- для упрощения мы используем только эти два элемента
    serialNumber         CertificateSerialNumber,
    validity             Validity
  }

CertificateSerialNumber  ::=  INTEGER

  Validity ::= SEQUENCE {
      notBefore      Time,
      notAfter       Time  }

  Time ::= CHOICE {
      utcTime        UTCTime,
      generalTime    GeneralizedTime }

Этот источник ASN.1 будет закодирован в DER с использованием реальных значений CertificateSerialNumber = 1 (мы долго думали над выбором), notBefore = 10th, August 2017 (применяется UTCTime) и notAfter = 10th, August 2027 (применяется GeneralizedTime). В обоих значениях времени будет использоваться вариант UTC.

DER-кодирование представляет собой классический метод кодирования в стиле TLV (Type, Length, Value), определённый в X.690. Чтобы разобраться с этим материалом, нужно быть хорошо знакомым с двоичной и битовой нумерацией. Бинарная закодированная в DER строка для нашего ASN.1 будет состоять из смежных последовательностей октетов. Ниже они показаны в виде иерархии, чтобы их было проще визуализировать. Мы использовали комментирование в стиле ASN.1, хотя в данном контексте оно неприменимо:

Предостережение для читателей RFC: ITU и IETF (RFC) используют радикально отличающиеся схемы нумерации бит.

-- DER-кодирование последовательности SEQUENCE, соответствующей
-- TBSCertificate  ::=  SEQUENCE

301d

-- Тип (тег) - 16 (шестнадцатеричное 10) = SEQUENCE
-- с установленным битом Constructed (шестнадцатеричное 20);
-- длина = 29 (шестнадцатеричное 1d), включает в себя все элементы последовательности
-- SEQUENCE (3 + 13 + 13);
-- значением будут все элементы последовательности SEQUENCE.


  -- DER-кодирование CertificateSerialNumber = 1

  020101

  -- Тип (тег) - 2 = INTEGER; длина - 1; значение - 01.

  -- DER-кодирование notBefore

  170b313730383130313030305a

  -- Тип (тег) - 17 = UTCTime; длина - 11 (шестнадцатеричное 0b);
  -- значением будет бинарная кодировка строки 1708101000Z (10th August 2017, 10AM UTC).


  -- DER-кодирование notAfter

  180b323032373038313031305a

  -- Тип (тег) - 18 = GeneralizedTime; длина - 11 (шестнадцатеричное 0b);
  -- значением будет бинарная кодировка строки 2027081010Z (10th August, 2027, 10AM UTC).

-- полная битовая строка DER для данного ASN.1
301d020101170b313730383130313030305a180b323032373038313031305a

Вот, собственно, и всё о DER-кодировке нашего далёкого от реальности примера.

Наверх

PKCS ContentInfo — ASN.1 версии 1988 года

Все контейнеры PKCS (а также CMS — RFC 5652) используют сущность ContentInfo для идентификации частей внутри контейнера. Разбираемый в этом разделе фрагмент ASN.1 взят из раздела 3 RFC 5652.

Примечание: В данном определении ContentInfo используется в настоящее время устаревшая версия ASN.1 1988 года. Она рассматривается в этом документе, поскольку так мы можем более просто (в щадящем режиме) представить некоторые концепции, чем если бы мы сразу начали с ASN.1 версии 2002 года, заменившей предыдущий стандарт, а также потому, что Вы всё ещё частенько будете сталкиваться с более старыми нотациями ASN.1. Получающееся в результате DER-кодирование для источника ASN.1 версий 1988 и 2002 годов будет идентичным. Далее мы покажем рабочий пример ContentInfo ASN.1 версии 2002 года (предостережение: в нём используется множество понятий из этого примера, а также из рабочего примера SubjectAltName).

ContentInfo ::= SEQUENCE {
     contentType ContentType,
     content
       [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }

   ContentType ::= OBJECT IDENTIFIER

Не слишком длинное определение ASN.1. Всё должно быть просто. Но не тут-то было.

ContentInfo представляет собой назначение пользовательского типа и использует последовательность SEQUENCE с двумя идентификаторами (contentType и content). Идентификатор contentType прост, пройдёмся по нему коротко:

-- Назначение пользовательского типа ContentInfo
ContentInfo ::= SEQUENCE {

     -- идентификатор contentType ссылается на пользовательский тип ContentType

     contentType ContentType,
     .... -- content опущен для упрощения
     }

  -- Назначение ContentType (пользовательского типа) ссылается на
  -- универсальный тип OBJECT IDENTIFIER (или OID).
  -- Данное определение позволяет использовать любой OID
  -- (нет конкретного назначенного значения).

   ContentType ::= OBJECT IDENTIFIER

Если Вы не знакомы с идентификаторами объектов (OBJECT IDENTIFIER, они же OID), найдите время ознакомиться с ними в описании данного универсального типа (по ссылке), либо на этой ориентированной на LDAP странице по OID. Они очень важны. Позже мы разберём ещё один пример OID со своими причудами (разогреваем Ваш аппетит). В данном же назначении просто говорится, что ContentType может содержать любой валидный OBJECT IDENTIFIER (OID). Но если уж совсем следовать букве закона, то в нём может быть абсолютно любой OBJECT IDENTIFIER, не обязательно валидный.

Примечание: Пользователей LDAP могут смутить ASN.1 OID, поскольку в ASN.1 в определении OID в качестве разделителей между элементами (на жаргоне стандарта X.680 они называются подидентификаторами) используются пробелы, а в LDAP — точки. При кодировании OBJECT IDENTIFIER в DER данный разделитель опускается. При сборке OID DER-декодером может быть использован любой предпочтительный по ситуации разделитель.

Второй идентификатор (content) позволяет нам рассмотреть целую гроздь новых концепций:

ContentInfo ::= SEQUENCE {
     ....  -- contentType опущен для упрощения

     -- В данной записи один элемент представлен в виде двух строк,
     -- ASN.1 - свободный формат.
     content
       [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }

Коротко о простых вещах. Зарезервированное/ключевое слово OPTIONAL обозначает ровно то, что следует из его названия. Оно говорит о том, что данный пункт может как присутствовать, так и не присутствовать в закодированной форме. Если кодировщику DER не предоставят content, он не будет кодировать ничего. Тем не менее, приёмник должен каким-то образом выяснить это отсутствие.

Тегированные типы

Во всех назначениях, которые встречались нам до этого момента (если, конечно, Вы терпеливо читали материал последовательно, а не прыгали с места на место), использовались универсальные типы. Однако, у универсальных типов есть потенциальные ограничения просто потому, что они очень общие. INTEGER — это INTEGER, верно? Если требуется использование чего-то большего, чем общий универсальный тип, это можно указать с помощью нотации тегированного типа (TaggedType) (раздел 30 стандарта X.680). Данная нотация может быть в форме [x], что означает контекстно-специфичный класс со значением тега x, [APPLICATION x], что означает класс приложения со значением тега x, и [PRIVATE x], что означает закрытый класс со значением тега x (во всех трёх случаях x представляет собой числовое значение в диапазоне от 0 до 30). В документах RFC используются только контекстно-специфичные тегированные типы. "Контекстно-специфичный" просто означает, что мы собираемся задать назначенному типу дополнительное свойство, которого нет у данного типа в чистом виде, внутри (в контексте) конкретной структуры, в которой он определяется. Если в источнике ASN.1 встречаются тегированные типы, их классы и числовые значения (номера тегов) кодируются в DER в виде битовой строки.

Прекрасно. Но что же делают эти самые тегированные типы, и когда/зачем нам использовать этот формат [x]?

Возьмём простейшее назначение пользовательского типа (определение типа):

StupidType ::= SEQUENCE {
      one        One,
      two        Two
      }
One ::= INTEGER
Two ::= INTEGER

-- Из закодированной в DER формы получатель всегда извлечёт два значения
-- типа INTEGER: One, за которым следует Two, причём второе можно
-- отличить от первого, основываясь на порядке следования.

Теперь давайте изменим определение, сделав оба пункта необязательными (OPTIONAL):


StupidType ::= SEQUENCE {
      one        One OPTIONAL,
      two        Two OPTIONAL
      }
One ::= INTEGER
Two ::= INTEGER

-- Если в DER кодируется такая структура, получатель может извлечь два значения
-- типа INTEGER: One, за которым следует Two, и отличить их по порядку следования;
-- либо ноль значений типа INTEGER, в этом случае, очевидно, One и Two отсутствуют;
-- либо одно значение типа INTEGER, тогда у получателя не будет возможности
-- разобраться, что это за число (One или Two).

-- Такая проблема решается с помощью контекстно-специфичного синтаксиса ([x])
-- следующим образом:

StupidType ::= SEQUENCE {
      one      [0]  One OPTIONAL,
      two      [1]  Two OPTIONAL
			}
One ::= INTEGER
Two ::= INTEGER

-- При кодировании такой структуры One будет закодировано в DER как контекстно-специфичный
-- тип с тегом 0, предшествующим INTEGER, а Two - как контекстно-специфичный тип с тегом 1,
-- предшествующим INTEGER.
-- Получатель сможет однозначно разобраться с любой комбинацией результатов.

-- Данная кодировка будет отличаться в зависимости от того, явный или неявный тип тегирования
-- задан или установлен по умолчанию в модуле ASN.1 (ключевые слова EXPLICIT или IMPLICIT).

-- Зарезервированные/ключевые слова IMPLICIT или EXPLICIT являются необязательными, какое
-- из них будет использовано зависит от установок по умолчанию в модуле (подробнее по ссылкам).

StupidType ::= SEQUENCE {
      one      [0] EXPLICIT | IMPLICIT One OPTIONAL,
      two      [1] EXPLICIT | IMPLICIT Two OPTIONAL
    }

А теперь давайте рассмотрим ещё один пример с более осмысленным универсальным типом:

StupidType ::= SEQUENCE {
      version  [0]  Version,
      two           Two
      }

Version ::= INTEGER
Two     ::= INTEGER

-- В этом случае при получении DER-декодером контекстно-специфичного класса с тегом 0
-- он будет знать, что целочисленное значение представляет собой версию, и сможет
-- обработать его соответствующим образом.

Вернёмся к определению пункта из нашего первоначального примера:

content
       [0] EXPLICIT ANY DEFINED BY contentType

Мы близки к развязке. Итак, у нас есть слово EXPLICIT, являющееся зарезервированным/ключевым словом и говорящее о том, что тегированный тип контекстно-специфичного класса со значением тега 0 (и с установленным битом Constructed) во время кодирования будет добавлен как отдельный пункт перед пунктом, содержащим данные. При отсутствии в модуле ASN.1 директив DEFINITIONS IMPLICIT TAGS | EXPLICIT TAGS, значением по умолчанию является EXPLICIT, и в определениях тегированных типов это ключевое слово может быть опущено.

Примечание: Алгоритмы DER-кодирования для тегированных типов с признаками EXPLICIT и IMPLICIT различаются. Многие RFC содержат отдельные модули источников ASN.1, где по умолчанию выставлен либо признак IMPLICT, либо EXPLICIT. В идеале, DER-декодеры должны быть готовы обработать любую из таких версий.

Финальная часть определения — ANY DEFINED BY contentType. Директива ANY DEFINED BY является устаревшим структурным типом ASN.1, который означал (как можно догадаться), что в пункте может содержаться всё что угодно, соответствующее OID этого пункта (contentType). Этот текст, по существу, является "синтаксическим сахаром", полезным для чтения человеком, но невидимый в закодированном DER. Возможно, разборщик ASN.1 или кодировщик DER сочтёт этот материал полезным. Конечный же смысл такого определения — показать человеку, что содержимое пункта content (часть значение) определяется идентификатором объекта (OBJECT IDENTIFIER) из пункта contentType.

Наверх

DER-кодировка ContentInfo

Источник ASN.1 для ContentInfo:

ContentInfo ::= SEQUENCE {
     contentType ContentType,
     content
       [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }

   ContentType ::= OBJECT IDENTIFIER

Чтобы при кодировании у нас фигурировали конкретные значения, определимся, что в качестве ContentType будет полностью фиктивный идентификатор объекта (OBJECT IDENTIFIER) 2 3 4 5 (в формате LDAP — 2.3.4.5), а в качестве content — всё, что угодно, соответствующее этому идентификатору ContentType (вспомните "синтаксический сахар" ANY DEFINED BY), для примера возьмём строку в формате IA5String 'wow'. Немного легкомысленный пример, ну да ладно. Используемые в реальных структурах PKCS идентификаторы ContentInfo будут указывать на то, что в пункте content будет содержаться либо сертификат X.509, либо подобная ему ужасно сложная структура, которую даже сын маминой подруги и то не с первого раза закодирует.

-- DER-кодировка последовательности SEQUENCE, соответствующей
-- ContentInfo ::= SEQUENCE

  300c

-- Код типа/тега - 16 (шестнадцатеричное 10)
-- с заданным битом Constructed (20 + 10 = 30 (шестнадцатеричное));
-- длина составляет 12 (шестнадцатеричное 0c) (5 + 2 + 5) (сюда входит вся последовательность);
-- значением будут все пункты последовательности SEQUENCE.

  -- DER-кодировка ContentType

  0603530405

  -- Код типа/тега - 6 (шестнадцатеричное 06) = OBJECT IDENTIFIER;
  -- длина - 3;
  -- кодируемое значение - 2 3 4 5 (в формате LDAP - 2.3.4.5)
  -- (подробнее о том, как это кодируется в DER).

  -- DER-кодировка content, в котором будет строка IA5String

  -- но перед этой строкой будет контекстно-специфичный пункт с тегом ноль ([0] EXPLICIT)

  a005

  -- Код типа/тега - a0 (шестнадцатеричное), состоит из
  -- признака контекстно-специфичного класса (шестнадцатеричное 80),
  -- бита Constructed (шестнадцатеричное 20) и тега тегированного типа = 0;
  -- длина - 5 (сюда входит вся последовательность SEQUENCE);
  -- значением будут все пункты последовательности SEQUENCE.

-- DER-кодировка content

  1603776f77

  -- Код типа/тега - 22 (шестнадцатеричное 16) = IA5String;
  -- длина - 3;
  -- значением будет шестнадцатеричное представление (ascii) строки 'wow'.

-- Полная битовая строка с DER-кодировкой для данного источника ASN.1
300c0603530405a0051603776f77

И всё это чтобы вывести простейшую IA5String-строку из трёх символов. Да уж!

Наверх

Расширение сертификата X.509 V3 SubjectAltName

Рассматриваемый источник ASN.1 взят из раздела 14 RFC 5912 (PKIX1Implicit-2009) и определяет расширение сертификата X.509 (SSL) версии 3 (V3) под названием SubjectAltName. Это расширение позволяет определять расширенный набор имён, которые охватывает сертификат X.509, в отличие от единственного имени, задаваемого в атрибуте Subject сертификата. Иногда имя этого расширения сокращают до SAN.

-- OID и синтаксис расширения "альтернативное имя субъекта"

   ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }
   id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

   GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

   GeneralName ::= CHOICE {
        otherName                   [0]  INSTANCE OF OTHER-NAME,
        rfc822Name                  [1]  IA5String,
        dNSName                     [2]  IA5String,
        x400Address                 [3]  ORAddress,
        directoryName               [4]  Name,
        ediPartyName                [5]  EDIPartyName,
        uniformResourceIdentifier   [6]  IA5String,
        iPAddress                   [7]  OCTET STRING,
        registeredID                [8]  OBJECT IDENTIFIER
   }


   OTHER-NAME ::= TYPE-IDENTIFIER

   EDIPartyName ::= SEQUENCE {
       nameAssigner    [0] DirectoryString {ubMax} OPTIONAL,
       partyName       [1] DirectoryString {ubMax}
   }

Это довольно сложный фрагмент ASN.1, поэтому мы будем разбирать его по частям. Начнём с простых вещей:


-- Это назначение пользовательского типа GeneralNames,
-- используемого во многих местах:

GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

-- Впервые мы сталкиваемся с однострочным назначением SEQUENCE.

-- В нём определяется ограничение с использованием
-- ключевого слова SIZE(min..max), где
--   min = 1 (должен присутствовать как минимум 1 пункт)
--   max = MAX (зарезервированное/ключевое слово)

-- Обратите внимание, что при наличии SIZE используется вариант SEQUENCE ... OF,
-- который означает, что все пункты последовательности должны быть
-- одного и того же типа.

-- Итак, пользовательский тип GeneralNames представляет собой
-- последовательность SEQUENCE, содержащую 1 или более экземпляров пунктов
-- с пользовательским типом GeneralName.

Несмотря на то, что MAX является зарезервированным/ключевым словом, для него не определено значение в каком-либо из стандартов X.680. По умолчанию это означает, что установление верхней границы оставлено на усмотрение разработчика отправителя закодированного сообщения. А бедный старенький получатель должен быть готов обработать любое количество элементов GeneralName. Для задания минимального значения в конструкции SIZE может использоваться зарезервированное слово MIN, имеющее сходные с MAX характеристики (то есть никаких). Стандартизация рулит.

Примечание: Конструкция SIZE(min..max) также может использоваться с универсальными типами, например, PrintableString или INTEGER, для ограничения (или задания) пределов значений этих типов.

Теперь давайте рассмотрим присвоение GeneralName, у которого имеются пара небольших и одна большая заковырка:

GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

-- CHOICE указывает на то, что только один из приведённых ниже типов
-- может быть выбран для каждого экземпляра GeneralName.

GeneralName ::= CHOICE {
 otherName                 [0]  INSTANCE OF OTHER-NAME, -- тип-заковырка!
 rfc822Name                [1]  IA5String,        -- универсальный тип
 dNSName                   [2]  IA5String,        -- универсальный тип
 x400Address               [3]  ORAddress,        -- пользовательский тип
 directoryName             [4]  Name,             -- пользовательский тип
 ediPartyName              [5]  EDIPartyName,     -- пользовательский тип
 uniformResourceIdentifier [6]  IA5String,        -- универсальный тип
 iPAddress                 [7]  OCTET STRING,     -- универсальный тип
 registeredID              [8]  OBJECT IDENTIFIER -- универсальный тип
 }

-- Все элементы используют неявный (IMPLICIT) тегированный тип,
-- поскольку в модуле ASN.1, откуда был взят этот фрагмент, имеется определение
-- DEFINITIONS IMPLICIT TAGS.
-- У пользовательских типов ORAddress и Name
-- имеются свои назначения, попробуйте найти их!

-- Назначение пользовательского типа EDIPartyName:

EDIPartyName ::= SEQUENCE {
       nameAssigner    [0] DirectoryString {ubMax} OPTIONAL,
       partyName       [1] DirectoryString {ubMax}

   }

В назначении EDIPartyName используется параметризация (определена в стандарте X.683) и немного ограничений (определены в стандарте X.682). Для полноты картины (чтобы Вы чувствовали себя увереннее) давайте проследим это назначение от начала до конца.


EDIPartyName ::= SEQUENCE {
       nameAssigner    [0] DirectoryString {ubMax} OPTIONAL,
       partyName       [1] DirectoryString {ubMax}
     }

-- {ubMax} представляет собой параметр, который будет передан в качестве параметра
-- в присвоение DirectoryString (родительского типа) с использованием
-- функции параметризации, определенной в стандарте X.683.

-- В рассматриваемом модуле ubMax присвоено значение:

ubMax INTEGER ::= 32768

-- (Обратите внимание, что ubMax представляет собой ссылку на значение).

-- DirectoryString - это пользовательский тип, его назначение:

DirectoryString{INTEGER:maxSize} ::= CHOICE {

      -- {INTEGER:maxSize} указывает на то, что параметр универсального типа INTEGER
      -- требуется при каждом вызове DirectoryString.
      -- Смотрите ubMax в представленном выше назначении EDIPartyName.
      -- maxSize представляет собой локальную переменную, которая принимает значение
      -- предоставляемого параметра.
      -- Конечный эффект заключается в том, что ubMax (значение = 32768 (или 32K))
      -- заменяет maxSize в каждом из приведённых ниже элементов, то есть
      -- SIZE(1..maxSize) в этом случае будет SIZE(1..32768).
      -- Просто меняя значение ubMax, мы можем изменить данное ограничение.

      teletexString    TeletexString(SIZE (1..maxSize)),
      printableString  PrintableString(SIZE (1..maxSize)),
      bmpString        BMPString(SIZE (1..maxSize)),
      universalString  UniversalString(SIZE (1..maxSize)),
      uTF8String       UTF8String(SIZE (1..maxSize))

  }

Ну что же, это было несложно. Самое интересное впереди:


GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

-- CHOICE указывает на то, что только один из приведённых ниже типов
-- может быть выбран для каждого экземпляра GeneralName.

GeneralName ::= CHOICE {
 otherName                 [0]  INSTANCE OF OTHER-NAME, -- тип-заковырка!
 rfc822Name                [1]  IA5String,        -- универсальный тип
 dNSName                   [2]  IA5String,        -- универсальный тип
 x400Address               [3]  ORAddress,        -- пользовательский тип
 directoryName             [4]  Name,             -- пользовательский тип
 ediPartyName              [5]  EDIPartyName,     -- пользовательский тип
 uniformResourceIdentifier [6]  IA5String,        -- универсальный тип
 iPAddress                 [7]  OCTET STRING,     -- универсальный тип
 registeredID              [8]  OBJECT IDENTIFIER -- универсальный тип
 }

В результате этого назначения GeneralNames будет состоять из одного или нескольких экземпляров пользовательского типа GeneralName, каждый из которых может быть закодирован только одним из способов, определяемых данной конструкцией CHOICE.

Примечание: Использование конструкции SEQUENCE ...OF в приведённом выше определении указывает на то, что, строго говоря, последовательность SEQUENCE должна состоять из пунктов одного и того же типа, однако тип GeneralName предлагает на выбор (CHOICE) различные типы. Выходит, мы нарушаем правила? На самом деле, нет. Последовательность SEQUENCE состоит только из экземпляров типа GeneralName, назначение GeneralName (в котором используется CHOICE) "не видно" из назначения GeneralNames. Тонкий момент, для нас, пожалуй, чересчур тонкий.

Теперь обратимся к использованию универсального типа INSTANCE OF в данном фрагменте:

GeneralName ::= CHOICE {
  otherName      [0]  INSTANCE OF OTHER-NAME,
  -- для упрощения остальные элементы опущены
  }

 OTHER-NAME ::= TYPE-IDENTIFIER

-- INSTANCE OF используется только с
-- тип-классами, в которых применяется TYPE-IDENTIFIER (встроенный тип-класс),
-- и расширяется до следующей последовательности SEQUENCE,
-- в которой будет содержаться стандартная информация (id и Type),
-- используемая в TYPE-IDENTIFIER.

SEQUENCE {
-- & является артефактом конструкции CLASS.
-- OTHER-NAME.&id указывает, что идентификатор id является специфичным
-- для экземпляра OTHER-NAME класса TYPE-IDENTIFIER
  type-id OTHER-NAME.&id,
  value [0] OTHER-NAME.&Type
}

-- Итак, если получателю приходит контекстно-специфичный класс с тегом 0
-- (с установленным битом Constructed) для типа GeneralName, то за ним
-- должна следовать закодированная последовательность SEQUENCE (смотрите выше),
-- содержащая идентификатор объекта OBJECT IDENTIFIER (id),
-- за которым следует другой контекстно-специфичный класс
-- (с тегом 0 и установленным битом Constructed), определяющий содержимое (Type),
-- неявно задаваемое предыдущим определением идентификатора объекта
-- OBJECT IDENTIFIER.
-- ПРИМЕЧАНИЕ: в даном описании подразумевается вариант кодирования
-- тегированных типов EXPLICIT (используется по умолчанию);
-- в варианте IMPLICIT кодирование выполняется иначе.

Итак, всё, что мы ещё не разобрали из исходного фрагмента SubjectAltName, это:

ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }
   id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

Начнём, как всегда, с того, что попроще:


 id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

-- Это назначение ссылки на значение, в котором
-- именованному типу (OBJECT IDENTIFIER) назначается
-- специфичное значение, в данном случае это идентификатор объекта (OID).
-- Значение (в правой части) представляет собой определение OID (id-ce 17), в котором
-- 17 - это специфичное значение в данном OID (подидентификатор из раздела 8.19.2 стандарта X.690),
-- а id-ce - ещё одна ссылка на значение, у которой имеется собственное назначение:

id-ce OBJECT IDENTIFIER  ::=  { joint-iso-ccitt(2) ds(5) 29 }

-- Таким образом, путём комбинации этих двух ссылок на значение, мы выяснили, что
-- id-ce-subjectAltName представляет собой тип OBJECT IDENTIFIER с назначенным значением
-- 2 5 29 17 (или, в формате LDAP, 2.5.29.17)

-- Примечание: При работе с OID возможны два формата: формат именованного числа,
-- например, { joint-iso-ccitt(2) ds(5) 29 }, где значения указаны в круглых скобках
-- после имени (в данном случае это имя - ничто иное, как "синтаксический сахар"),
-- которое может быть полезно в качестве индикатора делегирования OID,
-- либо простой числовой формат {2 5 29}.

Последнее назначение ссылки на значение, которое мы ещё не разобрали:

ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }

-- ext-SubjectAltName представляет собой ссылку на значение, в которой значение

{ SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }

-- назначается тип-классу EXTENSION  (имя типа состоит из заглавных букв
-- и не входит в список зарегистрированных/ключевых слов).

-- Элемент значения можно понять только в контексте назначения
-- тип-класса EXTENSION, которое мы разберём далее.

Назначение тип-класса EXTENSION берётся из общего модуля PKIX-CommonTypes-2009 с помощью импортирования (IMPORTS) в модуле источника ASN.1. В результате получается следующее назначение тип-класса (из раздела 2 RFC 5912):

Примечание: Приведённая ниже информация представляет собой краткий обзор того, что мы называем тип-классами, а формально называется классами информационных объектов. Если Вам нужно разобраться с ними детально, читайте стандарт X.681. И удачи Вам!

  -- В назначении тип-класса EXTENSION используется ключевое слово CLASS:

  EXTENSION ::= CLASS {

      -- Нотация & - это артефакт конструкции Class (разделы 7.4 и 7.5 стандарта X.681)
      -- id - это идентификатор объекта (OBJECT IDENTIFIER) расширения
      -- (идентификатор, или молчаливо назначенная ссылка на значение,
      -- если Ваше чувство юмора простирается так далеко).
      -- ExtnType - это пользовательский тип, определяющий структуру расширения.
      -- Critical - это универсальный тип, опционально указывающий на критичность
      -- этого расширения.

      &id  OBJECT IDENTIFIER UNIQUE,
      &ExtnType,
      -- ПРИМЕЧАНИЕ: поскольку здесь не указывается какого-либо конкретного типа,
      -- то обычно такое называется открытым типом (ещё один жаргонизм).

      &Critical    BOOLEAN DEFAULT {TRUE | FALSE }
      -- Запутанное определение.
      -- DEFAULT указывает на то, что есть значение по умолчанию,
      -- но никакого значения не определяется.
      -- {TRUE | FALSE} определяет допустимые значения, но не значение по умолчанию.
  }

-- Блок WITH SYNTAX позволяет разработчику тип-класса определять
-- удобный для пользователя (!) формат (внутри фигурных скобок), который
-- будет использован, когда этот тип-класс используется в назначении.
-- Литералы помогают человеку, читающему определения,
-- и больше никакой роли не играют.

	WITH SYNTAX {

      -- SYNTAX, IDENTIFIED BY и CRITICALITY - литералы, которые
      -- разработчик класса выбрал в качестве подсказок для пользователя.
      -- Они должны состоять из заглавных букв и могут быть
      -- зарезервированными/ключевыми словами, включая универсальные типы,
      -- просто для того, чтобы запутать нас, простых смертных
      -- (скромный список исключений дан в разделе 10.6 стандарта X.681).
      -- Литералы полезны для людей, но не имеют никакого значения при DER-кодировании.

      SYNTAX &ExtnType IDENTIFIED BY &id

      -- Квадратные скобки указывают на то, что это определение опциональное:

      [CRITICALITY &Critical]
  }

  -- Назначение, использующее тип-класс EXTENSION, может выглядеть примерно так:

  StupidExtension EXTENSION ::= {
	SYNTAX Ext-Structure DEFINED BY ext-oid
	CRITICAL TRUE }

  -- Помните, что использование нижнего и верхнего регистров
  -- символов имеет существенное значение.
  -- Ext-Structure будет пользовательским типом, а
  -- ext-oid - ссылкой на значение, определяющей OID.

Поскольку мы рассматриваем расширения, жизненно важно будет рассмотреть парочку других применений тип-класса EXTENSION, чтобы понимать, что происходит. Вот они:


-- Некоторые комментарии удалены и переформатированы для удаления разрывов строк.
-- Нотация EXTENSION. указывает на то, что поле, на которое происходит ссылка,
-- (например, &id), относится к данному тип-классу.

  Extension{EXTENSION:ExtensionSet} ::= SEQUENCE {
      extnID      EXTENSION.&id({ExtensionSet}),
      critical    BOOLEAN  DEFAULT FALSE,
      -- вот как правильно назначать значение по умолчанию (DEFAULT)
      extnValue   OCTET STRING (CONTAINING
                  EXTENSION.&ExtnType({ExtensionSet}{@extnID}))
                  --  Содержит закодированное в DER значение ASN.1,
                  --  соответствующее типу данного расширения,
                  --  идентифицируемому по extnID.
  }
  -- ЖИЗНЕННО ВАЖНО: содержимое всех расширений V3
  -- (данное назначение используется всеми расширениями V3)
  -- инкапсулируется в строку октетов (OCTET STRING).
  -- Почему? Хороший вопрос.
    -- EXTENSION.&id({ExtensionSet}) ограничивает допустимые значения (несколько).
	-- EXTENSION.&ExtnType({ExtensionSet}) ограничивает количество элементов.
	-- {@extnID} является компонент-ссылкой на OID.

	Extensions{EXTENSION:ExtensionSet} ::=
      SEQUENCE SIZE (1..MAX) OF Extension{{ExtensionSet}}

-- Extensions является назначением пользовательского типа и использует параметризацию.
-- {EXTENSIONS:ExtensionSet} указывает на то, что при обращении к Extentsions
-- должен предоставляться параметр тип-класса EXTENSIONS, в котором
-- устанавливается ограничение на количество расширений
-- ExtensionSet - локальная переменная, которая заменяется
-- на значение предоставляемого параметра.
-- Extensions используется во многих местах, вот, например,
-- выдержка из назначения TBSCertificate:

TBSCertificate ::= SEQUENCE {
   .... -- элементы опущены
   extensions      [3]  Extensions{{CertExtensions}} OPTIONAL

   -- CertExtensions - пользовательский тип, предоставляемый в Extensions
   -- в качестве параметра (X.683)
   .... -- элементы опущены
   }

-- В назначении пользовательского типа CertExtensions используется
-- тип-класс EXTENSION согласно требованиям к параметру для Extensions.

CertExtensions EXTENSION ::= {
           ext-AuthorityKeyIdentifier | ext-SubjectKeyIdentifier |
           ext-KeyUsage | ext-PrivateKeyUsagePeriod |
           ext-CertificatePolicies | ext-PolicyMappings |
           ext-SubjectAltName | ext-IssuerAltName |
           ext-SubjectDirectoryAttributes |
           ext-BasicConstraints | ext-NameConstraints |
           ext-PolicyConstraints | ext-ExtKeyUsage |
           ext-CRLDistributionPoints | ext-InhibitAnyPolicy |
           ext-FreshestCRL | ext-AuthorityInfoAccess |
           ext-SubjectInfoAccessSyntax, ... }

-- Список с возможностью дополнения, включающий все расширения,
-- определённые в данном модуле ASN.1.
-- Возможность дополнения указывается многоточием (...)
-- Примечание: все элементы списка являются ссылками на значение.

В завершении ещё раз обратим внимание на удобную для пользователя часть (определяется с помощью WITH SYTAX):

ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }

-- Как мы уже знаем, SYNTAX и IDENTIFIED BY являются литералами.
-- Итак, перед нами назначение ссылки на значение, в котором используется
-- тип-класс EXTENSION. Оно будет состоять из GeneralNames
-- (последовательности (SEQUENCE) из 1 или нескольких элементов GeneralName,
-- в каждом из которых может использоваться различный формат
-- (в соответствии со списком CHOICE)).
-- А само расширение (EXTENSION) идентифицируется OID
-- id-ce-subjectAltName (2 5 29 17)

-- Опциональный литерал CRITICAL и его логическое (BOOLEAN) значение
-- опущены из данного назначения, поскольку SubjectAltName не является
-- критическим расширением (в большинстве случаев).

Всё? Ну, почти.

Мы оставили кое-что напоследок, отчасти для того, чтобы поддержать драматизм сюжета, отчасти потому, что для обретения этим материалом хоть какого-то смысла требовалось накопить достаточно много знаний. По правде говоря, даже с солидным багажом знаний смысла от этого материала немного. Но уже после третьего прочтения кое-что проясняется.

В назначении GeneralName присутствует на первый взгляд безобидная строка directoryName Name,. Это так называемое DN (directoryName) со всеми штуками вроде cn=blah, O=blah, c=blah, и у него достаточно сложное (хоть и небольшое) определение, для интерпретации большей части которого у Вас уже достаточно знаний:


-- Чтобы понять всё это, пришлось копнуть достаточно глубоко.

Name ::= CHOICE { -- единственный на сегодняшний день допустимый вариант --
      rdnSequence  RDNSequence }

  RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

  DistinguishedName ::=   RDNSequence

  RelativeDistinguishedName  ::=
      SET SIZE (1 .. MAX) OF SingleAttribute { {SupportedAttributes} }

  --  Известные элементы имени для DN:

  SupportedAttributes ATTRIBUTE ::= {
      at-name | at-surname | at-givenName | at-initials |
      at-generationQualifier | at-x520CommonName |
      at-x520LocalityName | at-x520StateOrProvinceName |
      at-x520OrganizationName | at-x520OrganizationalUnitName |
      at-x520Title | at-x520dnQualifier | at-x520countryName |
      at-x520SerialNumber | at-x520Pseudonym | at-domainComponent |
      at-emailAddress, ... }

SingleAttribute{ATTRIBUTE:AttrSet} ::= SEQUENCE {
      type      ATTRIBUTE.&id({AttrSet}),
      value     ATTRIBUTE.&Type({AttrSet}{@type})
  }

-- MATCHING-RULE - ещё один тип-класс, который
-- мы не стали приводить здесь, чтобы не усложнять фрагмент.

ATTRIBUTE ::= CLASS {
      &id             OBJECT IDENTIFIER UNIQUE,
      &Type           OPTIONAL,
      &equality-match MATCHING-RULE OPTIONAL,
      &minCount       INTEGER DEFAULT 1,
      &maxCount       INTEGER OPTIONAL
  } WITH SYNTAX {
      [TYPE &Type]
      [EQUALITY MATCHING RULE &equality-match]
      [COUNTS [MIN &minCount] [MAX &maxCount]]
      IDENTIFIED BY &id
  }

Наверх

DER-кодировка SubjectAltName

SubjectAltName потенциально очень сложное расширение версии 3 сертификата X.509 (хотя до самых сложных ему ещё далеко). Для данного примера мы постарались сделать его настолько простым, насколько позволяет здравый смысл. Из назначения данного расширения нам известно, что его OID — 2 5 29 17. В общем случае оно не является критичным расширением (если только атрибут subject того сертификата X.509, в котором оно присутствует, не пуст (не имеет значения нулевой длины), в этом случае оно должно быть помечено как критичное). Содержимым данного расширения является пользовательский тип GeneralNames. У нас будет два имени GeneralNames (строго согласно вариантам CHOICE GeneralName): одно directoryName — пользовательского типа Name (DN со значением cn=me, o=my), а второе dNSName — универсального типа IA5String (со значением example.com). Кодировка будет приведена иерархически, а затем в виде собранной строки. Для удобства мы используем стиль комментирования ASN.1, хотя в данном случае он неприемлем.

Источник ASN.1, который мы будем использовать (только те части, которые относятся к данному примеру):


ext-SubjectAltName EXTENSION ::= { SYNTAX
       GeneralNames IDENTIFIED BY id-ce-subjectAltName }
   id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

   GeneralName ::= CHOICE {
        otherName                   [0]  INSTANCE OF OTHER-NAME,
        rfc822Name                  [1]  IA5String,
        dNSName                     [2]  IA5String,
        x400Address                 [3]  ORAddress,
        directoryName               [4]  Name,
        ediPartyName                [5]  EDIPartyName,
        uniformResourceIdentifier   [6]  IA5String,
        iPAddress                   [7]  OCTET STRING,
        registeredID                [8]  OBJECT IDENTIFIER
   }

   Extension{EXTENSION:ExtensionSet} ::= SEQUENCE {
      extnID      EXTENSION.&id({ExtensionSet}),
      critical    BOOLEAN  DEFAULT FALSE,
      extnValue   OCTET STRING (CONTAINING
                  EXTENSION.&ExtnType({ExtensionSet}{@extnID}))
                  --  Содержит закодированное в DER значение ASN.1,
                  --  соответствующее типу данного расширения,
                  --  идентифицируемому по extnID.
  }

  Name ::= CHOICE { -- единственный на сегодняшний день допустимый вариант --
      rdnSequence  RDNSequence }

  RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

  DistinguishedName ::=   RDNSequence

  RelativeDistinguishedName  ::=
      SET SIZE (1 .. MAX) OF SingleAttribute { {SupportedAttributes} }

  --  Известные элементы имени для DN:

  SupportedAttributes ATTRIBUTE ::= {
      at-name | at-surname | at-givenName | at-initials |
      at-generationQualifier | at-x520CommonName |
      at-x520LocalityName | at-x520StateOrProvinceName |
      at-x520OrganizationName | at-x520OrganizationalUnitName |
      at-x520Title | at-x520dnQualifier | at-x520countryName |
      at-x520SerialNumber | at-x520Pseudonym | at-domainComponent |
      at-emailAddress, ... }

  SingleAttribute{ATTRIBUTE:AttrSet} ::= SEQUENCE {
      type      ATTRIBUTE.&id({AttrSet}),
      value     ATTRIBUTE.&Type({AttrSet}{@type})
  }

ATTRIBUTE ::= CLASS {
-- порядок кодирования берётся из тела класса, а не из WITH SYNTAX
      &id             OBJECT IDENTIFIER UNIQUE,
      &Type           OPTIONAL,
      &equality-match MATCHING-RULE OPTIONAL,
      &minCount       INTEGER DEFAULT 1,
      &maxCount       INTEGER OPTIONAL
  } WITH SYNTAX {
      [TYPE &Type]
      [EQUALITY MATCHING RULE &equality-match]
      [COUNTS [MIN &minCount] [MAX &maxCount]]
      -- только OID является обязательным
      IDENTIFIED BY &id
  }

DirectoryString{INTEGER:maxSize} ::= CHOICE {
      teletexString    TeletexString(SIZE (1..maxSize)),
      printableString  PrintableString(SIZE (1..maxSize)),
      bmpString        BMPString(SIZE (1..maxSize)),
      universalString  UniversalString(SIZE (1..maxSize)),
      uTF8String       UTF8String(SIZE (1..maxSize))
  }

Определение получилось многословное, так что Вы рискуете за деревьями не увидеть леса (если вдуматься, идиома довольно глупая, хорошо что мы редко вдумываемся).

-- Каждое расширение обёрнуто в последовательность, ограничивающую это расширение.
-- Это обусловлено назначением Extension:
-- Extension{EXTENSION:ExtensionSet} ::= SEQUENCE

-- DER-кодирование SEQUENCE-обёртки EXTENSION:

3042
-- Тип (тег) - 16 (шестнадцатеричное 10) = SEQUENCE 
-- с установленным битом Constructed (шестнадцатеричное 20, 10 | 20 = 30 (шестнадцатеричное));
-- длина = 66 (шестнадцатеричное 42), включает все элементы данной SEQUENCE (5 + 61);
-- значением будут все элементы данной последовательности SEQUENCE.

  -- DER-кодирование id (OID = 2.5.29.17)
  -- id-ce-subjectAltName OBJECT IDENTIFIER ::=  { id-ce 17 }

  0603551d11

  -- Тип (тег) - 6 (шестнадцатеричное 06) = OBJECT IDENTIFIER;
  -- длина = 3;
  -- значение - кодировка идентификатора 2 5 29 17 (в LDAP-формате 2.5.29.17)
  -- (подробнее о том, как это кодируется в DER).

  -- Пункт critical (логическое значение (BOOLEAN)) будет включён сюда,
  -- если его значением будет TRUE (пункты в SEQUENCE упорядочены).
  -- Поскольку в этом примере его значение FALSE (по умолчанию (DEFAULT)),
  -- мы можем его опустить.

  -- DER-кодирование GeneralNames начинается с последовательности SEQUENCE
  -- GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
  -- Ключевое слово SIZE и назначенные ограничения в кодировке не отображаются.

  303b

  -- Тип (тег) - 16 (шестнадцатеричное 10) = SEQUENCE
  -- с установленным битом Constructed (шестнадцатеричное 20), 10 | 20 = 30 (шестнадцатеричное);
  -- длина = 59 (шестнадцатеричное 3b), включает все элементы данной SEQUENCE (13 + 46);
  -- значением будут все элементы данной последовательности SEQUENCE.

    -- DER-кодирование dNSName = example.com

    160b6578616d706c652e636f6d

    -- Тип (тег) - 22 (шестнадцатеричное 16) = IA5String;
    -- длина - 11 (шестнадцатеричное 0b);
    -- значением является шестнадцатеричное представление (ascii) строки 'example.com'.

    -- DER-кодирование directoryName (DN) cn=me, o=my
    -- RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
    -- Это будет последовательность (SEQUENCE) RDN: RDN = cn=me, RDN = o=my.

    301a

    -- Тип (тег) - 16 (шестнадцатеричное 10) = SEQUENCE
    -- с установленным битом Constructed (шестнадцатеричное 20), 10 | 20 = 30 (шестнадцатеричное);
    -- длина = 29 (шестнадцатеричное 1d), включает все элементы данной SEQUENCE (13 + 13);
    -- значением будут все элементы данной последовательности SEQUENCE.

      -- Каждое RDN начинается с набора (SET):
      -- RelativeDistinguishedName  ::=
      --      SET SIZE (1 .. MAX) OF SingleAttribute { {SupportedAttributes} }

      -- DER-кодирование RDN cn=my
      -- OID атрибута cn (commonName) = 2 5 4 3
      -- Начало набора SET:

      310b

      -- Тип (тег) - 17 (шестнадцатеричное 11) = SET
      -- с установленным битом Constructed (шестнадцатеричное 20), 11 | 20 = 31 (шестнадцатеричное);
      -- длина = 11 (шестнадцатеричное 0b), включает все элементы данного SET (11);
      -- значением будут все пункты данного набора SET.

        -- Далее следует SEQUENCE с пунктами данного атрибута (cn)

        3009

        -- Тип (тег) - 16 (шестнадцатеричное 10) = SEQUENCE
        -- с установленным битом Constructed (шестнадцатеричное 20), 10 | 20 = 30 (шестнадцатеричное);
        -- длина = 29 (шестнадцатеричное 1d), включает все элементы данной SEQUENCE (5 + 4);
        -- значением будут все элементы данной последовательности SEQUENCE.

        -- DER-кодирование cn (commonName) OID 2 5 4 3

        0603550403

        -- Тип (тег) - 6 (шестнадцатеричное 06 hex) = OBJECT IDENTIFIER;
        -- длина = 3;
        -- значение - кодировка идентификатора 2 5 4 3 (в LDAP-формате 2.5.4.3)
        -- (подробнее о том, как это кодируется в DER).

        -- DER-кодирование 'me' (cn=me)

        13026d65

        -- Тип (тег) - 19 (шестнадцатеричное 13) = PrintableString
        -- (один из вариантов CHOICE для DirectoryString);
        -- длина = 11 (шестнадцатеричное 0b);
        -- значением является шестнадцатеричное представление (ascii) строки 'me'.

      -- А теперь всё то же самое для RDN o=my (OID атрибута o (organization) = 2 5 4 10),
      -- начиная с определения SET для RDN:

      310b3009060355040a13026d79

-- Полная строка октетов с DER-кодировкой:
30420603551d11303b160b6578616d706c652e636f6d301a310b3009060355040
313026d65310b3009060355040a13026d79

Наверх

PKCS ContentInfo — ANS.1 версии 2002 года

В данном рабочем примере используется определение ASN.1 (версии 2002 года) для ContentInfo — базовой структуры, применяемой в большинстве структур PKCS и CMS. (Смотрите также рабочий пример ContentInfo с более простым определением ASN.1 (версии 1988 года)). Фрагмент взят из раздела 9 RFC 6268.

Если Вы внимательно изучили предыдущий пример, то в этом для Вас будет не так много нового, несмотря на то, что он значительно длиннее, чем его аналог 1988 года (а также охватывает немного больше материала). Мы добавили комментарии в попытке объяснить ставший более непонятным синтаксис:


-- Здесь представлена ещё одна удобная для пользователя (!) часть WITH SYNTAX
-- (функционально идентичная части TYPE-IDENTIFIER),
-- позволяющая использовать опциональную часть [TYPE &Type]

CONTENT-TYPE ::= CLASS {
     &id        OBJECT IDENTIFIER UNIQUE,
     &Type      OPTIONAL
   } WITH SYNTAX {
       [TYPE &Type] IDENTIFIED BY &id
   }

   -- При любом применении пользовательского типа ContentType
   -- будет использоваться id (&id) из класса CONTENT-TYPE, а не, скажем, TYPE-IDENTIFIER,
   -- в котором, к слову, используется тот же самый идентификатор.

   ContentType ::= CONTENT-TYPE.&id

   ContentInfo ::= SEQUENCE {
     contentType        CONTENT-TYPE.
                     &id({ContentSet}),
     content            [0] EXPLICIT CONTENT-TYPE.
                     &Type({ContentSet}{@contentType})}

   ContentSet CONTENT-TYPE ::= {
     -- Определяет набор распознаваемых типов контента.
     -- Все они являются ссылками на значение, указывающими на OID-значения:
     ct-Data | ct-SignedData | ct-EncryptedData | ct-EnvelopedData |
     ct-AuthenticatedData | ct-DigestedData, ... }

-- Удобный для пользователя (!) формат CONTENT-TYPE с опущенным опциональным [TYPE &Type]:

ct-Data CONTENT-TYPE ::= { IDENTIFIED BY id-data }

-- OID-значение, которое упомянуто выше:

id-data OBJECT IDENTIFIER ::= { iso(1) member-body(2)
     us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 }

Единственное отличие между этим определением и версией 1988 года в том, что здесь приводятся некоторые ограничения на OID, которые можно использовать, а в версии 1988 года их нет.

Наверх

DER-кодировка для ContentInfo (версии 2002 года)

DER-кодировка, генерируемая из определения ASN.1 (версии 2002 года) для ContentInfo, идентична той, которая получалась для определения ASN.1 версии 1988 года. Единственное отличие будет в том, что значение OID, используемое в примере 1988 года, будет отклонено разборщиком/кодировщиком ASN.1 (версии 2002 года), поскольку оно не находится в допустимом диапазоне (представлен в обновленной версии источника ASN.1 версии 2002 года). Однако, в связи с тем, что в примере использовался фиктивный OID, предназначенный только для иллюстрации общего принципа кодирования OID, кодировка остаётся актуальной.

Наверх

ASN.1 и DER для сертификата X.509 (полностью)

Итак, мы, наконец, возвращаемся к первоначальному примеру ASN.1, с которого когда-то начинали. Теперь он должен быть вполне понятен, за исключением одной довольно несуразной части синтаксиса. Ещё раз приведём полный фрагмент ASN.1:

Certificate  ::=  SIGNED{TBSCertificate}

  TBSCertificate  ::=  SEQUENCE  {
      version         [0]  Version DEFAULT v1,
      serialNumber         CertificateSerialNumber,
      signature            AlgorithmIdentifier{SIGNATURE-ALGORITHM,
                                {SignatureAlgorithms}},
      issuer               Name,
      validity             Validity,
      subject              Name,
      subjectPublicKeyInfo SubjectPublicKeyInfo,
      ... ,
      [[2:               -- При наличии такого расширения, version ДОЛЖНА быть v2
      issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
      subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL
      ]],
      [[3:               -- При наличии такого расширения, version ДОЛЖНА быть v3
      extensions      [3]  Extensions{{CertExtensions}} OPTIONAL
      ]], ... }

Нотация с двойными квадратными скобками определяет границы того, что называется группой добавления расширений. Определение такой группы начинается с [[ и заканчивается на ]]. В приведённом выше определении сертификата у нас два таких пункта. В обоих группах добавления расширений используется необязательное свойство номер версии (значения 2: и 3: сразу за открывающим ограничителем [[). Примечание: Слово расширение термина ASN.1 группа добавления расширений не имеет ничего общего с используемыми в источниках ASN.1, относящихся к сертификатам X.509, определениями Extension, EXTENSION или даже extensions, просто несчастливое совпадение.

Современные разборщики ASN.1 обожают такие штуки. Эффект от DER-кодирования данного дополнительного (по отношению к его предшественнику 1988 года) синтаксиса нулевой, то есть в итоге получится ничего. (Есть, однако, очень веские причины использовать этот расширенный синтаксис). На декодирование могут повлиять только следующие строки:


-- всё это опциональные элементы

 issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- При наличии такого расширения, version ДОЛЖНА быть v2 или v3
 subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- При наличии такого расширения, version ДОЛЖНА быть v2 или v3

 extensions      [3]  Extensions OPTIONAL
                          -- При наличии такого расширения, version ДОЛЖНА быть v3 --  }

  -- Эффектом от приведённого выше определения является то, что
  -- все расширения V3 X.509 (такие как SubjectAltName)
  -- инкапсулируются в следующую закодированную в DER обёртку:

  a3xx

  -- a3 (шестнадцатеричное) - это кодировка [3] EXPLICIT (EXPLICIT используется по умолчанию
  -- для данного модуля и было опущено).
  -- Тип (тег) состоит из кода контекстно-специфичного
  -- класса (шестнадцатеричное 80), установленного бита Constructed (шестнадцатеричное 20)
  -- и номера тегированного типа = 3 (шестнадцатеричное 03);
  -- длина = xx (сумма всех присутствующих расширений V3);
  -- значением будут все присутствующие расширения V3.
  -- Если в сертификате не присутствует никаких расширений V3, то, поскольку данный элемент
  -- является опциональным (и подразумевается, что получатель может распознать его отсутствие),
  -- эта обёртка (a3xx) отсутствует.

  -- В свою очередь, каждое расширение V3 оборачивается в стандартную последовательность,
  -- начинающуюся с:

  30yy

Наверх

DER-кодировка для сертификата X.509 (полностью)

На то, чтобы показать, как кодируются все элементы сертификата X.509, можно было бы затратить всю свою жизнь, но единственное, чего мы бы добились в итоге — смерть от скуки последнего нашего читателя (надеемся, что дочитав до этого момента, Вы ещё живы). Многие отдельные поля мы уже рассмотрели (весьма детально) в предыдущих рабочих примерах, а самые интересные моменты мы дополнительно разобрали в тексте выше. Определения ASN.1 были взяты из RFC 5912, но, как Вам уже известно, старые версии ASN.1 (RFC 5280 и более ранние), хотя и в меньшей степени нравятся современным разборщикам ASN.1/кодировщикам DER, возможно, более просты для понимания нами, обычными смертными. Кодировка DER любой версии ASN.1 выходит идентичной. До сих пор в ходу множество сертификатов, выпущенных давно (и даже очень давно).

Если Вы ожидали примера DER-кодирования и чувствуете себя облапошенными, не расстраивайтесь сильно. Возможно, мы сохранили Вашу жизнь или, хотя бы, душевное здоровье.

Наверх

Зарезервированные/ключевые слова ASN.1

Здесь приводится неполный список используемых в ASN.1 зарезервированных/ключевых слов, в который также входят универсальные типы. Полный список можно найти в разделе 11.27 стандарта X.680.

Зарезервированное/ключевое слово Применение/предназначение
ANY DEFINED BY ANY DEFINED BY — идиома, входившая в устаревший набор зарезервированных/ключевых слов и литералов (ANS.1 1988/X.208). Могла быть использована разборщиком/кодировщиком DER, но не отображалась в каком-либо закодированном в DER битовом потоке. Это "синтаксический сахар" — отлично подходит для людей, но не более того. В определениях по новому стандарту (ASN.1 2002) обычно заменяется на IDENTIFIED BY, как показано на примере информационного объекта TYPE-IDENTIFIER.
APPLICATION Используется только с тегированными типами ([x]). Указывает, что данный тег используется некоторым приложением, и порядок его использования определяется этим приложением. Для нас до сих пор загадка, в каких случаях нужно использовать ключевое слово APPLICATION, а в каких — PRIVATE.
BIT Появляется только в контексте универсального типа BIT STRING.
BY "Синтаксический сахар". Появляется в сочетании с другими словами, чаще всего с IDENTIFIED BY (X.681), ENCODED BY и CONSTRAINED BY. Может быть полезно для разборщиков ASN.1/кодировщиков DER, отлично подходит для чтения людьми, но в DER-кодировке отсутствует.
CHOICE Предваряет список элементов и указывает, что только один тип из доступных вариантов будет присутствовать в закодированной в DER форме. Само же ключевое слово CHOICE не присутствует в закодированной в DER форме. CHOICE ограничивается фигурными скобками {...}. Пример:
Time ::= CHOICE {
      utcTime        UTCTime,
      generalTime    GeneralizedTime }
CLASS Данное ключевое слово используется в назначениях того, что мы называем тип-классами, а в стандарте (X.681) это называется информационными объектами. Пример использования в назначении смотрите в описании ключевого слова TYPE-IDENTIFIER.
CONTAINING Указывает на то, что тип (универсальный или пользовательский) может иметь ограничение на содержимое (согласно разделу 11 стандарта X.682), то есть в нём может содержаться только некоторый другой тип (универсальный или пользовательский).
DEFAULT Указывает значение по умолчанию для данного назначения типа. При использовании с пользовательскими или универсальными типами обычно также будет присутствовать зарезервированное/ключевое слово OPTIONAL. При использовании с тегированными типами OPTIONAL подразумевается неявно. Пример:
TBSCertificate  ::=  SEQUENCE  {

  version         [0]  Version DEFAULT v1,

  -- Тегированный тип (контекстно специфичный) со значением тега = 0.
  -- Version - пользовательский тип.
  -- Значение по умолчанию (DEFAULT) - v1 (ссылка на значение).
  -- В качестве значения по умолчанию может указываться и явное значение,
  -- например, 0 или 537

  -- другие элементы для краткости пропущены
  }

Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }

-- Назначение пользовательского типа Version определяет диапазон
-- допустимых значений, включающий значение v1 (поименованное число),
-- числовое значение которого - 0.
EXPLICIT Используется совместно с синтаксисом [x] (тегированный тип, раздел 30 стандарта X.680), определяющим типы контекстно-специфичного класса, класса приложения или закрытого класса, и указывает, что пункт определяемого класса, содержащий заданное значение тега x, будет закодирован в DER и добавлен (с заданным битом Constructed) перед универсальным типом данного элемента. Значения по умолчанию для тегированных типов определяются для модуля ASN.1 с использованием директив DEFINITIONS IMPLICIT TAGS | EXPLICIT TAGS. При отсутствии таких директив, значением по умолчанию является EXPLICIT, и в определениях тегированных типов это ключевое слово может быть опущено. (Смотрите также описание ключевого слова IMPLICIT). Пример:
-- фрагмент источника ASN.1 (из назначения GeneralNames)
 ...
 dNSName    [2] EXPLICIT    IA5String,
 ....

-- кодируется в DER как (шестнадцатеричное представление)
A2071605AAAAAAAAAA

-- Тег = 2 (шестнадцатеричное 02), признак контекстно-специфичного класса (шестнадцатеричное 80)
-- и признак Constructed (шестнадцатеричное 20)  = (80 | 20 | 02) = A2 (шестнадцатеричное),
-- длина = 7 (включая последующий тип)
-- тег встроенного типа 22 (шестнадцатеричное 16) = IA5String, длина = 5
-- и значение AAAAAAAAAA (произвольные пять октетов в шестнадцатеричном виде для иллюстрации),
-- которое, согласно источнику ASN.1, должно интерпретироваться декодером как представление DNS-имени.
-- Примечание: в случае EXPLICIT указывается и порядок использования (тег), и формат данных.
Во многих случаях в определённых в RFC модулях источников ASN.1 предоставляются и IMPLICIT, и EXPLICIT-версии определения типов, что, по мнению авторов RFC, даёт разработчикам свободу выбора одного из них. Декодеры DER должны быть готовы обработать любой формат.
FROM Используется разборщиком ASN.1/кодировщиком DER для ограничения набора символов, которые могут присутствовать в строках универсальных типов. В кодировке DER не отображается.
IDENTIFIED BY Строго говоря, является не зарезервированным/ключевым словом, а литералом (раздел 10.7 стандарта X.681). Мы добавили его в этот список, поскольку... ну, просто добавили, и всё. Это литерал, используемый в удобной для пользователя(!) части (определяется ключевым словом WITH SYNTAX) класса TYPE-IDENTIFIER (информационного объекта, определяемого в приложении D стандарта X.681). "Синтаксический сахар" в чистом виде. Возможно, используется разборщиком ASN.1 и удобен для пользователя, но в кодировке DER не играет никакой роли.
IMPLICIT Используется совместно с синтаксисом [x] (тегированный тип, раздел 30 стандарта X.680), определяющим типы контекстно-специфичного класса, класса приложения или закрытого класса, и указывает, что пункт соответствующего класса, содержащий заданное значение тега x, будет закодирован в DER и заменит универсальный тип данного элемента. Значения по умолчанию для тегированных типов определяются для модуля ASN.1 с использованием директив DEFINITIONS IMPLICIT TAGS | EXPLICIT TAGS. При отсутствии таких директив, значением по умолчанию является EXPLICIT, и в определениях тегированных типов это ключевое слово может быть опущено. (Смотрите также описание ключевого слова EXPLICIT). Пример:
-- фрагмент источника ASN.1 (из назначения GeneralNames)
 ...
 dNSName    [2] IMPLICIT    IA5String,
 ....

-- кодируется в DER как (шестнадцатеричное представление)
8205AAAAAAAAAA

-- Тег = 2 (шестнадцатеричное 02) и признак контекстно-специфичного класса (шестнадцатеричное 80)
-- (80 | 02) = 82 (шестнадцатеричное),
-- длина = 5,
-- значение AAAAAAAAAA (произвольные пять октетов в шестнадцатеричном виде для иллюстрации),
-- которое, согласно источнику ASN.1, должно интерпретироваться декодером
-- как IA5String-представление DNS-имени.
-- В случае IMPLICIT указывается порядок использования (тег), а формат данных подразумевается
-- согласно этому тегу (в случае EXPLICIT формат данных идентифицируется явно).
-- В случае IMPLICIT кодирование всегда получается короче, чем в случае EXPLICIT,
-- но при этом требуется больше информации из источника ASN.1.
Во многих случаях в определённых в RFC модулях источников ASN.1 предоставляются и IMPLICIT, и EXPLICIT-версии определения типов, что, по мнению авторов RFC, даёт разработчикам свободу выбора одного из них. Декодеры DER должны быть готовы обработать любой формат.
IMPORTS Поддерживает несколько форматов (как и всё в ASN.1), но наиболее часто используемая версия указывает на то, что одно или несколько назначений (пользовательских типов или тип-классов) будут импортированы в модуль ASN.1 из (FROM) другого модуля ASN.1, идентифицируемого (с помощью OBJECT IDENTIFIER) и поименованного (с помощью имени модуля). Может быть использовано несколько последовательностей FROM. В примере ниже показано определение модуля, в котором используется директива IMPORTS (взято из раздела 14 RFC 5912):

PKIX1Explicit-2009
      {iso(1) identified-organization(3) dod(6) internet(1)
      security(5) mechanisms(5) pkix(7) id-mod(0)
      id-mod-pkix1-explicit-02(51)}

-- Имя и OBJECT IDENTIFIER этого модуля.

  DEFINITIONS EXPLICIT TAGS ::=

-- По существу, определяет модуль как назначение, начинающееся с
-- BEGIN, и продолжающееся до END.

  BEGIN

IMPORTS

-- Указывает на то, что одно или несколько назначений типов будет импортировано.

  Extensions{}, EXTENSION, ATTRIBUTE, SingleAttribute{}

-- Определяет список назначений, которые будут импортированы
-- из заданного модуля ASN.1.

  FROM PKIX-CommonTypes-2009
  -- Идентифицирует имя модуля ASN.1, из которого будут импортированы
  -- данные назначения.

      {iso(1) identified-organization(3) dod(6) internet(1) security(5)
      mechanisms(5) pkix(7) id-mod(0) id-mod-pkixCommon-02(57)}

      -- Идентифицирует OID модуля, из которого будут импортированы
      -- данные назначения.

  -- Опционально, любое количество импортируемых назначений и определений FROM.

  ....

  END
-- Конец модуля.
MAX Используется совместно с ключевым словом SIZE для задания неопределённой верхней границы для некоторых универсальных типов/тегов. Типичный пример использования:
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
-- Диапазон (1..MAX) задаёт верхнюю и нижнюю границы, в данном случае
-- в последовательности SEQUENCE должен присутствовать хотя бы один пункт.
-- Ключевое слово MAX не определено для SEQUENCE и указывает на то, что
-- разработчик "отправителя" волен выбрать устраивающее его ограничение,
-- а "получатель" должен быть готов получить любое количество пунктов.
		
Ключевое слово SIZE и любые верхние и нижние границы не кодируются в DER-форму (они используются только разборщиком ASN.1/кодировщиком DER). Примечание: При наличии ключевого слова SIZE в последовательности, должна использоваться форма SEQUENCE ...... OF. При использовании MAX с конкретными универсальными типами, он принимает значение, определяемое этими типами.
MIN Используется совместно с ключевым словом SIZE для задания неопределённой нижней границы для некоторых универсальных типов. MIN не имеет определённого значения в ASN.1, но получает своё значение из универсального типа, с которым оно ассоциировано.
OCTET Присутствует только в контексте OCTET STRING (универсального типа).
OPTIONAL Может присутствовать в любой строке пункта в составе последовательности SEQUENCE, и всегда указывается после определения типа. Говорит о том, что данный пункт может как присутствовать, так и не присутствовать в закодированной форме. Пример:
StupidType ::= SEQUENCE {
      one        One,
      two        Two OPTIONAL
      }
One ::= blah
Two ::= blah
PRIVATE Используется только с тегированными типами ([x]). Указывает на то, что данный тег закрытый (что бы это ни значило). Для нас до сих пор загадка, в каких случаях нужно использовать ключевое слово PRIVATE, а в каких — APPLICATION.
SIZE Указывает на наличие верхней и/или нижней границы значения универсального типа, с которым оно ассоциировано. Типичный пример использования:

GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

-- Диапазон (1..MAX) задаёт верхнюю и нижнюю границы, в данном случае
-- в последовательности SEQUENCE должен присутствовать хотя бы один пункт.
-- Ключевое слово MAX не определено для SEQUENCE (как и для любого другого типа),
-- и указывает на то, что разработчик "отправителя" волен выбрать устраивающее его
-- ограничение, а "получатель" должен быть готов получить любое количество пунктов.
Ключевое слово SIZE и любые верхние и нижние границы не кодируются в DER-форму. Они используются только в "отправителях" (при кодировании). Обратите внимание, что при использовании SEQUENCE совместно с SIZE, применяется форма SEQUENCE ......OF, и в этом случае ограничение SIZE может применяться только к пунктам одного и того же типа.
SYNTAX Путанная штука (впрочем, они тут на каждом шагу). Имеет два разных применения. Первое — в форме WITH SYNTAX, используется для введения удобного пользователю(!) синтаксиса в назначении тип-класса (раздел 10 стандарта X.681). Второе — в качестве литерала (разделы 10.6 и 10.7 стандарта X.681), используется само по себе в форме SYNTAX. Смотрите примеры обоих типов применения в определении TYPE-IDENTIFIER.
TYPE-IDENTIFIER Встроенный (поскольку его определение дано в приложении A стандарта X.681) информационный объект (мы называем их тип-классами). Его назначение:
TYPE-IDENTIFIER ::= CLASS
{
-- &-нотация - артефакт определения CLASS (класса информационного объекта).
-- В имени, следующем за &, регистр символов имеет существенное значение
-- (определяет, будет ли это идентификатор или имя типа).
  &id OBJECT IDENTIFIER UNIQUE,
  &Type
}
WITH SYNTAX
     {SYNTAX &Type IDENTIFIED BY &id}

-- SYNTAX и IDENTIFIED BY - это удобные для пользователя (!) литералы,
-- позволяющие человеку разобраться, что использовать и куда это помещать
-- (надеемся, наши технические термины Вас не пугают). Что-то вроде шаблона
-- для определения тип-класса, представляющего собой очень общую конструкцию,
-- состоящую из идентификатора объекта (id) и определения содержимого данных (SYNTAX),
-- которое является типом, определяемым (IDENTIED BY) этим идентификатором
-- id (OBJECT IDENTIFIER).

-- Пример использования (взят из назначения GeneralName):
OTHER-NAME ::= TYPE-IDENTIFIER

-- Таким образом, OTHER-NAME будет состоять из OID (id) и содержимого (типа),
-- определяемого данным OID.

-- А ссылка на значение, использующая данный класс, может выглядеть так:

my-OtherName OTHER-NAME ::= {
  SYNTAX My-Name IDENTIFIED BY my-oid
}
-- Обратите внимание на верхний и нижний регистр первых букв в именах.
My-Name ::= IA5String

my-oid OBJECT IDENTIFIER ::= {2 3 5 6}
Также смотрите описание универсального типа INSTANCE OF, который связан с этим классом и дополняет данное описание.
UNIQUE Название говорит само за себя. Разборщик ASN.1 будет следить за тем, чтобы каждый экземпляр элемента, для которого указано это ключевое слово, отличался от других (был уникальным) в рамках модуля. Используется только с тип-классами (информационными объектами, определёнными в X.681). Пример использования смотрите в определении TYPE-IDENTIFIER. Используется только разборщиком ASN.1, к DER-кодированию отношения не имеет.
WITH "Синтаксический сахар". Всегда появляется в паре с другим ключевым словом, например, WITH SYNTAX или WITH COMPONENTS. Возможно, полезно для разборщика ASN.1 и имеет смысл для людей, но в кодировке DER не присутствует.

Наверх

Универсальные типы и теги ASN.1

В этом разделе приведены распознаваемые ASN.1 универсальные типы. Эти же имена типов (состоящие из заглавных букв) также включены в список зарегистрированных/ключевых слов ASN.1. Полный список зарезервированных слов дан в разделе 11.27 стандарта X.680.

Имя Тег
(дес.)
Тег
(шест.)
Строковый
тип
Примитив (P)
Составной (C)
Описание
BOOLEAN 1 01 Нет P FALSE = 0 (шестнадцатеричное 00), TRUE — на усмотрение отправителя сообщения (отличное от 0 значение!), однако, чаще всего, 1 (шестнадцатеричное FF).
INTEGER 2 02 Нет P Определяет целое число. Может иметь ограничения по размеру и допустимым значениям, например:
Stupid ::= INTEGER
-- простое назначение
Dumb ::= INTEGER SIZE(12..200)
-- ограничение по размеру (используется кодировщиком, невидимо для декодировщика)
Dozy  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }
-- диапазон допустимых значений (используется кодировщиком, невидим для декодировщика)

Также смотрите описание типа REAL.
BIT STRING 3 03 Нет P/C В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых должна быть битовой строкой BIT STRING), и в этом случае исходный элемент будет составным (Constructed, C). Также этот тип может использоваться для значений со значимыми битами:
SignificantBits ::= BIT STRING {
  firstBit   (0)
  secondBit  (1)
  thirdBit   (3)
  -- другие биты, если нужно
}

-- Нумерация бит СЛЕВА НАПРАВО, начиная с 0
-- (не как в нормальном соглашении ITU),
-- позволяет определить бесконечно большое количество бит.
-- Например, если задан только secondBit,
-- значение будет 40 (шестнадцатеричное).
OCTET STRING 4 04 Да P/C Номинально, строковый тип с неограниченным набором символов. В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых должна быть строкой октетов OCTET STRING), и в этом случае исходный элемент будет составным (Constructed, C). То есть:
-- Подразумевается, что кодируется строка октетов AAAAAA (3 байта).
-- Она может быть закодирована (и обычно так и бывает) просто как:
0403AAAAAA

-- Но её также можно закодировать (тип 04 (OCTET STRING)
-- с установленым битом Constructed (шестнадцатеричное 24)):
2407
-- первая подстрока
0401AA
-- вторая подстрока
0402AAAA
-- Итоговая DER-кодировка варианта с использованием подстрок:
24070401AA0402AAAA
NULL 5 05 Нет P Что-то вроде "ничего". Кодируется с типом 5, длиной 0 и без значения. Иногда используется для заполнения пустого места, в особенности вместо параметров алгоритмов, в основном по историческим причинам.
OBJECT IDENTIFIER 6 06 Нет P Часто сокращается до OID. OID — это строка, состоящая из разделённых пробелами (в ASN.1) или точками (в LDAP) числовых значений (подидентификаторов), уникально идентифицирующая сущность. Так, 2 5 4 3 (2.5.4.3 в LDAP) уникально идентифицирует атрибут commonName (cn). Некоторая специфичная для LDAP информация об OID дана здесь. Детальное описание кодирования дано в разделе 8.19 стандарта X.690. Каждое числовое значение (подидентификатор в терминологии X.690) кодируется отдельно (без какого-либо разделителя), за исключением первых двух числовых значений (подидентификаторов), для которых применяется формула ((1 * 40) + 2). Так, для нашего примера 2 5 4 3 первые два значения (2 5) будет кодироваться как (2 * 40) + 5 = 85 (шестнадцатеричное 55), а третье и четвёртое значения (4 3) кодируются отдельно, в итоге получается шестнадцатеричная строка 550403. Значения, большие или равные 127, кодируются в один октет, в противном случае они должны использовать два или более октетов, в каждом из которых, кроме последнего, должен быть задан бит 8 (для кодирования IETF — бит 0). Итак, только 7 бит в каждом октете (биты с 7 по 1 (ITU) или с 1 по 7 (IETF)) используются для кодирования подидентификаторов. Предположим, мы хотим закодировать подидентификатор 327 (шестнадцатеричное 147); в результате кодирования мы получим два шестнадцатеричных октета 8247 (подробное описание смотрите здесь). Поскольку назначения OBJECT IDENTIFIER определяют специфическое значение (а именно OID), они всегда будут ссылками на значение. Пример:
id-data OBJECT IDENTIFIER ::= { iso(1) member-body(2)
     us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 }
-- В данном назначении используется формат namedNumber, но его можно было бы записать и так:
-- id-data OBJECT IDENTIFIER ::= { 1 2 840 113549 1 7 }
-- DER-кодировка
-- Тип/тег = 06, длина = 9
0609
 -- части 1 и 2: (1 * 40) = 40 + 2 = 42 (шестнадцатеричное 2a))
 2a
 -- часть 840: 840 = шестнадцатеричное 0348
 8648
 -- часть 113549: 113549 = шестнадцатеричное 01bb8d
 86f70d
 -- части 1 и 7:
 0107
- полная DER-кодировка
06092a864886f70d0107
На этом сайте можно часами наслаждаться OID, также как и на этом. О, эти сладкие OID!
ObjectDescriptor 7 07 Да P Номинально, строковый тип с неограниченным набором символов, кодируется так же, как и OCTET STRING. Пригодное для чтения человеком текстовое описание какого-либо объекта.
INSTANCE OF, EXTERNAL 8 08 Нет C Применим только к типу, которому назначается тип-класс (информационный объект) TYPE-IDENTIFIER. Тип INSTANCE OF расширяется до последовательности SEQUENCE, содержащей два пункта, используемых классом TYPE-IDENTIFIER (id и Type), либо до какого-либо производного от него типа:
-- Общее определение (приложение C стандарта X.681):
SEQUENCE {
  type-id <DefinedObjectClass>.&id,
  value [0] <DefinedObjectClass>.&Type
}

-- Пример из назначения GeneralName:
GeneralName ::= CHOICE {
      otherName   [0]  INSTANCE OF OTHER-NAME,
      -- остальные варианты опущены для упрощения
      }
OTHER-NAME ::= TYPE_IDENTIFIER

-- Таким образом, INSTANCE OF OTHER-NAME расширяется до:

SEQUENCE {
  type-id OTHER-NAME.&id,
  value [0] OTHER-NAME.&Type
}
-- В приложении C стандарта X.681 предполагается, что тегированный тип [0]
-- всегда будет явным (EXPLICIT).
REAL 9 09 Нет P Используется для чисел с плавающей запятой и недесятичных чисел.
ENUMERATED 10 0A Нет P Определяет именованный набор в котором (если нет никаких числовых переопределений) за элементами будут закреплены последовательные целочисленные значения. Примеры:
Dumb ::= ENUMERATED {
-- Используется простое перечисление идентификаторов,
-- начиная с нуля (red = 0, blue = 1 и так далее).
red
blue
green
}
Dumb ::= ENUMERATED {
-- Используется формат перечисления поименованные числа
-- (namedNumber), начиная с 7 (red = 7, blue = 8 и так далее).
red(7)
blue
green
}
--
Несмотря на то, что назначение ENUMERATED в источнике ASN.1 может иметь значительное количество идентификаторов или поименованных чисел, в DER будет закодировано только одно целое число, отражающее конкретный пункт, выбранный в экземпляре элемента, который ссылается на данное перечисление.
ENUMERATED PDV 11 0B Нет ?
UTF8String 12 0C Да P/C Набор символов ISO/IEC 10646-1 (поднаборы даны в приложении A).
RELATIVE-OID 13 0D Нет P Кодируется практически также, как и OBJECT IDENTIFIER, но представляет собой только частичный OID. Получатель должен знать (алгоритм зависит от приложения), каким образом собрать полный OID путём комбинирования данного относительного OID с каким-то другим OID, ранее каким-то волшебным образом закодированным. Может рассматриваться как способ сокращения объёма данных в ситуациях, когда используются множество OID, производных от одного и того же базового OID.
SEQUENCE, SEQUENCE OF 16 10 Нет C Предваряет упорядоченный список пунктов различного типа. Хотя, если подходить строго, SEQUENCE представляет собой упорядоченный список пунктов различного типа, а SEQUENCE OF представляет собой упорядоченный список пунктов одного и того же типа. Однако в кодировке DER SEQUENCE и SEQUENCE OF рассматриваются как одно и то же (они кодируются с одинаковым значением тега DER). Тем не менее, разборщик ASN.1 в праве принудительно применять различные правила обработки. На практике почти всегда используется вариант SEQUENCE, OF применяется в случаях, когда в определении присутствует ключевое слово SIZE. Пункты последовательности SEQUENCE ограничиваются фигурными скобками {...} — полезная деталь разметки. Тип SEQUENCE кодируется в DER, при этом всегда устанавливается бит Constructed (C).
SET, SET OF 17 11 Нет C Предваряет неупорядоченный список пунктов различного типа. Хотя, если подходить строго, SET представляет собой неупорядоченный список пунктов различного типа, а SET OF представляет собой неупорядоченный список пунктов одного и того же типа. Однако в кодировке DER SET и SET OF рассматриваются как одно и то же (они кодируются с одинаковым значением тега DER). Тем не менее, разборщик ASN.1 в праве принудительно применять различные правила обработки, чтобы учесть вариант OF. На практике почти всегда используется вариант SET, вариант SET OF применяется в случаях, когда в определении присутствует ключевое слово SIZE. Пункты SET ограничиваются фигурными скобками {}. Тип SET кодируется в DER, при этом всегда устанавливается бит Constructed (C).
NumericString 18 12 Да P/C Строковый тип с ограниченным набором символов. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 и SPACE. В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
PrintableString 19 13 Да P/C Строковый тип с ограниченным набором символов. a-z, A-Z, ' () +,-.?:/= и SPACE (подмножество ASCII/IRA5). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
TeletexString, T61String 20 14 Да P/C Строковый тип с ограниченным набором символов (ITU T.61 и T.101). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
VideotexString 21 15 Да P/C Строковый тип с ограниченным набором символов (ITU T.100 и T.101). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
IA5String 22 16 Да P/C Строковый тип с ограниченным набором символов. International Alphabet 5 (более корректно, International Reference Alphabet No. 5, IRA5), он же ISO 646 и ITU T.50. Национальный вариант для США — ASCII. В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
UTCTime 23 17 Да P Номинально, строковый тип с неограниченны набором символов. Кодируется также, как и OCTET STRING.
GeneralizedTime 24 18 Нет S Номинально, строковый тип с неограниченным набором символов. Кодируется также, как и OCTET STRING.
GraphicString 25 19 Да P/C Строковый тип с ограниченным набором символов. Печатные символы IRA5 (ISO 646/T.50), смотрите IA5String. В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
VisibleString, ISO646String 26 1A Да P/C Строковый тип с ограниченным набором символов. Печатные символы IRA5 (в основном исключены все управляющие символы из ISO 646/T.50). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
GeneralString 27 1B Да P/C Строковый тип с ограниченным набором символов. Печатные символы IRA5 (ISO 646/T.50). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
UniversalString 28 1C Да P/C Набор символов ISO10646-1 (или подмножество из приложения A). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).
CHARACTER STRING 29 1D Да ? Специальное использование, расширенный набор символов, определяемый по идентификатору OBJECT IDENTIFIER. Если Вы столкнетесь с этим, смело покупайте лотерейный билет: выигрыш обеспечен.
BMPString 30 1E Да P/C Часть 1 ISO/IEC 10646-1: Архитектура и базовый многоязычный уровень (подмножество определено в приложении A). В большинстве закодированных значений строки будут представлены целиком как единый примитив (Primitive, P). Однако стандарты позволяют конструировать строку из подстрок (каждая из которых может быть различного типа), и в этом случае исходный элемент будет составным (Constructed, C).

Наверх

Обзор DER (схема кодирования TLV)

DER (Distinguished Encoding Rules, Особые правила кодирования) представляют собой классическую схему кодирования TLV (Type, Length, Value (тип-длина-значение)). Они немного отличаются от BER (Basic Encoding Rules, Базовые правила кодирования) тем, что в них удалены некоторые опции с целью упрощения.

Чтобы разобраться со всеми деталями DER, следует изучить стандарт X.690. Версия стандарта X.690 2002 года свободно распространяется ITU, поскольку выпущены уже более новые версии, однако указанная версия стандарта используется в текущих (по состоянию на 2017 год) RFC. Поскольку этот документ разрабатывался ITU, он одновременно и невероятно увлекателен, и даже местами понятен (порой уже после первого прочтения). Но это не означает, что Вы сразу во всём разберётесь.

Мы приводим здесь кодирование поля типа, поскольку это имеет решающее значение для понимания многих концепций из настоящего руководства, при этом мы стараемся придерживаться выбранной для этого руководства терминологии. Далее приведены некоторые тривиальные заметки о кодировании длины, которые могут прояснить (или не прояснить) мгновенно возникающие животрепещущие вопросы.

Наверх

DER-кодирование типов/тегов

Поскольку значения типов повсеместно используются во многих концепциях данного руководства по выживанию, их построение подробно объясняется с использованием разработанной для этого руководства терминологии. Однако, в качестве дефинитивного источника следует рассматривать раздел 8.1.2 стандарта X.690 (2002), свободно распространяемого ITU.

DER-кодирование типов (в X.690 это называется идентифицирующими октетами) представляет собой первую часть любой последовательности DER-кодирования. В контексте кодирования тип может состоять из одного или нескольких октетов. Для значений тегов в диапазоне от 0 до 30 включительно (от 00 до 1E в шестнадцатеричном представлении) используется кодирование в один октет. Для значений тегов, больших 30 (больших 1E в шестнадцатеричном представлении) используется кодирование в два и более октетов.

DER-кодирование типов в один октет

Для значений тегов в диапазоне от 0 до 30 (включительно), в который входят все универсальные типы, кодирование в один октет выполняется следующим образом:

Нумерация бит ITU 8 7 6 5 4 3 2 1
Нумерация бит IETF 0 1 2 3 4 5 6 7
Класс C/P Значение тега
Универсальный 0 0 0 = Primitive
1 = Constructed
значения тегов от 0 (шестнадцатеричное 00) до 30 (шестнадцатеричное 1E)
Приложения 0 1
Контекстно-специфичный 1 0
Закрытый 1 1

Примечания:

  1. Биты кодирования класса соотносятся с тегированными типами (синтаксис [x]), охватывающими контекстно-специфичный класс, класс приложения и закрытый класс. "Универсальный" класс указывает на то, что кодируемый пункт является универсальным типом.

  2. При кодировании универсального типа специальным битом указывается, будет ли этот тип сконструированным (Constructed) или примитивным (Primitive). Контекстно-специфичный класс, класс приложения и закрытый класс также используют этот бит при указании (явном или подразумевающимся по умолчанию) зарезервированного/ключевого слова EXPLICIT. Пример кодирования контекстно-специфичного класса с битом Constructed.

DER-кодирование типов в несколько октетов

При значениях тегов больше 30 (шестнадцатеричное 1e) используется кодирование в два или более октетов как показано ниже.

Продолжение следует...

Наверх

DER-кодирование длины

Детальное описание кодирования длины пункта дано в стандарте X.690 (раздел 8.1.3). Стандарт X.690 2002 года свободно распространяется ITU, поскольку выпущены уже более новые версии, однако указанная версия стандарта используется в текущих (по состоянию на 2017 год) RFC. Поскольку этот документ разрабатывался ITU, он одновременно и невероятно увлекателен, и даже местами понятен.

В стандарте X.690 определяется два типа длины - определённый и неопределённый. В DER используется только определённый метод. По крайней мере, меньше читать.

Следующие замечания могут пролить (или не пролить) свет на некоторые животрепещущие вопросы:

  1. Поле длины относится только к части значение (в терминологии X.690 октеты содержимого) схемы кодирования TLV (тип-длина-значение), из него явно исключены части тип (один или несколько октетов) и длина (один или несколько октетов).

  2. Если при кодировании типа установлен бит Constructed, то длина включает в себя все пункты конструкции (например, SEQUENCE). Номинально часть значение составного пункта рассматривается как все пункты в этой конструкции.

  3. Пункты, часть значение которых (смотрите первое замечание выше) меньше или равна 127 (шестнадцатеричное 7F), могут быть закодированы в один октет. Пункты, длина значения которых превышает 129, будут кодироваться в два и более октета (у всех этих октетов, кроме последнего, будет задан бит 8 (ITU)), как описано в разделе 8.1.3.5 стандарта X.690.

DER-кодирование универсальных типов

Детальное описание кодирования универсальных типов дано в стандарте X.690 (разделы с 8.2 по 8.22). Стандарт X.690 2002 года свободно распространяется ITU, поскольку выпущены уже более новые версии, однако указанная версия стандарта используется в текущих (по состоянию на 2017 год) RFC. Поскольку этот документ разрабатывался ITU, он одновременно и невероятно увлекателен, и даже местами понятен.

Некоторые замечания по кодированию включены, там, где это уместно, в описание соответствующего типа в списках универсальных типов и зарезервированных/ключевых слов.

Наверх

Изменения страницы

Дата последнего изменения указана внизу страницы.

  1. 2 сентября 2017 г.: Первый вариант страницы с несколькими пропущенными элементами в зарезервированных/ключевых словах (в основном экзотическими и мало распространёнными), а также с несколькими недостающими пояснениями в описаниях универсальных типов.

Наверх



Проблемы, комментарии, предположения, исправления (включая битые ссылки) или есть что добавить? Пожалуйста, выкроите время в потоке занятой жизни, чтобы написать нам, вебмастеру или в службу поддержки. Оставшийся день Вы проведёте с чувством удовлетворения.

Нашли ошибку в переводе? Сообщите переводчикам!

Copyright © 1994-2018 ZyTrax, Inc. Все права защищены. Последнее изменение страницы: 12 мая 2018 г.
Переведено участниками проекта Pro-LDAP.ru в 2018 г.