Смотрите правильно отформатированную оригинальную статью на https://coolaj86.com/articles/asn1-for-dummies/

В моем предыдущем посте CSR, мой старый друг я рассказал об обстоятельствах, которые заставили меня погрузиться в ASN.1.

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

Вы сможете расшифровать и перекодировать одного из этих плохих парней:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiYydo27aNGO9DBUW
eGEPD8oNi1LZDqfxPmQlieLBjVShRANCAAQhPVJYvGxpw+ITlnXqOSikCfz/7zms
yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjK
-----END PRIVATE KEY-----

Если вам нравится, как это звучит, продолжайте читать!

(иначе уходи… наверное)

Первое: Резюме

ASN.1 — это абстрактная синтаксическая нотация. Как это часто бывало в темные века, это «мета»-описание стандарта без реальной стандартизации чего-либо, что вы могли бы конкретно реализовать (вроде OAuth2).

В основном он перечисляет теоретические примитивные и непримитивные типы, такие как число, строка, массив, объект.

Таким образом, было бы правильно сказать, что вывод моей демонстрации asn1-parser является допустимым ASN.1… просто в формате, который больше никто не использует.

x.509 – это набор схем (включая PKCS#1, SEC1, PKCS#8, SPKI, PKIX, CSR и другие), которые описывают определенные способы хранения ASN. .1 данные. Это похоже на json-schema.org или XLST.

Подобно тому, как схема JSON сужает дюжину способов выражения «у человека есть имя и фамилия» (например, firstName, first_name, firstname и first) до одного (возможно, first), x509 делает то же самое для DER ( и АСН.1).

DER (подмножество BER) – это конкретное двоичное представление, используемое в качестве формата соединения для описания схем ASN.1. Это единственная известная мне широко используемая сериализация ASN.1.

В основном это похоже на JSON (или Protobuf), который определяет, как сериализовать и десериализовать типы значений (например, []для массива против 0x30 для последовательности или "" для строки и 0x0c для строки UTF8).

PEM — это не что иное, как DER в кодировке base64.

Вниз и грязь Обзор

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

Данные ASN.1 в кодировке DER состоят из трех частей:

  • Тип
  • Длина
  • Ценность

Весь шпиль — это именно такая последовательность, одна за другой.

Вот как выглядит файл PEM:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiYydo27aNGO9DBUW
eGEPD8oNi1LZDqfxPmQlieLBjVShRANCAAQhPVJYvGxpw+ITlnXqOSikCfz/7zms
yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjK
-----END PRIVATE KEY-----

Это просто DER в кодировке Base64 с заголовком типа (и, если он зашифрован, заголовок для него).

Если вы отрежете -----BEGIN .* и -----END .* и декодируете base64 в буфер (и перекодируете его в шестнадцатеричный), это будет выглядеть так:

30 81 87 02 01 00 30 13  06 07 2A 86 48 CE 3D 02
01 06 08 2A 86 48 CE 3D  03 01 07 04 6D 30 6B 02
01 01 04 20 89 8C 9D A3  6E DA 34 63 BD 0C 15 16
78 61 0F 0F CA 0D 8B 52  D9 0E A7 F1 3E 64 25 89
E2 C1 8D 54 A1 44 03 42  00 04 21 3D 52 58 BC 6C
69 C3 E2 13 96 75 EA 39  28 A4 09 FC FF EF 39 AC
C8 E0 C8 2A 24 AE 78 C3  7E DE 98 FD 89 C0 E0 0E
74 C9 97 BB 0A 71 6C A9  E0 DC A6 73 DB B9 B3 FA
72 96 22 55 C9 DE BC D2  18 CA

Если вместо этого вы возьмете PEM и бросите его в https://lapo.it/asn1js/, вы увидите как шестнадцатеричное, так и абстрактное обозначение ASN.1 (избыточное…) на более или менее простом английском языке. :

SEQUENCE (3 elem)
  INTEGER 0
  SEQUENCE (2 elem)
    OBJECT IDENTIFIER 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type)
    OBJECT IDENTIFIER 1.2.840.10045.3.1.7 prime256v1 (ANSI X9.62 named elliptic curve)
  OCTET STRING (1 elem)
    SEQUENCE (3 elem)
      INTEGER 1
      OCTET STRING (32 byte) 898C9DA36EDA3463BD0C151678610F0F
                             CA0D8B52D90EA7F13E642589E2C18D54
      [1] (1 elem)
        BIT STRING (520 bit) 00000100001000010011110101010010010
                             11000101111000110110001101001110000…

Если вы сравните их, вы можете разбить шестнадцатеричный код на ту же древовидную структуру, что и ASN.1:

30 81 87
   02 01 00
   30 13
      06 07 2A8648CE3D0201
      06 08 2A8648CE3D030107
   04 6D
      30 6B
         02 01 01
         04 20 898C9DA36EDA3463 BD0C151678610F0F
                   CA0D8B52D90EA7F1 3E642589E2C18D54
         A1 44
            03 42 00
                  04 213D5258BC6C69C3 E2139675EA3928A4
                     09FCFFEF39ACC8E0 C82A24AE78C37EDE
                     98FD89C0E00E74C9 97BB0A716CA9E0DC
                     A673DBB9B3FA7296 2255C9DEBCD218CA

И если вы хотите увидеть JSON-представление того же самого, я создал демонстрацию синтаксического анализатора asn1 и код, который будет делать именно это:

{
  "type": "0x30", "lengthSize": 1, "length": 135,
  "children": [
    {
      "type": "0x02", "lengthSize": 0, "length": 1,
      "value": "0x00"
    },
    {
      "type": "0x30", "lengthSize": 0, "length": 19,
      "children": [
        {
          "type": "0x06", "lengthSize": 0, "length": 7,
          "value": "0x2a8648ce3d0201"
        },
        {
          "type": "0x06", "lengthSize": 0, "length": 8,
          "value": "0x2a8648ce3d030107"
        }
      ]
    },
    {
      "type": "0x04", "lengthSize": 0, "length": 109,
      "children": [
        {
          "type": "0x30", "lengthSize": 0, "length": 107,
          "children": [
            {
              "type": "0x02", "lengthSize": 0, "length": 1,
              "value": "0x01"
            },
            {
              "type": "0x04", "lengthSize": 0, "length": 32,
              "value": "0x898c9da36eda3463bd0c151678610f0f
                          ca0d8b52d90ea7f13e642589e2c18d54"
            },
            {
              "type": "0xa1", "lengthSize": 0, "length": 68,
              "children": [
                {
                  "type": "0x03", "lengthSize": 0, "length": 66,
                  "value": "0x04213d5258bc6c69c3e2139675ea3928
                            a409fcffef39acc8e0c82a24ae78c37ede
                            98fd89c0e00e74c997bb0a716ca9e0dca6
                            73dbb9b3fa72962255c9debcd218ca"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Как разобрать/построить ASN.1

Строго говоря, вы не можете разобрать ASN.1 — потому что это гипотетический (абстрактный) синтаксис. Но во всех смыслах «DER», «PEM» и «ASN.1» взаимозаменяемы, поэтому я буду говорить только о DER.

(перейдите к статье Abstract Syntax Notation One в Википедии, если вас интересуют теоретические обозначения)

Кроме того, если вы хотите работать с PEM / DER / ASN.1, почти наверняка вам придется использовать или написать парсер или упаковщик (в зависимости от того, что вам нужно), если не более чем для каскадные длины.

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

Всего три части

Тип

Тип всегда один байт.

Типы включают в себя примитивные значения (логическое значение, целое число, вещественное число, UTF8String и т. д.), а также составные значения (последовательность, набор) и метаданные (идентификатор объекта, что-то вроде ключа в паре ключ/значение).

Мне не удалось найти список всех типов и их кодировок, но у Microsoft есть несколько хороших документов, в которых перечислено несколько, и я нашел другой сайт, где перечислены еще несколько.

Длина

Длины представляют собой либо один байт (для небольших чисел до 128), несколько байтов (для больших чисел более 128), либо «неопределенные».

Если число больше 128, первый байт — это количество байтов длины (минус первый бит).

  • От 0x00 до 0x7F (0–127): это длина значения в байтах.
  • 0x02 будет означать, что длина составляет 2 байта.
  • от 0x80 до 0xFF: это длина длины
  • Пример 0x8180 означает, что 1-байтовая длина описывает 128-байтовое значение.
  • 0x81 и 0x7F = 0x01
  • 0x80 = 128
  • 0x820101 будет означать, что длина составляет 257 байт.
  • 0x80 и 0x7F = 0x02
  • 0x0101 = 257 байт
  • 0x80 точно: длина «неопределенная»
  • Я думаю, это означает чтение до конца файла?

И просто констатирую очевидное: длины каскадные. Настройка длины любого внутреннего значения влияет на длину всех внешних значений.

И хотя вы можете подумать, что «2048-битный ключ всегда будет 2048-битным, не так ли?», вы ошибаетесь, потому что есть 50%-ное изменение того, что вы получите 2047-битный ключ, и, следовательно, вероятность 0,4%. что вы получите 2040-битный ключ — на целый байт меньше, плюс проблема заполнения для любого ключа, использующего старший (2048-й, 2040-й или 2032-й) бит.

Ценность

В JSON значения часто говорят сами за себя или, по крайней мере, догадываются в контексте.

Совершенно ясно, что означает { "firstName": "Mike" }, и легко догадаться, что означает { "first": "Mike", "last": "Wazowski" }.

Становится немного менее ясно, что могут означать { "first": "Mike", "last": "Jon" } или [ "Mike", "Jon" ]. Возможно, это табло, список одноклассников, а может просто странная фамилия.

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

Четыре хитрых бита

Встроенные типы

Некоторые значения используются иногда как примитивы, а иногда как составные. Я видел это в основном с Bit String и Octet String.

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

Чтобы написать произвольный синтаксический анализатор, не зная схемы, у вас есть два варианта:

  • никогда не анализируйте примитивные значения при первом проходе (оставьте это логике приложения)
  • попытаться рекурсивно проанализировать некоторые примитивные значения и выполнить откат в случае сбоя (довольно легко обнаружить)
  • этот метод может давать ложноположительные результаты, теоретически

В коде эти параметры выглядят примерно так:

func parse(bytes) {
  if not isKnownCompositeType(bytes[0]) {
    return getValue(bytes)
  }
  return parse(bytes.slice(0, getFullLength(bytes)))
}
func parse(bytes) {
  if isKnownCompositeType(bytes[0]) {
    return parse(bytes.slice(0, getFullLength(bytes)))
  }
  if isKnownPrimitiveType(bytes[0]) {
    return getValue(bytes)
  }
  try {
    return parse(bytes.slice(0, getFullLength(bytes)))
  } catch {
    return getValue(bytes)
  }
}

Длина длины

Это не так уж сложно. Вот то, что было объяснено выше, но в псевдокоде

type = bytes[0]
len = bytes[1]
lenlen = 0x7f & len
if lenlen {
  len = int( bytes.slice(2, lenlen) )
}
value = bytes.slice(2 + lenlen, 2 + lenlen + len)

Целочисленное заполнение

Число может быть положительным или отрицательным.

В большинстве языков у вас есть возможность указать размер и знак числа:

  • uint16
  • int32
  • поплавок64

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

Однако при работе с BigInts размер может быть произвольным, поэтому «старший бит» — это просто первый бит первого байта, даже если это конкретное целое имеет длину 3 байта (или 37 байт).

Чтобы учесть это, целые числа ASN.1 дополняются 0x00, если установлен старший бит. Следовательно, «2048-битное» (256-байтовое) число может фактически использовать 257 байтов в ASN.1.

В коде, который выглядит так:

# parsing
if 0x00 == bytes[0] {
  bytes = bytes.slice(1)
}
# packing
if 0x80 & bytes[0] {
  bytes = bytes.prepend(0)
}

Заполнение битовой строки

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

По причине, которую я понимаю еще меньше, многие схемы x509 используют этот тип! (а также также с использованием типа Octet String, который кажется единственным разумным типом для этого)

По сути, это «нетипизированный тип» (произвольные биты), главный подозреваемый для встроенных типов, о которых я упоминал ранее.

DER выровнен по байтам (как и большинство вещей), что означает, что вы всегда должны использовать 8 бит за раз, даже если вам нужно только 3 бита для описания некоторого конкретного значения данных.

Поскольку тип Bit String не выровнен по байтам (полезен только в теоретическом мире, где, насколько я могу судить, можно отправить 3/8 байта по сети), он использует закладку byte, чтобы указать, какие биты первого байта игнорировать.

На практике эта панель всегда имеет значение «0x00», поэтому код будет выглядеть так:

# parser
if 0x03 == bytes[0] {
  bytes = bytes.slice(getFullLength(bytes) + 1)
}
# packer
if 0x03 == bytes[0] {
  bytes = bytes.prepend(0)
}

И я собирался показать вам, как будет выглядеть теоретический код, но он А) не имеет никакого смысла (потому что мне придется изобретать, а затем объяснять псевдокод, использующий биты вместо байтов) и Б) будет быть длинным и сложным.

Ресурсы

Ваша первая остановка должна состоять в том, чтобы загрузить Hex Fiend (macOS) или DHEX (на основе curses), сгенерировать несколько файлов der с помощью openssl и просто проверить, как все выглядит, как я описал в CSR, My Old Friend. ».

Затем я настоятельно рекомендую использовать https://lapo.it/asn1js/, который, по-видимому, имеет полную поддержку схемы x509, для анализа ваших PEM, чтобы точно увидеть, как они выглядят.

Затем посмотрите мою собственную демонстрацию asn1-parser.js, чтобы увидеть, как может выглядеть структура JSON для такого PEM.

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

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

Эй Джей Онил

Если вам понравилось и вы хотите больше таких, подписывайтесь!

Получайте еженедельные погружения с кодами

Я сделал твой день?

Купи мне кофе

(вы можете узнать о большой картине, над которой я работаю, на моей странице патреона)