Ласкаво просимо в ASN.1 і DER

Даний документ дасть вам вступну інформацію про структури даних та формати, які визначають сертифікати, що використовуються у HTTPS. Він повинен бути зрозумілим для всіх, хто має мінімальний досвід у сфері комп’ютерних технологій та трішки знайомий із сертифікатами.

HTTPS сертифікат - це тип файлу, зрештою, як і будь-який інший файл. Його вміст відповідає формату, визначеному за RFC 5280. Визначення передаються в ASN.1, яка є мовою, що використовується для того, щоб визначити формати файлів або структури даних, відповідно. До прикладу, у С ви, можливо, напишете:

struct point {
  int x, y;
  char label[10];
};

У Go ви б написали:

type point struct {
  x, y int
  label string
}

І в ASN.1 це виглядало б так:

Point ::= SEQUENCE {
  x INTEGER,
  y INTEGER,
  label UTF8String
}

Перевагою написання визначень в ASN.1 замість Go чи C є те, що вони є незалежними від мови. Ви можете виконати визначення ASN.1Точки у будь-якій мові або бажано використати засіб, що приймає визначення ASN.1 і автоматично генерує код на вашій улюбленій мові. Набір ASN.1 визначень називається “модулем.”

Не менш важлива річ про ASN.1 це те, що у ній наявні формати для серіалізації-- способи для перетворення структури даних у пам’яті у серію байтів, чи файлу і навпаки. Це дозволяє сертифікат згенерований одним комп’ютером читати на іншому, навіть якщо той комп’ютер використовує інший CPU та іншу операційну систему.

Є декілька інших мов, що роблять ті ж самі речі , що й ASN.1. Наприклад, Protocol Buffers пропонує як і мову для визначення типів, так і формат серіалізації для кодування об’єктів визначених типів. Thrift має також і мову і формат серіалізації. Protocol Buffers і Thrift можна легко використовувати, щоб визначити формат HTTPS сертифікатів, але ASN.1 (1984) мав значну перевагу перед усіма наявними у час, коли були розроблені сертифікати (1988) і HTTPS (1994).

Протягом років ASN.1 було переглянуто чимало разів і редакції, зазвичай, позначалися відповідно до року публікування. Даний документ має на меті навчити достатньо ASN.1, щоб чітко розуміти RFC 5280 та інші стандарти, що відносяться до HTTPS сертифікатів. Тому ми в основному говоритимемо про версію 1988 року із декількома новими функціями, що були додані у пізніших версіях. Ви можете завантажити різні версії безпосередньо з ITU, але із застереженням, що деякі будуть доступні тільки для ITU членів. Відповідні стандарти X.680 (визначають мову ASN.1) і X.690 (визначають формати серіалізацій DER і BER). Попередні версії даних стандартів були X.208 і X.209, відповідно.

Особливі правила кодування “Distinguished Encoding Rules” (DER) - головний формат серіалізації в ASN.1. Вони варіант основних правил кодування “Basic Encoding Rules” (BER) із доданою канонізацією. Наприклад, якщо тип містить SET OF, члени повинні будуть відсортовані по DER серіалізації.

Сертифікат відображений у DER часто надалі закодований у PEM, який використовує base64, щоб закодувати довільні байти (також ‘+’ і ‘/') і додати розділювальні рядки ("-----BEGIN CERTIFICATE-----" і “-----END CERTIFICATE-----"). PEM корисний простотою копіювання і вставляння.

Цей документ спочатку опише типи та позначення використовувані ASN.1, а тоді опише як об’єкти визначені за допомогою ASN.1 будуть закодовані. Ви можете вільно рухатися розділами, а саме тому, що деякі риси мови ASN.1 безпосередньо визначають деталі кодування. Цей документ надає перевагу звичнішим термінам, тому використовує “byte” замість “octet” і “value” замість “contents”. Тут “serialization” і “encoding” взаємозамінні.

Типи

INTEGER

Ваш добрий старий знайомий INTEGER. Він може бути як позитивним, так і негативним. Що є найбільш незвичним для INTEGER у мові ASN.1 це те, що він може бути довільної величини. Не вистачає місця в int64? Немає проблем. Це саме стає у пригоді, коли зображуєте речі такі, як модуль RSA, який набагато більший від int64 (наприклад, 22048 ). Технічно у DER є максимальний integer, але він незвично великий: довжина будь-якого поля у DER може бути виражена, як серія аж до 126 байтів. Отже, найбільший INTEGER, який ви можете відтворити у DER є 256(2**1008)-1. Для справжнього нескінченного INTEGER вам прийдеться закодовувати його у BER, який дозволяє мати нескінченні поля.

Рядки

ASN.1 має багато типів рядків: BMPString, GeneralString, GraphicString, IA5String, ISO646String, NumericString, PrintableString, TeletexString, T61String, UniversalString, UTF8String, VideotexString і VisibleString. Для цілей HTTPS сертифікатів здебільшого ви маєте турбуватися про PrintableString, UTF8String і IA5String. Для даного поля тип рядка визначається ASN.1 модулем, який визначає саме поле. Наприклад:

CPSuri ::= IA5String

PrintableString - це обмежена підмножина ASCII, яка дозволяє використовувати буквенно-цифрові символи, пробіли та певну кількість розділових знаків: ' () + , - . / : = ?. Зокрема вона не містить * чи @. Більш обмежувальні типи рядків не мають переваг у розмірі зберігання.

Такі поля, як DirectoryString у RFC 5280 дозволяють серіалізацію коду для вибору рядка певного типу серед інших. Оскільки DER кодування включає тип рядка, який ви використовуєте впевніться, що кодуючи щось, як PrintableString воно відповідає вимогам PrintableString.

IA5String, заснований на Міжнародному алфавіті №5, є менш обмеженим: він дозволяє майже будь-який ASCII символ і використовується для адреси електронної пошти, DNS імен та URL-адрес у сертифікатах. Візьміть до уваги, що є кілька значень байтів де IA5 значення байту буде іншим від ідентичного US-ASCII значення.

TeletexString, BMPString і UniversalString вже застаріли для використання у HTTPS сертифікатах, але ви їх все ще можете побачити під час аналізу старіших сертифікатів ЦС, які є довговічними та можуть передувати старінню.

Рядки в ASN.1 не закінчуються, як рядки у C і C++. Насправді це цілком нормально мати вбудовані нульові байти. Це може спричинити нестабільність, коли дві системи трактують один і той же ASN.1 рядок по-різному. Наприклад, деякі ЦС можна було обманути за видачу  “example.com\0.evil.com” на основі права власності evil.com. У той час бібліотеки перевірки сертифікатів видавали результат, як дійсний для “example.com”. Будьте уважні при опрацюванні ASN.1 рядків у C та C++, для уникнення нестабільності.

Дата і Час

Також є багато різних типів часу: UTCTime, GeneralizedTime, DATE, TIME-OF-DAY, DATE-TIME і DURATION. Для HTTPS сертифікатів вам потрібно потурбуватися тільки про UTCTime та GeneralizedTime.

UTCTime представляє дату і час у форматі YYMMDDhhmm[ss] із можливим зсувом часового поясу “Z” для позначення Зулу (він же UTC, він же 0 часовий пояс). Наприклад, обидва часи UTCTimes 820102120000Z і 820102070000-0500 позначають однаковий час: 2 січня, 1982 року, 7:00 у Нью-Йорку (UTC-5) і 12:00 ночі в UTC.

Оскільки UTCTime двозначний, щодо того чи це 1900-ті, чи 2000-ні, RFC 5280 уточнює те, що він представляє роки із 1950 по 2050 роки. RFC 5280 також вимагає, щоб часова зона “Z” використовувалася і секунди були включені.

GeneralizedTime підтримує дати після 2050 шляхом простого позначення років чотирма цифрами. Він також дозволяє дробові секунди (досить дивно з комою або крапкою як десятковий роздільник). RFC 5280 забороняє дробові секунди та вимагає “Z.”

ІДЕНТИФІКАТОР ОБ’ЄКТА

Ідентифікатори об’єктів є глобально унікальними, ієрархічні ідентифікатори утворені послідовністю цілих чисел. Вони можуть посилатися на будь-які “речі”, але зазвичай використовуються для визначення стандартів, алгоритмів, розширень сертифікатів, організацій або програмної документації. Ось, наприклад: 1.2.840.113549 визначає ТОВ “RSA Security”. RSA пізніше можуть призначити OID (Ідентифікатори об’єкта), що починаються із тим самим префіксом, типу 1.2.840.113549.1.1.11, який ідентифікує sha256WithRSAEncryption, як визначено у RFC 8017.

Подібно, 1.3.6.1.4.1.11129 ідентифікує Google, Inc. Google призначив 1.3.6.1.4.1.11129.2.4.2 щоб ідентифікувати список SCT розширень , які використовуються у Certificate Transparency(що було спочатку розроблено у Google), як визначено у RFC 6962.

Набір дочірніх OID, які можуть існувати у даному префіксі називаються “OID arc.” Оскільки представлення коротших OID менше, вважається, що призначення OID під коротшими дугами(arc) є більш цінним, особливо для форматів де OID мають часто посилатися. Дуга OID 2.5 є призначеною для “Directory Services,” ряд специфікацій, що включає X.509 на яких будуються HTTPS сертифікати. Багато полів у сертифікатах починаються із цієї зручної дуги. Наприклад, 2.5.4.6 означає “countryName,” у той час, як 2.5.4.10 - “organizationName.” Оскільки більшість сертифікатів мають закодовувати кожен із поданих OID щонайменше один раз, це зручно, коли вони короткі.

OID у специфікаціях зазвичай представлені із читабельною назвою та можуть бути вказані шляхом об’єднання з іншим OID. Ось приклад із RFC 8017:

   pkcs-1    OBJECT IDENTIFIER ::= {
       iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
   }
   ...

   sha256WithRSAEncryption      OBJECT IDENTIFIER ::= { pkcs-1 11 }

Нуль

НУЛЬ - це просто НУЛЬ, ви в курсі?

SEQUENCE та SEQUENCE OF

Нехай імена вас не обманюють: оці є двома дуже різними типами. SEQUENCE - є еквівалентом слова “struct” у більшості мов програмування. У ньому міститься фіксована кількість полів різних типів. Наприклад, перегляньте приклад сертифікату нижче.

A SEQUENCE OF містить довільну кількість полів одного типу. Це аналогічно масиву або списку у мові програмування. Наприклад:

   RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

Це може бути 0, 1 чи 7,000 Відносних унікальних імен(RelativeDistinguishedNames) у визначеному порядку.

Виявляється, що SEQUENCE та SEQUENCE OF мають одну схожість - вони обидва закодовані в один і той же спосіб! Більше про це у секції Кодування.

SET та SET OF

Вони є дуже схожими до SEQUENCE та SEQUENCE OF, за винятком лише того, що немає ніякої навмисної семантики прив’язаної до впорядкування елементів, що вони містять. Однак у закодованій формі вони мають бути відсортованими. Приклад:

RelativeDistinguishedName ::=
  SET SIZE (1..MAX) OF AttributeTypeAndValue

Візьміть до уваги: у цьому прикладі використовується ключове слово SIZE, щоб додатково вказати на те, що RelativeDistinguishedName повинен мати хоча б один член, але у загальному SET чи SET OF дозволено мати значення нуля.

BIT STRING і OCTET STRING

Вони містять довільні біти чи байти відповідно. Вони можуть використовуватися для збереження неструктурованих даних таких, як nonces або вивід хеш-функції. Вони також можуть використовуватися як покажчик void в C або порожній тип інтерфейсу (interface{}) в Go: спосіб зберігання даних, які мають структуру, але де ця структура розуміється або визначається окремо від системи типів. Наприклад, підпис на сертифікаті визначається як BIT STRING:

Сертифікат  ::=  SEQUENCE  {
     tbsCertificate       TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signature            BIT STRING  }

Пізніші версії мови ASN.1 допускають більш детальну специфікаціювмісту всередині бітового рядка (і те ж саме відноситься до OCTET STRINGs).

CHOICE і ANY

CHOICE - тип, який може містити рівно один з типів, перерахованих в його визначення. Наприклад, Час може містити рівно один з UTCTime або a GeneralizedTime:

Час ::= CHOICE {
     utcTime        UTCTime,
     generalTime    GeneralizedTime }

ANY вказує на те, що значення може бути будь-яким типом. На практиці це зазвичай обмежується речами, які не можуть бути повністю виражені в граматиці ASN.1. Наприклад:

   AttributeTypeAndValue ::= SEQUENCE {
     type     AttributeType,
     value    AttributeValue }

   AttributeType ::= OBJECT IDENTIFIER

   AttributeValue ::= ANY -- DEFINED BY AttributeType

Це особливо корисно для розширень, де ви хочете залишити місце для додаткових полів, які будуть визначені окремо після публікації основної специфікації, щоб у вас був спосіб зареєструвати нові типи (ідентифікатори об’єктів) і дозволити визначенням для цих типів вказувати, якою має бути структура нових полів.

Зверніть увагу, що ANY є реліквією позначення ASN.1 1988 року. У виданні 1994 року ANY був визнаний застарілим і замінений класами інформаційних об’єктів, які представляють собою химерний, формалізований спосіб визначення типу поведінки розширення, якого люди хотіли від будь-кого. Зміна на сьогодні настільки стара, що останні ASN.1 специфікації (з 2015) навіть не згадують про ANY. Проте якщо ви подивитися у видання 1994 року, то побачите деяке обговорення про перемикання. Я включив старий синтаксис тут, тому що він досі використовується RFC 5280. RFC5912використовує синтакс 2002 ASN.1, щоб виразити ті ж типи з RFC 5280 та кількох пов’язаних специфікацій.

Інші позначення

Коментарі починаются з --. Поля SEQUENCE або SET можуть бути позначені OPTIONAL, або ж вони можуть бути позначені DEFAULT foo, що означає те ж саме, що і OPTIONAL за винятком того, що коли поле відсутнє, слід вважати, що воно містить “foo” Типи з довжиною (стрічки, октет і бітові рядки, набори та послідовність речей) можуть бути задані параметром SIZE, що обмежує їх довжину, або до точної довжини або до діапазону.

Типи можуть бути обмежені певними значеннями за допомогою фігурних дужок після визначення типу. Цей приклад визначає, що поле Версії може мати три значення і присвоює змістовні назви таким значенням:

Версія ::= INTEGER { v1(0), v2(1), v3(2) }

Це також використовується у присвоєнні назв конкретним OIDs (зверніть увагу, що це - єдине значеня, без ком, які вказують на альтернативні значення). Приклад з RFC 5280.

id-pkix OBJECT IDENTIFIER ::=
         { iso(1) identified-organization(3) dod(6) internet(1)
                    security(5) mechanisms(5) pkix(7) }

Також, ви побачите [number], IMPLICIT, EXPLICIT, UNIVERSAL, і APPLICATION. Вони визначають деталі того, як має бути закодовано значення, про що ми поговоримо нижче.

Кодування

ASN.1 асоціюється з багатьма кодуваннями: BER, DER, PER, XER, і більше. Основні правила кодування (BER) досить гнучкі. Особливі правила кодування (DER) є підмножиною BER з правилами канонізації, тому існує тільки один спосіб вираження даної структури. Упаковані правила кодування (PER) використовують менше байтів для введення речей, так що вони корисні, коли час пробілу або передачі припадає на преміум-версію. XML правила кодування (XER) корисні тоді, коли ви з деякої причини маєте бажання використати XML.

HTTPS сертифікати, як правило, закодовані в DER. Їх можна закодувати у BER, але оскільки значення підпису обчислюється за еквівалентним кодуванням DER, а не за точними байтами в сертифікаті, кодування сертифіката в BER створює непотрібні проблеми. Я опишу BER і поясню по ходу додаткові обмеження, передбачені DER.

Я закликаю вас прочитати цей розділ з цієюрозшифровкою реального сертифіката< / a>, відкритої в іншому вікні.

Значення довжини типу

BER - це кодування довжиною типу, так само, як посилювачі протоколу, Thrift. Це означає, що під час читання байтів, які закодовані BER, спочатку ви стикаєтеся з типом під назвою ASN.1 як тегу. Це байт, або серія байтів, які повідомляють вам, який тип кодований: INTEGER, або UTF8String, або структура, тощо.

тип довжина значення
02 03 01 00 01

Потім ви стикаєтеся з довжиною: числом, яке вказує на те, скільки байтів даних вам потрібно прочитати для отримання значення. Потім, звичайно, приходить байт, що містить саме значення. Як приклад, шістнадцяткові байти 02 03 01 00 01 предствалятимуть INTEGER (тег 02 відповідає INTEGER типу), з довжиною 03 та трибайтовим значенням, що складається з 01 00 01.

Тип-довжина-значення відрізняється від розділених кодувань, таких як JSON, CSV, або XML, де замість визнання довжини поля наперед, ви зчитуєте байти, поки не натиснули на очікуваний роздільник (наприклад, } в JSON, або </some-tag> в XML).

Тег

Мітка зазвичай є одна байта. Є засіб для довільного кодування великі номери тегів із використанням кількох байтів (форма “великий номер тега”), але зазвичай це не потрібно.

Ось декілька прикладів:

Тег (десятковий) Тег (hex) Тип
2 02 INTEGER
3 03 ПОЧАТОК
4 04 ЗАХИСТ
5 05 Нуль
6 06 ІДЕНТИФІКАТОР ОБ’ЄКТА
12 0C UTF8 Рядок
16 10 (та 30)* SEQUENCE та SEQUENCE OF
17 11 (та 31)* SET та SET OF
19 13 PrintableString
22 16 IA5Струн
23 17 UTCTime
24 18 Узагальнений час

Це і декілька інших, яких я пропустив за нудні, це теги “універсальні”, тому що вони вказані в ядрі ASN. специфікація та означають те ж саме у всіх модулях ASN.1.

Ці теги можуть бути більшими за 31 (0x1F), і це є гарною причиною: Біти 8, 7 і 6 (високі біти тега байт) використовуються для кодування додаткової інформації таким чином, будь-який універсальний тег повинен бути використовувати форму “high tag”, яка приймає додаткові байти. Є небагато універсальних тегів вище 31, але вони досить рідкісні.

Два теги, позначені *, завжди закодовані як 0x30 або 0x31, тому що біт 6 використовується для позначення того, чи є поле конструкцією для Примітивно. Ці теги завжди Конструкційовані, тому кодування має біт 6 встановлений в 1. Перегляньте розділ зібраний проти Примітивних для деталей.

Класи тегів

Просто тому, що універсальний клас використав усі “хороші” номери тегів, це не означає, що нам не пощастило визначити власні теги. Існує також класи “застосунок”, “приватний” і “залежний від контексту”. Ці відрізняються бітами 8 і 7:

Клас Біт-8 Біт-7
Універсальний 0 0
Програма 0 1
Контекстно-конкретний 1 0
Приватний 1 1

Специфікації переважно використовують теги у універсальному класі, оскільки вони надають найважливіші будівельні блоки. Наприклад, серійний номер в сертифікаті закодовано його в звичайному ‘INTEGER, тег 0x02. Але іноді специфікація повинна визначити теги в контекстно-конкретному класі розмежувати записи у SET або SEQUENCE який визначає додаткові матеріали, або розчленувати CHOICE з декількома записами, які мають такий тип. Наприклад, розгляньмо це визначення:

Point ::= SEQUENCE {
  x INTEGER OPTIONAL,
  y INTEGER OPTIONAL
}

Оскільки OPTIONAL поля повністю виключені з кодування коли вони відсутні, було б неможливо відрізнити точку з лише координату з точки х з лише координатою у. Наприклад, ви кодуєте точку з єдиною координатою х з 9, схожою на (30 означає SEQUENCE тут):

30 03 02 01 09

Ось SEQUENCE довжиною 3 (байт), що містить INTEGER довжиною 1, яка має значення 9. Але ви також кодуєте точку з координатами у на 9 точно однаковим чином, тобто існує двозначність.

Інструкції з кодування

Для вирішення цієї двозначності специфікація повинна вказати інструкції  з кодування, які призначать унікальний тег для кожного запису. І тому, що нам не дозволено застосовувати теги UNIVERSAL, ми маємо використовувати один з інших, наприклад APPLICATION:

Point ::= SEQUENCE {
  x [APPLICATION 0] INTEGER OPTIONAL,
  y [APPLICATION 1] INTEGER OPTIONAL
}

Хоча для цього використання ви можете набагато частіше використовувати спеціальний контекст, який представлений числом в дужках самотужки:

Point ::= SEQUENCE {
  x [0] INTEGER OPTIONAL,
  y [1] INTEGER OPTIONAL
}

Так що тепер, щоб закодувати точку з просто координатами х в 9, замість кодування х в якості UNIVERSAL INTEGER, ви б задали біт 8 і 7 закодованого тегу (1, 0) щоб вказати контекст визначений клас, і встановити низькі біти в 0, даючи це кодування:

30 03 80 01 09

І щоб представити точку з просто у=9, ви б зробили те саме, за винятком того, що ви встановите низькі біти до 1:

30 03 81 01 09

Або ви можете представити точку з координатами х і у, що дорівнює 9:

30 06 80 01 09 81 01 09

Довжина

Довжина кортежу довжиною мітки завжди відображає загальну кількість байтів у об’єкті, включаючи всі субоб’єкти. Таким чином, SEQUENCE з одним полем не має довжини 1; він має задану довжину кількість байт форми це поле займе вгору.

Кодування довжини може приймати дві форми: короткий або довгий. Коротка форма - один байт, від 0 до 127.

Довга форма є принаймні два байти довгими і має трохи 8 першого байту встановлений в 1. Bits 7-1 з першого байта вказує на те,  скільки більше байт знаходиться в полі довжини. Потім решта байтів задає довжину себе, як ціле число з мультибайтом.

Як ви можете уявити, це дозволяє дуже довгі значення. Найдовша можлива довжина почнеться з 254 (довжина байту 255 зарезервована для майбутніх розширень), встановлюючи що на 126 більше бактів знайдеться лише поле довжини. Якщо кожен з цих 126 байт був 255, то це означатиме 21008-1 байт слідувати у полі значення.

Тривала форма дозволяє закодувати однакову довжину декількох способів - для екземпляра, використовуючи два байти для вираження довжини, яка може вміститися в один, або використовуючи довгу форму для вираження довжини, яка може поміститися у короткій формі. DER каже, що завжди використовувати найменшу можливу довжину представлення.

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

Невизначена довжина

Також можливо, в BER, закодувати рядок, SEQUENCE, SEQUENCE OF, SET, SET, або ВСТАНОВІТЬ, де ви не знаєте довжини заздалегідь (для екземпляра, коли виводиться потокове передавання). Для цього ви закодуєте довжину в один байт зі значенням 80, і закодовувати значення як серію закодованих об’єктів разом об’єднуються, з кінцем зазначеним двома байтами 00 (який можна розглядати як об’єкт з нульовою довжиною з тегом 0). Так, для наприклад, невизначеною довжиною кодування UTF8String буде кодування одного або більше UTF8Stres, об’єднаних разом, і об’єднані, нарешті, з 00 00.

Невизначеність може бути довільно вкладена! Так, наприклад, UTF8Рядки, які ви збираєтесь разом для створення невизначеної довжини UTF8String може бути закодована або з заданою довжиною або невизначеною довжиною.

Довжина байту від 80 відрізняється тому, що це не коректна коротка форма або довга довжина. Оскільки біт 8 встановлений на 1, це зазвичай відбувається інтерпретувати як довгу форму, але решта бітів має бути інтерпретованою вказати кількість додаткових байтів, які утворюють довжину. Оскільки біти 7-1 - це все 0, що буде вказувати на довгу кодування з нульовим байтами, що утворюють довжину, що не допускається.

DER забороняє кодування невизначеної довжини. Слід використовувати визначену довжину кодування (тобто з довжиною, вказаною на початку).

Складний чи примітивний

Біт 6 першого байта тегу використовується для того, щоб вказати, чи значення закодоване в примітивній формі чи в складній формі. Примітивне кодування представляє значення безпосередньо - наприклад, у UTF8String значення буде складатися виключно з самого рядка в байтах UTF-8. Складне кодування представляє значення як об"єднання інших закодованих значень. Наприклад, як описано в розділі «Невизначена довжина», UTF8String у складному кодуванні буде складатися з кількох закодованих рядків UTF8 (кожен з тегом і довжиною), об’єднаних разом. Довжина загального рядка UTF8String дорівнюватиме загальній довжині в байтах усіх цих об"єднаних закодованих значень. Складне кодування може використовувати як визначену, так і невизначену довжину. Примітивне кодування завжди використовує визначену довжину, тому що немає способу виразити невизначену довжину без використання складного кодування.

INTEGER, OBJECT IDENTIFIER i NULL мають використовувати примітивне кодування. SEQUENCE, SEQUENCE OF, SET і SET OF мають використовувати складне кодування (оскільки вони за своєю суттю є об"єднанням кількох значень). BIT STRING, OCTET STRING, UTCTime, GeneralizedTime та різні типи рядків можуть використовувати або примітивне кодування, або складне кодування, на розсуд відправника-- у BER. Однак у DER всі типи, які мають вибір кодування між примітивним і складним, повинні використовувати примітивне кодування.

EXPLICIT vs IMPLICIT

Описані вище інструкції кодування, наприклад [1] або [APPLICATION 8] також можуть містити ключове слово EXPLICIT або IMPLICIT (приклад з RFC 5280):

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  Version DEFAULT v1,
     serialNumber         CertificateSerialNumber,
     signature            AlgorithmIdentifier,
     issuer               Name,
     validity             Validity,
     subject              Name,
     subjectPublicKeyInfo SubjectPublicKeyInfo,
     issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     extensions      [3]  Extensions OPTIONAL
                          -- If present, version MUST be v3 --  }

Це визначає, як тег має бути закодований; це не має відношення до того, чи номер тега призначений явно чи ні (оскільки і IMPLICIT, і EXPLICIT завжди йдуть поряд із конкретним номером тега). IMPLICIT кодує поле так само, як і основний тип, але з номером тега та класом, вказаними в модулі ASN.1. EXPLICIT кодує поле як основний тип, а потім обертає його у зовнішнє кодування. Зовнішнє кодування має номер тега та клас із модуля ASN.1 і додатково має встановлений біт Constructed.

Ось приклад інструкції кодування ASN.1 з використанням IMPLICIT:

[5] IMPLICIT UTF8String

Це буде кодувати “привіт” як:

85 02 68 69

Порівняйте з вижчезазначеною інструкцію кодування ASN.1 з використанням EXPLICIT:

[5] EXPLICIT UTF8String

Це буде кодувати “привіт” як:

A5 04 0C 02 68 69

Якщо ключове слово IMPLICIT або EXPLICIT відсутнє, за промовчанням використовується значення EXPLICIT, якщо модуль не встановлює інше значення за замовчуванням у верхній частині за допомогою команди “EXPLICIT TAGS,” “IMPLICIT TAGS,” or “AUTOMATIC TAGS.” Наприклад, RFC 5280 визначає два модулі, в одному з яких за замовчуванням використовуються теги EXPLICIT, а в другому, який імпортує перший, за замовчуванням використовуються теги IMPLICIT. Неявне кодування використовує менше байтів, ніж явне кодування.

AUTOMATIC TAGS - це те саме, що і IMPLICIT TAGS, але з додатковою властивістю: номери тегів ([0], [1] і т. д.) автоматично присвоюються в тих місцях, де вони необхідні, наприклад, у SEQUENCE з необов’язковими полями.

Кодування конкретних типів

У цьому розділі ми поговоримо про те, як кодується значення кожного типу, з наведенням прикладів.

Кодування INTEGER

Integers кодуються як один або більше байтів, у вигляді двох доповнень зі високим бітом (біт 8) крайнього лівого байта як знаковий біт. Як говорить специфікація BER:

Значення двійкового числа, що доповнює двійку, виводиться шляхом нумерації бітів у октетах вмісту, починаючи з біта 1 останнього октету як нульовий біт і закінчуючи нумерацією бітом 8 першого октету. Кожному біту надається числове значення 2N, де N - його положення в наведеній вище послідовності нумерації. Значення двійкового числа з додатком два виходить шляхом підсумовування числових значень, присвоєних кожному біту для тих бітів, які встановлені в одиницю, за винятком біта 8 першого октету, а потім зменшення цього значення на числове значення, присвоєне біту 8 першого октету, якщо цей біт встановлений на одиницю.

Так, наприклад, це однобайтове значення (представлене у двійковій формі) кодує десяткове число 50:

00110010 (== десяткове 50)

Це однобайтове значення (представлене у двійковій формі) кодує десяткове значення -100:

10011100 (== десяткове -100)

Це п’ятибайтове значення (представлене у двійковій формі) кодує десяткове -549755813887 (тобто -239 + 1):

10000000 00000000 00000000 00000000 00000001 (== десяткове -549755813887)

BER та DER вимагають, щоб цілі числа були представлені у максимально короткій формі. Це забезпечується за допомогою цього правила:

... біти першого октету та біт 8 другого октету:

1.  не всі будуть єдиними;
2.  не всі дорівнюють нулю.

Правило (2) приблизно означає: якщо в кодуванні є провідні нульові байти, їх можна просто опустити і отримати те ж число. Біт 8 другого байта тут також важливий, тому що якщо ви хочете представити певні значення, ви повинні використовувати провідний нульовий байт. Наприклад, десяткове число 255 кодується як два байти:

00000000 11111111

Це тому, що однобайтове кодування 11111111 саме по собі означає -1 (біт 8 розглядається як біт знака).

Правило (1) найкраще пояснити на прикладі. Десяткове число -128 кодується як:

10000000 (== десяткове -128)

Однак це також може бути закодовано як:

11111111 10000000 (== десяткове -128, але недійсне кодування)

Якщо розширити, то це -215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 == -27 == -128. Зверніть увагу, що 1 “10000000” був знаковим бітом в однобайтовому кодуванні, але означає 27 в двобайтовому кодуванні.

Це загальне перетворення: для будь-якого від"ємного числа, закодованого як BER (або DER), ви можете додати до нього префікс 11111111 і отримати те саме число. Це називається sign extension. Або еквівалентно, якщо є від’ємне число, де кодування значення починається з 11111111, ви можете видалити цей байт і отримати те саме число. Тому BER і DER потребують найкоротшого кодування.

Кодування INTEGERs з двома доповненнями має практичну дію при видачі сертифікатів: RFC 5280 вимагає, щоб серійні номери були додатними. Оскільки перший біт завжди є знаковим, це означає, що серійні номери, закодовані в DER у вигляді 8 байтів, можуть мати не більше 63 бітів. Кодування 64-бітового додатного серійного номера потребує 9-байтового кодованого значення (при цьому перший байт дорівнює нулю).

Ось кодування INTEGER зі значенням 263+1 (яке виявляється 64-бітовим додатним числом):

02 09 00 80 00 00 00 00 00 00 01

Кодування String

Рядки кодуються як їхні буквальні байти. Оскільки IA5String та PrintableString просто визначають різні підмножини допустимих символів, їх кодування відрізняються лише тегами.

PrintableString, що містить “привіт”:

13 02 68 69

IA5String, що містить “привіт”:

16 02 68 69

UTF8Strings - це те саме, але може кодувати ширший спектр символів. Наприклад, це кодування UTF8String, що містить U+1F60E усміхнене обличчя з сонцезахисними окулярами (😎):

0c 04 f0 9f 98 8e

Кодування дати та часу

UTCTime і GeneralizedTime насправді кодуються як рядки, на диво! Як описано вище в розділі «Типи», UTCTime представляє дати у форматі YYMMDDhhmmss. GeneralizedTime використовує чотиризначний рік YYYY замість YY. Обидва мають додаткове зміщення часового поясу або «Z» (Zulu), щоб позначити відсутність зміщення часового поясу від UTC.

Наприклад, 15 грудня 2019 року о 19:02:10 у часовому поясі PST (UTC-8) представлено в часі UTC як: 191215190210-0800. Закодовано у BER, це:

17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30

Для кодування BER секунди є необов’язковими як у UTCTime, так і в GeneralizedTime, і допускається зміщення часового поясу. Однак DER (поряд із RFC 5280) визначає, що секунди повинні бути присутніми, долі секунди не повинні бути присутніми, а час повинен бути виражений як UTC з формою “Z”.

Вищенаведена дата буде закодована в DER як:

17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a

Кодування OBJECT IDENTIFIER

Як описано вище, OID концептуально є рядом цілих чисел. Вони завжди складаються з принаймні двох компонентів. Першим компонентом завжди є 0, 1, або 2. Коли перший компонент дорівнює 0 або 1, другий компонент завжди менше 40. Через це перші дві складові однозначно представлені як 40*X+Y, де X – перший компонент, а Y – другий.

Так, наприклад, щоб закодувати 2.999.3, ви поєднаєте перші два компоненти в 1079 десяткову (40*2 + 999), що дасть вам “1079.3”.

Після застосування цього перетворення кожен компонент кодується на підставі 128, причому старшим байтом є перший. Біт 8 встановлюється в “1” в кожному байті, крім останнього в компоненті; так ви дізнаєтеся, коли один компонент завершено і починається наступний. Таким чином, компонент «3» буде представлений просто як байт 0x03. Компонент “129” буде представлений у вигляді байтів 0x81 0x01. Після кодування всі компоненти OID об’єднуються разом, щоб утворити закодоване значення OID.

OID мають бути представлені якнайменшою кількістю байтів, будь то BER чи DER. Тому компоненти не можуть починатися з байта 0x80.

Як приклад, OID 1.2.840.113549.1.1.11 (представляючи sha256WithRSAEncryption) кодується так:

06 09 2a 86 48 86 f7 0d 01 01 0b

Кодування NULL

Значення об’єкта, що містить NULL, завжди має нульову довжину, тому кодування NULL завжди є лише тегом і полем довжини, що дорівнює нулю:

05 00

Кодування SEQUENCE

Перше, що потрібно знати про SEQUENCE, це те, що він завжди використовує складне кодування, оскільки містить інші об’єкти. Іншими словами, значення байтів SEQUENCE містять об"єднання закодованих полів цього SEQUENCE (у тому порядку, в якому ці поля були визначені). Це також означає, що біт 6 тегу SEQUENCE (складний чи примітивний біт) завжди має значення 1. Тому, навіть якщо номер тега SEQUENCE технічно дорівнює 0x10, його байт тега після кодування завжди дорівнює 0x30.

Якщо SEQUENCE має поля з анотацією OPTIONAL, вони просто опускаються з кодування, якщо відсутні. У міру того. як декодер обробляє елементи SEQUENCE, він може визначити, який тип декодується на основі того, що було декодовано на даний момент, і байтів тегу, які він зчитує. Якщо існує неоднозначність, наприклад коли елементи мають однаковий тип, модуль ASN.1 повинен визначити інструкції кодування, які присвоюють елементам різні номери тегів.

Поля DEFAULT подібні до полів OPTIONAL. Якщо значення поля є значенням за замовчуванням, воно може бути опущене в кодуванні BER. У кодуванні DER він повинен бути опущений.

Як приклад, RFC 5280 визначає AlgorithmIdentifier як SEQUENCE:

   AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }

Ось кодування AlgorithmIdentifier, що містить 1.2.840.113549.1.1.11. RFC 8017 каже, що “параметри” повинні мати тип NULL для цього алгоритму.

30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00

Кодування SEQUENCE OF

SEQUENCE OF кодується точно так само, як SEQUENCE. Він навіть використовує той самий тег! Якщо ви займаєтеся декодування, єдиний спосіб відрізнити SEQUENCE від SEQUENCE OF - це звернутися до модуля ASN.1.

Ось кодування SEQUENCE OF INTEGER, що містить числа 7, 8 та 9:

30 09 02 01 07 02 01 08 02 01 09

Кодування SET

Як і SEQUENCE, SET є складним, що означає, що його значення байтів є об"єднанням його закодованих полів. Номер його тегу - 0x11. Оскільки біт Constructed vs Primitive (біт 6) завжди встановлений на 1, це означає, що він закодований байтом тегу 0x31.

Кодування SET, як і SEQUENCE, опускає поля OPTIONAL і DEFAULT, якщо вони відсутні або мають значення за замовчуванням. Будь-яка неоднозначність, яка виникає через поля з однаковим типом, повинна бути вирішена модулем ASN.1, а поля DEFAULT ПОВИННІ бути вилучені з кодування DER, якщо вони мають значення за замовчуванням.

У BER SET може бути закодований у будь-якому порядку. У DER SET має бути закодований у порядку зростання за тегом.

Кодування SET OF

Елементи SET OF кодуються так само, як і SET, включаючи байт тегу 0x31. Для кодування DER існує аналогічна вимога, згідно з якою SET OF мають бути закодовані в порядку зростання. Оскільки всі елементи в SET OF мають однаковий тип, упорядкування за тегами недостатньо. Таким чином, елементи SET OF сортуються за їхніми закодованими значеннями, а коротші значення розглядаються так, ніби вони були доповнені нулями праворуч.

Кодування BIT STRING

A BIT STRING з N бітів кодується як N/8 байтів (округлюється в більшу сторону) з однобайтовим префіксом, який містить «кількість невикористаних бітів», для наочності, коли кількість бітів не кратна 8. Наприклад, під час кодування бітового рядка 011011100101110111 (18 біт), нам потрібно щонайменше три байти. Але це трохи більше, ніж нам потрібно: це дає нам можливість використовувати 24 біти. Шість з цих бітів залишаться невикористаними. Ці шість бітів записуються в крайньому правому кінці бітового рядка, так що це кодується як:

03 04 06 6e 5d c0

У BER невикористані біти можуть мати будь-яке значення, тому останнім байтом цього кодування може бути c1, c2, c3 тощо. У DER усі невикористані біти мають дорівнювати нулю.

Кодування OCTET STRING

Рядок OCTET STRING кодується у вигляді байтів, які він містить. Ось приклад OCTET STRING, що містить байти 03, 02, 06 та A0:

04 04 03 02 06 A0

Кодування CHOICE and ANY

A CHOICE or ANY кодується як будь-який тип, який насправді має, якщо не змінено інструкціями кодування. Отже, якщо поле CHOICE в специфікації ASN.1 дозволяє INTEGER або UTCTime, а конкретний об’єкт, який кодується, містить INTEGER, тоді він кодується як INTEGER.

На практиці поля CHOICE часто містять інструкції з кодування. Наприклад, розглянемо цей приклад із RFC 5280, де інструкції кодування необхідні, щоб відрізнити rfc822Name від dNSName, оскільки вони обидва мають базовий тип IA5String:

   GeneralName ::= CHOICE {
        otherName                       [0]     OtherName,
        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 }

Ось приклад кодування GeneralName, що містить rfc822Name a@example.com (нагадаємо, що [1] означає використання тега номер 1, у класі тегів “контекстно-специфічний” (біт 8 встановлений на 1) з методом кодування тегу IMPLICIT):

81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d

Ось приклад кодування GeneralName, що містить dNSName “example.com”:

82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d

Безпека

Важливо бути дуже обережним при декодуванні BER та DER, особливо в мовах, що не забезпечують збереження пам’яті, таких як C та C++. Існує довга історія вразливостей у декодерах. Розбір вхідних даних загалом є поширеним джерелом вразливостей. Формати кодування ASN.1, зокрема, є особливим магнітом для вразливостей. Це складні формати із великою кількістю полів змінної довжини. Навіть довжина має змінну довжину! Крім того, вхідні дані ASN.1 часто контролюються зловмисниками. Якщо вам потрібно розпізнати сертифікат, щоб відрізнити авторизованого користувача від неавторизованого, ви повинні припустити, що в деяких випадках ви розбиратимете не сертифікат, а якийсь дивний вхід, створений для використання помилок у вашому коді ASN.1.

Щоб уникнути цих проблем, найкраще використовувати мову, безпечну для пам’яті, коли це можливо. І незалежно від того, чи можете ви використовувати безпечну для пам’яті мову, краще використовувати компілятор ASN.1 для генерації коду аналізу, ніж писати його з нуля.

Вираження подяки

Я дуже зобов’язаний A Layman’s Guide to a Subset of ASN.1, DER, and BER, який є значною частиною того, як я вивчав ці теми. Я також хотів би подякувати авторам книги A warm welcome to DNS, яка є чудовою для читання та надихнула тон цього документа.

Невеличкий бонус

Ви коли-небудь помічали, що сертифікат із кодуванням PEM завжди починається з “MII”? Наприклад:

-----ПОЧАТКОВИЙ СЕРТИФІКАТ-----

MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
...

Тепер ви знаєте достатньо, щоб пояснити чому! Сертифікат це SEQUENCE, тому він почнеться з байта 0x30. Наступні байти – це поле довжини. Сертифікати майже завжди мають розмір понад 127 байт, тому поле довжини має використовувати довгу форму довжини. Це означає, що перший байт буде 0x80 + N, де N - кількість наступних байтів довжини. N майже завжди дорівнює 2, оскільки саме стільки байтів потрібно для кодування довжин від 128 до 65535, і майже всі сертифікати мають довжину в цьому діапазоні.

Отже, тепер ми знаємо, що перші два байти DER-кодування сертифіката дорівнюють 0x30 0x82. який кодує 3 байти двійкового введення в 4 символи ASCII.Кодування PEM використовує base64, який кодує 3 байти двійкового введення в 4 символи ASCII. Або, говорячи інакше: base64 перетворює 24 біти двійкового введення в 4 символи ASCII, при цьому кожному символу призначається 6 біт введення. Ми знаємо, якими будуть перші 16 біт кожного сертифіката. Щоб довести, що першими символами (майже) кожного сертифіката буде «MII», нам потрібно два, щоб подивитися на наступні 2 біти. Це будуть найзначніші біти найзначнішого байта з двох байтів довжини. Чи будуть колись ці біти встановлені на 1? Тільки якщо сертифікат має довжину понад 16383 байт! Таким чином, ми можемо передбачити, що перші символи сертифіката PEM завжди будуть однаковими. Спробуйте зробити це самі:

xxd -r -p <<<308200 | base64