Expertus metuit
Ещё больше и лучше о цифровых сертификатах: X.509, PKI, PKCS
Опубликовано 2020-09-05 в 00:12

Четыре года назад я написал статью Человеческим языком о цифровых сертификатах: ASN.1, X.509, PKI, в которой постарался максимально понятно рассказать о цифровых сертификатах с примерами их использования через консоль и openssl. С того времени многое изменилось: openssl версии 1 ушёл в массы, появились и стали активно использоваться его форки (libressl и boringssl), появилась (типа) поддержка гостовских алгоритмов. Плюс я получил неожиданно много обратной связи, чего совершенно не ожидал, и в итоге появился этот полностью переработанный и актуализированный текст.

*Целевая аудитория этой статьи — айтишники, поверхностно знакомые с понятием цифрового сертификата и сопутствующими понятиями и техническими стандартами (X.509, PKI, PKCS). Текст не является пересказом документации или сборником рецептов, воспринимайте его как короткий учебник, рассказывающий о базовых концептах криптографии и сложившейся вокруг стандарта X.509 инфраструктуры.

Чаще всего цифровой сертификат ассоциируется с браузером, веб-сайтом и шифрованием трафика в HTTPS-соединении. И обычно на самом факте существования сертификата у большинства айтишников понимание темы заканчивается. Многие статьи на эту тему с самого начала топят читателя в абстрактных и несущественных деталях (например, раз, два, три). Между тем, базовые концепции системы сертификатов очень простые и я о них расскажу.

В центре внимания у меня именно сертификат, так как именно вокруг него крутятся все остальные концепты.

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

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

Для полноценной работы вам понадобится linux с терминалом и установленным openssl версии 1.0 или выше. С некоторым оговорками подойдёт macos с терминалом (в macos последних версий установлен форк openssl под названием libressl).

Теория

В теоретическом разделе я коротко расскажу о программе и библиотеке OpenSSL, затем будет небольшой обзор необходимых для дальнейшего понимания криптографических концептов, далее о том, как они используются для создания сертификатов и сопутствующих объектов. Завершу всё небольшим обзором инфраструктуры доверия к сертификатам на основе удостоверяющих центров.

Что такое openssl

OpenSSL — это опенсорсный набор библиотек и программ для работы с SSL/TLS и некоторыми распространёнными криптоалгоритмами. В этой статье мы будем работать только с программой openssl, она представляет собой коллекцию утилит (команд) для операций над крипто-объектами: ключами, сертификатами, зашифрованными данными.

OpenSSL — очень старая система, в ней огромное количество legacy-кода и legacy-интерфейсов. Различные команды из её состава принимают разные аргументы и имеют разную логику работы. Команд этих очень много и я не буду рассказывать о них, вместо этого я сфокусируюсь на конкретных задачах, в рамках которых буду давать примеры использования команд для её решения. Иногда путей для решения задачи будет несколько.

Что такое и зачем нужны сертификаты

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

В русском языке слово сертификат употребляется наравне со словами удостоверение, свидетельство; особенно это характерно для бюрократии, официальных текстов и государственных стандартов. Например, в российской Системе документов по аккредитации в документе СДА 06-2009 используется такое определение:

Удостоверение (сертификат) - документ, выданный органом по сертификации персонала, удостоверяющий компетентность специалиста в определенной области испытаний в соответствии с присвоенным уровнем квалификации.

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

Термин цифровой сертификат (digital certificate) означает, что первичная его форма — в виде байтов на диске, памяти компьютера, на носителе информации. Цифровой сертификат можно легко скопировать и полученная копия будет обладать всеми его свойствами. В основе цифровых сертификатов лежит криптография, именно с помощью криптоалгоритмов происходит работа с сертификатом на всех этапах его жизненного цикла: создание, использование, уничтожение. Без знания основ криптографии, хотя бы самых элементарных, невозможно полноценно понять суть и смысл цифровых сертификатов, поэтому я про них тоже расскажу.

В дальнейшем я под словом сертификат буду подразумевать исключительно цифровой сертификат.

Основы криптографии

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

Википедия нам даёт такое определение:

Криптогра́фия (от др.-греч. κρυπτός «скрытый» + γράφω «пишу») — наука о методах обеспечения конфиденциальности (невозможности прочтения информации посторонним), целостности данных (невозможности незаметного изменения информации), аутентификации (проверки подлинности авторства или иных свойств объекта), шифрования (кодировка данных).

Все эти аспекты используются при работе с сертификатами. Начнём с шифрования, в упрощённой форме это процесс преобразования открытого текста в зашифрованный и наоборот, из зашифрованного в открытый — этот процесс называется дешифрованием, дешифровкой, расшифровкой. Шифрование и дешифрование работают на самом деле с произвольными наборами байтов, поэтому фразу открытый текст нужно воспринимать как открытый набор байтов.

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

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

Важным для нас вариантом асимметричного шифра является криптосистема с открытым ключом (public-key cryptography). В её основе лежат такие принципы:

  • ключ состоит из двух компонентов: закрытого ключа / private key (предполагается, что он секретный и тщательно охраняется) и открытого ключа / public key (который может и должен распространяться свободно по любым каналам);
  • открытый ключ вычисляется из закрытого простым способом, а вот обратная процедура (вычисление закрытого ключа из открытого) является чрезвычайно сложной, затратной и невыполнимой за разумное время на любом доступном оборудовании;
  • существует открытые, общеизвестные и надёжные алгоритмы, которые используют закрытый и открытый ключ для криптографических операций, например, для шифрования/дешифрования.

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

Первая схема использования пары закрытого и открытого ключей — шифрование: отправитель шифрует текст, затем отправляет получателю зашифрованный текст через открытый канал, а получатель их расшифровывает снова в открытый текст. Эта схема предназначена для защиты информации, вот что для неё требуется:

  • предполагается, что у отправителя уже есть открытый ключ, а у получателя — закрытый (парный к открытому, естественно);
  • отправитель при помощи открытого ключа и алгоритма (шифра) шифрует данные и отправляет результат через открытые каналы;
  • получатель при помощи закрытого ключа расшифровывает данные и восстанавливает исходный текст.

Поскольку открытый ключ общеизвестен, любой человек может зашифровать при помощи него сообщение, а дальше подменить оригинальное. У получателя нет никакого способа автоматически проверить (то есть аутентифицировать), что полученное им сообщение не было изменено и пришло именно от нужного отправителя. И тут на помощь приходит вторая схема использования пары ключей — цифровая подпись / digital signature. Она используется для подтверждения авторства сообщения, то есть для аутентификации. Вот пример:

  • у отправителя файла (и только у него!) есть доступ до закрытого ключа, а открытый ключ является публичным и доступен в том числе получателю;
  • отправитель берёт файл и при помощи алгоритма подписи и закрытого ключа создаёт цифровую подпись — ещё один набор байтов в виде отдельного файла, который отправляется получателю вместе с оригинальным текстовым файлом по открытому каналу;
  • получатель берёт текстовый файл и файл подписи и при помощи алгоритма проверки цифровой подписи и открытого ключа верифицирует, создана ли эта подпись закрытым ключом, парой которому является данный открытый ключ.

Алгоритм подписи чаще всего оперирует не с сообщением целиком, а с его дайджестом (digest), то есть «сжатым» при помощи алгоритма криптографического хеширования (например, SHA-1 или гостовского СТРИБОГ). И в целом схема выглядит так:

Схема работы цифровой подписи

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

Во многих статьях (например, здесь) можно встретить некорректное и устаревшее описание цифровой подписи, в нём она является зашифрованным с помощью закрытого ключа дайджестом, а соответственно верификация производится расшифровкой подписи с помощью открытого ключа и последующим сравнением результата с отдельно подсчитанным на стороне получателя дайджестом сообщения. Такой процесс возможен для RSA, почти во всех остальных криптосистемах шифрование собственно открытыми/закрытыми ключами принципиально не поддерживается, а поддерживается только создание цифровой подписи. Однако для организации защищённого канала этого достаточно и я ниже в разделе Используемые криптоалгоритмы подробно расскажу, как при помощи произвольного алгоритма цифровой подписи организовать безопасное шифрование и дешифрование данных. А в практическом разделе расскажу, как это сделать командами openssl.

──────────────────

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

Использование криптографии в X.509-сертификатах

Цифровой сертификат в общем случае — это набор байтов, состоящий из двух блоков: информационного (например, название веб-сайта, название организации и т.п.) и цифровой подписи для информационного блока. Цифровая подпись создаётся удостоверяющим центром (УЦ) / certification authority (CA) и таким образом удостоверяет аутентичность данных из информационного блока. Но каким образом удостоверяющий центр проводит проверки, что подписываемый сертификат содержит данные той персоны или организации, которая подаёт его для подписи? Каким образом клиенты проверяют достоверность цифровой подписи в сертификате? Обо всём этом я подробно расскажу. Но сначала начнём со стандартов.

Существует несколько бинарных форматов для представления сертификатов, однако в абсолютном большинстве случаев вам придётся иметь дело с X.509-сертификатами. Все остальные форматы нишевые и я про них здесь не буду рассказывать.

X.509 — это технический стандарт, определяющий формат для сертификата с открытым ключом, то есть для такого сертификата, в информационном блоке которого записан (помимо других данных) открытый ключ. Как правило вместе с открытым ключом указываются личные данные / identity персоны или организации, владеющей соответствующим закрытым ключом.

Существует также набор стандартов для криптографии с открытым ключом PKCS (Public Key Cryptography Standards). На данный момент в нём 15 стандартов, их принято обозначать как PKCS#1, PKCS#2 и т.д. С полным списком можно ознакомиться в википедии: https://en.wikipedia.org/wiki/PKCS, а я в тексте буду на нужные мне части ссылаться по мере необходимости.

Изначально стандарты семейства PKCS разрабатывались компанией RSA Security LLC в целях рекламы крипто-алгоритмов, патенты на которые были на руках у компании, поэтому в индустрии отношение к этому наборы было и остаётся настороженным.

В середине девяностых IETF и NIST сформировали рабочую группу Public-Key Infrastructure (X.509), которая позднее стала называться просто PKIX. В рамках рабочей группы были разработаны стандарты, детально описывающие, как нужно использовать X.509 на практике: RFC 3280 и его наследник RFC 5280.

Для описания структуры используемых данных чрезвычайно широко используется стандарт Abstract Syntax Notation One (ASN.1), фактически все типы данных в прикладной криптографии описаны через ASN.1-нотацию.

Используемые криптоалгоритмы

В инфраструктуре открытых ключей чаще всего используются RSA, DSA и ECC.

  • RSA — криптосистема, названная по первым буквам имён её создателей: Rivest-Shamir-Adleman. В основе её теории лежит сложная задача факторизации (разделения на множители) произведения двух очень больших простых чисел. Выбирается два случайных очень больших простых числа p и q, их произведение n = p · q называется модулем / modulus и длина модуля в битах задаёт длину закрытого ключа. Чем больше длина, тем более надёжным считается ключ. Также выбирается сравнительно небольшое простое число e (чаще всего это 65537), называемое открытой экспонентой / public exponent. Дальше из модуля n вычисляется по специальному алгоритму (который я тут не буду объяснять) закрытая экспонента / private exponent. В итоге формируется открытый ключ в виде пары чисел (e, n) и закрытый ключ в виде пары (d, n). Этот алгоритм долгое время был самым распространённым и по умолчанию фигурировал во всех инструкциях и мануалах. Однако в середине девяностых был разработан алгоритм для квантовых компьютеров, выполняющий факторизацияю чисел. Хотя квантовых компьютеров ещё нет, но теперь RSA считается потенциально слабым, если длина закрытого ключа меньше 2048 бит. RSA содержит алгоритмы как для шифрования, так и для цифровой подписи.
  • DSA — криптографический алгоритм, названием расшифровывается как Digital Signature Algorithm, то есть алгоритм цифровой подписи. Он основан на математической теории возведения в степень по модулю и задаче дискретного логарифмирования. В отличие от RSA, этот алгоритм может использоваться только для подписывания данных, но не для их шифрования. Исторически DSA использовался и продолжает использовать в операциях с государственными органами США, однако нас он не интересует, поэтому больше о нём не будем, просто имейте в виду, что он поддерживается при создании сертификатов тоже. DSA может использоваться только для цифровой подписи.
  • ECC (или просто EC) — набор криптосистем на основе теории эллиптических кривых над конечным полем. Сейчас эти виды шифров используется всё активнее вместо RSA, поскольку они быстрее, размер ключа значительно меньше, потенциально устойчивее к взлому на квантовых компьютерах. Эллиптическая криптография построена на основе набора алгебраических операций на эллиптической кривой, которые, в свою очередь, строятся на основе операций в конечном поле, над которым задана кривая. В общем, это такая хардкорная высшая математика, знать которую совсем не обязательно для использования шифров. Крипто-алгоритмов на основе EC достаточно много, часть из них была перенесена из старых классических крипто-систем, часть придумана заново. Например, алгоритм цифровой подписи ГОСТ 34.10-2018 также использует эллиптические кривые для работы. Некоторые из ECC-алгоритмов могут использоваться для шифрования, а некоторые для цифровой подписи.

Из широко известных криптосистем только RSA позволяет собственными ключами шифровать и подписывать, все остальные непосредственно используются только для создания цифровой подписи. Однако можно пользоваться алгоритмом Diffie-Hellman для создания общего ключа для симметричного алгоритма (например, AES) и дальше уже им зашифровывать и расшифровывать данные.

Протокол Diffie-Hellman (Diffie–Hellman key exchange, дальше я буду его коротко называть DH) играет чрезвычайно большую роль в современной криптографии, поэтому я о нём расскажу подробно.

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

Итак, на разных сторонах открытого сетевого канала находятся Алиса и Боб которым нужно договориться о ключе (в моём примере это целое число) для симметричного шифра. Просто так передать его по сети нельзя, так как канал открытый и априори небезопасный.

  1. Алиса и Боб придумывают по секретному числу, например, Алиса выбирает 9, а Боб — 2.
  2. Алиса и Боб через открытый канал договариваются о каком-нибудь общем целом числе, скажем, 18. Это число не является секретным, но при каждом сеансе DH оно должно быть новым. Самый простой способ договориться, когда сторона, начинающая коммуникацию, просто выбирает число и другая сторона его принимает.
  3. Дальше каждая сторона складывает своё секретное число и общее число, у Алисы получается 9 + 18=27, а у Боба 2 + 18=20. Полученный результат каждый отправляет другой стороне по открытому каналу.
  4. Полученное число Алиса и Боб складывают со своим секретным числом: у Алисы получается 20 + 9 = 29, а у Боба 27 + 2 = 29.
  5. Вот это число 29 и является ключом, который каждая из сторон использует для шифрования/дешифрования.

Я выбрал операцию сложения ради простоты объяснения, в реальном протоколе используется сложно-обратимая операция. Если её обозначить через ⊕, то для произвольных значений X и Y вычисление Z = X ⊕ Y выполняется легко; а зная Z и X, вычислить Y исключительно сложно и затратно. В реальном DH используется функция возведения в степень в мультипликативной группе вычетов по простому модулю, её обратная операция — дискретный логарифм — считается крайне сложной и на данный момент не имеет эффективного решения.

──────────────────

Протокол DH, очевидно, уязвим для атак типа Man-in-the-middle, то есть третья сторона может незаметно вклиниться в обмен данными и полностью перехватывать и расшифровывать все данные. Поэтому в реальной жизни обмен блоками данных в DH сопровождается алгоритмами аутентификации, например, цифровой подписью.

Рассмотрим такую ситуацию: есть клиент и есть сервер, к которому подключается клиент через небезопасный и незащищённый канал. Также выбран некоторый криптоалгоритм цифровой подписи. У сервера есть закрытый ключ, а его открытая часть уже есть у клиента. Тогда аутентифицированный DH может работать, например, так:

  1. Клиент генерирует случайное секретное значение X общее публичное значение A.
  2. Клиент инициирует соединение к серверу и отправляет по нему A.
  3. Сервер получает A, генерирует своё секретное значение Y и вычисляет публичное значение B на основе публичного значения A и своего секретного Y.
  4. Сервер вычисляет общий секретный ключ K на основе публичного значения A и своего секретного значения Y.
  5. Сервер вычисляет цифровую подпись S от объединённого набора значений A и B и отправляет клиенту публичное значение B и цифровую подпись S.
  6. Клиент получает публичное значение B, вычисляет на основе B и своего секретного значения X общий секретный ключ K.
  7. Клиент проверяет цифровую подпись K объединённого набора значений A и только что полученного B (помним, что у клиента уже есть открытый ключ сервера).
  8. Если верификация подписи проходит успешно, вычисленный ключ K используется для шифрования и дешифрования данных между клиентом и сервером при помощи симметричного алгоритма.

В такой схеме клиент может быть уверен, что между ним и сервером нет третьей стороны, которая перехватывает данные, гарантией этому служит цифровая подпись. Такой ключ K, который создаётся на время сессии, называется эфемерным ключом (ephemeral key).

Ранее мы уже договорились, что контейнером для распространения открытого ключа является цифровой сертификат. И теперь остаётся вопрос: каким образом сертификат сервера оказывается у клиента перед установкой соединения.

Концепт CSR и подписывания сертификата удостоверяющим центром

Я буду использовать здесь и дальше слово заявитель / applicant для обозначения персоны или организации, которая хочет выпустить цифровой сертификат. Я специально не использую для этого контекста слово клиент, так как оно слишком общее и обычно применяется в контексте типа клиент/пользователь вебсайта.

Под личными данными заявителя я подразумеваю его имя, название организации, адрес, город и прочую информацию, которую в англоязычной терминологии принято называть identity.

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

  1. Заявитель является именно тем, за кого себя выдаёт, то есть предоставленные им личные данные точно его или его организации, уполномоченным представителем которой он является.
  2. У заявителя есть закрытый ключ для того открытого, который он предоставил удостоверяющему центру для подписи вместе с личными данными.

Первый пункт — это процедура, которая проводится административными, а не криптографическими методами, поэтому о ней здесь не будем рассказывать — это проверка паспорта, учредительных документов, звонки по телефонам, различные заверенные у нотариуса документы и так далее. А вот вторая отлично решается в автоматическом режиме криптографически — достаточно заявителю подписать собственным закрытым ключом блок с информационными данными (куда также записан собственно этот же открытый ключ!). В этом случае удостоверяющий центр берёт открытый ключ из информационного блока и верифицирует им цифровую подпись всего блока.

Вот этот вот информационный блок (состоящий из личных данных и открытого ключа) плюс цифровая подпись для него формируют запрос на подпись сертификата / certificate signing request (CSR). Для X.509 формат данных CSR определён в спецификации PKCS#10, он достаточно простой и по сути представляет собой линейный список информационных полей, поэтому я особо не буду углубляться в описание его формата.

В удостоверяющем центре после верификации CSR выделяют из всего блока личных данных нужные, дополняют их данными УЦ, после чего получившийся новый блок подписывают закрытым ключом УЦ и получается X.509-сертификат.

Все эти шаги я нарисовал на одной схеме:

CSR Explained

TBS Certificate расшифровывается как to be signed certificate, то есть, данные для подписи, подписываемый сертификат.

Такая инфраструктура называется Public key infrastructure (Инфраструктура открытых ключей) или сокращённо PKI.

Инфраструктура УЦ и сертификаты

X.509-сертификат состоит из стандартного набора полей, часть из них обязательна, часть нет. Официального перевода этих названий на русский нет, поэтому я преимущественно буду пользоваться оригинальными именами с пояснениями при необходимости.

В каждом X.509-сертификате есть два обязательных поля: issuer и subject. Поле subject (то есть субъект, в философском смысле: носитель деятельности, осуществляющий активность) содержит личные данные заявителя и по сути является названием сертификата, его главным идентифицирующим признаком, именем. Я специально не использую термин идентификатор, поскольку в контексте сертификата такое поле уже есть, причём оно является необязательным. Содержимое берётся из одноимённого поля в CSR при создании сертификата и обычно представляет собой имя персоны, организации или домен веб-сайта.

В поле issuer хранится имя сертификата удостоворяющего центра. Постоянно помним, что открытые ключи у нас распространяются исключительно внутри сертификата. Поэтому в поле issuer находится содержимое поля subject сертификата УЦ, внутри которого находится открытый ключ, закрытая часть которого используется для подписи сертификата заявителя.

Поля issuer и subject являются структурированными, то есть это не просто строчки текста, а оформленные в жёстко заданную структуру данные типа distinguished name. Об этом подробнее я расскажу в практическом разделе.

Таким образом любой сертификат A в подобной инфраструктуре подписан закрытым ключом, открытая часть которого записана в каком-то другом сертификате B. Для простоты обычно в таком случае говорят, что сертификат A подписан сертификатом B. В свою очередь сертификат B подписан сертификатом C и так далее. Образуется цепочка зависимостей, которая завершается сертификатом Z специального вида, в котором поля issuer и subject совпадают. Это означает, что сертификат Z подписан закрытым ключом, открытая часть которого записана в этом же сертификате. Он так и называется — самоподписанный сертификат (self-signed certificate).

──────────────────

Считается, что вы как клиент доверяете (trust) сертификату A, если вы доверяете записанным в нём данным. Например, если вы получили сертификат организации через надёжный канал от надёжного представителя организации и записали на надёжном диске. Используя доверенный сертификат, а точнее, открытый ключ из него, вы можете организовать криптографически защищённый канал связи, например, до веб-сайта. Однако вы физически не сможете для каждого веб-сайта в таком режиме содержать «реестр» доверенных сертификатов и отслеживать его актуальность (сертификаты у сайтов часто меняются, например).

Описанная выше схема зависимостей между сертификатами лежит в основе системы доверия на базе удостоверяющих центров. Подразумевается, что если вы доверяете сертификату B, то вы также доверяете сертификату A, который им подписан. Так как организация-владелец сертификата B может им подписать множество других сертификатов, вы автоматически доверяете всем им. Следуя по цепочке доверия вы в итоге спускаетесь до самоподписанных сертификатов. Если вы доверяете самоподписанному сертификату Z, то вы автоматически доверяете всем остальным, которые имеют Z в цепочке доверия.

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

В итоге, только удостоверяющие центры (certification authority) обладают сертификатами, которые можно использовать для подписывания других сертификатов, чтобы они могли включаться в цепочку доверия. Их принято называть CA-сертификатами (CA certificate). А финальный самоподписанный сертификат в цепочке доверия называется сертификатом корневого удостоверяющего центра или просто корневым сертификатом (root certificate).

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

──────────────────

Также в сертификате есть множество других полей, которые ограничивают его применение. Например, диапазон дат, внутри которых сертификат можно считать доверенным. Практически все системы помечают сертификат недоверенным, если текущая дата в системе лежит вне указанного в сертификате диапазона. Диапазон действия корневых сертификатов обычно очень большой, порядка 10-20 лет. Этот диапазон дат указывается в полях notBefore и notAfter.

Здесь нужно отметить, что интерпретация «срока годности» сертификата целиком лежит на стороне программного обеспечения, которое сертификат использует. Нет никаких объективных причин, по которым сертификат перестаёт быть доверенным после достижения некоторой даты. Однако ограниченный срок жизни гарантирует стабильный источник дохода удостоверяющим центрам, которые выписывают сертификаты за весьма большие деньги.

──────────────────

Когда удостоверяющий центр выписывает сертификат, он записывает в поле serialNumber числовое значение, которое должно быть разным для каждого сертификата, заверенного этим конкретным сертификатом УЦ (X.509, п. 4.1.2.2). Туда можно записывать порядковый номер, можно текущую дату-время, можно случайное число, главное требование — это число должно быть уникальным для каждого выписанного сертификата.

──────────────────

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

Цепочки доверия сертификатов на примере

Понятнее всего будет объяснить цепочки доверия на примере веб-сайтов и браузера.

У каждого работающего через SSL/TLS сайта имеется X.509-сертификат, подтверждающий его identity. Когда браузер устанавливает первое защищённое соединение с веб-сервером, они обмениваются информацией об используемых криптоалгоритмах, в рамках этого обмена веб-сервер отдаёт браузеру набор сертификатов, в котором есть обязательно сертификат собственно сайта, а остальные — промежуточные сертификаты для построения цепочки доверия.

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

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

Отзыв сертификата

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

Изначально каждый удостоверяющий центр поддерживал собственный ресурс (веб-сайт) с регулярно обновляемым списком отозванных сертификатов, они так и назывались — списки отозванных сертификатов (certificate revocation list, сокращённо CLR). Программы должны скачивать эти CLR, парсить их и затем принудительно помечать указанные там сертификаты как недоверенные. Такая схема очень неэффективна и ресурсозатратна, поэтому она постепенно заменяется на новую — Online Certificate Status Protocol или OCSP.

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

──────────────────

Помимо утечки закрытого ключа удостоверяющего центра, причиной отзыва может стать халатность самого УЦ, который выписывал сертификаты без должной проверки направо и налево. Либо использовал небезопасные или уязвимые криптоалгоритмы.

Как работает активное проксирование TLS/SSL

Активное проксирование означает, что провайдер вклинивается в потом сетевого трафика и пропускает его через специальные программы, иногда модифицируя. В случае зашифрованного трафика задача провайдера усложняется, так как для получения доступа к содержимому он должен выступить в роли клиента, расшифровать данные, а затем снова их зашифровать, отдавая настоящему клиенту.

Естественно, зашифровать тем же закрытым ключом он не может, так как он есть только у оригинального владельца сайта-источника TLS-трафика, поэтому провайдер шифрует отдаваемый настоящему клиенту поток данных собственным закрытым ключом. При этом у клиента браузер начнёт выдавать ошибки, поскольку в его базе нет доверенного сертификата, закрытым ключом которого провайдер шифрует данные. Чтобы ошибок не было, провайдер может заставить клиентов установить в систему свой корневой сертификат и сделать его доверенным.

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

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

Практика

Я начну с обзора форматов данных, на которых всё построено, и дальше последовательно пройду по всем этапам жизненного цикла сертификата: создание закрытого ключа, создание CSR, создание сертификата на основе этого CSR. Мы будем очень интенсивно использовать консольную программу openssl из состава одноимённой библиотеки. Версия openssl должна быть 1 или лучше 1.1. Бо́льшая часть команд будет работать и в форках, например, libressl в макоси или boringssl в андроиде. Удобнее всего пользоваться терминалом в linux, macos, либо в новом режиме windows.

Форматы данных

Первичным форматом представления сертификатов (а часто и других объектов в openssl) в бинарном виде является DER (Distinguished Encoding Rules). DER в свою очередь является способом представления в бинарной форме структурированных данных, описанных на специальном языке ASN.1, который традиционно используется в телекоме и сетях. ASN.1 не просто язык описания структурированных данных, для него разработаны инструменты трансляции ASN.1-нотации в код на множестве языков программирования, который кодирует и декодирует бинарные объекты в типизированные объекты языка, например, в структуры или классы. При этом важным свойством DER-объектов является интегрированность информации о структуре прямо в данных, то есть вы можете взять DER-файл и программно разобрать его на составные части даже без формального описания в ASN.1. Естественно, вы не будете знать смысл отдельных полей и какие именно данные там закодированы, но вы как минимум увидите структуру и стандартные типы: кортежи, целые числа, строки (в том числе юникодные).

Если вам интересно, можете прочитать подробное описание стандарта и правил кодирования в статье википидии про стандарт X.680 (именно он описывает ASN.1) и X.690 (он описывает кодирование бинарных объектов для ASN.1), также эту тему я затронул в своей статье про кодирование данных на смарт-картах. В этой статье я не буду рассказывать о синтаксисе и других деталях ASN.1, однако будут приводить упрощённые описания на нём, так как они достаточно простые и лёгкие для базового понимания.

Структуру и данные («дамп») DER-объекта можно посмотреть командой openssl asn1parse. Примеры её использования будут дальше в этой статье, когда я буду рассказывать о внутреннем устройстве различных бинарных крипто-объектов и буду приводить «дампы» их структуры.

──────────────────

Второй распространённый формат данных — PEM, это своего рода контейнер, позволяющий записать бинарные данные в ограниченном символьном наборе Base64, пригодном для передачи через электронную почту, чаты или даже бумагу. Один блок данных в PEM-формате выглядит так:

-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKhGNt7cXXgzTk9NaAjdJy5Lpfq3mIqws4Ev7zf1Idh043hcAFbB
/uQr/BISsrAU170bLmAXE14s3edWkQNIWaECAwEAAQJAFcFmLK/+4aB4emY+kg7N
lv2uythbv2qS+pvQ6MIniw10AZ+Ypa78x67DaZMeIRFPB9EkKs2AyYzzQmd/DBId
MQIhANOlzUg24pOCT3bsB3ZcDXs52iyBYa+vk99w0S7ybF7NAiEAy4mSyjbJI1Uv
e9zhdYwXjYutp+z6tjlYtIUAMBNNPiUCIE++mANOkr5Lig9fzUv+USIN4TOFqD3e
5NN6mYab1tM9AiALHkzCdxOttm2NmpdGUIzI0qR909guNBvAYLON7L//cQIhALr7
12CHrerflaB6KknHPDtA+1rEKdut36RQ/5WmqVdX
-----END RSA PRIVATE KEY-----

Блок начинается с символов -----BEGIN, дальше идёт метка, обозначающая тип хранимых данных, потом -----, затем кусок закодированных в Base64 данных и дальше -----END, снова та же метка и в конце опять -----.

Внутри одного файла может быть несколько PEM-блоков, причём вокруг них может быть произвольный текст, который обычно программами игнорируется. У PEM-файлов часто используется расширение .pem, либо расширения, обозначающие тип хранимых там объектов, к примеру, .cer или .crt для сертификатов или .key для закрытых ключей.

Если раскодировать через base64 текст внутри PEM-блока, обычно получим корректный DER-объект. Таким образом, для программы метка в PEM-блоке служит подсказкой, какой тип данных находится внутри.

Практически всегда мы будем работать именно с PEM, более того, openssl по умолчанию использует его во всех своих командах для ввода и вывода.

Типы данных

Object identifier (OID)

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

OID используется везде, где нужно сослаться на некую «вещь», которая определена где-то ещё. Например, если нужно указать используемый алгоритм, то записываем OID этого алгоритма вместо, скажем, строки или локального идентификатора.

Структура OID иерерахичная, в человекочитаемом виде их обычно представляют набором чисел, разделённых точкой, например, 1.2.840.113549.1.1.1 — OID алгоритма шифрования RSA. При этом OID 1.2.840.113549 является идентификатором компании RSA® Security Limited Liability Company (LLC), соответственно, этот алгоритм находится в «пространстве имён» этой компании, которая может создавать любые новые идентификаторы, начинающиеся с её OID (1.2.840.113549). Также каждый сегмент-число может иметь строковое представление, оно локально для конкретного «родительского» OID. Например, корневой сегмент 1 имеет альтернативное имя iso, корневой сегмент 0 — itu-t, корневой сегмент 2 — joint-iso-itu-t, других корневых сегментов нет. Внутри корневого сегмента 1 определены дочерние сегменты: standard(0), registration-authority(1), member-body(2), identified-organization(3). Ну и так далее. В таком виде строятся OID в ASN.1 нотации: {iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) rsaEncryption(1)}. Существует ещё одна эквивалентная нотация — OID-IRI (Internationalized Resource Identifier), в неё идентификатор выглядит так: /ISO/Member-Body/US/113549/1/1/1. Однако мы в этом тексте из всего этого великолепия будем пользоваться исключительно точечной нотацией: 1.2.840.113549.1.1.1.

В ASN.1 определён специальный тип OBJECT IDENTIFIER для OID и он используется чрезвычайно широко в самых разных объектах, если вы в спецификации блока данных видите OBJECT IDENTIFIER, то в этом блоке лежит OID.

В интернете есть специальный сайт oid-info.com, на котором в едином реестре сведено множество OID, например, для нашего идентификатора алгоритма шифрования RSA адрес страницы: http://oid-info.com/get/1.2.840.113549.1.1.1 . Вы можете им пользоваться, если вам встретится незнакомый OID, или же вы захотите узнать числовое представление идентификатора, указанного в виде строки. Дальше я в тексте буду стараться указывать OID разных сущностей в виде ссылки на соответствующую страницу oid-info.com.

Distinguished Name (DN)

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

Понятие Distinguished Name (DN) пришло из LDAP, по сути это способ записи имени некоторого объекта/субъекта/сущности в строго структурированном виде.

DN представляет собой кортеж (то есть упорядоченную последовательность) из нескольких Relative DN (RDN), а каждый RDN состоит из пары значений AttributeType (тип атрибута) и AttributeValue (значение атрибута), при этом AttributeType является OID, а в AttributeValue чаще всего находится значение какого-нибудь из строковых типов.

Формальное определение DN даётся в RFC 5280 в таком виде:

RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

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

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

AttributeType ::= OBJECT IDENTIFIER

AttributeValue ::= ANY -- DEFINED BY AttributeType

DirectoryString ::= CHOICE {
      teletexString           TeletexString (SIZE (1..MAX)),
      printableString         PrintableString (SIZE (1..MAX)),
      universalString         UniversalString (SIZE (1..MAX)),
      utf8String              UTF8String (SIZE (1..MAX)),
      bmpString               BMPString (SIZE (1..MAX)) }

Строго говоря, каждый RelativeDistinguishedName представляет собой не пару AttributeType и AttributeValue, а множество таких пар. Несколько пар в рамках одного RDN называется multi-valued RDN и теоретически может встретиться в реальной жизни, но мы пока оставим эту тему за рамками статьи.

В отличие от LDAP, в X.509 нет единого общепринятого способа сериализации DN в строковое значение (и обратно), поэтому различные программы и библиотеки придумывают свои способы. Но общий принцип такого кодирования примерно одинаковый: каждый RDN кодируется в виде строки KEY=VALUE, а такие пары ключ-значение собираются в строку при помощи разделителя, например, A1=V1; A2=V2; A3=V3. Во входных данных openssl используется нотация в такой форме:

/C=RU/ST=NSO/L=Novosibirsk/O=Regolit/CN=Sergey Stolyarov

При интерпретации такой строки каждый сегмент вида /type=value декодируется в соответствующее бинарное значение. Типы атрибутов C, L, O и некоторые другие определены в стандарте и декодируются в соответствующие OID. При этом можно сразу указать нужный OID. Например, вот эквивалентное представление DN:

/2.5.4.6=RU/2.5.4.8=NSO/L=Novosibirsk/O=Regolit/CN=Sergey Stolyarov

Здесь мы заменили C/Country на его OID 2.5.4.6, а ST/stateOrProvinceName — на OID 2.5.4.8. Также можно писать «длинные» типы атрибутов, например, вот тоже эквивалентное представление DN:

/Country=RU/stateOrProvinceName=NSO/L=Novosibirsk/O=Regolit/CN=Sergey Stolyarov

Все три строки будут в итоге сконвертированы в одинаковый бинарный код, представляющий DN.

Важное замечание

Последовательность компонентов в DN имеет значение, /C=RU/L=Novosibirsk и /L=Novosibirsk/C=RU — это разные DN.

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

При выводе DN в текстовом виде openssl использует другую нотацию, что, впрочем, вполне в духе программы: C=RU, ST=NSO, L=Novosibirsk, O=Regolit, CN=Sergey Stolyarov

AlgorithmIdentifier

Под типом AlgorithmIdentifier почти всегда подразумевается подобная структура:

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

Это последовательность из двух полей: идентификатор (OID) алгоритма и опциональное поле с параметрами алгоритма. Названия конкретных полей могут различаться, но суть именно такая.

Генерация закрытого ключа

Первым этапом создания сертификата является генерация нового закрытого ключа на основе случайных данных. Я специально выделил это в отдельный пункт, чтобы вы смогли больше понять, как всё устроено. В других инструкциях сразу начинают с этапа создания certificate signing request (CSR), но мы к нему придём более последовательным путём.

Ключ можно дополнительно зашифровать симметричным шифром, используя отдельный пароль. Это дополнительный слой защиты для такого важного объекта: даже если злоумышленник получит доступ к файловой системе, он найдёт только зашифрованный объект. При использовании такого ключа необходимо ввести пароль при старте программы или в другой момент, когда он требуется.

Начнём с выбора криптографического алгоритма. Изначально он был ограничен двумя — RSA и DSA, позднее к ним добавились алгоритмы на эллиптических кривых (ECC).

Закрытый ключ RSA

Классическая команда для создания ключа RSA длиной 512 бит в файле rsa512_1.pem выглядит так:

[user@shell]% openssl genrsa -out rsa512_1.pem 512
Generating RSA private key, 512 bit long modulus (2 primes)
...+++++++++++++++++++++++++++
........+++++++++++++++++++++++++++
e is 65537 (0x010001)

Результатом будет файл с одним PEM-блоком с меткой RSA PRIVATE KEY:

-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBALCwLuqdlD0h/ZiNd1iORH5Zgbz6Una9iAb1uT2+5+VKAHjC53RC
p6RG95VKVvQFhKic78Wy0hvxTbVbPGABS5MCAwEAAQJAa+OJInYKWLHyuj5Xy9lD
dauODykDRcJB144gCNYTn+vmIvaUpe15DCCLpOe1KtkowFbS8HKsWHgAL28zn3nx
0QIhAN0qYrvboEMJJkHmbYIOEyheS1S2xMHD1EsR8YdyU92/AiEAzIRtP52v8trb
xpm+VGbvqdoqyuaRC2yElpt8AjBC7y0CIQCaLopWXG4FTcOV/YYqPJWuds4daK0S
R+sfyoqO2m0NEQIgBSCQyJaAcbsw5VK3ZdBK09xHVFzhaALpdAkj274v/2UCIEM3
WFaS5azuCXD53wMrJEC5GlqjPHfXbm6Zssyi15xh
-----END RSA PRIVATE KEY-----

Внутри этого блока закодирован объект в формате, описанном в стандарте PKCS#1. PKCS#1 описывает алгоритм RSA, на данный момент его актуальный текст опубликован в виде RFC 3447. В стандарте описаны бинарные структуры ключей, именно в этом формате их создаёт команда openssl genrsa.

Вот как выглядит разобранный на составные закрытый ключ RSA в формате PKCS#1 (я сократил слишком длинные строки для читабельности, помним, что почти все объекты в нашей области закодированы в DER, который позволяет проводить автоматический разбор структуры):

[user@shell]% openssl asn1parse -i -in rsa512_1.pem
    0:d=0  hl=4 l= 314 cons: SEQUENCE
    4:d=1  hl=2 l=   1 prim:  INTEGER           :00
    7:d=1  hl=2 l=  65 prim:  INTEGER           :B12CA08DF04E4D4AD4149E789...skip...B0D7E0644D7186EC5C6B7D
   74:d=1  hl=2 l=   3 prim:  INTEGER           :010001
   79:d=1  hl=2 l=  64 prim:  INTEGER           :7B5CD76DFD24882C9F74ADAEC...skip...17D123C69EE06BFEAFF01
  145:d=1  hl=2 l=  33 prim:  INTEGER           :D543F842828515251F369D601752BC08602383B4A15A34D6C705877D791B54DB
  180:d=1  hl=2 l=  33 prim:  INTEGER           :D4AD434262295D2BB2F3DE825AF8F6697B72D0902694112773A72EB36161C487
  215:d=1  hl=2 l=  32 prim:  INTEGER           :4D84381F8CAB6CC522744A7D9BDCA1A5F5B3D2F27BD77AEF3A45E33A93238113
  249:d=1  hl=2 l=  32 prim:  INTEGER           :56A1DD6C0520645B90A1D659B34506DB20F63C0EFC280474D59F9C5E65A4B5B1
  283:d=1  hl=2 l=  33 prim:  INTEGER           :B5F1F5F4CA1771A6990851CFBCBDF5E2B6A032AD02AFA8AF914E47A73E8AE0DB

Для бо́льшей наглядности я использовал аргумент -i, он отделяет вложенные блоки отступом. В данном случае мы видим, что внутри файла последовательно записаны девять чисел, в RFC 3447, раздел A.1.2 даётся такое ASN.1 определение этой структуры:

RSAPrivateKey ::= SEQUENCE {
    version           Version,
    modulus           INTEGER,  -- n
    publicExponent    INTEGER,  -- e
    privateExponent   INTEGER,  -- d
    prime1            INTEGER,  -- p
    prime2            INTEGER,  -- q
    exponent1         INTEGER,  -- d mod (p-1)
    exponent2         INTEGER,  -- d mod (q-1)
    coefficient       INTEGER,  -- (inverse of q) mod p
    otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

Первое поле — версия данных, это стандартная практика, а дальше идут восемь чисел, определяющих закрытый ключ RSA. Программы, использующие закрытый ключ, разбирают эти данные на числа и дальше пользуются ими для работы.

Команда openssl genrsa не умеет записывать ключ сразу в формате DER. Если вам нужен именно он, можно сконвертировать ключ из rsa512_1.pem в файл rsa512_1.der вот такой командой:

[user@shell]% openssl rsa -in rsa -in rsa512_1.pem -outform DER -out rsa512_1.der
writing RSA key

Аргумент -outform DER как раз задаёт выходной формат данных, если его не указывать, то подразумевается -outform PEM.

──────────────────

Как я уже упоминал выше, данные закрытого ключа можно зашифровать симметричным алгоритмом. Для этого нужно в команду openssl genrsa передать дополнительный аргумент, после чего программа попросит вас ввести два раза пароль, которым будут зашифрованы сгенерённые данные. Например, чтобы зашифровать ключ шифром AES-128-CBC, нужно выполнить команду:

[user@shell]% openssl genrsa -out rsa512_1_e.pem -aes128 512
Generating RSA private key, 512 bit long modulus
...............++++++++++++
......++++++++++++
e is 65537 (0x10001)
Enter pass phrase for rsa512_1_e.pem:
Verifying - Enter pass phrase for rsa512_1_e.pem:

Результирующий файл rsa512_1_e.pem содержит один PEM-блок с такой же меткой RSA PRIVATE KEY, однако к блоку добавлены дополнительные атрибуты (они описаны в RFC 1421), указывающие, что данные внутри зашифрованы:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,49FDC7C579C43CA931FAC9BCB6AD829B

SORCkWmb3Nv1Zzge5+kDLWbNDBdVl4w5bDQaGY4ebArDTqKPHAwQMJ2Kv7H2XUEo
U94CJ8BT1X9Ycn4CCawY7DQoUn17TfHHGpW7Kn8Nzh6k7CWKzsvdQHxfnKM5QRN7
00COHepj2fAN5+UIT+2Md3c4QcKCjLfA4sIzTLBrrNl7HdwUs9cFEyKxl0j4DkzE
t0OytYL2o/yrLY90QHohLsqMVDRr2I+O+QtaJm/3zSbGAJmsEDYkt9InG9MfikYd
/HTCV3Mw/uLVLUEBbHT48lFUNZcaYD2a17r0hinsmNIwY9PVNfc4zt5WRKFYV+MV
Xw44d0cM8aA9dLx76VGSbyshBR/m2DRvwvUonjH7Sv/zKe6mz990NZ4e6pgwWCek
XmLu+z9k7hOERYwMJC0vweLGlxR3Cu1fErcbHiDfIGI=
-----END RSA PRIVATE KEY-----

Расшифровать такой файл можно такой командой (нужно знать пароль, которым зашифрованы данные):

[user@shell]% openssl rsa -in rsa512_1_e.pem -out rsa512_1.pem
Enter pass phrase for rsa512_1_e.pem:
writing RSA key

Ну и также вы можете зашифровать уже имеющийся закрытый ключ RSA (rsa512_1.pem) командой openssl rsa (воспользуемся алгоритмом AES-192-CBC):

[user@shell]% openssl rsa -in rsa512_1.pem -out rsa512_1_e.pem -aes192
writing RSA key
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

Замечания

Список доступных алгоритмов для шифрования зависит от версии и сборки openssl. Базовые алгоритмы типа AES-128-CBC, AES-192-CBC, DES-CBC, DES-EDE3-CBC и несколько других обычно есть везде.

Как вы могли заметить, для создания ключей RSA используется команды openssl genrsa, а для модификации — openssl rsa. Вторая команда также может использоваться для конвертации PEM в DER и обратно, генерации открытого ключа из закрытого и некоторых других операций.

Зашифрованный ключ формата PKCS#1 нельзя хранить в виде DER-файла. Если вы попытаетесь сконвертировать закрытый RSA-ключ командой openssl rsa, то программа спросит у вас пароль и запишет DER-файл с расшифрованным ключом внутри.

Никогда не используйте закрытый RSA ключ длиной менее 2048 бит, я в статье использую 512 только для демонстрации!

Закрытый ключ эллиптической криптосистемы

Важное замечание.

Разные программы и библиотеки поддерживают разные эллиптические кривые, и несмотря на то что openssl понимает их очень много, лишь очень ограниченный набор применим для реальных ситуаций, например, для SSL/TLS. См., например, таблицу совместимости кривых и TLS-библиотек: https://en.wikipedia.org/wiki/Comparison_of_TLS_implementations#Supported_elliptic_curves. Я буду использовать в примерах лишь те кривые, которые имеют максимально возможную поддержку среди библиотек.

Для создания закрытого ключа эллиптической криптосистемы (EC-ключа) используется команда openssl ecparam:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-eckey.pem

Аргумент -noout нужен для того, чтобы в результирующий файл был записан только сгенерённый закрытый ключ, без параметров использованной эллиптической кривой в виде отдельного PEM-блока. Название используемой кривой указываем в аргументе -name prime256v1.

Команда openssl ecparam создаёт ключ в формате, описанном в стандарте RFC 5915, в PEM-файле это блок с маркером EC PRIVATE KEY, его структура описывается вот таким упрощённым ASN.1-определением:

ECPrivateKey ::= SEQUENCE {
  version        INTEGER,
  privateKey     OCTET STRING,
  parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
  publicKey  [1] BIT STRING OPTIONAL
}

В нём два обязательных поля: version и privateKey, а также два опциональных: parameters — с параметрами эллиптической кривой и publicKey — с открытым ключом.

Посмотрим на созданный ключ, он записывается в PEM-блок с маркером EC PRIVATE KEY:

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIP6zRS0PbMX8QZZb7KMc0Us/XFa/NMDWVgXLuVEeas0LoAoGCCqGSM49
AwEHoUQDQgAEFoLqkuY6BGIKFrQE7B1ui+rrEit4vLr34toMd2IX7tKwn+WlRRcZ
CfvZUi/8NVK5T3ru2Y0RpmW5qC1QUGPWcA==
-----END EC PRIVATE KEY-----

Если воспользоваться командой openssl asn1parse, увидим структуру данных внутри этого блока:

[user@shell]% openssl asn1parse -i -in prime256v1-eckey.pem
    0:d=0  hl=2 l= 119 cons: SEQUENCE          
    2:d=1  hl=2 l=   1 prim:  INTEGER           :01
    5:d=1  hl=2 l=  32 prim:  OCTET STRING      [HEX DUMP]:FEB3452D0F6CC5FC41965BECA31CD14B3F5C56BF34C0D65605CBB9511E6ACD0B
   39:d=1  hl=2 l=  10 cons:  cont [ 0 ]        
   41:d=2  hl=2 l=   8 prim:   OBJECT            :prime256v1
   51:d=1  hl=2 l=  68 cons:  cont [ 1 ]        
   53:d=2  hl=2 l=  66 prim:   BIT STRING        

Видим, что в первом поле записана версия (число 01), во втором поле — закрытый ключ в виде байтовой строки, в третьем находится блок с параметрами в виде идентификатора кривой (в форме OID, которое преобразуется программой в человекочитаемый формат prime256v1) и в конце — открытый ключ.

При генерации ключа вместо prime256v1 можно указать название другой эллиптической кривой, openssl их знает очень много, полный список можно вывести командой openssl ecparam -list_curves:

[user@shell]% openssl ecparam -list_curves
  secp112r1 : SECG/WTLS curve over a 112 bit prime field
  secp112r2 : SECG curve over a 112 bit prime field
  secp128r1 : SECG curve over a 128 bit prime field
...skip...
  brainpoolP512t1: RFC 5639 curve over a 512 bit prime field
  SM2       : SM2 curve over a 256 bit prime field

За каждым названием типа secp112r1 или secp112r1 скрыт набор параметров, задающих эту кривую, и вы можете их включить в закрытый ключ вместо предопределённого названия кривой. Это делается добавлением аргумента -param_enc explicit в вызов команды openssl ecparam:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout -param_enc explicit -out prime256v1-eckey-full.pem

Результирующий файл prime256v1-eckey-full.pem получается заметно больше:

-----BEGIN EC PRIVATE KEY-----
MIIBaAIBAQQg90po7E2F5ushBs6832qyZDKu34YUL6XnQgTzICeYgbaggfowgfcC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
YyVRAgEBoUQDQgAEYYr/jahQc8DXGycDUG+4uYPPvAM447vdt6mAtNOEisSPeKVW
CTBUZ+Bm/rTzuDqIkRVFiGibIh/irFnK3gTvAg==
-----END EC PRIVATE KEY-----

А его структура уже сложнее — вместо идентификатора кривой появился новый блок с её полными параметрами:

[user@shell]% openssl asn1parse -i -in prime256v1-eckey-full.pem
    0:d=0  hl=4 l= 360 cons: SEQUENCE          
    4:d=1  hl=2 l=   1 prim:  INTEGER           :01
    7:d=1  hl=2 l=  32 prim:  OCTET STRING      [HEX DUMP]:F74A68EC4D85E6EB2106CEBCDF6AB26432AEDF86142FA5E74204F320279881B6
   41:d=1  hl=3 l= 250 cons:  cont [ 0 ]        
   44:d=2  hl=3 l= 247 cons:   SEQUENCE          
   47:d=3  hl=2 l=   1 prim:    INTEGER           :01
   50:d=3  hl=2 l=  44 cons:    SEQUENCE          
   52:d=4  hl=2 l=   7 prim:     OBJECT            :prime-field
   61:d=4  hl=2 l=  33 prim:     INTEGER           :FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
   96:d=3  hl=2 l=  91 cons:    SEQUENCE          
   98:d=4  hl=2 l=  32 prim:     OCTET STRING      [HEX DUMP]:FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
  132:d=4  hl=2 l=  32 prim:     OCTET STRING      [HEX DUMP]:5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
  166:d=4  hl=2 l=  21 prim:     BIT STRING        
  189:d=3  hl=2 l=  65 prim:    OCTET STRING      [HEX DUMP]:046B17D1F2E12C4247F8...skipped...CBB6406837BF51F5
  256:d=3  hl=2 l=  33 prim:    INTEGER           :FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
  291:d=3  hl=2 l=   1 prim:    INTEGER           :01
  294:d=1  hl=2 l=  68 cons:  cont [ 1 ]        
  296:d=2  hl=2 l=  66 prim:   BIT STRING        

──────────────────

Для любой именованной кривой, которую знает openssl, вы можете посмотреть её параметры в человекочитаемом формате:

[user@shell]% openssl ecparam -name prime256v1 -text -param_enc explicit -noout
Field Type: prime-field
Prime:
    00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
    00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
    ff:ff:ff
A:   
    00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
    00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
    ff:ff:fc
B:   
    5a:c6:35:d8:aa:3a:93:e7:b3:eb:bd:55:76:98:86:
    bc:65:1d:06:b0:cc:53:b0:f6:3b:ce:3c:3e:27:d2:
    60:4b
Generator (uncompressed):
    04:6b:17:d1:f2:e1:2c:42:47:f8:bc:e6:e5:63:a4:
    40:f2:77:03:7d:81:2d:eb:33:a0:f4:a1:39:45:d8:
    98:c2:96:4f:e3:42:e2:fe:1a:7f:9b:8e:e7:eb:4a:
    7c:0f:9e:16:2b:ce:33:57:6b:31:5e:ce:cb:b6:40:
    68:37:bf:51:f5
Order: 
    00:ff:ff:ff:ff:00:00:00:00:ff:ff:ff:ff:ff:ff:
    ff:ff:bc:e6:fa:ad:a7:17:9e:84:f3:b9:ca:c2:fc:
    63:25:51
Cofactor:  1 (0x1)
Seed:
    c4:9d:36:08:86:e7:04:93:6a:66:78:e1:13:9d:26:
    b7:81:9f:7e:90

Здесь параметр -text означает, что нужно вывести информацию в человекочитаемой форме, а -param_enc explicit означает, что параметры кривой нужно вывести в явной форме без сокращённого до имени названия. -noout отключает вывод собственно указанного объекта на экран.

Вы можете сравнить вывод этого и предыдущего листингов, чтобы убедиться, что они описывают один и тот же объект.

──────────────────

Параметры кривой можно также вывести в отдельный PEM-файл:

[user@shell]% openssl ecparam -name prime256v1 -param_enc explicit -out prime256v1-curve.pem

Получается такой файл prime256v1-curve.pem с PEM-блоком с маркером EC PARAMETERS:

-----BEGIN EC PARAMETERS-----
MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
ucrC/GMlUQIBAQ==
-----END EC PARAMETERS-----

И мы опять можем через asn1parse посмотреть его структуру:

[user@shell]% openssl asn1parse -i -in prime256v1-curve.pem
    0:d=0  hl=3 l= 247 cons: SEQUENCE          
    3:d=1  hl=2 l=   1 prim:  INTEGER           :01
    6:d=1  hl=2 l=  44 cons:  SEQUENCE          
    8:d=2  hl=2 l=   7 prim:   OBJECT            :prime-field
   17:d=2  hl=2 l=  33 prim:   INTEGER           :FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
   52:d=1  hl=2 l=  91 cons:  SEQUENCE          
   54:d=2  hl=2 l=  32 prim:   OCTET STRING      [HEX DUMP]:FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
   88:d=2  hl=2 l=  32 prim:   OCTET STRING      [HEX DUMP]:5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
  122:d=2  hl=2 l=  21 prim:   BIT STRING        
  145:d=1  hl=2 l=  65 prim:  OCTET STRING      [HEX DUMP]:046B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F......406837BF51F5
  212:d=1  hl=2 l=  33 prim:  INTEGER           :FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
  247:d=1  hl=2 l=   1 prim:  INTEGER           :01

──────────────────

Если у вас есть файл с параметрами эллиптической кривой, то вы можете его передать в команду openssl ecparam и сгенерировать ключ на этой кривой, даже если вашей версии openssl ничего про неё не известно. Вот полезный пример, как создать ключ для алгоритма ГОСТ 34.10-2012 в версии openssl, которая по умолчанию не содержит определения этой кривой. Параметры я записал в файл id-tc26-gost-3410-2012-512-paramSetA.pem, вы можете его скачать и использовать для создания ключа таким образом:

[user@shell]% openssl ecparam -in id-tc26-gost-3410-2012-512-paramSetA.pem -genkey -noout -out gost-eckey.pem

Если вы запускали все вышеперечисленные команды, у вас должно быть создано два закрытых ключа: secp160k1-eckey.pem и gost-eckey.pem. Если посмотрите размеры этих файлов, то увидите, что в первом байтов в несколько раз меньше (922 байта против 174):

[user@shell]% ls -l prime256v1-eckey.pem gost-eckey.pem
-rw------- 1 sigsergv sigsergv 922 сен  1 23:56 gost-eckey.pem
-rw------- 1 sigsergv sigsergv 227 сен  1 23:48 prime256v1-eckey.pem

Это происходит потому, что для второго ключа (gost-eckey.pem) мы явным образом указали все параметры эллиптической кривой, в то время как для первого — только название предопределённой. Разница особенно заметна, если сравнить содержимое через asn1parse:

[user@shell]% openssl asn1parse -i -in prime256v1-eckey.pem
    0:d=0  hl=2 l= 119 cons: SEQUENCE          
    2:d=1  hl=2 l=   1 prim:  INTEGER           :01
    5:d=1  hl=2 l=  32 prim:  OCTET STRING      [HEX DUMP]:FEB3452D0F6CC5FC41965BECA31CD14B3F5C56BF34C0D65605CBB9511E6ACD0B
   39:d=1  hl=2 l=  10 cons:  cont [ 0 ]        
   41:d=2  hl=2 l=   8 prim:   OBJECT            :prime256v1
   51:d=1  hl=2 l=  68 cons:  cont [ 1 ]        
   53:d=2  hl=2 l=  66 prim:   BIT STRING        

[user@shell]% openssl asn1parse -i -in gost-eckey.pem
    0:d=0  hl=4 l= 631 cons: SEQUENCE          
    4:d=1  hl=2 l=   1 prim:  INTEGER           :01
    7:d=1  hl=2 l=  64 prim:  OCTET STRING      [HEX DUMP]:0310F98B02AAC16490DCBD61...skipped...E4589F44F219EFFF4
   73:d=1  hl=4 l= 422 cons:  cont [ 0 ]        
   77:d=2  hl=4 l= 418 cons:   SEQUENCE          
   81:d=3  hl=2 l=   1 prim:    INTEGER           :01
   84:d=3  hl=2 l=  76 cons:    SEQUENCE          
   86:d=4  hl=2 l=   7 prim:     OBJECT            :prime-field
   95:d=4  hl=2 l=  65 prim:     INTEGER           :FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF...skipped...FFFFFFFFFFFFDC7
  162:d=3  hl=3 l= 132 cons:    SEQUENCE          
  165:d=4  hl=2 l=  64 prim:     OCTET STRING      [HEX DUMP]:FFFFFFFFFFFFFFFFFFFFFFFFFFFF...skipped...FFFFFFFDC4
  231:d=4  hl=2 l=  64 prim:     OCTET STRING      [HEX DUMP]:E8C2505DEDFC86DDC1BD0B2...skipped...4761503190785A71C760
  297:d=3  hl=3 l= 129 prim:    OCTET STRING      [HEX DUMP]:04000000000000...skipped...5B889A589CB5215F2A4
  429:d=3  hl=2 l=  65 prim:    INTEGER           :FFFFFFFFFFFFFFFFFF...skipped...11F10B275
  496:d=3  hl=2 l=   1 prim:    INTEGER           :01
  499:d=1  hl=3 l= 133 cons:  cont [ 1 ]        
  502:d=2  hl=3 l= 130 prim:   BIT STRING        

Замечание

Команда openssl ecparam умеет сама записывать создаваемый ключ в формате DER, для этого нужно указать аргумент -outform DER:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout -outform DER -out prime256v1-eckey.der

Обратите внимание, что этот бинарный файл prime256v1-eckey.der по-прежнему подчиняется RFC 5915, это НЕ закодированный в DER PKCS#8-ключ. Если вам нужен бинарный PKCS#8-ключ, смотрите раздел Конвертация закрытых ключей в формат PKCS#8 и обратно.

──────────────────

Как и в случае RSA, вы можете зашифровать создаваемый закрытый ключ (только внутри PEM, шифрование DER не поддерживается), однако для этого нужно использовать сразу две соединённые команды, если хотите сгенерённый ключ зашифровать сразу при создании:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout | openssl ec -aes-128-cbc -out prime256v1-eckey-e.pem

──────────────────

Для операций над ключами эллиптических криптосистем существует отдельная команда openssl ec, она по сути аналогична openssl rsa, только работает для другого типа ключей. Например, конвертация ключа из формата PEM в DER делается так:

[user@shell]% openssl ec -in prime256v1-eckey.pem -outform DER -out prime256v1-eckey.der
read EC key
writing EC key

А шифрование закрытого ключа — так:

[user@shell]% openssl ec -in prime256v1-eckey.pem -aes192 -out prime256v1-eckey-e.pem
read EC key
writing EC key
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

Закрытые ключи в формате PKCS#8

Важное замечание насчёт PKCS#8.

Ключи в данном формате могут до сих пор считаться несколько экзотичными и некоторые библиотеки или программы их не понимают. Но важно понимать, что это точно такие же закрытые ключи, только в специальном «универсальном» формате-контейнере.

Выше мы видели, что форматы данных для RSA и EC-ключей совершенно разные, а отличить их друг от друга можно только через разные PEM-маркеры. Если же ключи представлены в DER-файлах, распознать их можно только эмпирическим путём, пытаясь разобрать данные для разных спецификаций.

Эту ситуацию пытается разрешить стандарт PKCS#8, он описывает способ представления закрытых ключей для произвольных алгоритмов, а также способ шифрования данных ключа.

Структура закрытого ключа в формате PKCS#8 очень простая и описывается такой ASN.1-схемой (я её упростил для читаемости):

PrivateKeyInfo ::= SEQUENCE {
    version                   Version,
    privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
    privateKey                PrivateKey,
    attributes           [0]  IMPLICIT Attributes OPTIONAL }

PrivateKeyAlgorithmIdentifier ::= SEQUENCE {
       algorithm AlgorithmIdentifier,
       parameters AlgorithmParameters OPTIONAL
   }

По сути это набор из как минимум трёх полей: версии синтаксиса (сейчас там может быть только 0), идентификатора алгоритма закрытого ключа (см. Типы данных → AlgorithmIdentifier) и собственно данных закрытого ключа.

Идентификатор алгоритма (privateKeyAlgorithm) представляет собой набор из одного обязательного поля algorithm, в котором записывается OID (см. Типы данных → Object identifier (OID)), и опционального поля с параметрами алгоритма.

Обычно с конкретным идентификатором алгоритма закрытого ключа связан способ его кодирования, именно в таком формате должны быть записаны данные в поле privateKey.

──────────────────

Для создания закрытых ключей в формате PKCS#8 используется команда openssl genpkey. Например, для создания RSA-ключа длиной 512 бит команда выглядит так (ключ записывается в файл rsa512_8.pem):

[user@shell]% openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:512 -out rsa512_8.pem

А вот как создать ключ для эллиптического алгоритма на кривой secp160k1 и сохранить его в файл secp160k1_8.pem:

[user@shell]% openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -pkeyopt ec_param_enc:named_curve -out prime256v1-eckey-8.pem
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

В обоих примерах создаётся файл в формате PEM, причём с одинаковыми маркерами. Вот как выглядит файл rsa512_8.pem:

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaqCEC9Y6I3VC8
+QtqA6oD6vxvEJczu+YV5Kn8b7CvcqWBfXI3RCG8VB/Hbt1cj5n0jc266d/XDimT
......skipped......
G0SVctomV0+dx5azDC5Dj1Mt8xLgnkn6924yPLGLomGGVzsCpzxyqND7g4bU5yQV
XILO58GSBmyOuWI7+EFb7s6N
-----END PRIVATE KEY-----

А вот файл prime256v1-eckey-8.pem, для его создания мы указали аргумент -pkeyopt ec_param_enc:named_curve, поэтому полные параметры кривой в него не включены, а только имя:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIcYPIBeg/FEzbxU7
gfUTGkSYULyfLAErmi/BWXUCq6+hRANCAARULnWxbWLN1bEP2l4tKHo8j4TjMU54
pldrPAXxlsRTG1A68AP8w+ATR3SAKGauy25+fTdgbk3gn2TbBXyoM3IE
-----END PRIVATE KEY-----

Если запустить openssl asn1parse, то можно увидеть внутреннюю структуру этих данных, например:

[user@shell]% openssl asn1parse -i -in prime256v1-eckey-8.pem
    0:d=0  hl=3 l= 135 cons: SEQUENCE          
    3:d=1  hl=2 l=   1 prim:  INTEGER           :00
    6:d=1  hl=2 l=  19 cons:  SEQUENCE          
    8:d=2  hl=2 l=   7 prim:   OBJECT            :id-ecPublicKey
   17:d=2  hl=2 l=   8 prim:   OBJECT            :prime256v1
   27:d=1  hl=2 l= 109 prim:  OCTET STRING      [HEX DUMP]:306B020101042021C60F...skipped...D37606E4DE09F64DB057CA8337204

Мы видим три поля: версию (со значением 0), идентификатор алгоритма, состоящий из OID 1.2.840.10045.2.1 (обозначается идентификатором id-ecPublicKey) и OID параметра 1.2.840.10045.3.1.7 именованной эллиптической кривой (обозначается идентификатором prime256v1).

Если при создании ключа указать аргумент -pkeyopt ec_param_enc:explicit, то в поле параметра алгоритма будут стоять параметры кривой вместо идентификатора prime256v1.

──────────────────

Команда genpkey позволяет также создавать зашифрованные ключи, однако по необъяснимым причинам позволяет это делать только в формате PEM, а чтобы преобразовать зашифрованный ключ в DER, нужно использовать команду openssl pkcs8, о которой я расскажу в следующем разделе.

Зашифрованный ключ создаётся при указании аргумента с алгоритмом симметричного шифрования (в данном случае -aes-128-cbc):

[user@shell]% openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -aes-128-cbc -out rsa512_8_e.pem
.....+++++
................+++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

Конвертация закрытых ключей в формат PKCS#8 и обратно

В принципе, все современные библиотеки и платформы должны корректно принимать ключи в формате PKCS#8, однако в некоторых из них требуют ключ только в традиционном формате для данного алгоритма (например, PKCS#1 для RSA-ключей). В openssl есть команда для конвертации закрытых ключей между PKCS#8 и традиционным форматом и обратно.

Например, для преобразования файла RSA-ключа rsa512_1.der из PKCS#1 DER в PKCS#8 PEM без симметричного шифрования используется такая команда:

[user@shell]% openssl pkcs8 -topk8 -nocrypt -in rsa512_1.der -inform DER

Если же вы хотите зашифровать ключ, замените аргумент -nocrypt на -v2 aes128 (вместо aes128 можно также использовать aes256 или des3):

[user@shell]% openssl pkcs8 -topk8 -v2 aes128 -in rsa512_1.der -inform DER
Enter Encryption Password:
Verifying - Enter Encryption Password:

Команда запросит новый пароль и его подтверждение.

Важное замечание насчёт пароля.

Симметричный алгоритм обычно требует в качестве ключа шифрования набор байтов строго определённой длины, например, 128 бит в случае AES-128-CBC, однако пользователю разрешено ввести произвольный пароль, длина которого отличается от требуемых 128 бит. Поэтому пароль сначала преобразуется специальными методами в набор байтов нужной длины и только потом этот набор байтов используется в качестве ключа.

Такая процедура называется KDF — key derivation function, в русскоязычной терминологии — функция формирования ключа. И если выбранный алгоритм симметричного шифрования использует очень короткий ключ, ваш длинный «безопасный» пароль будет преобразован в короткий ключ, который злоумышленник сможет легко подобрать (то есть ему нужно подобрать существенно менее длинный ключ, чем пароль, из которого он генерируется).

В некоторых вариантах openssl, например, в libressl из macos по умолчанию используется очень слабый алгоритм с очень коротким ключом, поэтому всегда указывайте алгоритм шифрования, как минимум это должен быть AES-128-CBC (что делается аргументом -v2 aes128).

Ну а вообще вся эта криптография с паролем соответствует PKCS#5, последняя версия которого описана в RFC 2898. Аргумент -v2 включает как раз её (PKCS#5 v2.0), однако не все программы могут такие данные принимать и может понадобиться использовать прошлую версию (PKCS#5 v1.5), но это уже детали, о которых я тут не буду рассказывать.

По умолчанию результат выводится в терминал, чтобы вывести в файл rsa512_8_e.pem, укажите аргумент -out rsa512_8_e.pem:

[user@shell]% openssl pkcs8 -topk8 -v2 aes128 -in rsa512_1.der -inform DER -out rsa512_8_e.pem

──────────────────

Если вы не хотите шифровать данные ключа, используйте аргумент -nocrypt:

[user@shell]% openssl pkcs8 -topk8 -nocrypt -in rsa512_1.der -inform DER -out rsa512_8.pem

❈ ❈ ❈

Для обратного преобразования из PKCS#8 в традиционный формат используется аргумент -traditional:

[user@shell]% openssl pkcs8 -nocrypt -in rsa512_8.pem -traditional

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

Хаос с генерацией ключей в openssl

В документации к openssl написано, что genpkey заменяет целый класс других команд типа genrsa, gendsa и других. Однако формат создаваемых файлов у genrsa и genpkey совершенно разный: первая генерирует RSA-ключ в формате PKCS#1, а вторая — PKCS#8. Но если указать вывод в DER, то genpkey создаёт не PKCS#8 в формате DER, а тоже PKCS#1 DER! Однако для эллиптических алгоритмов и PEM, и DER оба ключа создаются в формате PKCS#8.

Более того, в ряде команд нигде не указано, что результирующий файл будет записан в формате PKCS#8, например, этим грешит команда openssl req, ниже я подробнее о ней расскажу.

Создание Certificate Signing Request

Жизненный цикл сертификата продолжается созданием запроса не подпись сертификата (Certificate Signing Request, CSR). По сути он является шаблоном, на основе которого удостоверяющий центр создаст и подпишет сертификат. CSR содержит обязательные личные данные заявителя, открытый ключ и дополнительные необязательные поля с другими данными. И всё это должно быть подписано закрытым ключом, который соответствует приложенному открытому.

Создание CSR в диалоговом режиме

Мы будем делать CSR на основе закрытого ключа, ранее созданного в файле prime256v1-eckey.pem.

Для создания CSR используется команда openssl req, обычно её запускают в интерактивном режиме, в котором она запрашивает у пользователя личные данные:

[user@shell]% openssl req -new -key prime256v1-eckey.pem -out regolit-1.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:RU
State or Province Name (full name) []:NSO
Locality Name (eg, city) []:Novosibirsk
Organization Name (eg, company) []:Regolit
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:test.regolit.com
Email Address []:[email protected]

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:

Стандартный набор полей:

  • страна (Country Name) → RU, здесь вводится двухсимвольный код страны в соответствии с ISO 3166-1;
  • область/регион (State or Province Name) → NSO, название области, края, провинции и так далее;
  • название населённого пункта (Locality Name) → Novosibirsk, название города, посёлка и так далее;
  • название организации (Organization Name) → Regolit, название организации;
  • отдел организации (Organizational Unit Name) → название отдела в организации (я это поле не заполнял);
  • общее имя (Common Name) → regolit.com, это по сути название сертификата, ранее сюда принято было записывать доменное имя сайта, подробнее об этом я расскажу позднее;
  • электронная почта (Email Address) → [email protected], адрес электронной почты;
  • опознавательный пароль (A challenge password) → 123456, опознавательный пароль, об этом я тоже расскажу отдельно.

Я указал закрытый ключ в аргументе -key secp160k1-eckey.pem, в процессе генерации CSR в файле domain.csr из него был извлечён открытый ключ, добавлен в CSR, добавлены остальные личные данные, и в итоге этим же закрытым ключом CSR был подписан.

Формат CSR описан в PKCS#10, последняя на текущий момент его версия — RFC 2986. Как и в случае с ключом, вы можете посмотреть структуру только что созданного файла командой openssl ans1parse:

[user@shell]% openssl asn1parse -dump -i -in domain.csr
    0:d=0  hl=4 l= 341 cons: SEQUENCE          
    4:d=1  hl=3 l= 251 cons:  SEQUENCE          
    7:d=2  hl=2 l=   1 prim:   INTEGER           :00
   10:d=2  hl=3 l= 129 cons:   SEQUENCE          
   13:d=3  hl=2 l=  11 cons:    SET               
   15:d=4  hl=2 l=   9 cons:     SEQUENCE          
   17:d=5  hl=2 l=   3 prim:      OBJECT            :countryName
   22:d=5  hl=2 l=   2 prim:      PRINTABLESTRING   :RU
   26:d=3  hl=2 l=  12 cons:    SET               
   28:d=4  hl=2 l=  10 cons:     SEQUENCE          
   30:d=5  hl=2 l=   3 prim:      OBJECT            :stateOrProvinceName
   35:d=5  hl=2 l=   3 prim:      UTF8STRING        :NSO
   40:d=3  hl=2 l=  20 cons:    SET               
   42:d=4  hl=2 l=  18 cons:     SEQUENCE          
   44:d=5  hl=2 l=   3 prim:      OBJECT            :localityName
   49:d=5  hl=2 l=  11 prim:      UTF8STRING        :Novosibirsk
   62:d=3  hl=2 l=  16 cons:    SET               
   64:d=4  hl=2 l=  14 cons:     SEQUENCE          
   66:d=5  hl=2 l=   3 prim:      OBJECT            :organizationName
   71:d=5  hl=2 l=   7 prim:      UTF8STRING        :Regolit
   80:d=3  hl=2 l=  25 cons:    SET               
   82:d=4  hl=2 l=  23 cons:     SEQUENCE          
   84:d=5  hl=2 l=   3 prim:      OBJECT            :commonName
   89:d=5  hl=2 l=  16 prim:      UTF8STRING        :test.regolit.com
  107:d=3  hl=2 l=  33 cons:    SET               
  109:d=4  hl=2 l=  31 cons:     SEQUENCE          
  111:d=5  hl=2 l=   9 prim:      OBJECT            :emailAddress
  122:d=5  hl=2 l=  18 prim:      IA5STRING         :[email protected]
  142:d=2  hl=2 l=  89 cons:   SEQUENCE          
  144:d=3  hl=2 l=  19 cons:    SEQUENCE          
  146:d=4  hl=2 l=   7 prim:     OBJECT            :id-ecPublicKey
  155:d=4  hl=2 l=   8 prim:     OBJECT            :prime256v1
  165:d=3  hl=2 l=  66 prim:    BIT STRING        
      0000 - 00 04 16 82 ea 92 e6 3a-04 62 0a 16 b4 04 ec 1d   .......:.b......
      0010 - 6e 8b ea eb 12 2b 78 bc-ba f7 e2 da 0c 77 62 17   n....+x......wb.
      0020 - ee d2 b0 9f e5 a5 45 17-19 09 fb d9 52 2f fc 35   ......E.....R/.5
      0030 - 52 b9 4f 7a ee d9 8d 11-a6 65 b9 a8 2d 50 50 63   R.Oz.....e..-PPc
      0040 - d6 70                                             .p
  233:d=2  hl=2 l=  23 cons:   cont [ 0 ]        
  235:d=3  hl=2 l=  21 cons:    SEQUENCE          
  237:d=4  hl=2 l=   9 prim:     OBJECT            :challengePassword
  248:d=4  hl=2 l=   8 cons:     SET               
  250:d=5  hl=2 l=   6 prim:      UTF8STRING        :123456
  258:d=1  hl=2 l=  10 cons:  SEQUENCE          
  260:d=2  hl=2 l=   8 prim:   OBJECT            :ecdsa-with-SHA256
  270:d=1  hl=2 l=  73 prim:  BIT STRING        
      0000 - 00 30 46 02 21 00 f2 e7-5d 8b b5 23 90 84 00 13   .0F.!...]..#....
      0010 - a7 39 f7 36 78 d1 31 dc-99 9c 90 0e 6c 1a 33 56   .9.6x.1.....l.3V
      0020 - 81 b1 b0 0c b2 ab 02 21-00 90 7b 5d 8c 58 ec 06   .......!..{].X..
      0030 - f7 88 bf cd 19 22 20 29-d7 ff aa 19 09 0b c1 c1   ....." )........
      0040 - 66 94 18 39 9f 47 b4 0d-07                        f..9.G...

Полную ASN.1-схему вы найдёте в RFC 2986. А я сейчас простыми словами её расскажу. Весь блок CSR состоит из трёх последовательно идущих элементов:

toBeSigned
содержательная часть запроса с личными данными, открытым ключом и другими атрибутами, в RFC 2986 этот блок обозначается как CertificationRequestInfo;
algorithm
идентификатор (OID) алгоритма подписи, в нашем случае openssl знает, на какой именно объект ссылается OID и вместо его «сырого» представления (1.2.840.10045.4.3.2) подставляет «человеческое» название ecdsa-with-SHA256;
signature
собственно цифровая подпись содержательной части.

Структура блока CertificationRequestInfo стандартная:

version
номер версии;
subject
структурированный блок с личными данными;
subjectPKInfo
открытый ключ;
attributes
опциональный набор дополнительных атрибутов.

Практически все поля, которые вы заполняли в диалоговом режиме, ушли в блок subject, его тип — distinguished name и я про него писал подробно в разделе Типы данных → Distinguished Name (DN).

Значение поля опознавательный пароль (A challenge password) было записано в отдельный атрибут, детально оно в стандартах не определено и отдано на усмотрение удостоверяющим центрам. Оно не фигурирует в итоговом сертификате или в процессе его изготовления, однако может использоваться в отдельных административных процедурах самого центра. К примеру, для отзыва сертификата УЦ может потребовать от заявителя сообщить тот же самый пароль, который был передан в оригинальном CSR. Также обратите внимание, что это чисто информационное поле и оно никак не используется в криптографических операциях на протяжении всего жизненного цикла сертификата.

Замечание

Команда openssl req позволяет сгенерировать закрытый ключ вместе с запросом, для этого используется аргумент -newkey, однако я намеренно этот случай не рассматриваю и явным образом отделяю этап создания закрытого ключа. Вы можете сами изучить этот режим, если хотите.

При использовании -newkey закрытый ключ всегда создаётся в формате PKCS#8 и поэтому может потребовать принудительной конвертации в традиционный формат.

Создание CSR в командном режиме

У openssl req есть командный режим, в котором CSR создаётся без интерактивного взаимодействия, а все требуемые значения передаются в командной строке, например, так:

[user@shell]% openssl req -new -batch -key prime256v1-eckey.pem \
 -subj '/C=RU/ST=NSO/L=Novosibirsk/O=Regolit/CN=test.regolit.com/[email protected]' \
 -out regolit-2.csr

Результат будет практически тот же самый, только поле challengePassword в таком режиме передать невозможно.

В аргументе -subj указываются личные данные в формате /type1=value1/type2=value2/type3=value3, его я подробно разбирал в разделе Типы данных → Distinguished Name (DN). В качестве type1, type2 и т.п. могут выступать следующие значения:

  • C или countryName
  • CN или commonName
  • L или localityName
  • ST или stateOrProvinceName
  • O или organizationName
  • OU или organizationalUnitName
  • emailAddress

❈ ❈ ❈

Создание CSR в расширенном командном режиме

Возможности стандартного командного режима сильно ограничены и функциональность openssl по-настоящему раскрывается при использовании конфигурационных файлов. Привычный режим работы программы во многом определяется штатным конфигом, который устанавливается вместе с openssl. Например, интерактивный режим запроса личных данных настраивается именно в файле и вы можете написать свой собственный конфиг с собственными вопросами и параметрами.

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

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

[ req ]
prompt = no
distinguished_name = req_distinguished_name
attributes = req_attributes

[ req_distinguished_name ]
C = RU
ST = NSO
L = Novosibirsk
O = Regolit
CN = test.regolit.com
emailAddress = [email protected]

[ req_attributes ]
challengePassword = 123456

Вы можете скачать его с моего гитхаба (domain-csr.cnf) в каталог с закрытым ключом, а потом запустить в этом же каталоге такую команду:

[user@shell]% openssl req -new -batch -config domain-csr.cnf -key prime256v1-eckey.pem -out regolit.csr

cnf-файлы мы ещё будем использовать позднее при создании сертификата. Пока лишь отмечу, что в реальной жизни вам вряд ли понадобится создание CSR таким способ, поскольку удостоверяющий центр обычно берёт из файла запроса очень ограниченный набор данных, фактически только имя и открытый ключ, а всё остальное берёт по другим каналом, например, через форму в кабинете заявителя на сайте УЦ.

Создание тестового удостоверяющего центра

Чтобы продемонстрировать весь жизненный цикл сертификата, мы должны также выступить в роли удостоверяющего центра (УЦ, Certification Authority, CA), который проверит переданный ему CSR и сгенерирует на его основе сертификат.

Для более полного понимания мы создадим два простейших УЦ: корневой и промежуточный. Вместе с подписанным сертификатом заявителя мы получим цепочку из трёх. И если корневой сертификат будет доверенным, автоматически будут и оба других.

Сразу важное замечание: описанный здесь процесс ни в коем случае не описывает принципы работы и технологии настоящего удостоверяющего центра, однако общие принципы переданы верно. Также в составе openssl есть команда openssl ca, которая в минимальной степени реализует некоторые технические операции удостоверяющего центра, однако это тоже тема отдельной статьи и в этом тексте я её рассматривать не буду.

Самоподписанный сертификат для корневого УЦ

Для корневого удостоверяющего центра мы должны создать отдельный ключ (demo-root-ca.key) и самоподписанный сертификат (demo-root-ca.crt). Сначала сгенерируем закрытый ключ (эллиптическая кривая secp384r1 ради разнообразия, она тоже поддерживается всеми библиотеками):

[user@shell]% openssl ecparam -name secp384r1 -genkey -out demo-root-ca.key

Создать самоподписанный сертификат можно той же командой openssl req, которой создаётся CSR, даже аргументы идентичны, только добавляется ещё аргумент -x509:

[user@shell]% openssl req -new -x509 -batch -days 1000 -key demo-root-ca.key -subj '/C=AQ/O=Penguin Co./CN=Demo Root CA' -out demo-root-ca.crt

Также мы добавили аргумент -days 1000, указывающий, что нужно срок действия сертификата выставить в 1000 дней, начиная с текущей даты.

В аргументе -subj передаётся DN поля subject, закодированный в виде строки, об этом формате я писал в разделе Типы данных → Distinguished Name (DN). А о типах атрибутов в нём я писал в разделе Создание CSR в командном режиме.

Просмотр сертификата

Просмотр сертификата — это, пожалуй, самая частая операция, которую вы будете делать. Так как первый сертификат мы сделали только что, то и команда для его просмотра в этом разделе. Вот как это выглядит на примере только что созданного сертификата demo-root-ca.crt:

[user@shell]% openssl x509 -text -noout -in demo-root-ca.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            01:67:67:4d:23:ef:1e:91:4d:41:b1:df:0b:97:52:70:80:34:a9:03
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = AQ, O = Penguin Co., CN = Demo Root CA
        Validity
            Not Before: Sep  1 08:00:26 2020 GMT
            Not After : May 29 08:00:26 2023 GMT
        Subject: C = AQ, O = Penguin Co., CN = Demo Root CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:2d:3a:84:fe:c1:d8:54:ee:93:55:5c:8b:33:15:
                    2f:bb:46:44:e3:8e:b7:63:b4:17:9c:1b:c3:43:83:
                    c2:ce:ab:6e:bd:01:7b:92:e0:cf:12:f1:0f:ab:db:
                    bc:9f:35:38:da:b8:c0:b1:2e:40:e2:5f:c9:ff:61:
                    06:d8:2f:95:1d:41:64:ec:eb:8a:da:37:94:0c:41:
                    12:54:bb:26:5a:44:97:3f:c7:a7:94:e0:67:ad:38:
                    14:f3:0d:83:f4:5c:0b
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                89:12:EA:73:BC:F2:E2:43:7F:E0:A4:54:A6:60:AE:B7:4D:EA:06:0C
            X509v3 Authority Key Identifier: 
                keyid:89:12:EA:73:BC:F2:E2:43:7F:E0:A4:54:A6:60:AE:B7:4D:EA:06:0C

            X509v3 Basic Constraints: critical
                CA:TRUE
    Signature Algorithm: ecdsa-with-SHA256
         30:65:02:31:00:ba:85:60:9a:18:02:3f:c9:ac:10:ff:f4:7a:
         be:59:3f:b1:3b:c5:ee:b5:ea:0f:d6:98:45:ee:bb:4f:61:32:
         23:f2:5a:b8:aa:a2:aa:9e:0a:fa:e0:da:45:0f:3e:a3:62:02:
         30:50:a9:ff:7f:9c:e5:47:1f:e3:95:cb:c2:4c:27:86:75:be:
         4f:23:9a:fa:5a:f1:3b:0e:59:1b:09:10:ca:cd:62:e2:38:70:
         71:1c:00:22:06:e0:91:50:5c:c5:97:d7:06

Аргумент -text означает, что мы хотим посмотреть представление сертификата в текстовом человекочитаемом виде, а -noout означает, что собственно содержимое сертификата в виде PEM-блока на экран выводить не нужно.

──────────────────

Структура сертификата в виде ASN.1-определения задана в RFC 5280, раздел 4.1. Basic Certificate Fields:

Certificate  ::=  SEQUENCE  {
     tbsCertificate       TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signatureValue       BIT STRING  }

Если вы внимательно читали предыдущие разделы, то заметите, что структура аналогична CSR: блок данных типа TBSCertificate, идентификатор алгоритма и цифровая подпись. Вся содержательная часть сертификата находится внутри блока с типом TBSCertificate (напомню, что tbs расшифровывается как to be signed, то есть подлежит подписи или требует подписи). TBSCertificate определяется там же в RFC 5280 следующим образом:

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  EXPLICIT 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]  EXPLICIT Extensions OPTIONAL
                          -- If present, version MUST be v3
     }

Первые семь базовых полей являются обязательными, они всегда есть в любом сертификате (их семантика описана в RFC 5280, раздел 4.1.2 TBSCertificate):

version
версия формата сертификата, здесь может быть либо значение 0 (означает версию 1, указывается, когда в рассматриваемом блоке tbsCertificate есть только базовые поля); либо значение 2 (означает версию 3, указывается, когда есть дополнительные поля). Другие значения помимо 0 или 2 в стандарте не определены и на практике не должны использоваться.
serialNumber
порядковый номер сертификата в реестре УЦ, про него я писал выше, здесь должно быть число, уникальное для каждого сертификата, подписанного конкретным сертификатом УЦ.
signature
в этом поле указывается идентификатор алгоритма цифровой подписи, которую использовал удостоверяющий центр для подписи сертификата. Формат поля зависит от использованного алгоритма, но всегда включает в себя как минимум OID конкретного алгоритма.
issuer
здесь содержится имя сертификата УЦ, которым был подписан этот сертификат. Как и в случае с CSR, здесь записывается не строка, а строго структурированное значение distinguished name (DN), я о нём подробно писал в разделе Типы данных → Distinguished Name (DN).
validity
интервал, в течение которого сертификат может быть доверенным, состоит из двух значений: notBefore и notAfter.
subject
субъект сертификата, то есть та персона или организация, для которой он был выписан, формат поля — DN, такой же, как и для issuer. Также значение из этого поля принято считать названием или именем сертификата,именно оно используется для отображение в браузере, например.
subjectPublicKeyInfo
в этом поле записывается открытый ключ субъекта, он состоит из двух последовательных значений: идентификатора алгоритма (поле algorithm, см. Типы данных → AlgorithmIdentifier)) и собственно открытого ключа в бинарной форме (поле subjectPublicKey).

Расширения сертификата (Certificate extensions)

Когда openssl создаёт самоподписанный сертификат, то в него автоматически добавляются три расширения: Subject Key Identifier, Authority Key Identifier и Basic Constraints. В текстовом представлении сертификата расширения показываются в секции X509v3 extensions вот так:

        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                89:12:EA:73:BC:F2:E2:43:7F:E0:A4:54:A6:60:AE:B7:4D:EA:06:0C
            X509v3 Authority Key Identifier: 
                keyid:89:12:EA:73:BC:F2:E2:43:7F:E0:A4:54:A6:60:AE:B7:4D:EA:06:0C

            X509v3 Basic Constraints: critical
                CA:TRUE

Я пока отмечу только расширение Basic Constraints, в нём указывается флаг-разрешение, может ли сертификат использоваться для подписывания других сертификатов. Если может, то подписанные им другие сертификаты корректным образом встраиваются в цепочку доверия. Если же разрешения нет, то подписанные сертификаты будут автоматически считаться недоверенными. Так как мы создаём сертификат УЦ, он автоматически помечается как разрешённый для подписывания других.

В расширениях обычно записывается дополнительная информация, которой не нашлось места в базовых полях.

Когда только X.509-сертификаты стали использоваться в SSL, название сайта (домен) записывалось в поле subject, в атрибуте CommonName (CN). Однако очень быстро стало понятно, что этого недостаточно, так как один и тот же сайт может иметь несколько доменных имён и для каждого нужен свой сертификат, поэтому было придумано расширение subjectAltName. В общем же случае расширение subjectAltName призвано отделить имя сертификата от identity, которая в сертификате хранится. Имя записывается в subject, identity — в subjectAltName. Полная спецификация этого расширения описана в RFC 5280, раздел 4.2.1.6 Subject Alternative Name.

Другой пример расширения — Basic Constraints, оно описано в RFC 5280, раздел 4.2.1.9 Basic Constraints. Через него удостоверяющий центр разрешает этому сертификату выступать в роли промежуточного (intermediate). Если такое разрешение прописано, то подписанные таким сертификатом другие сертификаты корректным образом встраиваются в цепочку доверия. Если же разрешения нет, то подписанные сертификаты будут автоматически считаться недоверенными.

У каждого расширения есть идентификатор — OID, например, для Subject Alternative Name это 2.5.29.17, а для Basic Constraints2.5.29.19. А формальное описание одного расширения на ASN.1 выглядит так:

Extension  ::=  SEQUENCE  {
     extnID      OBJECT IDENTIFIER,
     critical    BOOLEAN DEFAULT FALSE,
     extnValue   OCTET STRING
     }

В поле extnID записывается OID расширения. В булевском поле critical записывается специальный флаг критичности расширения, если он установлен и приложение не знает про такое расширение, то весь сертификат должен помечаться как недоверенный; если же флаг отсутствует, значение подразумевается false. В поле extnValue записывается байтовая строка, интерпретация которой зависит от конкретного расширения. Как правило, она закодирована в DER и для неё где-то существует соответствующее ASN.1 описание.

──────────────────

Вы можете указать нужные вам расширения при создании CSR, однако практически все удостоверяющие центры полностью игнорируют эти данные и заполняют их в сертификате самостоятельно.

Создание сертификата промежуточного УЦ

Сертификат корневого удостоверяющего центра практически никогда не используется для подписи конечных сертификатов заявителей, вместо этого им подписываются сертификаты промежуточных УЦ, а уже потом они используются для подписи конечных сертификатов.

Удостоверяющий центр называется промежуточным (intermediate), если его сертификат (который он использует для подписи) подписан каким-то другим сертификатом и при этом имеет разрешение на подписывание других сертификатов (через расширение Basic Constraints). Мы сделаем свой тестовый промежуточный УЦ, у него будет собственный ключ и собственный сертификат. Закрытый ключ на этот раз будет RSA 2048 бит и в формате PKCS#8:

[user@shell]% openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -out demo-intermediate-ca.key
..+++++
...........................................+++++

Дальше создаём CSR:

[user@shell]% openssl req -new -batch -key demo-intermediate-ca.key \
  -subj '/C=RU/L=Omsk/CN=Demo Intermediate Authority' -out demo-intermediate-ca.csr

Создаём файл demo-intermediate-ca-crt.cnf с параметрами расширений, которые мы хотим включить в сертификат:

[cert_ext]
authorityKeyIdentifier = keyid
subjectKeyIdentifier = hash
basicConstraints = critical, CA:true
keyUsage = digitalSignature, keyCertSign

Вот что каждая строчка в нём означает:

authorityKeyIdentifier = keyid
Идентификатор ключа «родительского» удостоверяющего центра автоматически берётся из расширения Subject Key Identifier того сертификата, которым подписываем данный.
subjectKeyIdentifier = hash
Идентификатора собственного ключа порождается автоматически из открытого ключа по алгоритму из RFC 5280.
basicConstraints = critical, CA:true
Критичное расширение Basic Constraints, разрешающее выступать в роли удостоверяющего центра.
keyUsage = digitalSignature, keyCertSign
Разрешения, как можно использовать сертификата: цифровая подпись и подписывание других сертификатов.

И, наконец, создаём сертификат demo-intermediate-ca.crt, используя конфигурационный файл:

[user@shell]% openssl x509 -req -in demo-intermediate-ca.csr -days 365 \
  -CA demo-root-ca.crt -CAkey demo-root-ca.key -CAcreateserial -extfile demo-intermediate-ca-crt.cnf \
  -extensions cert_ext -out demo-intermediate-ca.crt

Пояснения по новым аргументам:

-req
указывает, что на вход в аргументе -in regolit.csr ожидается именно Certificate Signing Request;
-days 365
выписываем сертификат на 365 дней;
-CA demo-root-ca.crt -CAkey demo-root-ca.key
указываем сертификат и закрытый ключ УЦ, из сертификата demo-root-ca.pem будет взято поле subject и вставлено в итоговой сертификат в поле issuer;
-CAcreateserial
это означает, что будет создан файл с именем demo-root-ca.srl, в который будет записан серийный номер сертификата, а дальше openssl из этого файла возьмёт значение и запишет в сертификат, про серийный номер я уже писал выше в теоретическом разделе, это должно быть число, уникальное для каждого сертификата, подписанного конкретным сертификатом УЦ. В реальном удостоверяющем центре ведётся реестр выписанных сертификатов и для каждого создаётся собственный уникальный serialNumber. Вы можете создать файл demo-root-ca.srl самостоятельно и прописать туда любое число. При каждом запуске команды создания сертификата на базе CSR значение в этом файле увеличивается на единицу. Если файла нет и аргумент -CAcreateserial не указан, то openssl выдаст ошибку. Если файл уже существует, то аргумент -CAcreateserial игнорируется.
-extfile demo-intermediate-ca-crt.cnf
Путь к файлу с параметрами расширений.
-extensions cert_ext
Название секции в файле, где собственно и записаны параметры расширений.

──────────────────

К этом моменту у нас есть тестовый удостоверяющий центр, сертификатом которого (demo-intermediate-ca.crt) мы можем подписывать сертификаты заявителей.

Создание сертификата из CSR

Несколькими разделами ранее мы создали CSR в файле regolit.csr, также мы сделали цепочку из двух тестовых удостоверяющих центров: корневого и промежуточного. И теперь готовы подписать CSR-запрос сертификатом промежуточного УЦ demo-intermediate-ca.crt. Последовательность действий здесь практически такая же, только будут прописаны другие расширения.

Удостоверяющий центр при создании сертификата на базе CSR заявителя обычно действует так:

  • из CSR берутся: имя из поля subject и открытый ключ;
  • из сертификата УЦ берётся значение поле subject, оно пойдёт в поле issuer сертификата;
  • из другого источника берутся данные для расширений, например, список дополнительных доменов для расширения Subject Alternative Name, которые заявитель указал через форму на веб-сайте УЦ;
  • из внутренней базы удостоверяющего центра берутся данные для других полей и расширений;
  • на основе собранных данных собирается сертификат, подписывается закрытым ключом и отдаётся заявителю.

Создаём файл с параметрами расширений (regolit-crt.cnf) со следующим содержимым:

[cert_ext]
authorityKeyIdentifier = keyid
basicConstraints = critical, CA:false
subjectAltName = DNS:regolit.com, DNS:www.regolit.com
keyUsage = digitalSignature

В нём заданы такие параметры:

authorityKeyIdentifier = keyid
Идентификатор ключа удостоверяющего центра автоматически берётся из расширения Subject Key Identifier того сертификата, которым подписываем данный, то есть из demo-intermediate-ca.crt.
basicConstraints = critical, CA:false
УЦ явным образом запрещает создаваемый сертификат использовать для подписывания других сертификатов. Кроме того, значение помечено как критическое.
subjectAltName = DNS:regolit.com, DNS:www.regolit.com
Перечисляем все дополнительные записи-identity, которые мы хотим добавить в сертификат
keyUsage = digitalSignature
Разрешаем использование сертификата для проверки цифровой подписи.

И вот итоговая команда:

[user@shell]% openssl x509 -req -in regolit.csr -days 365 -CA demo-intermediate-ca.crt \
  -CAkey demo-intermediate-ca.key -CAcreateserial -extfile regolit-crt.cnf -extensions cert_ext -out regolit.crt

Получаем вот такой сертификат с расширениями:

[user@shell]% openssl x509 -in regolit.crt -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            5a:25:eb:62:a4:84:9f:75:42:9a:6e:d0:ac:aa:0b:d8:eb:34:26:d4
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = RU, L = Omsk, CN = Demo Intermediate Authority
        Validity
            Not Before: Sep  1 10:06:40 2020 GMT
            Not After : Sep  1 10:06:40 2021 GMT
        Subject: C = RU, ST = NSO, L = Novosibirsk, O = Regolit, CN = test.regolit.com, emailAddress = [email protected]
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:16:82:ea:92:e6:3a:04:62:0a:16:b4:04:ec:1d:
                    ...skipped...
                    50:50:63:d6:70
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Alternative Name: 
                DNS:regolit.com, DNS:www.regolit.com
            X509v3 Key Usage: 
                Digital Signature
    Signature Algorithm: sha256WithRSAEncryption
         3b:2a:fd:fc:0d:9a:ae:9a:d5:83:d8:74:28:a1:87:6a:58:ab:
         ...skipped...
         1f:8c:70:29

Тестирование сертификатов

Для проверки сертификатов можно воспользоваться командами openssl s_server и openssl s_client. Первая запускает простейший TLS веб-сервер с указанными сертификатами и на указанном адресе. А вторая обращается к любому указанному адресу и верифицирует соединение.

Важное свойство этих команд в том, что вы полностью контролируете формирование цепочки доверия. Например, вы можете указать для openssl s_client файл с доверенными сертификатами и тогда при установке соединения цепочки будут строиться только с использованием указанных сертификатов, а не всех системных. Аналогично с openssl s_server — вы можете указать, какие промежуточные сертификаты передавать клиентами при установке соединения.

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

Сначала выступаем в роли заявителя, который хочет запустить веб-сервер с только что полученным сертификатом. У заявителя есть сертификат — файл regolit.crt, закрытый ключ — prime256v1-eckey.pem, а также сертификат удостоверяющего центра — demo-intermediate-ca.crt. При этом предполагается, что в браузере клиента в списке доверенных сертификатов есть корневой сертификат demo-root-ca.crt, но при этом там нет промежуточного сертификата demo-intermediate-ca.crt.

В первом терминале запускаем такую команду для старта TLS веб-сервера:

[user@shell]% openssl s_server -accept 127.0.0.1:9443 -key prime256v1-eckey.pem -cert regolit.crt -cert_chain demo-intermediate-ca.crt
Using default temp DH parameters
ACCEPT

Если всё сделано правильно и в каталоге лежат нужные файлы, программа запустит веб-сервер по адресу https://127.0.0.1:9443, вы даже можете открыть его в браузере, но он сразу выдаст вам ошибку вида NET::ERR_CERT_AUTHORITY_INVALID.

Теперь запустим в другом терминале команду для проверки соединения (указываем аргументы -no-CApath, чтобы не использовать системное хранилище доверенных сертификатов и -quiet, чтобы не показывать подробную информацию о соединении, в аргументе -servername www.regolit.com указываем название домена, которое клиент передаёт при инициализации соединения в расширении протокола TLS Server Name Indication / SNI 1):

[user@shell]% openssl s_client -connect 127.0.0.1:9443 -no-CApath -CAfile demo-root-ca.crt -servername www.regolit.com -quiet
depth=2 C = AQ, O = Penguin Co., CN = Demo Root CA
verify return:1
depth=1 C = RU, L = Omsk, CN = Demo Intermediate Authority
verify return:1
depth=0 C = RU, ST = NSO, L = Novosibirsk, O = Regolit, CN = test.regolit.com, emailAddress = [email protected]
verify return:1

Программа выдаёт поле subject каждого из сертификатов в построенной цепочке, в данном случае она успешно построена и проверифицирована, так как сообщений об ошибке нет. Сертификат сервера показывается в самом низу. Программа ждёт от вас ввода HTTP-запроса, но мы пока это пропустим и просто нажмём Ctrl+C, чтобы завершить команду.

Если мы в команде вместо аргумента -CAfile demo-root-ca.crt укажем -no-CAfile (означает, что у клиента нет доверенного корневого сертификата), то получим такое:

[user@shell]% openssl s_client -connect 127.0.0.1:9443 -no-CApath -no-CAfile -servername www.regolit.com -quiet
depth=1 C = RU, L = Omsk, CN = Demo Intermediate Authority
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = RU, ST = NSO, L = Novosibirsk, O = Regolit, CN = test.regolit.com, emailAddress = [email protected]
verify return:1

Так как доверенного сертификата у клиента нет, он не может построить корректную цепочку доверия, поэтому выдаёт ошибку verify error:num=20:unable to get local issuer certificate. Однако по-прежнему показывает, что от сервера пришло два сертификата. Снова завершаем программу по Ctrl+C.

──────────────────

Следующий сценарий: сервер при соединии отдаёт только сертификат домена без промежуточного (убираем аргумент -cert_chain):

[user@shell]% openssl s_server -accept 127.0.0.1:9443 -key prime256v1-eckey.pem -cert regolit.crt
Using default temp DH parameters
ACCEPT

Если мы теперь попытаемся подключиться клиентом, то увидим сразу две ошибки верификации сертификата:

[user@shell]% openssl s_client -connect 127.0.0.1:9443 -no-CApath -CAfile demo-root-ca.crt -servername www.regolit.com -quiet
depth=0 C = RU, ST = NSO, L = Novosibirsk, O = Regolit, CN = test.regolit.com, emailAddress = [email protected]
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = RU, ST = NSO, L = Novosibirsk, O = Regolit, CN = test.regolit.com, emailAddress = [email protected]
verify error:num=21:unable to verify the first certificate
verify return:1
verify error:num=20:unable to get local issuer certificate
Эта ошибка означает, что клиент не смог найти сертификат, которым подписан сертификат домена — сервер промежуточный сертификат в запросе не передал. Буквальный текст описания этой ошибки из openssl: the issuer certificate could not be found: this occurs if the issuer certificate of an untrusted certificate cannot be found.
verify error:num=21:unable to verify the first certificate
Клиент не смог создать верифицированную цепочку доверия, включающую сертификат сайта. Буквальный текст описания этой ошибки из openssl: no signatures could be verified because the chain contains only one certificate and it is not self signed.

──────────────────

Если вы хотите протестировать на «живом» окружении, можете импортировать корневой сертификат demo-root-ca.crt в системное хранилище сертификатов и пометить его как доверенный. Тогда вы сможете открыть тестовый URL в обычном браузере.

Верификация цепочки сертификатов

Для верификации сертификата вам нужна цепочка доверия, завершающаяся корневым сертификатом. У нас уже есть все нужные для этого файлы и мы можем записать сертфикаты удостоверяющих центров в один файл и затем использовать его для верификации при помощи команды openssl verify:

[user@shell]% cat demo-intermediate-ca.crt demo-root-ca.crt > chain.pem
[user@shell]% openssl verify -no-CApath -CAfile chain.pem regolit.crt
regolit.crt: OK

Здесь мы сначала записываем оба наших сертификата УЦ в один файл chain.pem и дальше используе его для верификации.

Если мы не хотим строить целиком цепочку, а хотим только проверить, подписан ли один сертификат другим, нужно добавить аргумент -partial_chain:

[user@shell]% openssl verify -no-CApath -CAfile demo-intermediate-ca.crt -partial_chain regolit.crt
regolit.crt: OK

Здесь мы указали в качестве «цепочки» только один — промежуточный — сертификат удостоверяющего центра demo-intermediate-ca.crt, и без аргумента -partial_chain мы бы получили ошибку верификации:

[user@shell]% openssl verify -no-CApath -CAfile demo-intermediate-ca.crt regolit.crt              
C = RU, L = Omsk, CN = Demo Intermediate Authority
error 2 at 1 depth lookup: unable to get issuer certificate
error regolit.crt: verification failed

Использование сертификатов для цифровой подписи любых файлов

От одного из предыдущих этапов у нас остался сертификат в файле regolit.crt и закрытый ключ для него — prime256v1-eckey.pem, мы им подпишем файл demo-intermediate-ca.crt:

[user@shell]% openssl dgst -sha256 -sign prime256v1-eckey.pem -out demo-intermediate-ca.crt.sig demo-intermediate-ca.crt

Здесь мы используем команду openssl dgst, основное назначение которой — подсчёт дайджеста файла (то есть криптографического хеша). Однако эта же команда умеет генерировать и верифицировать цифровые подписи. Для генерации мы передаём закрытый ключ в аргументе -sign prime256v1-eckey.pem.

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

Выделяем открытый ключ из сертификата:

[user@shell]% openssl x509 -in regolit.crt -pubkey -noout > regolit-pubkey.pem

И верифицируем файл demo-intermediate-ca.crt, используя цифровую подпись и выделенный только что открытый ключ:

[user@shell]% openssl dgst -sha256 -verify regolit-pubkey.pem -signature demo-intermediate-ca.crt.sig demo-intermediate-ca.crt
Verified OK

Проверка соответствия сертификата и закрытого ключа

Есть два файла: сертификат (cert.pem) и закрытый ключ (key.pem), нужно проверить, соответствует ли открытый ключ из сертификата этому закрытому ключу. Обычно этот вопрос решают через экспорт открытого ключа из сертификата и закрытого ключа, и последующего их сравнения, однако этот метод для разных алгоритмов использует очень разные команды, поэтому я буду пользоваться способом из предыдущего раздела — через цифровую подпись.

Алгоритм такой:

  1. Сделаем цифровую подпись файла cert.pem, используя закрытый ключ.

    [user@shell]% openssl dgst -sha256 -sign key.pem -out test.sig cert.pem
    
  2. Выделим открытый ключ из сертификата и запишем его в файл cert-pubkey.pem:

    [user@shell]% openssl x509 -in cert.pem -pubkey -noout -out cert-pubkey.pem
    
  3. Проверим цифровую подпись:

    [user@shell]% openssl dgst -sha256 -verify cert-pubkey.pem -signature test.sig cert.pem
    Verified OK
    

Использование сертификатов для шифрования

Этот раздел довольно экстремальный, поскольку описанный в нём метод мало подходит для практического применения. Однако он описывает подход, применяемый в современном TLS-шифровании веб-трафика.

В общем случае X.509-сертификаты не предназначены для шифрования данных, единственный пригодный для этого алгоритм — RSA, однако это исключение. Тем не менее, в разделе Используемые криптоалгоритмы я рассказал, как можно использовать сертификаты совместно с протоколом Diffie-Hellman для безопасного обмена ключами симметричного шифрования. А в этом разделе я расскажу, как это можно реализовать на практике через команды openssl.

Действующие лица: Алиса и Боб, у каждого из них есть свой X.509-сертификат с закрытым ключом, причём Алиса и Боб заранее обменялись сертификатами каким-то надёжным способом (например, при личной встрече). И с этого момента весь обмен данными происходит через небезопасный открытый канал.

Алиса хочет организовать безопасный сеанс связи с Бобом поверх небезопасного канала. Для этого отлично подходит симметричный алгоритм AES-128-CBC, однако для него обе стороны должны использовать одинаковый ключ — набор байтов размером 128 бит. Алиса и Боб будут для обмена ключом пользоваться протоколом Diffie-Hellman с аутентификацией через цифровые подписи.

Создадим сначала сертификаты и закрытые ключи к ним для каждой из сторон. Алиса создаёт закрытый ключ alice-x509.key и самоподписанный сертификат alice-x509.crt:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout -out alice-x509.key
[user@shell]% openssl req -new -x509 -batch -days 5000 -key alice-x509.key -subj '/CN=Alice' -out alice-x509.crt

И Боб делает то же самое — ключ в файле bob-x509.key и самоподписанный сертификат bob-x509.crt:

[user@shell]% openssl ecparam -name prime256v1 -genkey -noout -out bob-x509.key
[user@shell]% openssl req -new -x509 -batch -days 5000 -key bob-x509.key -subj '/CN=Bob' -out bob-x509.crt

Дальше стороны должны заранее надёжным и безопасным способом передать друг-другу свои сертификаты.

──────────────────

Через какое-то время Алиса захотела обменяться важными данными с Бобом через e-mail. Это начало новой сессии обмена, в которой будут использоваться новые ключи (эфемерные, то есть не фиксированные, а созданные специально для этого момента, после сессии эфемерные ключи стираются), поэтому Алиса должна выбрать параметры протокола Diffie-Hellman, это делается такой командой:

[user@shell]% openssl dhparam -dsaparam -out dhp.pem 4096
Generating DSA parameters, 4096 bit long prime
...+.................+

Здесь 4096 означает размер случайного простого числа в битах, он в настоящее время считается безопасным. Теперь Алиса на основе этого файла с параметрами создаёт свой закрытый ключ dh-alice.key (это ещё не эфемерный ключ, а только заготовка для него!):

[user@shell]% openssl genpkey -paramfile dhp.pem -out alice-dh.key

И выделяет из него открытый ключ:

[user@shell]% openssl pkey -in alice-dh.key -pubout -out alice-dh-pub.pem

Затем Алиса конкатенирует два файла dhp.pem и alice-dh-pub.pem в один alice2bob.pem:

[user@shell]% cat dhp.pem alice-dh-pub.pem > alice2bob.pem

И создаёт цифровую подпись для него (в виде отдельного файла alice2bob.pem.sig), используя свой закрытый ключ от X.509-сертификата:

[user@shell]% openssl dgst -sha256 -sign alice-x509.key -out alice2bob.pem.sig alice2bob.pem

Дальше Алиса отправляет Бобу два файла: alice2bob.pem и alice2bob.pem.sig.

──────────────────

Боб получает от Алисы файл и цифровую подпись. Для проверки подписи ему сначала нужно извлечь из сертификата Алисы открытый ключ в файл alice-x509-pubkey.pem:

[user@shell]% openssl x509 -in alice-x509.crt -pubkey -noout > alice-x509-pubkey.pem

И дальше проверить им цифровую подпись:

[user@shell]% openssl dgst -sha256 -verify alice-x509-pubkey.pem -signature alice2bob.pem.sig alice2bob.pem     
Verified OK

Проверка прошла успешно, теперь Боб уверен, что полученный им файл alice2bob.pem действительно пришёл от Алисы, поэтому можно продолжать. Боб создаёт свой закрытый DH-ключ на основе полученных параметров:

[user@shell]% openssl genpkey -paramfile alice2bob.pem -out bob-dh.key

Обратите внимание, что в аргументе с параметрами указан файл alice2bob.pem, в котором записано два разных PEM-блока, но так как PEM — это контейнер, openssl берёт из него только те данные, которые ему нужны в текущем контексте, а здесь контекст — это параметры DH, именно такой блок и будет прочитан. Другими словами, нет никакой необходимости разделять файл на части.

Дальше Боб выделяет открытый ключ на основе только что созданного закрытого:

[user@shell]% openssl pkey -in bob-dh.key -pubout -out bob-dh-pub.pem

И, наконец, создаёт свой эфемерный закрытый ключ в файле bob-secret.key:

[user@shell]% openssl pkeyutl -derive -inkey bob-dh.key -peerkey alice2bob.pem -out bob-secret.key

Теперь Боб создаёт цифровую подпись для своего открытого DH-ключа:

[user@shell]% openssl dgst -sha256 -sign bob-x509.key -out bob-dh-pub.pem.sig bob-dh-pub.pem

И отправляет Алисе два файла: bob-dh-pub.pem и bob-dh-pub.pem.sig.

──────────────────

Алиса проверяет цифровую подпись аналогичным образом: выделяет открытый ключ из сертификата Боба и верифицирует файл bob-dh-pub.pem с подписью bob-dh-pub.pem.sig:

[user@shell]% openssl x509 -in bob-x509.crt -pubkey -noout > bob-x509-pubkey.pem
[user@shell]% openssl dgst -sha256 -verify bob-x509-pubkey.pem -signature bob-dh-pub.pem.sig bob-dh-pub.pem
Verified OK

И, наконец, Алиса создаёт свой эфемерный ключ на основе собственного закрытого DH-ключа (alice-dh.key) и полученного от Боба открытого DH-ключа (bob-dh-pub.pem):

[user@shell]% openssl pkeyutl -derive -inkey alice-dh.key -peerkey bob-dh-pub.pem -out alice-secret.key

Мы можем убедиться, что оба их эфемерных ключа одинаковые:

[user@shell]% diff -s alice-secret.key bob-secret.key
Files alice-secret.key and bob-secret.key are identical

──────────────────

Теперь Алиса может зашифровать файл alice-message.txt (внутри которого только одна строчка: Very secret message.), используя эфемерный ключ в качестве пароля:

[user@shell]% openssl enc -e -md md5 -pass file:alice-secret.key -aes-128-cbc \
  -in alice-message.txt -out alice-message.txt.encrypted

Здесь мы используем алгоритм хеширования MD5, чтобы получить из файла эфемерного ключа ровно 128 бит AES-ключа.

Боб теперь может расшифровать файл так:

[user@shell]% openssl enc -d -md md5 -pass file:bob-secret.key -aes-128-cbc -in alice-message.txt.encrypted
Very secret message.

Аналогичным образом Боб может зашифровать свой текст и отправить Алисе, ключ для его расшифровки есть только у них двоих. После завершения обмена сообщениями они оба удаляют свои эфемерные ключи и для следующего сеанса создают их заново.

Несколько примеров необычных сертификатов

Возьмём вот этот сертификат, это корневой сертификат Казахстанского центра межбанковских расчётов национального банка Республики Казахстан.

Вот как выглядит попытка распечатать его в текстовом виде:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            1e:97:16:12:b3:4f:8d:e4:e8:39:8b:da:34:f5:1e:f5:3f:c6:0f:b8:29:cf:7a:07:c0:7a:db:f5:9f:e9:12:0b
    Signature Algorithm: 1.3.6.1.4.1.6801.1.2.2
        Issuer: CN=KISC Root CA, O=KISC, C=KZ
        Validity
            Not Before: Sep  2 12:28:57 2008 GMT
            Not After : Aug 28 12:28:57 2028 GMT
        Subject: CN=KISC Root CA, O=KISC, C=KZ
        Subject Public Key Info:
            Public Key Algorithm: 1.3.6.1.4.1.6801.1.5.8
            Unable to load Public Key
4594828908:error:06FFF09C:digital envelope routines:CRYPTO_internal:unsupported algorithm:/p_lib.c:241:
4594828908:error:0BFFF06F:x509 certificate routines:CRYPTO_internal:unsupported algorithm:x_pubkey.c:199:
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Subject Key Identifier:
                1E:97:16:12:B3:4F:8D:E4:E8:39:8B:DA:34:F5:1E:F5:3F:C6:0F:B8:29:CF:7A:07:C0:7A:DB:F5:9F:E9:12:0B
            X509v3 Authority Key Identifier:
                keyid:1E:97:16:12:B3:4F:8D:E4:E8:39:8B:DA:34:F5:1E:F5:3F:C6:0F:B8:29:CF:7A:07:C0:7A:DB:F5:9F:E9:12:0B
                DirName:/CN=KISC Root CA/O=KISC/C=KZ
                serial:1E:97:16:12:B3:4F:8D:E4:E8:39:8B:DA:34:F5:1E:F5:3F:C6:0F:B8:29:CF:7A:07:C0:7A:DB:F5:9F:E9:12:0B

    Signature Algorithm: 1.3.6.1.4.1.6801.1.2.2
         b4:dc:79:0f:a7:94:8f:fa:90:22:18:f9:22:27:30:83:33:59:
         af:b9:68:6b:1d:40:75:ad:87:e0:ff:46:37:3c:0a:78:55:b4:
         c3:b1:1a:8f:6c:62:37:ad:38:1b:9c:b6:1c:ac:68:16:37:c1:
         8e:ae:6e:9c:7a:c4:00:6d:ff:3a

Сразу же видим, что openssl не знает ни алгоритма открытого ключа (OID 1.3.6.1.4.1.6801.1.5.8), ни алгоритма цифровой подписи (OID 1.3.6.1.4.1.6801.1.2.2). Репозиторий oid-info.com тоже не знает про них ничего, однако знает google — это идентификаторы объектов из проприетарного ПО TumarCSP, которое используется в Казахстане. По факту это другие идентификаторы для объектов ГОСТовских криптоалгоритмов.

──────────────────

Следующий файл для препарирования — старый сертификат удостоверяющего центра Бурятии (ca-bur.der). Его текстовое представление великолепно:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            1e:5d:f6:44:00:00:00:00:02:51
        Signature Algorithm: GOST R 34.11-94 with GOST R 34.10-2001
        Issuer: INN = 007710474375, OGRN = 1047702026701, emailAddress = [email protected], 
street = 125375 \D0\B3. \D0\9C\D0\BE\D1\81\D0\BA\D0\B2\D0\B0 \D1\83\D0\BB. \D0\A2\D0\B2\D0
\B5\D1\80\D1\81\D0\BA\D0\B0\D1\8F \D0\B4.7, O = \D0\9C\D0\B8\D0\BD\D0\BA\D0\BE\D0\BC\D1\81
\D0\B2\D1\8F\D0\B7\D1\8C \D0\A0\D0\BE\D1\81\D1\81\D0\B8\D0\B8, L = \D0\9C\D0\BE\D1\81\D0\BA
\D0\B2\D0\B0, ST = 77 \D0\B3.\D0\9C\D0\BE\D1\81\D0\BA\D0\B2\D0\B0, C = RU, CN = \D0\A3\D0\A6 
2 \D0\98\D0\A1 \D0\93\D0\A3\D0\A6
        Validity
            Not Before: Jun 17 06:35:00 2014 GMT
            Not After : Jul 22 06:54:00 2017 GMT
        Subject: OGRN = 1020300972361, INN = 000323082280, street = \D1\83\D0\BB. \D0\9B\D0\B5
\D0\BD\D0\B8\D0\BD\D0\B0 54, emailAddress = [email protected], C = RU, ST = 03 \D0\A0\D0\B5\D1
\81\D0\BF\D1\83\D0\B1\D0\BB\D0\B8\D0\BA\D0\B0 \D0\91\D1\83\D1\80\D1\8F\D1\82\D0\B8\D1\8F, L = 
\D0\A3\D0\BB\D0\B0\D0\BD-\D0\A3\D0\B4\D1\8D, O = \D0\90\D0\B4\D0\BC\D0\B8\D0\BD\D0\B8\D1\81\D1\82
\D1\80\D0\B0\D1\86\D0\B8\D1\8F \D0\93\D0\BB\D0\B0\D0\B2\D1\8B \D0\A0\D0\91 \D0\B8 \D0\9F\D1\80\D0
\B0\D0\B2\D0\B8\D1\82\D0\B5\D0\BB\D1\8C\D1\81\D1\82\D0\B2\D0\B0 \D0\A0\D0\91, CN = \D0\A3\D0\A6 \D0
\A0\D0\B5\D1\81\D0\BF\D1\83\D0\B1\D0\BB\D0\B8\D0\BA\D0\B8 \D0\91\D1\83\D1\80\D1\8F\D1\82\D0\B8\D1\8F
        Subject Public Key Info:
            Public Key Algorithm: GOST R 34.10-2001
            Unable to load Public Key
140269877257344:error:0609E09C:digital envelope routines:pkey_set_type:unsupported algorithm:../crypto/evp/p_lib.c:210:
140269877257344:error:0B09406F:x509 certificate routines:x509_pubkey_decode:unsupported algorithm:../crypto/x509/x_pubkey.c:114:
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier:
                55:D2:55:96:82:D3:B1:E3:62:EC:60:34:BE:D3:42:19:C3:45:B5:74
            X509v3 Key Usage:
                Digital Signature, Certificate Sign, CRL Sign
            1.3.6.1.4.1.311.21.1:
                .....
            X509v3 Certificate Policies:
                Policy: 1.2.643.100.113.1
                Policy: 1.2.643.100.113.2
                Policy: X509v3 Any Policy

            Signing Tool of Subject:
                .-".................. CSP" (............ 3.6.1)
            X509v3 Authority Key Identifier:
                keyid:C6:6B:C1:02:A2:92:AA:14:0A:0A:4A:14:FD:19:1D:0D:57:D0:44:9C
                DirName:/[email protected]/C=RU/ST=77 \xD0\xB3. \xD0\x9C\xD0\xBE\xD1
\x81\xD0\xBA\xD0\xB2\xD0\xB0/L=\xD0\x9C\xD0\xBE\xD1\x81\xD0\xBA\xD0\xB2\xD0\xB0
                serial:3C:82:25:19:00:00:00:00:00:18

            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://rostelecom.ru/cdp/vguc2.crl

                Full Name:
                  URI:http://reestr-pki.ru/cdp/vguc2.crl

            Authority Information Access:
                CA Issuers - URI:http://rostelecom.ru/cdp/vguc2.crt
                CA Issuers - URI:http://reestr-pki.ru/cdp/vguc2.crt

            X509v3 Private Key Usage Period:
                Not Before: Jun 17 06:35:00 2014 GMT, Not After: Jun 17 06:35:00 2018 GMT
            Signing Tool of Issuer:
                0...-".................. CSP" (............ 3.6.1).S"............................ 
.......... ".................. ...." ............ 1.5.%... ..../124-2239 .... 04.10.2013 ....%... 
..../128-1823 .... 01.06.2012 ...
    Signature Algorithm: GOST R 34.11-94 with GOST R 34.10-2001
         3c:eb:c0:76:c9:3a:71:89:1a:b4:66:00:7f:ab:4d:ef:06:d8:
         bf:df:f7:23:d8:41:56:8e:65:af:5a:59:f9:f9:83:7a:d4:56:
         c5:53:26:44:bf:c8:46:e8:18:e9:ca:7d:74:4d:83:50:f5:d9:
         ac:0c:3b:2b:b1:ac:17:2b:9c:41

Здесь мы сразу видим, что openssl крайне плохо справляется с отображением сертификатов, содержащих юникодные символы, в нашем случае это кириллица в полях issuer и subject.

Отображение кириллицы частично можно исправить, добавив аргумент -nameopt sep_multiline,utf8:

[user@shell]% openssl x509 -inform DER -noout -text -in ca-bur.der -nameopt sep_multiline,utf8
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            1e:5d:f6:44:00:00:00:00:02:51
        Signature Algorithm: GOST R 34.11-94 with GOST R 34.10-2001
        Issuer:
            INN=007710474375
            OGRN=1047702026701
            [email protected]
            street=125375 г. Москва ул. Тверская д.7
            O=Минкомсвязь России
            L=Москва
            ST=77 г.Москва
            C=RU
            CN=УЦ 2 ИС ГУЦ
        Validity
            Not Before: Jun 17 06:35:00 2014 GMT
            Not After : Jul 22 06:54:00 2017 GMT
        Subject:
            OGRN=1020300972361
            INN=000323082280
            street=ул. Ленина 54
            [email protected]
            C=RU
            ST=03 Республика Бурятия
            L=Улан-Удэ
            O=Администрация Главы РБ и Правительства РБ
            CN=УЦ Республики Бурятия
        Subject Public Key Info:
            Public Key Algorithm: GOST R 34.10-2001
            Unable to load Public Key
140283884340352:error:0609E09C:digital envelope routines:pkey_set_type:unsupported algorithm:../crypto/evp/p_lib.c:210:
140283884340352:error:0B09406F:x509 certificate routines:x509_pubkey_decode:unsupported algorithm:../crypto/x509/x_pubkey.c:114:
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier:
                55:D2:55:96:82:D3:B1:E3:62:EC:60:34:BE:D3:42:19:C3:45:B5:74
            X509v3 Key Usage:
                Digital Signature, Certificate Sign, CRL Sign
            1.3.6.1.4.1.311.21.1:
                .....
            X509v3 Certificate Policies:
                Policy: 1.2.643.100.113.1
                Policy: 1.2.643.100.113.2
                Policy: X509v3 Any Policy

            Signing Tool of Subject:
                .-".................. CSP" (............ 3.6.1)
            X509v3 Authority Key Identifier:
                keyid:C6:6B:C1:02:A2:92:AA:14:0A:0A:4A:14:FD:19:1D:0D:57:D0:44:9C
                DirName:/[email protected]/C=RU/ST=77 \xD0\xB3.
\xD0\x9C\xD0\xBE\xD1\x81\xD0\xBA\xD0\xB2\xD0\xB0/L=\xD0\x9C\xD0\xBE\xD1\x81
\xD0\xBA\xD0\xB2\xD0\xB0
                serial:3C:82:25:19:00:00:00:00:00:18

            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://rostelecom.ru/cdp/vguc2.crl

                Full Name:
                  URI:http://reestr-pki.ru/cdp/vguc2.crl

            Authority Information Access:
                CA Issuers - URI:http://rostelecom.ru/cdp/vguc2.crt
                CA Issuers - URI:http://reestr-pki.ru/cdp/vguc2.crt

            X509v3 Private Key Usage Period:
                Not Before: Jun 17 06:35:00 2014 GMT, Not After: Jun 17 06:35:00 2018 GMT
            Signing Tool of Issuer:
                0...-".................. CSP" (............ 3.6.1).S"............................ 
.......... ".................. ...." ............ 1.5.%... ..../124-2239 .... 04.10.2013 ....%... 
..../128-1823 .... 01.06.2012 ...
    Signature Algorithm: GOST R 34.11-94 with GOST R 34.10-2001
         3c:eb:c0:76:c9:3a:71:89:1a:b4:66:00:7f:ab:4d:ef:06:d8:
         bf:df:f7:23:d8:41:56:8e:65:af:5a:59:f9:f9:83:7a:d4:56:
         c5:53:26:44:bf:c8:46:e8:18:e9:ca:7d:74:4d:83:50:f5:d9:
         ac:0c:3b:2b:b1:ac:17:2b:9c:41

Однако данные внутри расширений по-прежнему отображаются в закодированном виде.

В качестве алгоритма открытого ключа используется GOST R 34.10-2001, а для цифровой подписи — хеш-функция GOST R 34.11-94 с алгоритмом цифровой подписи GOST R 34.10-2001

Примечания


  1. Расширение протокола TLS Server Name Indication (сокращённо SNI) позволяет при инициализации TLS-соединения передать серверу домен веб-сайта, чтобы сервер выбрал подходящий сертификат именно для этого сайта. Без такого расширения сервер всегда отдавал одинаковый сертификат при соединеннии к конкретному IP-адресу и если на нём было несколько разных HTTPS-сайтов, начинались проблемы с сертификатом. 

Комментарии

Гость: Исакий | 2020-12-28 в 14:02

"Протокол DH, очевидно, уязвим для атак типа Man-in-the-middle..." непонятно где была взята вами формула для этого алгоритма.Оригинальная фромула A = gX mod p и B = gY

Протокол DH не уязвим для MiTM при достаточно длином ключе

Sergey Stolyarov | 2021-02-15 в 15:10

Я прямым текстом написал: Я выбрал операцию сложения ради простоты объяснения, в реальном протоколе используется сложно-обратимая операция

Для предотвращения MITM как раз используются цифровые подписи, без подобного слоя DH уязвим к MITM.

Текст комментария (допустимая разметка: *курсив*, **полужирная**, [ссылка](http://example.com) или <http://example.com>) Посетители-анонимы, обратите внимение, что более чем одна гиперссылка в тексте (включая оную из поля «веб-сайт») приведёт к блокировке комментария для модерации. Зайдите на сайта с использованием аккаунта на twitter, например, чтобы посылать комментарии без этого ограничения.
Имя (обязательно, 50 символов или меньше)
Опциональный email, на который получать ответы (не будет опубликован)
Веб-сайт
© 2006—2024 Sergey Stolyarov | Работает на pyrengine