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

Стандарты

Формат сертификата открытого ключа описан в стандарте «X.509», который создан «Международным союзом электросвязи» (сокращенно «МСЭ», по-английски «ITU»). Точнее, над стандартом работали и работают в отделе (секторе) стандартизации электросвязи этой организации (по-английски «ITU-T», в прошлом веке он назывался «CCITT»).

Первая версия (редакция) этого стандарта появилась в 1988 году, а сейчас актуальна версия 9 от 2019 года. Над стандартом продолжают работать. Однако, насколько я понимаю, сейчас в интернете больше ориентируются на документ «RFC 5280» (над документами «RFC» работает «Инженерный совет Интернета», по-английски «IETF»), адаптирующий стандарт «X.509» к реалиям интернета. Вроде бы, именно из документа «RFC 5280» появился термин «PKIX» (инфраструктура открытых ключей [PKI] на базе стандарта «X.509»). Документ «RFC 5280» вышел в 2008 году, в нем идет речь о реализации версии 3 стандарта «X.509» (эта версия разрабатывалась в период 1997-2004 годов).

Файл для анализа и инструменты анализа

Я создал из диспетчера веб-сервера IIS самозаверенный сертификат открытого ключа для тестирования работы с локальным сайтом по протоколу HTTPS. Этот файл я и буду тут анализировать.

В качестве инструмента для анализа я в основном использую программу-оболочку «PowerShell» версии 7. Напомню, я работаю в операционной системе «Windows 10».

Иногда заглядываю в сохраненные консоли «certlm.msc» (хранилище сертификатов компьютера, для работы требуются права администратора компьютера) и «certmgr.msc» (хранилище сертификатов текущего пользователя, для работы достаточно прав текущего пользователя). Их легко можно вызвать прямо из командной строки из любого местоположения с помощью команд certlm и certmgr, так как эти файлы хранятся в системной папке %windir%\System32\. Файлы сохраненных консолей с расширением «.msc» по умолчанию привязаны (их имена при запуске передаются в качестве входного параметра) к исполняемому файлу «mmc.exe» (компонента «Консоль управления Microsoft» операционной системы), который тоже хранится в системной папке %windir%\System32\.

Просмотр хранилищ сертификатов и сертификатов в них из «PowerShell»

В файловых системах операционных систем «Windows» обычно бывает несколько «корней» (в отличие от Unix-подобных операционных систем; там в файловой системе один «корень»), которые обозначают латинскими буквами с двоеточием (например, «A:», «C:», «D:» и так далее) и называют «логическими дисками» (или «томами»), по-английски «logical drive» (или «volume»).

В принципе, слово «том» кажется теперь более правильным, потому что слова «диск» и «привод» тянутся со времен, когда запоминающие устройства (накопители) были в основном с магнитными дисками и с подвижными частями, которые приводились в движение «приводами». У нас сейчас многие переводят с английского языка слово «drive» как «диск», хотя «drive» — это «привод». Во многих случаях буквенные обозначения томов соответствуют реальным физическим устройствам-накопителям, поэтому их часто называют словом «drive».

В документации программы-оболочки «PowerShell» буквенные обозначения томов называют «file system drive» (том файловой системы).

По принципу томов файловой системы в языке «PowerShell» существуют так называемые «провайдеры» или «поставщики» (по-английски «provider»). У этих провайдеров тоже есть «приводы»: это короткие слова или даже части слов (или аббревиатуры), которые используются для предоставления (обеспечения, поставки) быстрого и простого доступа к данным или компонентам, доступ к которым другими способами затруднен. Например, для доступа к переменным среды можно использовать привод «Env:» провайдера переменных среды, для доступа к реестру Windows можно использовать приводы «HKLM:» и «HKCU:» провайдера реестра и так далее. См. статью «about_Providers» документации программы-оболочки «PowerShell».

Для доступа к хранилищам сертификатов и сертификатам в этих хранилищах можно использовать привод Cert: провайдера сертификатов. При этом с хранилищами сертификатов можно работать так же, как с обычными папками файловой системы (напомню, хранилища сертификатов не соответствуют какой-либо папке файловой системы, это просто что-то вроде фильтра, инструмента, который собирает сведения о цифровых сертификатах, физически хранящихся в разных местах на компьютере), а с сертификатами можно работать так же, как с обычными файлами в файловой системе. Удобно продолжать использовать псевдонимы cd (сменить папку) и dir (показать список файлов и папок в текущей папке) командлетов Set-Location и Get-ChildItem соответственно. См. статью «about_Certificate_Provider» документации «PowerShell».

Итак, в командной строке программы-оболочки «PowerShell» перейдем в том Cert: и выведем список хранилищ сертификатов. Вот как это выглядит у меня:

PS C:\> cd Cert:\
PS Cert:\> dir

Location   : CurrentUser
StoreNames : {ADDRESSBOOK, Trust, My, SmartCardRoot…}

Location   : LocalMachine
StoreNames : {ClientAuthIssuer, WindowsServerUpdateServices, Trust, AAD Token Issuer…}

Это те самые два главных хранилища сертификатов, которые описаны, к примеру, в статье «Working with Certificates» на сайте компании «Microsoft»: хранилище сертификатов текущего пользователя и хранилище сертификатов компьютера. Под названием каждого из этих хранилищ виден массив названий подхранилищ (каждый массив завершается многоточием, которое обозначает то, что все названия из массива не влезли в отведенную строку).

Теперь можно последовательно перейти к нужному хранилищу (подхранилищу) сертификатов, либо можно сразу перейти к нужному подхранилищу, набрав полный путь (если вы уже сразу знаете нужный путь целиком):

PS C:\> cd Cert:\LocalMachine\My
PS Cert:\LocalMachine\My> dir

   PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\My

Thumbprint                                Subject         EnhancedKeyUsageList
----------                                -------         --------------------
D4A1741E3ACC4C8A89E5F7AA7901D8FE1C95C013  CN=IlyaComp     Проверка подлинности сервера

Как видно из кода выше, в подхранилище Cert:\LocalMachine\My есть только один сертификат открытого ключа. Команда dir уже вывела таблицу с тремя свойствами этого сертификата в окно терминала. Но следует понимать, что у сертификата гораздо больше свойств, чем три. Просто остальные свойства не поместились по ширине в окно терминала. Для вывода всех свойств сертификата есть более удобные способы, чем способ по умолчанию.

Просмотр всех свойств сертификата из «PowerShell»

К конкретному сертификату можно обратиться по его так называемому «отпечатку» (thumbprint). Как видно из кода выше, отпечаток представляет собой довольно длинное шестнадцатеричное число. Очевидно, что при использовании его лучше скопировать, а не набирать вручную.

Итак, для удобства сохраним нужный объект сертификата в переменную $c, чтобы не обращаться каждый раз к длиннющему отпечатку сертификата. Затем через оператор конвейера (pipeline) | передадим объект сертификата командлету Format-List, который выведет все свойства сертификата в окно терминала в виде списка (символ звездочки * обозначает вывод всех свойств сертификата, но этому командлету можно передать ограниченный список свойств, чтобы вывести только те, которые нужны в данный момент):

PS Cert:\LocalMachine\My> $c = Get-Item D4A1741E3ACC4C8A89E5F7AA7901D8FE1C95C013
PS Cert:\LocalMachine\My> $c | Format-List *

Результат:

PSPath                   : Microsoft.PowerShell.Security
                           \Certificate::LocalMachine\My
                           \D4A1741E3ACC4C8A89E5F7AA7901D8FE1C95C013
PSParentPath             : Microsoft.PowerShell.Security\Certificate::LocalMachine\My
PSChildName              : D4A1741E3ACC4C8A89E5F7AA7901D8FE1C95C013
PSDrive                  : Cert
PSProvider               : Microsoft.PowerShell.Security\Certificate
PSIsContainer            : False
EnhancedKeyUsageList     : {Проверка подлинности сервера (1.3.6.1.5.5.7.3.1)}
DnsNameList              : {IlyaComp}
SendAsTrustedIssuer      : False
EnrollmentPolicyEndPoint
    : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
EnrollmentServerEndPoint
    : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
PolicyId                 :
Archived                 : False
Extensions               : {System.Security.Cryptography.Oid,
                            System.Security.Cryptography.Oid,
                            System.Security.Cryptography.Oid}
FriendlyName             : localhost
HasPrivateKey            : True
PrivateKey               :
IssuerName               
    : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter                 : 20.10.2023 3:00:00
NotBefore                : 20.10.2022 17:56:04
PublicKey                : System.Security.Cryptography.X509Certificates.PublicKey
RawData                  : {48, 130, 2, 233…}
SerialNumber             : 4931C370DB54C8B8426A88B0D254E17E
SignatureAlgorithm       : System.Security.Cryptography.Oid
SubjectName              
    : System.Security.Cryptography.X509Certificates.X500DistinguishedName
Thumbprint               : D4A1741E3ACC4C8A89E5F7AA7901D8FE1C95C013
Version                  : 3
Handle                   : 2125934225808
Issuer                   : CN=IlyaComp
Subject                  : CN=IlyaComp

Как видно из кода выше, для некоторых свойств, которые попроще, в списке сразу выводятся значения этих свойств. Для свойств, в которых записана ссылка на объект, выводится только тип объекта. Чтобы просмотреть записанный в таком свойстве объект, нужно забираться поглубже. Значением некоторых свойств является массив байтов, в этом случае значения байтов в массиве выводятся через запятую. В некоторые свойства не записано никакого значения, поэтому их значение равно значению $null (в списке выше напротив названий таких свойств после двоеточия ничего не выведено).

В свойстве Version записано значение 3. Насколько я понимаю, тут подразумевается версия 3 стандарта «X.509», о которой упоминалось в начале данной статьи.

В этот раз меня в первую очередь интересовали свойства, в которых может содержаться информация об именах и названиях: DnsNameList, Extensions, FriendlyName, IssuerName, SubjectName, Issuer и Subject.

При анализе значений свойств сертификата может понадобиться умение читать содержимое массивов байтов. Это не очень сложно, если знать, по каким стандартам эти байтовые массивы в значениях свойств сертификата организованы и где можно удобно просмотреть стандарты, почитать соответствующие справочники. Ниже я покажу, как можно это делать, на примере свойства IssuerName («имя издателя», то есть имя сущности, создавшей и заверившей данный сертификат открытого ключа).

Читаем байтовый массив

Как видно из списка свойств сертификата выше, свойство IssuerName содержит ссылку на объект класса System.Security. Cryptography. X509Certificates. X500DistinguishedName. Просмотрим список свойств со значениями для этого объекта:

PS Cert:\LocalMachine\My> $c.IssuerName | Format-List *

Name    : CN=IlyaComp
Oid     : System.Security.Cryptography.Oid
RawData : {48, 19, 49, 17…}

Очевидно, что значение свойства Issuer сертификата в данном случае совпадает со значением поля Name свойства IssuerName сертификата. Но мне стало интересно, что содержится в поле RawData свойства IssuerName сертификата. Видно, что там содержится массив байтов. Но что они означают?

Сначала я предположил, что там записано то же самое, что в поле Name. И действительно, часть байтового массива содержит строку IlyaComp в кодировке UTF-8. Но там есть и что-то еще. С налета разобраться в этом не получилось, поэтому я приступил к подробному анализу. Получим в окно терминала полное содержимое разбираемого массива байтов:

PS Cert:\LocalMachine\My> "" + $c.IssuerName.RawData
48 19 49 17 48 15 6 3 85 4 3 19 8 73 108 121 97 67 111 109 112

По умолчанию программа-оболочка «PowerShell» выводит массив «вертикально», то есть выделяя для каждого элемента массива отдельную строку. Это неудобно. Для вывода элементов массива «горизонтально» (в одной строке) я применяю в коде выше конкатенацию пустой строки и анализируемого массива.

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

PS Cert:\LocalMachine\My> "" + ($c.IssuerName.RawData | ForEach-Object ToString x2)
30 13 31 11 30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70

В вышеприведенном коде передаем элементы массива байтов через конвейер (pipeline) | командлету ForEach-Object, который применяет к каждому входящему элементу массива (байту) метод System.Byte.ToString. Этот метод преобразует значение байта (целое число) в строку в указанном формате. В данном случае в качестве указания формата в метод System.Byte.ToString передаем строку x2.

В этой строке формата латинская буква х указывает на вывод числа в шестнадцатеричной системе счисления (регистр буквы х в данном случае имеет значение, от него зависит регистр букв, которыми будут обозначены шестнадцатеричные цифры a, b, c, d, e, f). Число 2 указывает на число цифр в выводимых шестнадцатеричных числах. Очевидно (если вы понимаете, как сочетаются двоичная и шестнадцатеричная системы счисления), что для обозначения значения каждого байта нужно шестнадцатеричное число из двух цифр. (Тут подробнее про настройку форматирования чисел в строке.)

Язык ASN.1, способ сериализации DER и дерево идентификаторов объектов OID

Как оказалось, заполнение байтовых массивов в значениях сертификата открытого ключа по стандарту «X.509» выполняется на языке описания абстрактного синтаксиса данных «ASN.1» (расшифровывается как «Abstract Syntax Notation One»). Я немного о нем почитал и он мне понравился. Тут речь об «абстрактном синтаксисе» потому, что этот язык описывает любые структуры данных, независимо от конкретного языка программирования. Этот язык был создан в 1984 году и до сих пор разрабатывается уже упоминавшейся выше организацией «ITU-T».

Кроме описания структур данных в этом языке описаны разные способы сериализации полученного описания данных. Под словом «сериализация» подразумевают перевод данных в последовательность байтов (или в «серию» байтов, откуда и произошел термин «сериализация»). Там описано более десятка разных способов. Для сертификатов открытого ключа по стандарту «X.509» применяется способ сериализации DER (Distinguished Encoding Rules). Тут подробнее: раздел «Encodings» англоязычной статьи википедии «ASN.1», про способ сериализации DER в англоязычной статье википедии про стандарт «X.690», статья «A Warm Welcome to ASN.1 and DER» на сайте «Let's Encrypt».

Давайте посмотрим, как это работает на практике на примере байтового массива, который я получил выше.

При сериализации по методу «DER» согласно языка «ASN.1» применяется известный метод записи данных «TLV» (расшифровывается как «type-length-value» или «tag-length-value»). По-русски название этого метода можно озвучить как «тип-длина-значение». Согласно этого метода данные (в том числе вложенные) записываются в указанном порядке: сначала тип данных, потом — длина данных в байтах, и, наконец, значение (собственно, сами данные).

Будем последовательно применять это правило для расшифровки вышеуказанного массива байтов. Возьмем первые два байта:

30 13 31 11 30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
30 - SEQUENCE (тип данных)
13 - размер данных: 19₁₀ байтов
    данные: 31 11 30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70

Первый байт нашего массива со значением 30 в языке «ASN.1» означает тип данных «SEQUENCE» (набор данных разных типов; например, в языке Си эквивалентом этого типа данных является тип struct). Кстати, для быстрого ознакомления с языком «ASN.1» рекомендую ознакомиться с большой обзорной статьей «A Warm Welcome to ASN.1 and DER» на сайте известного бесплатного центра сертификации «Let's Encrypt». Эта статья очень удобна для начинающих, там есть разные таблицы соответствия, например, значений байтов и типов данных.

Как видно из кода выше, в нашем случае в байтовом массиве записана структура данных типа «SEQUENCE», имеющая размер в 13 (шестнадцатеричная система) байтов или 19 (десятичная система) байтов. Данными являются 19 байтов, идущие после байта, указывающего размер данных. Если посчитать, получается, что весь остаток массива, кроме первых двух байт, имеет размер в 19 байт, следовательно, кроме структуры данных «SEQUENCE» в наш массив больше ничего не записано.

Продолжим расшифровку. Возьмем следующие два байта.

SEQUENCE {
    31 11 30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
    31 - SET (тип данных)
    11 - размер данных: 17₁₀ байтов
        данные: 30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
}

Тип данных «SET» в языке «ASN.1», в принципе, похож на «SEQUENCE», только в данных типа «SEQUENCE» (последовательность) порядок вложенных элементов сохраняется (важен), а в данных типа «SET» (множество) порядок вложенных элементов не сохраняется (не имеет значения). Перейдем к следующим двум байтам (думаю, принцип действия метода «type-length-value» к этому моменту уже должен был проясниться).

SEQUENCE {
    SET {
        30 0f 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
        30 - SEQUENCE (тип данных)
        0f - размер данных: 15₁₀ байтов
            данные: 06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
    }
}

Четвертый шаг:

SEQUENCE {
    SET {
        SEQUENCE {
            06 03 55 04 03 13 08 49 6c 79 61 43 6f 6d 70
            06 - OBJECT IDENTIFIER (тип данных)
            03 - размер данных: 3₁₀ байта
                данные: 55 04 03
            13 - PrintableString (тип данных)
            08 - размер данных: 8₁₀ байтов
                данные: 49 6c 79 61 43 6f 6d 70
        }
    }
}

На этом шаге расшифровки получилось, что в структуру данных типа «SEQUENCE» входит сразу два вложенных элемента данных: типа «OBJECT IDENTIFIER» и типа «PrintableString». Второй из них — это тип данных, эквивалентный обычной строке. В данном случае в 8 байтах 49 6c 79 61 43 6f 6d 70 закодировано имя IlyaComp в кодировке UTF-8.

Под типом данных «OBJECT IDENTIFIER» (сокращенно «OID») подразумевают стандартизированное уже несколько раз упоминавшейся организацией «ITU» и международной организацией по стандартизации «ISO/IEC» дерево идентификаторов объектов. В интернете есть ряд сайтов, предоставляющих возможность просмотреть это дерево объектов и найти нужный объект. Например: http://oidref.com.

Каждый идентификатор объекта представляет собой набор номеров пунктов (веток дерева, целых чисел) через точку. В нашем случае в трех байтах 55 04 03 закодирован идентификатор объекта из четырех чисел. Первые два числа X и Y идентификатора объекта кодируются по формуле 40₁₀ * X + Y в первый байт данных, представляющих идентификатор объекта. Раскодируем идентификатор объекта:

55 04 03 = 85₁₀.4.3 = (40₁₀ * 2 + 5).4.3 = 2.5.4.3 = http://oidref.com/2.5.4.3

В идентификаторе объекта все пункты имеют определенное значение:

  • 2 — ветка дерева, общая для организаций «ISO» и «ITU-T»;

  • 2.5 — ветка дерева, представляющая службы каталогов;

  • 2.5.4 — ветка дерева, представляющая типы атрибутов (свойств) объектов в службах каталогов;

  • 2.5.4.3 — ветка дерева, представляющая тип «Common name» атрибута (сокращенно «CN»).

Результат расшифровки байтового массива:

SEQUENCE {
    SET {
        SEQUENCE {
            OBJECT IDENTIFIER: 2.5.4.3 (Common name)
            PrintableString: "IlyaComp"
        }
    }
}

Это же коротко записано как CN=IlyaComp и в виде строки содержится в поле Name свойства IssuerName сертификата открытого ключа, а также в свойствах Issuer и Subject сертификата.

Комментарии (6)


  1. saboteur_kiev
    23.10.2022 22:29
    +2

    При этом с хранилищами сертификатов можно работать так же, как с обычными папками файловой системы

    Почему же этот "драйв" недоступен из других инструментов? cmd, проводник, менеджеры?
    Только из PS?

    Потому что это не драйв, а абстракция исключительно для повершелл, в то время как в Линукс /proc и /dev - действительно тома, с которыми можно работать как с обычными папками файловой системы.

    Или я неправ?


    1. ilyachalov Автор
      23.10.2022 23:36

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


    1. MaxKozlov
      25.10.2022 17:02

      Это абстракции.

      И поэтому заголовок статьи "Файл сертификата"... не в тему

      в тему было бы если бы описывались PEM/DER

      А по факту описываются X509Certificate Класс (System.Security.Cryptography.X509Certificates)

      и немножко ASN.1


      1. ilyachalov Автор
        25.10.2022 23:54

        Про «файл» — была такая мысль. Но, с другой стороны, что такое «файл»? Его, ведь, тоже можно назвать абстракцией. Юниксоиды, вон, вообще считают, что «всё есть файл».

        К упомянутому вами классу еще не забудьте X509Certificate2.


        1. MaxKozlov
          26.10.2022 00:42

          Да я-то помню :) И даже, скорее, ваша ссылка правильнее.

          Но с точки зрения .net, обсуждаемая тема - объект класса. Никак не файл.


        1. saboteur_kiev
          26.10.2022 00:48
          +1

          Юниксоиды и Линуксоиды считают так, потому что ядро предоставляет удобный API в виде виртуальных файловых систем, и грубо говоря обратиться к процессу/устройству или специфичным данным операционной системы можно банальным чтением/записью в файл в /dev или /proc или еще что