В статье хочу рассказать опыт изучения Certificate Signing Request (CSR) формата. О том, что такое PEM, DER, какова структура приватных и публичных ключей, самого CSR файла и самое страшное - как этот CSR подписывается. Итак... Есть у меня маленький домашний Web-сервер. Долгие годы работал он на одном лишь HTTP, но вот решил я обуздать SSL и бесплатные сертификаты от Let's Encrypt, чтобы в дальнейшем и телеграм ботов на нём настраивать, и PWA, и прочие радости HTTPS жизни. Потратил вечер-другой на подбор приятного в работе ACME клиента, коим стал написанный на bash bacme, настройку сервера на прохождение проверок, редиректы с HTTP на HTTPS - и вот сервисы моего сервера уже имеют желанный замочек в адресной строке. Проблемы начались с автоматизации этого процесса. Так как сертификаты сервис раздаёт всего на три месяца, нужно озаботиться их регулярным обновлением. С этой целью я полез в документацию ACME протокола и начал писать свой скрипт. Детали о том, как генерировались ключи и JWS запросы, опять же, опущу. Самая весёлая часть началась, когда я решил генерировать свои CSR. От простого к сложному Privacy-Enhanced Mail - формат изначально созданный для хранения криптографических данных с целью повышения безопасности обмена электронной почтой (ибо сам протокол изначально о безопасности вообще ничего не знает). Многие из нас не раз генерировали пары закрытых/открытых ключей через openssl. Давайте сгенерируем одну пару и будем далее от них отталкиваться: Длина специально выбрана минимальная, чтобы и места в тексте меньше занимало, но и для подписания CSR хватало. В продакшене, разумеется, размер нужно указывать значительно толще. Итого имеем Вряд ли кому нужно объяснять, что мы видим нечто закодированное в base64 между хедером и футером вида -----BEGIN [TYPE]----- и -----END [TYPE]----- - это и есть PEM контейнер. По сути, там могут содержаться данные любого вида, но сейчас меня интересуют всякие крипто-ништяки, а это бинарные данные, классически представленные в DER формате. Distinguished Encoding Rules - один из способов представления ASN.1 данных, вероятно наиболее распространённый. Переведём для начала наш публичный ключ (он самый маленький) из base64 в HEX DER формат имеет формат TLV (Type, Length, Value). В нашем случае Type - всегда один байт, Length - от одного байта и выше (больше трёх пока не встречал, но спецификацией допускается до 128 байт), Value - <Length> последующих байт. Все типы перечислять не буду, но нас сейчас наиболее интересуют следующие: 02 INTEGER Целое число 03 BIT STRING Последовательность битов 04 OCTET STRING Последовательность байтов 05 NULL Пустое значение 06 OBJECT Объект в OID базе данных  0c UTF8 STRING Строка в кодировке UTF8 30 SEQUENCE Упорядоченная последовательность элементов 31 SET Последовательность элементов без определённого порядка Длина рассчитывается по следующему принципу: Если значение первого байта между 0 и 127 включительно, то это и есть длина содержимого, начинающаяся со следующего байта. Если первый бит установлен в единицу (то есть значение > 127), то мы исключаем эту единицу, а полученное значение - это сколько следующих байт содержат целочисленное значение длины, вычитываем значение из них. Как прочитать данные для каждого типа углубляться не буду. Я пользовался этим источником. Таким образом представим ключ в следующем виде: Внимательно присмотревшись, можно увидеть, что данный BIT STRING вполне также можно распарсить: Это зашифрованные в нашем ключе modulus и exponent. Те самые, которые можно получить командой openssl rsa -pubin -text. Абсолютно аналогично можно получить все необходимые данные из нашего приватного ключа: Здесь мы видим наши modulus и exponent, которые как раз экспортируются в public.key при создании публичного ключа (`openssl rsa -pubout`): Плюс прочие целые числа, использующиеся в алгоритмах шифрования, но они вновь выходят за скоуп моего сегодняшнего повествования. Сейчас же, наконец, перейдём к основной теме моих приключений: Для начала сгенерируем наш запрос с помощью `openssl`: И посмотрим на его структуру Основные части: Version - на данный момент всегда 0. Subject - основные поля запроса, вроде commonName, countryName и прочие поля по желанию заказчика или требованию CA. RSA Public Key - точная копия публичного ключа Extended attribute list - Группа дополнительных полей, например `Subject Alternative Names` (Не изучал, что может быть ещё) Signature algorithm - Сообщаем CA, каким методом мы подписываем наш запрос Signature - сама подпись В целом, основная часть полей прямолинейна: константы пишем как есть, публичный ключ копипастим, необходимые поля заполняем, алгоритм подписания выбираем сами и сообщаем CA. Но вот подпись... (Если нет желания читать про творческие муки, а интересуют сухие технические сведения - смело пропускайте эту часть) Провозился с этим делом я около недели свободных вечеров в попытках либо подписать, либо свалидировать существующую подпись. Чтение документации ни к чему не приводило, гугление приводило куда угодно, но ни одно из решений не отвечало на вопрос в контексте CSR, спрашивал в рабочих чатах по бэкендерам - не отвечает никто. Пошёл формулировать подробный вопрос на StackOverflow, параллельно пробуя ещё раз. Сам вопрос я так и не задал, так как удалось найти ошибку в процессе его описания, но, если кому интересно, положу его шаблон под спойлер, а далее перескажу на родном русском, опустив всё то, что уже описано выше. I'm diving into cryptography algorithms with current task of getting certificates from Let's Encrypt by ACME protocol. At a step of generating Certificate Signing Request (CSR) I've encountered behaviour I can't understand: how document signature is generated and why signature generated by me doesn't match the one generated by openssl req -new. For my test I've generated key pair: Then generated CSR signed by private.key: Documentation of CSR: I've got my utility to parse and read PEM with DER and ASN.1, so I've extracted all parts of req.csr (converted to HEX): Now where is fun part begins. For my experiments I'm using NodeJS built-in crypto library. My main goal is to generate signature myself, so let's try doing it right away. That doesn't really match signature generated by openssl. Tried to do straight forward as I understand from documentation (mentioned above): This doesn't match neither CSR signature, nor output of my first attempt. Ok, let's just try to verify signature. Let's just try to decrypt signature, if it actually encrypted SHA256 hash. Needless to say they doesn't match. Also the length of decrypted sign is far from 256 bits. I sliced first 19 bytes (that exceeded 256 bits) to take a closer look. It's definitely looks like DER encoded line Сначала обратимся к документации Получим из запроса необходимые данные: Для своих экспериментов я использую NodeJS и встроенную в него библиотеку crypto. Моя основная цель - научиться генерировать подпись самостоятельно, так что с этого и начну Ниже по тексту я обнаружу, что заголовок (30 72) обязателен. Если бы я сразу попробовал его добавить, и подписал с ним, то у меня бы получилась ровно та подпись, что и в CSR. Не очень похоже на подпись, сгенерированную openssl. Попробуем немного другой способ: Просто захешировать данные и зашифровать хеш ключом недостаточно. Цифровая подпись также подразумевает обёртку в DER формате, содержащую информацию о подписи. Поэтому полученная строка и не сошлась ни с искомой, ни с предыдущим примером. Как эта обёртка выглядит - ниже опишу. Не совпадает ни с изначальной подписью, ни с полученной предыдущим способом. Решил с последним не разбираться, а ехать дальше. Попробуем провалидировать. Что ж, давайте просто расшифруем нашу подпись публичным ключом, раз уж это просто зашифрованный SHA256 хеш. Излишне говорить, что строки не совпадают. Более того, расшифрованная подпись существенно длиннее 256 бит, а её начало выглядит подозрительно структурированной. Отрежем все первые биты, превышающие свойственные SHA256 32 байта, чтобы посмотреть на них поближе. Изначально я подумал, что это нечто вроде соли, но раскодировав в последствии с десяток-другой всяких ключей и CSR-ов, и взглянув на это дело свежим взглядом, конечно, я уловил, что это тоже DER структура! И вот он, наш хеш! Все 256 бит (32 байта). Неудивительно, что стандартные библиотечные утилиты не в состоянии провалидировать эту подпись, никто, думаю, не рассчитывает, что она будет так завёрнута. Вот только с нашим хешем она по-прежнему не совпадает...  Вернувшись к изначальным данным и внимательнее на них посмотрев, я увидел, что ошибка была на поверхности. А может заголовок-то всё же нужен? Добавляю в начало 3073, хэширую в SHA256 и - Вуаля! Итог моих мук оказался следующим. Для того, чтобы подписать CSR, давайте сразу выделим ту часть, которую собираемся подписывать (вместе с заголовком (Тип 30 + длина)):  Выбираем тип хеширования, в нашем случае это SHA256, и загоняем туда наши данные. Создаём обёртку в DER формате с указанием хеширующего алгоритма  Нашим приватным ключом шифруем полученную обёртку, получаем Добавляем заголовок BIT STRING (учитываем особенности типа и не забываем добавить 00 в начало строки) и описание алгоритма подписи:  Последним шагом кладём это всё на один уровень с нашим основным документом, заворачиваем в SEQUENCE с нужной длиной - и получаем ровно тот документ, который описан в части 1.3 этого текста. Данную статью я решил написать, так как в процессе своих поисков сумел найти довольно мало полезной информации. Ту, что нашёл, и которая оказалась максимально полезной в изучении вопроса, привёл в виде ссылок в тексте, плюс приведу ещё несколько в конце. Однако встретил ещё немало сложностей, до решения которых пришлось с трудами докапываться самостоятельно. Надеюсь, что если кто-то задастся подобной задачей, поиски приведут его сюда, и у него процесс займёт существенно меньше времени. Это моя первая статья, которую я осмелился отправить на Хабр. Очень надеюсь, что её сочтут познавательной и/или интересной. https://www.rfc-editor.org/rfc/rfc2986 - документация по CSR синтаксисуhttps://oidref.com/ - база данных по OIDhttps://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-der-encoding-of-asn-1-types - хорошее описание типов ASN.1 в DER структуреhttps://luca.ntop.org/Teaching/Appunti/asn1.html - более подробное описание DER структуры, но к некоторым местам у меня остались вопросыhttps://mbed-tls.readthedocs.io/en/latest/kb/cryptography/asn1-key-structures-in-der-and-pem/ - подробное описание структуры приватного и публичного ключейhttps://www.geeksforgeeks.org/rsa-algorithm-cryptography/ - алгоритм генерации ключей шифрование, если хочется понять, что все эти странные огромные числа в ключах означают