Раньше я не уделял внимания формату файла сертификата открытого ключа. Теперь я немного покопался в этом формате и нашел там для себя много нового и интересного. В этой статье я не ставлю себе цель описать формат файла сертификата во всей полноте (это слишком большая работа; кроме того, эта работа уже сделана в тексте соответствующего стандарта), но хочу отметить некоторые интересные моменты. Формат сертификата открытого ключа описан в стандарте «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\. В файловых системах операционных систем «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: и выведем список хранилищ сертификатов. Вот как это выглядит у меня: Это те самые два главных хранилища сертификатов, которые описаны, к примеру, в статье «Working with Certificates» на сайте компании «Microsoft»: хранилище сертификатов текущего пользователя и хранилище сертификатов компьютера. Под названием каждого из этих хранилищ виден массив названий подхранилищ (каждый массив завершается многоточием, которое обозначает то, что все названия из массива не влезли в отведенную строку). Теперь можно последовательно перейти к нужному хранилищу (подхранилищу) сертификатов, либо можно сразу перейти к нужному подхранилищу, набрав полный путь (если вы уже сразу знаете нужный путь целиком): Как видно из кода выше, в подхранилище Cert:\LocalMachine\My есть только один сертификат открытого ключа. Команда dir уже вывела таблицу с тремя свойствами этого сертификата в окно терминала. Но следует понимать, что у сертификата гораздо больше свойств, чем три. Просто остальные свойства не поместились по ширине в окно терминала. Для вывода всех свойств сертификата есть более удобные способы, чем способ по умолчанию. К конкретному сертификату можно обратиться по его так называемому «отпечатку» (thumbprint). Как видно из кода выше, отпечаток представляет собой довольно длинное шестнадцатеричное число. Очевидно, что при использовании его лучше скопировать, а не набирать вручную. Итак, для удобства сохраним нужный объект сертификата в переменную $c, чтобы не обращаться каждый раз к длиннющему отпечатку сертификата. Затем через оператор конвейера (pipeline) | передадим объект сертификата командлету Format-List, который выведет все свойства сертификата в окно терминала в виде списка (символ звездочки * обозначает вывод всех свойств сертификата, но этому командлету можно передать ограниченный список свойств, чтобы вывести только те, которые нужны в данный момент): Результат: Как видно из кода выше, для некоторых свойств, которые попроще, в списке сразу выводятся значения этих свойств. Для свойств, в которых записана ссылка на объект, выводится только тип объекта. Чтобы просмотреть записанный в таком свойстве объект, нужно забираться поглубже. Значением некоторых свойств является массив байтов, в этом случае значения байтов в массиве выводятся через запятую. В некоторые свойства не записано никакого значения, поэтому их значение равно значению $null (в списке выше напротив названий таких свойств после двоеточия ничего не выведено). В свойстве Version записано значение 3. Насколько я понимаю, тут подразумевается версия 3 стандарта «X.509», о которой упоминалось в начале данной статьи. В этот раз меня в первую очередь интересовали свойства, в которых может содержаться информация об именах и названиях: DnsNameList, Extensions, FriendlyName, IssuerName, SubjectName, Issuer и Subject. При анализе значений свойств сертификата может понадобиться умение читать содержимое массивов байтов. Это не очень сложно, если знать, по каким стандартам эти байтовые массивы в значениях свойств сертификата организованы и где можно удобно просмотреть стандарты, почитать соответствующие справочники. Ниже я покажу, как можно это делать, на примере свойства IssuerName («имя издателя», то есть имя сущности, создавшей и заверившей данный сертификат открытого ключа). Как видно из списка свойств сертификата выше, свойство IssuerName содержит ссылку на объект класса System.Security. Cryptography. X509Certificates. X500DistinguishedName. Просмотрим список свойств со значениями для этого объекта: Очевидно, что значение свойства Issuer сертификата в данном случае совпадает со значением поля Name свойства IssuerName сертификата. Но мне стало интересно, что содержится в поле RawData свойства IssuerName сертификата. Видно, что там содержится массив байтов. Но что они означают? Сначала я предположил, что там записано то же самое, что в поле Name. И действительно, часть байтового массива содержит строку IlyaComp в кодировке UTF-8. Но там есть и что-то еще. С налета разобраться в этом не получилось, поэтому я приступил к подробному анализу. Получим в окно терминала полное содержимое разбираемого массива байтов: По умолчанию программа-оболочка «PowerShell» выводит массив «вертикально», то есть выделяя для каждого элемента массива отдельную строку. Это неудобно. Для вывода элементов массива «горизонтально» (в одной строке) я применяю в коде выше конкатенацию пустой строки и анализируемого массива. Следует иметь в виду, что по умолчанию значения байтов выводятся в десятичной системе счисления. Это неудобно. Для анализа байтовых массивов принято пользоваться значениями в шестнадцатеричной системе счисления. Выведем значения байтов из байтового массива в виде чисел в шестнадцатеричной системе счисления. В языке PowerShell это сделать несложно: В вышеприведенном коде передаем элементы массива байтов через конвейер (pipeline) | командлету ForEach-Object, который применяет к каждому входящему элементу массива (байту) метод System.Byte.ToString. Этот метод преобразует значение байта (целое число) в строку в указанном формате. В данном случае в качестве указания формата в метод System.Byte.ToString передаем строку x2. В этой строке формата латинская буква х указывает на вывод числа в шестнадцатеричной системе счисления (регистр буквы х в данном случае имеет значение, от него зависит регистр букв, которыми будут обозначены шестнадцатеричные цифры a, b, c, d, e, f). Число 2 указывает на число цифр в выводимых шестнадцатеричных числах. Очевидно (если вы понимаете, как сочетаются двоичная и шестнадцатеричная системы счисления), что для обозначения значения каждого байта нужно шестнадцатеричное число из двух цифр. (Тут подробнее про настройку форматирования чисел в строке.) Как оказалось, заполнение байтовых массивов в значениях сертификата открытого ключа по стандарту «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 в языке «ASN.1» означает тип данных «SEQUENCE» (набор данных разных типов; например, в языке Си эквивалентом этого типа данных является тип struct). Кстати, для быстрого ознакомления с языком «ASN.1» рекомендую ознакомиться с большой обзорной статьей «A Warm Welcome to ASN.1 and DER» на сайте известного бесплатного центра сертификации «Let's Encrypt». Эта статья очень удобна для начинающих, там есть разные таблицы соответствия, например, значений байтов и типов данных. Как видно из кода выше, в нашем случае в байтовом массиве записана структура данных типа «SEQUENCE», имеющая размер в 13 (шестнадцатеричная система) байтов или 19 (десятичная система) байтов. Данными являются 19 байтов, идущие после байта, указывающего размер данных. Если посчитать, получается, что весь остаток массива, кроме первых двух байт, имеет размер в 19 байт, следовательно, кроме структуры данных «SEQUENCE» в наш массив больше ничего не записано. Продолжим расшифровку. Возьмем следующие два байта. Тип данных «SET» в языке «ASN.1», в принципе, похож на «SEQUENCE», только в данных типа «SEQUENCE» (последовательность) порядок вложенных элементов сохраняется (важен), а в данных типа «SET» (множество) порядок вложенных элементов не сохраняется (не имеет значения). Перейдем к следующим двум байтам (думаю, принцип действия метода «type-length-value» к этому моменту уже должен был проясниться). Четвертый шаг: На этом шаге расшифровки получилось, что в структуру данных типа «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»). Результат расшифровки байтового массива: Это же коротко записано как CN=IlyaComp и в виде строки содержится в поле Name свойства IssuerName сертификата открытого ключа, а также в свойствах Issuer и Subject сертификата.