golang

От Root CA до User Authorization в nginx+apache. Часть 4. Свой web-УЦ: выпуск из браузера, роли и а…

  • четверг, 3 сентября 2026 г. в 00:00:17
https://habr.com/ru/articles/1077772/

Продолжение цикла. В первой части мы развернули двухуровневую инфраструктуру: Root CA и три промежуточных центра. Во второй научились отзывать сертификаты, раздавать CRL и подняли OCSP-responder. В третьей настроили вход по клиентскому сертификату в nginx и Apache. Осталось ответить на вопрос, который возникает сразу после первого успешного входа: а откуда у людей берутся сертификаты?

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

Формат прежний: сначала рабочее решение, затем расширенный справочник по всем параметрам — синтаксис, значения, умолчания и подводные камни, сверенные с официальной документацией, а не с чужими туториалами. Справочников здесь четыре, в них 237 параметров.

Проверялось на версиях. OpenSSL master для справочников; живой стенд собран на nginx 1.29.8, OpenSSL 3.3.7 и Go 1.24. Браузерная часть — Web Crypto API по спецификации W3C. Где документация версию не сообщает, в таблицах стоит прочерк — я не угадываю.

В этой части:

  1. Кнопка «получить сертификат»: ключ рождается в браузере и не покидает его — и почему такой кнопке нельзя доверять отличительное имя.

  2. Движок управления сертификатами: архитектурные развилки, из-за которых ключ УЦ не должен находиться в одном процессе с веб-приложением.

  3. Три дефекта, которые вылезли только на живом стенде.

  4. Полный цикл на живом стенде — от входа до отзыва.

Код движка: github.com/DidenkoMS/pkidesk. Дерево УЦ, конфиги и docker-макет остались в репозитории цикла: github.com/DidenkoMS/ssl_article.


Шаг 1. Сертификат в один клик: ключ, который не покидает браузер

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

Самый очевидный способ — попросить сервер: пусть он сгенерирует ключ, соберёт PKCS#12 и отдаст готовый файл. Удобно — и ровно поэтому неправильно. Ключ, созданный сервером, на мгновение существует вне контроля владельца: в памяти чужого процесса, в теле HTTP-ответа, возможно, в журнале обратного прокси. Для сертификата с nonRepudiation это разрушает саму неотказуемость: доказать, что копией не воспользовался никто другой, уже нельзя.

Правильный путь — сгенерировать ключ у владельца и отправить на сервер только запрос. Обычно это означает «объясните человеку две команды openssl». Но у браузера всё нужное есть: WebCrypto умеет и генерировать ключи, и подписывать.

1.1. PKCS#10 руками, без библиотек

Запрос на сертификат — это структура ASN.1 из RFC 2986:

CertificationRequest ::= SEQUENCE {
    certificationRequestInfo  CertificationRequestInfo,
    signatureAlgorithm        AlgorithmIdentifier,
    signature                 BIT STRING }

CertificationRequestInfo ::= SEQUENCE {
    version        INTEGER { v1(0) },
    subject        Name,
    subjectPKInfo  SubjectPublicKeyInfo,
    attributes     [0] IMPLICIT Attributes }

Всё это укладывается в сотню строк на голом JavaScript, и внешняя библиотека здесь не нужна — читать RFC полезнее, чем подключать зависимость. Два места, где обычно спотыкаются:

  • exportKey('spki') уже отдаёт готовый SubjectPublicKeyInfo — его нужно вставить в структуру дословно, а не разбирать и собирать заново.

  • attributes [0] IMPLICIT — не «необязательное поле»: пустой набор кодируется тегом с нулевой длиной, а не отсутствует. Если запрашивается subjectAltName, туда кладётся атрибут extensionRequest (OID 1.2.840.113549.1.9.14) с вложенной последовательностью расширений.

Плюс общие правила DER, о которые режутся все: длинная форма длины, ведущий нулевой байт у INTEGER со старшим битом, число неиспользуемых бит первым байтом BIT STRING.

Ключ обязан быть создан с extractable: true — иначе его нельзя будет сохранить в файл. Это честная плата: любой скрипт на странице теоретически может его прочитать, и единственная защита здесь — строгая политика содержимого (script-src 'self') и отсутствие сторонних скриптов вообще.

Проверять такой код «на глаз» бессмысленно, поэтому он прогоняется вне браузера: Node даёт тот же WebCrypto, а результат проверяет настоящий openssl — тот самый, который потом будет подписывать. Двадцать семь проверок: примитивы кодировщика, разбор запроса, совпадение ключа с запросом, subjectAltName всех типов, кириллица в UTF8String, отказ на слабом ключе.

== CSR без subjectAltName ==
  ok    openssl принял запрос и подпись сошлась
  ok    страна PrintableString
  ok    алгоритм подписи sha256WithRSAEncryption
== закрытый ключ ==
  ok    ключ соответствует запросу
== CSR с subjectAltName ==
  ok    расширение subjectAltName запрошено
  ok    IP-адрес

Расширенный справочник: структура PKCS#10

Показать таблицу — 55 полей (структура PKCS#10)

Параметр

Синтаксис / значения

По умолч.

Описание и нюансы

AlgorithmIdentifier

AlgorithmIdentifier {ALGORITHM:IOSet } ::= SEQUENCE { algorithm ALGORITHM.&id({IOSet}), parameters ALGORITHM.&Type({IOSet}{@algorithm}) OPTIONAL}знач.: SEQUENCE (тег 0x30): OID + необязательные параметры

Параметризованная версия типа AlgorithmIdentifier из X.509: жёстко связывает пары «OID алгоритма — тип его параметров». Кодированные значения эквивалентны значениям обычного типа AlgorithmIdentifier.⚠️ parameters помечен OPTIONAL, но «отсутствуют» и «присутствуют как NULL» — это разные байты. Для RSA PKCS#1 требуется явный NULL, для ECDSA параметры отсутствуют; произвольная замена одного другим приводит к отказу проверки у строгих реализаций.

AlgorithmIdentifier (RFC 5280)

AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL }знач.: SEQUENCE (тег 0x30): OID + необязательные параметры

Классическая (1988-синтаксис) форма AlgorithmIdentifier: OID алгоритма и параметры типа ANY DEFINED BY. Кодирование идентично параметризованной версии из RFC 2986.⚠️ Если вы пишете один парсер и для CSR, и для сертификата, помните: тип один и тот же на уровне байтов, но в RFC 2986 он объявлен параметризованным (AlgorithmIdentifier{{…}}) — различие чисто нотационное и на DER не влияет.

Attribute

Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { type ATTRIBUTE.&id({IOSet}), values SET SIZE(1…MAX) OF ATTRIBUTE.&Type({IOSet}{@type})}знач.: SEQUENCE (тег 0x30) из OID и SET значений

Один атрибут запроса: идентификатор типа (OID) и множество значений, тип которых определяется этим OID через открытый тип.⚠️ values — это SET, а не одно значение: даже для однозначных атрибутов вроде extensionRequest вокруг единственного значения обязательна обёртка 0x31. Размер SIZE(1…MAX) запрещает пустое множество — атрибут без значений невалиден.

Attribute (RFC 5280)

Attribute ::= SEQUENCE { type AttributeType, values SET OF AttributeValue } – at least one value is requiredзнач.: SEQUENCE (тег 0x30): OID + SET значений (не менее одного)

Непараметризованная форма типа Attribute. Совпадает по кодированию с Attribute{} из RFC 2986; требование «не менее одного значения» вынесено в комментарий, а не в SIZE-ограничение.⚠️ В RFC 5280 ограничение записано комментарием (-- at least one value is required), а не как SIZE(1…MAX), поэтому автогенерированные из этого модуля парсеры пустой SET пропустят. Проверяйте непустоту сами.

AttributeType

AttributeType ::= OBJECT IDENTIFIERзнач.: OBJECT IDENTIFIER (тег 0x06)

Идентификатор объекта, задающий тип компонента отличительного имени (например, id-at-commonName).⚠️ OID определяет и семантику, и допустимый строковый тип значения. Реализация обязана уметь пропускать незнакомые типы атрибутов, а не падать на них.

AttributeTypeAndValue

AttributeTypeAndValue ::= SEQUENCE { type AttributeType, value AttributeValue }знач.: SEQUENCE (тег 0x30): OID + одно значение

Пара «тип атрибута (OID) — значение атрибута». Тип значения определяется типом атрибута; как правило это DirectoryString.⚠️ Здесь value — ровно одно значение (в отличие от Attribute из PKCS#10, где values это SET). Тег значения зависит от выбранного строкового типа: 0x13 для PrintableString, 0x0C для UTF8String, 0x16 для IA5String — единого «строкового» тега нет.

AttributeValue

AttributeValue ::= ANY – DEFINED BY AttributeTypeзнач.: любой ASN.1-тип, определяемый значением AttributeType

Значение атрибута; конкретный тип определяется типом атрибута — в общем случае это DirectoryString.⚠️ ANY DEFINED BY означает, что парсер обязан хранить сырые байты значения: перекодирование PrintableString в UTF8String «для единообразия» меняет DN и разрывает цепочку сопоставления имён (name chaining) с уже выпущенными сертификатами.

Attributes

Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }}знач.: SET OF (базовый тег 0x31, в поле attributes заменён на [0] = 0xA0)

Параметризованный тип-контейнер: множество атрибутов, допустимые типы которых ограничены переданным информационным набором объектов.⚠️ SET OF, а не SEQUENCE OF: по DER (X.690) кодированные значения элементов должны быть отсортированы по возрастанию как октетные строки. Пересортировка после подписи или при повторной сериализации меняет байты и ломает подпись; RFC прямо предупреждает не полагаться на порядок компонентов атрибутов.

CRIAttributes

CRIAttributes ATTRIBUTE ::= { … – add any locally defined attributes here – }знач.: информационный набор объектов ATTRIBUTE; содержит только маркер расширения (…)

Набор информационных объектов, задающий допустимые типы атрибутов запроса. В RFC 2986 пуст, кроме маркера расширения.⚠️ Именно поэтому сам PKCS#10 не определяет ни extensionRequest, ни challengePassword — они приходят из PKCS #9 (RFC 2985). Валидатор, знающий только RFC 2986, обязан принимать любые атрибуты благодаря (…).

CertificationRequest

CertificationRequest ::= SEQUENCE { certificationRequestInfo CertificationRequestInfo, signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, signature BIT STRING}знач.: SEQUENCE (тег 0x30), ровно три компонента в указанном порядке

Корневая структура запроса на сертификат. Состоит из подписываемой части, идентификатора алгоритма подписи и самой подписи. Именно её значение сохраняют в файл .csr (в DER или в PEM-обёртке).⚠️ Подписывается НЕ вся CertificationRequest, а только DER-кодирование поля certificationRequestInfo, включая его собственные байты тега и длины. Частая ошибка ручного кодирования — подписать содержимое SEQUENCE без заголовка или пересобрать certificationRequestInfo заново при проверке: любое отличие в байтах (порядок SET OF, длина в неминимальной форме) ломает подпись, поэтому исходные байты подписанной части надо сохранять как есть.

CertificationRequestInfo

CertificationRequestInfo ::= SEQUENCE { version INTEGER { v1(0) } (v1,…), subject Name, subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }}, attributes [0] Attributes{{ CRIAttributes }}}знач.: SEQUENCE (тег 0x30), четыре компонента в указанном порядке

Подписываемая часть запроса: версия, отличительное имя субъекта, открытый ключ субъекта и набор атрибутов.⚠️ Все четыре поля обязательны — OPTIONAL нет ни у одного, в том числе у attributes. Если атрибутов нет, поле всё равно кодируется (пустым набором), а не пропускается.

DirectoryString

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)) }знач.: teletexString (0x14) | printableString (0x13) | universalString (0x1C) | utf8String (0x0C) | bmpString (0x1E); соответствующие УЦ ОБЯЗАНЫ использовать PrintableString или UTF8String

Строковый тип-выбор для значений атрибутов имени. RFC 5280 сужает выбор: соответствующие профилю УЦ должны применять PrintableString или UTF8String, а прочие кодировки — только ради обратной совместимости.⚠️ PrintableString не содержит символов ‘@’, ‘_’, ‘*’ и кириллицы — попытка положить такое значение в PrintableString даёт формально валидный DER с недопустимыми символами, который отвергнут строгими валидаторами. Минимальная длина 1: пустая строка недопустима.

Extension

Extension ::= SEQUENCE { extnID OBJECT IDENTIFIER, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING – contains the DER encoding of an ASN.1 value – corresponding to the extension type identified – by extnID }знач.: SEQUENCE (тег 0x30): OID, необязательный BOOLEAN, OCTET STRING

Одно расширение: его идентификатор, признак критичности и закодированное значение.⚠️ Ровно три поля и жёсткий порядок. Значение расширения лежит внутри OCTET STRING как вложенный DER — это двойное кодирование, при разборе требуется второй проход парсера по содержимому extnValue.

ExtensionRequest

ExtensionRequest ::= Extensionsзнач.: значение типа Extensions (SEQUENCE, тег 0x30)

Синтаксис значения атрибута extensionRequest — простой псевдоним типа Extensions, импортируемого из X.509 / профиля PKIX.⚠️ Псевдоним не добавляет собственной обёртки, но обёртка SET от Attribute.values остаётся. Полная цепочка байтов такова: A0 (attributes) → 30 (Attribute) → 06 (OID .9.14) → 31 (values SET) → 30 (Extensions) → 30 (Extension). Пропуск слоя 0x31 — самая частая ошибка при ручной сборке extensionRequest.

Extensions

Extensions ::= SEQUENCE SIZE (1…MAX) OF Extensionзнач.: SEQUENCE OF (тег 0x30), не менее одного элемента

Последовательность расширений сертификата. Внутри extensionRequest перечисляет те расширения, которые запрашивающая сторона просит включить в сертификат.⚠️ Это SEQUENCE OF (порядок сохраняется, сортировка не нужна) — в отличие от attributes, которое SET OF. И SIZE (1…MAX): пустой список расширений недопустим, атрибут extensionRequest без расширений писать нельзя — его просто не добавляют.

Name

Name ::= CHOICE { – only one possibility for now – rdnSequence RDNSequence }знач.: единственная альтернатива rdnSequence

Тип отличительного имени X.501. Формально CHOICE, но на сегодня имеет ровно одну альтернативу — rdnSequence.⚠️ У CHOICE нет собственного тега: в байтах после заголовка поля subject сразу идёт SEQUENCE от RDNSequence. Ожидание «ещё одного 0x30» — типичная ошибка при ручном разборе.

PKInfoAlgorithms

PKInfoAlgorithms ALGORITHM ::= { … – add any locally defined algorithms here – }знач.: информационный набор объектов ALGORITHM; содержит только маркер расширения (…)

Набор информационных объектов, задающий допустимые алгоритмы открытого ключа. В самом RFC 2986 пуст, кроме маркера расширения; конкретные алгоритмы определяют ссылающиеся спецификации.⚠️ Пустой набор с (…) означает, что RFC 2986 не фиксирует ни одного конкретного алгоритма — не ищите в нём OID для RSA или EC, их надо брать из PKCS #1 / RFC 3279 и подобных.

RDNSequence

RDNSequence ::= SEQUENCE OF RelativeDistinguishedNameзнач.: SEQUENCE OF (тег 0x30), порядок значим

Упорядоченная последовательность относительных отличительных имён (RDN), образующая иерархическое имя: от старших компонентов к младшим.⚠️ Это SEQUENCE OF — порядок значим и сохраняется. В строковой записи DN (RFC 4514, вывод openssl) порядок обратный порядку в DER, поэтому «CN=…, O=…, C=…» в тексте соответствует C, O, CN в байтах — при сборке вручную легко записать иерархию задом наперёд.

RelativeDistinguishedName

RelativeDistinguishedName ::= SET SIZE (1…MAX) OF AttributeTypeAndValueзнач.: SET OF (тег 0x31), не менее одного элемента

Одно относительное отличительное имя — множество из одной или более пар «тип атрибута — значение». Обычно содержит ровно один элемент (multi-valued RDN встречается редко).⚠️ Внутри RDN это SET: при нескольких элементах DER требует сортировки по возрастанию кодированных октетов, и порядок в байтах не совпадёт с порядком, в котором вы их добавляли. Плюс SIZE (1…MAX) — пустой RDN (31 00) невалиден, хотя пустой RDNSequence (30 00) синтаксически допустим.

SIGNED { ToBeSigned }

SIGNED { ToBeSigned } ::= SEQUENCE { toBeSigned ToBeSigned, algorithm AlgorithmIdentifier { {SignatureAlgorithms} }, signature BIT STRING}знач.: SEQUENCE (тег 0x30)

Параметризованный тип из примечания RFC: CertificationRequest можно записать как SIGNED { EncodedCertificationRequestInfo } с ограничением CONSTRAINED BY.⚠️ Эта запись эквивалентна по кодированию, но имена полей у неё другие (toBeSigned/algorithm/signature). Не переносите их в код как имена полей PKCS#10 — нормативные имена берутся из разд. 4.2.

SignatureAlgorithms

SignatureAlgorithms ALGORITHM ::= { … – add any locally defined algorithms here – }знач.: информационный набор объектов ALGORITHM; содержит только маркер расширения (…). Пример расширения из RFC: { NULL IDENTIFIED BY md5WithRSAEncryption }

Набор допустимых алгоритмов подписи запроса. Пример в RFC 2986 показывает, как добавить объект для md5WithRSAEncryption с типом параметров NULL.⚠️ Пример в RFC приведён с md5WithRSAEncryption — это иллюстрация синтаксиса информационного объекта, а не рекомендация алгоритма; повторять его в боевом коде не нужно.

SubjectPublicKeyInfo

SubjectPublicKeyInfo {ALGORITHM: IOSet} ::= SEQUENCE { algorithm AlgorithmIdentifier {{IOSet}}, subjectPublicKey BIT STRING}знач.: SEQUENCE (тег 0x30): AlgorithmIdentifier + BIT STRING

Структура открытого ключа: идентификатор алгоритма ключа и битовая строка с закодированным ключом.⚠️ Обратите внимание на несовпадение имён: тип называется SubjectPublicKeyInfo, а поле в CertificationRequestInfo — subjectPKInfo. Внутри второго поля лежит вложенный DER (например, RSAPublicKey), выровненный по границе байта.

SubjectPublicKeyInfo (RFC 5280)

SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }знач.: SEQUENCE (тег 0x30)

Структура открытого ключа в профиле PKIX. Байт-в-байт совместима с параметризованной версией из RFC 2986, поэтому SPKI из CSR переносится в сертификат без перекодирования.⚠️ УЦ обычно копирует эти байты из запроса в сертификат как есть — если ваш CSR-генератор «нормализует» параметры алгоритма ключа, отпечаток ключа (SPKI SHA-256, используемый в pinning и в CT) изменится.

algorithm

algorithm ALGORITHM.&id({IOSet})знач.: OBJECT IDENTIFIER (тег 0x06)

OID алгоритма. Для signatureAlgorithm — алгоритм подписи, для SubjectPublicKeyInfo.algorithm — алгоритм открытого ключа.⚠️ Один и тот же тип используется в двух разных ролях, и OID из этих ролей не взаимозаменяемы: rsaEncryption в поле подписи или sha256WithRSAEncryption в поле ключа — распространённая ошибка при сборке запроса вручную.

attributes

attributes [0] Attributes{{ CRIAttributes }}знач.: контекстный тег [0]; в байтах DER — 0xA0 (constructed), содержимое SET OF Attribute

Набор атрибутов с дополнительными сведениями о субъекте: challenge-password, extensionRequest и другие типы из PKCS #9.⚠️ Модуль объявлен как DEFINITIONS IMPLICIT TAGS, поэтому [0] здесь IMPLICIT: тег SET (0x31) заменяется на 0xA0, а не заворачивается в него. Кодировать как A0 xx 31 yy … — распространённая ошибка (это был бы EXPLICIT). Поле не OPTIONAL: при отсутствии атрибутов пишется пустой конструктор нулевой длины — ровно два байта A0 00.

certificationRequestInfo

certificationRequestInfo CertificationRequestInfoзнач.: значение типа CertificationRequestInfo (SEQUENCE, тег 0x30)

«Информация запроса на сертификацию» — подписываемая часть. Содержит имя субъекта, его открытый ключ и набор атрибутов.⚠️ В тексте RFC 2986 описание компонента напечатано с опечаткой — «certificateRequestInfo», тогда как в ASN.1 поле называется certificationRequestInfo. В код надо переносить имя из ASN.1-модуля, а не из абзаца с пояснениями.

critical

critical BOOLEAN DEFAULT FALSEзнач.: TRUE (01 01 FF) | FALSE

FALSE

Признак критичности расширения. Если расширение критично, реализация, не умеющая его обрабатывать, обязана отвергнуть сертификат.⚠️ Единственное поле во всей рассматриваемой иерархии с настоящим DEFAULT — и по DER значение, равное умолчанию, кодировать ЗАПРЕЩЕНО: FALSE опускается полностью, писать 01 01 00 нельзя. TRUE кодируется как 01 01 FF (все биты единицы), а не 01 01 01.

extensionRequest

extensionRequest ATTRIBUTE ::= { WITH SYNTAX ExtensionRequest SINGLE VALUE TRUE ID pkcs-9-at-extensionRequest}знач.: ровно одно значение (SINGLE VALUE TRUE) типа ExtensionRequest

Тип атрибута, которым запрашивающая сторона переносит расширения сертификата, которые она хочет видеть в выпущенном сертификате (SAN, keyUsage, basicConstraints и т. д.).⚠️ Ни OID, ни синтаксис этого атрибута в RFC 2986 не приведены — там есть только фраза «the extensionRequest attribute from PKCS #9», поэтому нормативный источник для него RFC 2985. И главное: расширения из CSR — это лишь пожелание, УЦ вправе их проигнорировать или переписать.

extnID

extnID OBJECT IDENTIFIERзнач.: OBJECT IDENTIFIER (тег 0x06)

Идентификатор типа расширения; определяет ASN.1-тип значения, лежащего внутри extnValue.⚠️ Одно и то же расширение не должно встречаться в списке дважды; дубликаты extnID — типичная ошибка при слиянии расширений из шаблона УЦ и из CSR.

extnValue

extnValue OCTET STRINGзнач.: OCTET STRING (тег 0x04), содержит DER-кодирование значения расширения

Значение расширения: октетная строка, содержащая DER-кодирование ASN.1-значения того типа, который определён идентификатором extnID.⚠️ Внутрь кладётся именно закодированное значение, а не его содержимое: для subjectAltName это байты 30 xx … целиком, обёрнутые в 04 yy. Забыть внешний OCTET STRING — самая частая ошибка при ручной сборке расширений в CSR.

id-at

id-at OBJECT IDENTIFIER ::= { joint-iso-ccitt(2) ds(5) 4 }знач.: 2.5.4

Корневая дуга стандартных атрибутов именования X.500, от которой отходят commonName, countryName, organizationName и остальные компоненты DN.⚠️ Первые две дуги 2 и 5 упаковываются в один байт 0x55 (40*2+5), поэтому все OID вида 2.5.4.n начинаются с байтов 55 04. Если в дампе видите иное начало — это не атрибут имени.

id-at-commonName / X520CommonName

id-at-commonName AttributeType ::= { id-at 3 }

X520CommonName ::= CHOICE {
teletexString TeletexString (SIZE (1…ub-common-name)),
printableString PrintableString (SIZE (1…ub-common-name)),
universalString UniversalString (SIZE (1…ub-common-name)),
utf8String UTF8String (SIZE (1…ub-common-name)),
bmpString BMPString (SIZE (1…ub-common-name)) }
знач.: OID 2.5.4.3 (CN); значение — PrintableString или UTF8String (прочие альтернативы только для обратной совместимости); длина 1…ub-common-name = 64

Общее имя (CN). В TLS-запросах традиционно содержит доменное имя, но нормативно имя проверяется по subjectAltName.⚠️ Ограничение 64 символа — жёсткое, длинные FQDN и «человеческие» названия в него не влезают. Для сертификатов TLS не полагайтесь на CN: имя хоста обязано дублироваться в расширении subjectAltName, которое передаётся через extensionRequest.

id-at-countryName / X520countryName

id-at-countryName AttributeType ::= { id-at 6 }

X520countryName ::= PrintableString (SIZE (2))
знач.: OID 2.5.4.6 ©; только PrintableString, ровно 2 символа — диграф по IS 3166

Код страны. В отличие от остальных компонентов имени, не является DirectoryString: тип фиксирован как PrintableString ровно из двух символов.⚠️ Ровно два символа и никакого выбора кодировки: ‘RU’ — можно, ‘RUS’, ‘Russia’ или пустая строка — нет, и UTF8String здесь недопустим даже несмотря на то, что для остальных атрибутов он предпочтителен. Код берётся из IS 3166 (alpha-2), регистр — верхний; кодируется как 13 02 ‘R’ ‘U’.

id-at-localityName / X520LocalityName

id-at-localityName AttributeType ::= { id-at 7 }

X520LocalityName ::= CHOICE {
teletexString TeletexString (SIZE (1…ub-locality-name)),
printableString PrintableString (SIZE (1…ub-locality-name)),
universalString UniversalString (SIZE (1…ub-locality-name)),
utf8String UTF8String (SIZE (1…ub-locality-name)),
bmpString BMPString (SIZE (1…ub-locality-name)) }
знач.: OID 2.5.4.7 (L); PrintableString или UTF8String; длина 1…ub-locality-name = 128

Название населённого пункта (город).⚠️ OID 2.5.4.7 стоит по номеру перед stateOrProvinceName (2.5.4.8), но в иерархии DN локальность младше региона — числовой порядок дуг не отражает порядок компонентов в RDNSequence.

id-at-organizationName / X520OrganizationName

id-at-organizationName AttributeType ::= { id-at 10 }

X520OrganizationName ::= CHOICE {
teletexString TeletexString
(SIZE (1…ub-organization-name)),
printableString PrintableString
(SIZE (1…ub-organization-name)),
universalString UniversalString
(SIZE (1…ub-organization-name)),
utf8String UTF8String
(SIZE (1…ub-organization-name)),
bmpString BMPString
(SIZE (1…ub-organization-name)) }
знач.: OID 2.5.4.10 (O); PrintableString или UTF8String; длина 1…ub-organization-name = 64

Название организации. Один из атрибутов, которые УЦ проверяет по регистрационным данным при выпуске OV/EV-сертификатов.⚠️ Название на кириллице требует UTF8String (тег 0x0C): в PrintableString допустимы только латиница, цифры и ограниченный набор знаков препинания. Ограничение 64 символа считается в символах строкового типа, а не в байтах UTF-8, — длинные названия обрежутся раньше, чем ждёт наивная проверка по len(bytes).

id-at-organizationalUnitName / X520OrganizationalUnitName

id-at-organizationalUnitName AttributeType ::= { id-at 11 }

X520OrganizationalUnitName ::= CHOICE {
teletexString TeletexString
(SIZE (1…ub-organizational-unit-name)),
printableString PrintableString
(SIZE (1…ub-organizational-unit-name)),
universalString UniversalString
(SIZE (1…ub-organizational-unit-name)),
utf8String UTF8String
(SIZE (1…ub-organizational-unit-name)),
bmpString BMPString
(SIZE (1…ub-organizational-unit-name)) }
знач.: OID 2.5.4.11 (OU); PrintableString или UTF8String; длина 1…ub-organizational-unit-name = 64

Название подразделения организации. Единственный из перечисленных компонентов, который штатно повторяется в DN несколько раз.⚠️ Несколько OU идут отдельными RDN в RDNSequence и сохраняют порядок; если вы упакуете их в один RDN (multi-valued), DER отсортирует их и порядок пропадёт. Обратите также внимание: в модуле есть похожий устаревший ub-organizational-unit-name-length = 32 (для X.400-адресов) — это другое ограничение, к DN оно не относится.

id-at-stateOrProvinceName / X520StateOrProvinceName

id-at-stateOrProvinceName AttributeType ::= { id-at 8 }

X520StateOrProvinceName ::= CHOICE {
teletexString TeletexString (SIZE (1…ub-state-name)),
printableString PrintableString (SIZE (1…ub-state-name)),
universalString UniversalString (SIZE (1…ub-state-name)),
utf8String UTF8String (SIZE (1…ub-state-name)),
bmpString BMPString (SIZE (1…ub-state-name)) }
знач.: OID 2.5.4.8 (ST); PrintableString или UTF8String; длина 1…ub-state-name = 128

Название штата, области или региона.⚠️ Сокращение атрибута в строковой записи — ST, а не S и не State; в конфигурационных файлах openssl используется ключ stateOrProvinceName. Предел здесь 128, а не 64, как у CN и O — не переносите одну проверку длины на все компоненты имени.

id-emailAddress / EmailAddress

id-emailAddress AttributeType ::= { pkcs-9 1 }

EmailAddress ::= IA5String (SIZE (1…ub-emailaddress-length))
знач.: OID 1.2.840.113549.1.9.1 (emailAddress); только IA5String (тег 0x16); длина 1…ub-emailaddress-length = 255

Адрес электронной почты, встроенный в отличительное имя субъекта. Тип значения — IA5String, чтобы допустить символ ‘@’, которого нет в наборе PrintableString. Значения нечувствительны к регистру.⚠️ Это единственный из перечисленных компонентов не из арки 2.5.4 (он из pkcs-9) и единственный со строковым типом IA5String — попытка закодировать его как UTF8String или PrintableString невалидна. RFC 5280 помечает его как legacy: адрес почты полагается класть в subjectAltName как rfc822Name, а сравнение значений идёт без учёта регистра, хотя local-part адреса по RFC 2821 регистрозависим — отсюда риск склеить два разных ящика.

parameters

parameters ALGORITHM.&Type({IOSet}{@algorithm}) OPTIONALзнач.: открытый тип, определяемый значением поля algorithm; может отсутствовать

Параметры алгоритма; их ASN.1-тип определяется OID в поле algorithm через информационный набор объектов ALGORITHM.⚠️ У поля нет DEFAULT — только OPTIONAL, поэтому «значения по умолчанию NULL» не существует: NULL, если он нужен, надо записать явными байтами 05 00.

pkcs-9

pkcs-9 OBJECT IDENTIFIER ::= {iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 9}знач.: 1.2.840.113549.1.9

Корневая дуга идентификаторов атрибутов PKCS #9, от которой отходят extensionRequest (14), challengePassword (7) и emailAddress (1).⚠️ В RFC 5280 та же арка объявлена в разделе «-- Legacy attributes» и записана как { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 9 } — числа те же, форматирование разное; сравнивать надо числовое значение, а не строку.

pkcs-9-at-extensionRequest

pkcs-9-at-extensionRequest OBJECT IDENTIFIER ::= {pkcs-9 14}знач.: 1.2.840.113549.1.9.14 (в DER: 06 09 2A 86 48 86 F7 0D 01 09 0E)

Идентификатор объекта атрибута extensionRequest, четырнадцатая дуга арки pkcs-9.⚠️ Легко перепутать с pkcs-9-at-extendedCertificateAttributes {pkcs-9 9} (устаревший) и pkcs-9-at-signingDescription {pkcs-9 13}. Проверяйте последнюю дугу: 14, то есть байт 0x0E в конце кодированного OID.

signature

signature BIT STRINGзнач.: BIT STRING (тег 0x03)

Результат подписи DER-кодирования компонента certificationRequestInfo закрытым ключом субъекта запроса, представленный битовой строкой.⚠️ Первый октет содержимого BIT STRING — это счётчик неиспользуемых бит, а не начало подписи. Для подписи он всегда 0x00, и его нельзя забыть при кодировании и нужно отбросить при разборе; иначе подпись «съезжает» на байт и проверка проваливается.

signatureAlgorithm

signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}знач.: AlgorithmIdentifier, ограниченный информационным набором SignatureAlgorithms (расширяемым: …)

Идентифицирует алгоритм подписи (и его параметры), которым подписана информация запроса. Пример из RFC — объект ALGORITHM для md5WithRSAEncryption с параметрами NULL.⚠️ Для RSA-алгоритмов PKCS#1 поле parameters обязано присутствовать и быть явным NULL (05 00), а не отсутствовать; для ECDSA параметры, наоборот, опускаются. Верификаторы сравнивают байты AlgorithmIdentifier побайтово, и лишний/пропущенный NULL — типичная причина «signature algorithm mismatch».

subject

subject Nameзнач.: Name (CHOICE → rdnSequence, кодируется как SEQUENCE, тег 0x30)

Отличительное имя (DN) субъекта сертификата — сущности, чей открытый ключ должен быть сертифицирован.⚠️ Name — это CHOICE, а не SEQUENCE: у самого CHOICE нет собственного тега, поэтому в байтах сразу появляется SEQUENCE от RDNSequence, без дополнительной обёртки. Пустой DN кодируется как 30 00 — синтаксически валидно для PKCS#10, но многие УЦ такой запрос отклоняют, требуя непустое имя.

subjectPKInfo

subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }}знач.: SubjectPublicKeyInfo, ограниченный информационным набором PKInfoAlgorithms (расширяемым: …)

Сведения об открытом ключе: идентификатор алгоритма ключа с параметрами и битовая строка с самим ключом. Для rsaEncryption битовая строка содержит DER-кодирование значения типа RSAPublicKey из PKCS #1.⚠️ Имя поля в ASN.1 — subjectPKInfo, а в поясняющем тексте RFC 2986 оно названо subjectPublicKeyInfo; в коде и в схемах используйте subjectPKInfo. Кроме того, внутрь BIT STRING кладётся вложенная DER-структура ключа, а не «сырые» байты модуля и экспоненты.

subjectPublicKey

subjectPublicKey BIT STRINGзнач.: BIT STRING (тег 0x03)

Битовое представление открытого ключа субъекта. Для rsaEncryption содержит DER-кодирование значения типа RSAPublicKey из PKCS #1.⚠️ Первый байт содержимого — число неиспользуемых бит последнего октета, для ключа он всегда 0x00. При разборе этот байт нужно отбросить перед парсингом вложенной структуры, иначе вложенный DER не распознается; при сборке — не забыть добавить.

type

type ATTRIBUTE.&id({IOSet})знач.: OBJECT IDENTIFIER (тег 0x06)

Идентификатор объекта, задающий тип атрибута; через него определяется допустимый ASN.1-тип значений в поле values.⚠️ OID кодируется в base-128 с первым байтом 40*x+y для первых двух дуг — писать компоненты по одному байту нельзя. Для 1.2.840.113549.1.9.14 первый байт равен 0x2A, а 840 и 113549 занимают по два и три байта соответственно.

ub-common-name

ub-common-name INTEGER ::= 64знач.: 64

Предельная длина значения атрибута commonName в символах.⚠️ 64 символа — именно тот предел, из-за которого CN нельзя использовать как надёжное место для длинных доменных имён и списков хостов.

ub-emailaddress-length

ub-emailaddress-length INTEGER ::= 255знач.: 255

Предельная длина значения атрибута emailAddress в символах.⚠️ В RFC 3280 это значение равнялось 128 и было увеличено до 255 в RFC 5280 ради согласования с PKCS #9. Старые реализации и старые тестовые векторы всё ещё режут адрес на 128 символах.

ub-locality-name

ub-locality-name INTEGER ::= 128знач.: 128

Предельная длина значения атрибута localityName в символах.⚠️ Совпадает с ub-state-name (128) и отличается от ub-common-name (64) — пределы у компонентов DN разные, единой константы нет.

ub-organization-name

ub-organization-name INTEGER ::= 64знач.: 64

Предельная длина значения атрибута organizationName в символах.⚠️ Не путайте с ub-organization-name-length INTEGER ::= 64 из X.400-части того же модуля: значение совпадает, но это ограничение для других типов, и опора на «то же имя константы» приведёт вас не в тот раздел.

ub-organizational-unit-name

ub-organizational-unit-name INTEGER ::= 64знач.: 64

Предельная длина значения атрибута organizationalUnitName в символах.⚠️ В том же модуле есть ub-organizational-unit-name-length INTEGER ::= 32 — это X.400-константа с другим значением. Для DN действует именно 64.

ub-state-name

ub-state-name INTEGER ::= 128знач.: 128

Предельная длина значения атрибута stateOrProvinceName в символах.⚠️ 128, а не 64 — общая валидация «все компоненты DN не длиннее 64» отбракует формально корректные запросы.

values

values SET SIZE(1…MAX) OF ATTRIBUTE.&Type({IOSet}{@type})знач.: SET (тег 0x31), от одного значения до MAX

Множество значений атрибута; конкретный ASN.1-тип каждого значения определяется OID из поля type (открытый тип, связанный ограничением {@type}).⚠️ Элементы SET сортируются по DER по их кодированным октетам. Если атрибут содержит несколько значений, порядок в вашей структуре данных и порядок в байтах могут не совпадать — при повторной сборке запроса это даёт другой DER.

version

version INTEGER { v1(0) } (v1,…)знач.: v1(0); ограничение (v1,…) — расширяемое, но для этой версии стандарта значение 0

Номер версии, введённый для совместимости с будущими редакциями. Для текущей версии стандарта равен 0.⚠️ У INTEGER нет значения DEFAULT — ноль кодируется явно как 02 01 00 и никогда не опускается. Общее правило для INTEGER: если старший бит первого содержательного байта равен 1, надо добавить ведущий 0x00, иначе значение прочтётся как отрицательное (важно для сериального номера и модуля RSA, а не для самой версии).

Версии и подводные камни:

  • Источник — RFC 2986; типы компонентов отличительного имени и их строковые типы — RFC 5280.

  • attributes объявлено как [0] IMPLICIT: пустой набор кодируется тегом нулевой длины, а не отсутствует. Это первое, обо что спотыкаются при ручной сборке.

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

  • Сверено с: https://www.rfc-editor.org/rfc/rfc2986, https://www.rfc-editor.org/rfc/rfc5280, https://www.rfc-editor.org/rfc/rfc2985

Расширенный справочник: WebCrypto

Показать таблицу — 62 параметра (Web Crypto API)

Параметр

Синтаксис / значения

По умолч.

Описание и нюансы

AES-KW

Registration §30.2: wrapKey | None | byte sequence; unwrapKey | None | byte sequence; generateKey | AesKeyGenParams; importKey | None; exportKey | None

Единственный алгоритм, специально предназначенный для обёртывания ключей (RFC 3394).⚠️ encrypt/decrypt для AES-KW НЕ зарегистрированы — использовать его как обычный шифр нельзя. И наоборот: wrapKey/unwrapKey работают не только с AES-KW, а с любым алгоритмом, у которого есть encrypt/decrypt (AES-GCM, AES-CBC, RSA-OAEP), — просто у AES-KW это единственный режим. Parameters = None, дополнительный iv не нужен.

AesCbcParams / AesCtrParams

dictionary AesCbcParams : Algorithm { required BufferSource iv; }; dictionary AesCtrParams : Algorithm { required BufferSource counter; required [EnforceRange] octet length; };

Параметры encrypt/decrypt для AES-CBC и AES-CTR соответственно.⚠️ У CTR член называется counter, а не iv, и рядом обязателен length — это число битов счётчика ВНУТРИ блока counter (тип octet), а не длина ключа и не длина данных. Оба режима не аутентифицируют данные: целостность придётся обеспечивать отдельно (в отличие от AES-GCM).

AesGcmParams

dictionary AesGcmParams : Algorithm { required BufferSource iv; BufferSource additionalData; [EnforceRange] octet tagLength; };знач.: tagLength: octet (в битах)

tagLength — необязателен; additionalData — необязателен

Параметры encrypt/decrypt для AES-GCM. Обязателен только iv.⚠️ iv обязателен и должен быть УНИКАЛЬНЫМ для каждого шифрования тем же ключом — повторение полностью ломает GCM. tagLength имеет тип octet, то есть максимум 255, и задаётся в битах (обычно 128); тег включается в выходной ArrayBuffer, а не возвращается отдельно. additionalData аутентифицируется, но не шифруется, и при decrypt должен совпадать байт в байт.

AesKeyGenParams / AesDerivedKeyParams

dictionary AesKeyGenParams : Algorithm { required [EnforceRange] unsigned short length; }; dictionary AesDerivedKeyParams : Algorithm { required [EnforceRange] unsigned short length; };знач.: length: 128, 192, 256 (бит)

Параметры generateKey (и, соответственно, derivedKeyType при deriveKey) для AES-CTR, AES-CBC, AES-GCM, AES-KW.⚠️ Два разных словаря с идентичным составом — легко перепутать, но в цепочке deriveKey нужен именно AesDerivedKeyParams. length в БИТАХ; тип unsigned short, поэтому 256 — это 256, а не 32 байта.

Algorithm

dictionary Algorithm { required DOMString name; };

Базовый словарь для ВСЕХ словарей параметров алгоритмов. Содержит ровно один член — name.⚠️ Это корень иерархии: RsaKeyGenParams : Algorithm, EcKeyGenParams : Algorithm, EcdsaParams : Algorithm, RsaPssParams : Algorithm, AesGcmParams : Algorithm и так далее. Если в цепочке наследования не дописать «: Algorithm», получится, что у словаря нет обязательного name — а без него нормализация алгоритма невозможна.

Algorithm.name

required DOMString name;знач.: Зарегистрированные имена: “RSASSA-PKCS1-v1_5”, “RSA-PSS”, “RSA-OAEP”, “ECDSA”, “ECDH”, “Ed25519”, “X25519”, “AES-CTR”, “AES-CBC”, “AES-GCM”, “AES-KW”, “HMAC”, “SHA-1”, “SHA-256”, “SHA-384”, “SHA-512”, “HKDF”, “PBKDF2”

Член принадлежит БАЗОВОМУ словарю Algorithm. Дочерние словари (RsaHashedKeyGenParams, EcKeyGenParams, EcdsaParams, …) получают его по наследству и НЕ объявляют собственный name.⚠️ Приписывать name дочернему словарю — ошибка чтения IDL: в §20.4 у RsaHashedKeyGenParams объявлен только hash, а в §23.4 у EcKeyGenParams — только namedCurve. Сравнение имён регистрозависимое («a case-sensitive string match»): “aes-gcm” не распознается. Строка вместо объекта допустима именно потому, что DOMString нормализуется в Algorithm с этим name.

AlgorithmIdentifier

typedef (object or DOMString) AlgorithmIdentifier;

Тип аргумента algorithm у всех методов: либо объект-словарь, либо просто строка с именем.⚠️ Строка — синтаксический сахар: она разворачивается в «a new Algorithm dictionary whose name attribute is alg». Поэтому строку можно передавать только там, где Parameters = None (SHA-*, HMAC.sign, RSASSA-PKCS1-v1_5.sign, Ed25519.sign). Для RSA-PSS, ECDSA, AES-GCM и прочих нужен полноценный объект. Неизвестные члены объекта при нормализации просто отбрасываются — молча.

BigInteger

typedef Uint8Array BigInteger;

Тип члена publicExponent в RsaKeyGenParams — big-endian представление целого числа.⚠️ Это Uint8Array, а не number и не BigInt: экспонента 65537 записывается как new Uint8Array([0x01, 0x00, 0x01]). Передача числа 65537 приведёт к ошибке конверсии WebIDL.

Crypto.getRandomValues()

ArrayBufferView getRandomValues(ArrayBufferView array);

Заполняет переданный типизированный массив криптографически стойкими случайными значениями и возвращает его же (тот же объект, не копию).⚠️ Жёсткий лимит: больше 65536 байт за вызов — QuotaExceededError. Float32Array и Float64Array запрещены (TypeMismatchError). Не требует secure context, в отличие от subtle и randomUUID.

Crypto.randomUUID()

[SecureContext] DOMString randomUUID();

Возвращает случайно сгенерированный UUID v4 в виде строки.⚠️ Помечен [SecureContext] — в HTTP-контексте метода нет. В спецификации 2017 года его нет вовсе, поэтому это частый источник расхождений между «спекой» и реальными браузерами.

Crypto.subtle

[SecureContext] readonly attribute SubtleCrypto subtle;

Единственная точка входа ко всем криптографическим операциям. Сам объект Crypto доступен как window.crypto / self.crypto.⚠️ [SecureContext] стоит на атрибуте subtle, а не на интерфейсе Crypto: в незащищённом контексте объект crypto существует и getRandomValues работает, а crypto.subtle — undefined. Проверять надо именно наличие subtle, а не crypto.

CryptoKey

[SecureContext,Exposed=(Window,Worker),Serializable] interface CryptoKey { readonly attribute KeyType type; readonly attribute boolean extractable; readonly attribute object algorithm; readonly attribute object usages; };

Непрозрачная ссылка на ключевой материал. РОВНО четыре атрибута, все readonly: type, extractable, algorithm, usages. Методов у интерфейса нет.⚠️ Это interface, а не dictionary — конструктора нет, объект нельзя собрать литералом, только получить из generateKey/importKey/deriveKey/unwrapKey. Помечен [Serializable]: ключ можно передать через postMessage и положить в IndexedDB, и неизвлекаемый ключ при этом переживёт перезагрузку страницы, не появившись в JS в открытом виде.

CryptoKey.algorithm

readonly attribute object algorithm;

Кешированный ECMAScript-объект, описывающий алгоритм ключа (name плюс параметры вида modulusLength, namedCurve, length, hash).⚠️ В IDL тип — object, а не KeyAlgorithm: содержимое слота «shall be, or be derived from, a KeyAlgorithm», конкретный подтип (RsaHashedKeyAlgorithm, EcKeyAlgorithm, AesKeyAlgorithm, HmacKeyAlgorithm) зависит от алгоритма. Возвращается кеш — мутировать его бессмысленно, на поведение ключа это не влияет.

CryptoKey.extractable

readonly attribute boolean extractable;

Отражает [[extractable]] — можно ли выгрузить сырой ключевой материал наружу.⚠️ Значение задаётся ОДИН РАЗ при создании ключа и readonly навсегда — «переключить» его нельзя. Для пар ключей спецификация задаёт разное: публичному ключу [[extractable]] всегда ставится true, а переданный флаг применяется только к приватному. Значение попадает в поле ext при экспорте в JWK.

CryptoKey.type

readonly attribute KeyType type;знач.: “public” | “private” | “secret”

Отражает внутренний слот [[type]] — вид ключа.⚠️ Операции жёстко проверяют этот атрибут раньше usages: sign с публичным ключом даёт InvalidAccessError («If the [[type]] internal slot of key is not “private”, then throw an InvalidAccessError»), а не false.

CryptoKey.usages

readonly attribute object usages;знач.: Массив строк из enum KeyUsage

Кешированный объект со списком разрешённых операций.⚠️ В IDL объявлен как object, хотя фактически это массив строк, а содержимое слота [[usages]] имеет тип Sequence. Итоговый набор — это «usage intersection» запрошенного списка и того, что допускает алгоритм: у ECDSA приватному ключу оставят только [“sign”], у публичного — только [“verify”], лишние запрошенные usages молча отсекаются.

CryptoKeyPair

dictionary CryptoKeyPair { CryptoKey publicKey; CryptoKey privateKey; };знач.: Члены: publicKey, privateKey

Это DICTIONARY, а не interface. Два необязательных члена типа CryptoKey. Возвращается из generateKey для RSA, ECDSA, ECDH, Ed25519, X25519.⚠️ Раз это словарь, а не интерфейс — прототипа CryptoKeyPair не существует, instanceof работать не будет, а generateKey отдаёт обычный объект. Отсюда же объединяющий тип возврата Promise<(CryptoKey or CryptoKeyPair)>: перед доступом к .privateKey надо проверять, вернулся ли ключ или пара. Оба члена объявлены БЕЗ required.

ECDH

Registration §24.2: generateKey | EcKeyGenParams; deriveBits | EcdhKeyDeriveParams; importKey | EcKeyImportParams; exportKey | None

Согласование ключей на эллиптических кривых.⚠️ deriveKey в таблице регистрации ОТСУТСТВУЕТ — зарегистрирован только deriveBits, а метод deriveKey работает поверх него (он нормализует алгоритм с op “deriveBits”). Тот же ключ подписывать не может: sign/verify для ECDH не зарегистрированы, несмотря на общий с ECDSA словарь EcKeyGenParams.

ECDSA

Registration §23.2: sign | EcdsaParams; verify | EcdsaParams; generateKey | EcKeyGenParams; importKey | EcKeyImportParams; exportKey | None

Подпись на эллиптических кривых. Кривая задаётся при генерации (EcKeyGenParams.namedCurve), хеш — при подписи (EcdsaParams.hash).⚠️ Разделение кривой и хеша между двумя разными словарями — источник путаницы: hash в EcKeyGenParams не предусмотрен и будет отброшен. Экспорт поддерживает “spki”, “pkcs8”, “jwk” и “raw”, но “raw” — только для публичного ключа.

EcKeyGenParams

dictionary EcKeyGenParams : Algorithm { required NamedCurve namedCurve; };знач.: Собственный член: namedCurve. Унаследованный: name (от Algorithm)

Параметры generateKey для ECDSA и ECDH. Наследует Algorithm напрямую: EcKeyGenParams : Algorithm.⚠️ Собственный член ровно один — namedCurve; name принадлежит базовому Algorithm и приписывать его этому словарю нельзя. В отличие от RSA, промежуточного словаря здесь нет — сравните с RsaHashedKeyGenParams : RsaKeyGenParams : Algorithm. Хеш при генерации EC-ключа НЕ задаётся: он указывается позже, в EcdsaParams при подписи.

EcKeyImportParams / EcKeyAlgorithm

dictionary EcKeyImportParams : Algorithm { required NamedCurve namedCurve; }; dictionary EcKeyAlgorithm : KeyAlgorithm { required NamedCurve namedCurve; };

EcKeyImportParams — параметры importKey для ECDSA и ECDH; EcKeyAlgorithm — то, что видно в CryptoKey.algorithm.⚠️ Словари одинаковы по составу члена, но наследуют РАЗНЫЕ базы: параметры импорта — от Algorithm, описание ключа — от KeyAlgorithm. namedCurve обязателен даже при импорте spki/pkcs8, где кривая уже закодирована в самих данных: расхождение приведёт к ошибке.

EcdhKeyDeriveParams

dictionary EcdhKeyDeriveParams : Algorithm { required CryptoKey public; };

Параметры deriveBits/deriveKey для ECDH и X25519. Содержит публичный ключ второй стороны.⚠️ Член называется public — это зарезервированное слово JS, но как ключ объекта оно допустимо: {name: “ECDH”, public: theirKey}. Тип члена — CryptoKey, а не буфер: чужой публичный ключ надо предварительно провести через importKey. Тот же словарь используется и для X25519.

EcdsaParams

dictionary EcdsaParams : Algorithm { required HashAlgorithmIdentifier hash; };

Параметры sign и verify для ECDSA. Наследование дословно: EcdsaParams : Algorithm.⚠️ hash объявлен required, поэтому строка “ECDSA” вместо объекта не пройдёт — нужен {name: “ECDSA”, hash: “SHA-256”}. Хеш здесь задаётся на КАЖДУЮ подпись и в ключе не хранится: один и тот же EC-ключ можно использовать с SHA-256 и SHA-384. Подпись возвращается в сыром формате r||s, а не в DER — с OpenSSL напрямую не стыкуется.

Ed25519

Registration §25.2: sign | None | byte sequence; verify | None | boolean; generateKey | None | CryptoKeyPair; importKey | None | CryptoKey; exportKey | None | objectзнач.: Зарегистрированы РОВНО пять операций: sign, verify, generateKey, importKey, exportKey

Подпись и проверка по RFC 8032. В документе по адресу /TR/WebCryptoAPI/ (то есть в Level 2) алгоритм ПРИСУТСТВУЕТ — раздел 25, вместе с X25519 в разделе 26.⚠️ Приписывать Ed25519 операции сверх зарегистрированных пяти нельзя: encrypt, decrypt, deriveKey, deriveBits, wrapKey и unwrapKey для него НЕ определены. У всех пяти операций Parameters = None — отдельного словаря параметров у Ed25519 нет вовсе, передаётся строка “Ed25519” (хеш всегда SHA-512 и задавать его негде). Историческая оговорка: в Recommendation 2017 года его нет, он пришёл из отдельной работы WICG (webcrypto-secure-curves), на issue-трекер которой спецификация до сих пор ссылается; в тексте раздела висят два открытых Issue — про рандомизированные подписи и про проверку small-order точек, которую «not all implementations perform».

HMAC

Registration §31.2: sign | None; verify | None; generateKey | HmacKeyGenParams; importKey | HmacImportParams; exportKey | None

Симметричная подпись. generateKey возвращает CryptoKey (не пару), type = “secret”.⚠️ У sign/verify Parameters = None: хеш нельзя переопределить на вызове, он зафиксирован в ключе через HmacKeyGenParams.hash. verify для HMAC — правильный способ сравнения тегов: самостоятельное побайтовое сравнение результата sign уязвимо к тайминг-атакам. Экспорт: только “raw” и “jwk”.

HashAlgorithmIdentifier

typedef AlgorithmIdentifier HashAlgorithmIdentifier;

Псевдоним AlgorithmIdentifier, используемый в членах hash словарей RsaHashedKeyGenParams, RsaHashedImportParams, EcdsaParams, HmacKeyGenParams, HmacImportParams, HkdfParams, Pbkdf2Params.⚠️ Отдельного типа тут нет — это тот же самый (object or DOMString), поэтому hash: “SHA-256” и hash: {name: “SHA-256”} эквивалентны. Никакого ограничения «только SHA» на уровне IDL нет, оно возникает при нормализации с op “digest”.

HkdfParams

dictionary HkdfParams : Algorithm { required HashAlgorithmIdentifier hash; required BufferSource salt; required BufferSource info; };

Параметры deriveBits для HKDF. Обязательны все три члена, включая info.⚠️ info объявлен required — пропустить его нельзя, даже если контекст не нужен: передавайте пустой буфер new Uint8Array(). Как и у PBKDF2, базовый ключ импортируется только как “raw” и только с extractable = false. Операция «Get key length» у HKDF возвращает null, поэтому в deriveKey длина обязана прийти из derivedKeyType.

HmacKeyGenParams / HmacImportParams

dictionary HmacKeyGenParams : Algorithm { required HashAlgorithmIdentifier hash; [EnforceRange] unsigned long length; }; dictionary HmacImportParams : Algorithm { required HashAlgorithmIdentifier hash; [EnforceRange] unsigned long length; };

length — необязателен (по умолчанию берётся размер блока хеш-функции)

Параметры generateKey и importKey для HMAC. Оба наследуют Algorithm напрямую, состав членов идентичен.⚠️ length опционален и, если его не задать, берётся размер блока хеша (для SHA-256 это 512 бит, а не 256). Сам sign/verify для HMAC регистрирует Parameters = None — там достаточно строки “HMAC”, хеш уже зашит в ключ.

JsonWebKey

dictionary JsonWebKey { DOMString kty; DOMString use; sequence key_ops; DOMString alg; boolean ext; DOMString crv; DOMString x; DOMString y; DOMString d; DOMString n; DOMString e; DOMString p; DOMString q; DOMString dp; DOMString dq; DOMString qi; sequence oth; DOMString k; };

Тип второго аргумента importKey при format = “jwk” и тип результата exportKey(“jwk”, key).⚠️ Ни один член не помечен required — валидность проверяется шагами алгоритма, а не WebIDL. Это ЕДИНСТВЕННЫЙ формат, где exportKey возвращает объект, а не ArrayBuffer, поэтому Promise<(ArrayBuffer or JsonWebKey)> надо разбирать по format. При экспорте key_ops заполняется из usages, а ext — из [[extractable]].

KeyAlgorithm

dictionary KeyAlgorithm { required DOMString name; };

Базовый словарь для описания содержимого CryptoKey.algorithm. Наследники: RsaKeyAlgorithm, RsaHashedKeyAlgorithm, EcKeyAlgorithm, AesKeyAlgorithm, HmacKeyAlgorithm.⚠️ Не путать с Algorithm: Algorithm описывает ПАРАМЕТРЫ вызова, KeyAlgorithm — СВОЙСТВА уже существующего ключа. Совпадают они только по имени члена name.

KeyFormat

enum KeyFormat { “raw”, “spki”, “pkcs8”, “jwk” };знач.: “raw”, “spki”, “pkcs8”, “jwk”

Тип аргумента format у importKey, exportKey, wrapKey и unwrapKey. Четыре значения: сырые байты, SubjectPublicKeyInfo (DER), PKCS#8 (DER), JSON Web Key.⚠️ Всё строчными буквами: “spki”, а не “SPKI”; “pkcs8”, а не “PKCS8”. Формат PEM не поддерживается вообще — заголовки BEGIN/END и base64 надо снимать самому и подавать DER. Поддержка конкретного формата определяется алгоритмом, а не перечислением: значение из enum ещё не гарантирует, что операция пройдёт.

KeyType

enum KeyType { “public”, “private”, “secret” };знач.: “public”, “private”, “secret”

Перечисление видов ключа; тип атрибута CryptoKey.type.⚠️ Ровно три значения, все в нижнем регистре. Симметричные ключи (AES, HMAC) и базовые ключи KDF (PBKDF2, HKDF) — это “secret”, отдельного типа под KDF нет.

KeyUsage

enum KeyUsage { “encrypt”, “decrypt”, “sign”, “verify”, “deriveKey”, “deriveBits”, “wrapKey”, “unwrapKey” };знач.: “encrypt”, “decrypt”, “sign”, “verify”, “deriveKey”, “deriveBits”, “wrapKey”, “unwrapKey”

Восемь допустимых значений элементов sequence — того самого позиционного аргумента keyUsages.⚠️ Значения camelCase и чувствительны к регистру: “derivekey” или “DeriveKey” — TypeError на конверсии WebIDL. Пары не подразумеваются: “encrypt” не даёт права расшифровывать, “wrapKey” не даёт права разворачивать. Для deriveKey нужны ОБА usage — “deriveKey” у базового ключа и корректный набор у производного.

NamedCurve

typedef DOMString NamedCurve;знач.: “P-256”, “P-384”, “P-521” (имена, используемые спецификацией для ECDSA/ECDH)

Тип члена namedCurve — просто DOMString, не enum.⚠️ Раз это typedef, а не enum, WebIDL НЕ проверит значение: опечатка вроде “P256” или “secp256r1” пройдёт конверсию и упадёт только при нормализации алгоритма, уже внутри Promise. Обратите внимание на P-521 — именно 521, а не 512.

Pbkdf2Params

dictionary Pbkdf2Params : Algorithm { required BufferSource salt; required [EnforceRange] unsigned long iterations; required HashAlgorithmIdentifier hash; };

Параметры deriveBits (и, через него, deriveKey) для PBKDF2. Все три члена обязательны.⚠️ Базовый ключ PBKDF2 импортируется по особым правилам: format обязан быть “raw”, а extractable обязан быть false — «If extractable is not false, then throw a SyntaxError». Значение iterations спецификация не ограничивает и умолчания не даёт, выбирать его — задача приложения. PBKDF2 не регистрирует операцию deriveKey: она работает через deriveBits + importKey.

RSA-OAEP

Registration §22.2: encrypt | RsaOaepParams; decrypt | RsaOaepParams; generateKey | RsaHashedKeyGenParams; importKey | RsaHashedImportParams; exportKey | None

Единственный в спецификации асимметричный алгоритм шифрования.⚠️ sign/verify для RSA-OAEP не зарегистрированы, а для RSA-PSS/RSASSA не зарегистрированы encrypt/decrypt — один RSA-ключ нельзя использовать и для подписи, и для шифрования: имя алгоритма зашивается в CryptoKey. Объём шифруемых данных ограничен размером модуля, потоков нет. “raw” при экспорте недоступен.

RSA-PSS

Registration §21.2: sign | RsaPssParams; verify | RsaPssParams; generateKey | RsaHashedKeyGenParams; importKey | RsaHashedImportParams; exportKey | None

RSA-подпись с рандомизированной солью. Для sign и verify требуется словарь RsaPssParams, у которого saltLength объявлен required.⚠️ Три РАЗНЫХ словаря на один алгоритм: RsaPssParams при подписи/проверке, RsaHashedKeyGenParams при генерации, RsaHashedImportParams при импорте. saltLength не наследуется ключом и передаётся при каждом вызове; при verify он должен совпасть со значением, использованным при sign, иначе получите false. Формат “raw” не поддерживается.

RSASSA-PKCS1-v1_5

Registration §20.2: sign | None; verify | None; generateKey | RsaHashedKeyGenParams; importKey | RsaHashedImportParams; exportKey | None

Классическая RSA-подпись PKCS#1 v1.5. Хеш фиксируется в ключе при генерации/импорте.⚠️ У sign/verify Parameters = None, поэтому здесь достаточно строки “RSASSA-PKCS1-v1_5” — прямая противоположность RSA-PSS, где словарь с saltLength обязателен. Экспорт поддерживает только “spki”, “pkcs8” и “jwk”; “raw” даёт NotSupportedError.

RsaHashedImportParams

dictionary RsaHashedImportParams : Algorithm { required HashAlgorithmIdentifier hash; };

Параметры importKey для всех трёх RSA-алгоритмов.⚠️ Наследует Algorithm НАПРЯМУЮ, а не RsaKeyGenParams: при импорте modulusLength и publicExponent не задают — они берутся из самого ключевого материала. Попытка передать их сюда ничего не изменит.

RsaHashedKeyGenParams

dictionary RsaHashedKeyGenParams : RsaKeyGenParams { required HashAlgorithmIdentifier hash; };знач.: Собственный член: hash. Унаследованные: modulusLength, publicExponent (от RsaKeyGenParams), name (от Algorithm)

Параметры generateKey для RSASSA-PKCS1-v1_5, RSA-PSS и RSA-OAEP. Полная цепочка наследования — RsaHashedKeyGenParams : RsaKeyGenParams : Algorithm, промежуточный словарь пропускать нельзя.⚠️ Собственный член у него РОВНО ОДИН — hash. modulusLength и publicExponent приходят от RsaKeyGenParams, а name — от Algorithm; приписывать name этому словарю неверно. Итого при вызове обязательны все четыре поля сразу: name, modulusLength, publicExponent, hash.

RsaKeyAlgorithm / RsaHashedKeyAlgorithm

dictionary RsaKeyAlgorithm : KeyAlgorithm { required unsigned long modulusLength; required BigInteger publicExponent; }; dictionary RsaHashedKeyAlgorithm : RsaKeyAlgorithm { required KeyAlgorithm hash; };

Описывают то, что возвращает CryptoKey.algorithm у RSA-ключа. Цепочка: RsaHashedKeyAlgorithm : RsaKeyAlgorithm : KeyAlgorithm.⚠️ Не путать с *KeyGenParams: в них hash имеет тип HashAlgorithmIdentifier (можно строкой), а здесь hash — это KeyAlgorithm, то есть уже нормализованный объект {name: “SHA-256”}. Поэтому key.algorithm.hash — всегда объект, а не строка.

RsaKeyGenParams

dictionary RsaKeyGenParams : Algorithm { required [EnforceRange] unsigned long modulusLength; required BigInteger publicExponent; };знач.: Члены: modulusLength, publicExponent (+ унаследованный name)

Базовый словарь генерации RSA-ключа. Наследует Algorithm, откуда берётся required name.⚠️ Сам по себе почти не используется — все три RSA-алгоритма регистрируют для generateKey наследника RsaHashedKeyGenParams. [EnforceRange] означает, что нецелое или выходящее за диапазон modulusLength даёт TypeError, а не тихое приведение.

RsaOaepParams

dictionary RsaOaepParams : Algorithm { BufferSource label; };

label отсутствует (член не required)

Параметры encrypt/decrypt для RSA-OAEP. Единственный член label — необязательный.⚠️ label имеет тип BufferSource, а не строку — текстовую метку надо кодировать через TextEncoder. Хеш-функция здесь не задаётся: она уже зафиксирована в ключе на этапе generateKey/importKey через RsaHashedKeyGenParams.hash.

RsaOtherPrimesInfo

dictionary RsaOtherPrimesInfo { DOMString r; DOMString d; DOMString t; };

Элемент последовательности oth в JsonWebKey — описание дополнительных простых множителей RSA (multi-prime).⚠️ Поля определяются не этой спецификацией, а Section 6.3.2.7 of JSON Web Algorithms; здесь только IDL-обёртка. На практике реализации multi-prime RSA обычно не поддерживают.

RsaPssParams

dictionary RsaPssParams : Algorithm { required [EnforceRange] unsigned long saltLength; };знач.: Член: saltLength (в байтах, required)

Обязательный словарь параметров для sign и verify алгоритмом RSA-PSS. saltLength объявлен required — умолчания у него нет.⚠️ Именно поэтому RSA-PSS нельзя вызвать строкой: sign(“RSA-PSS”, key, data) не пройдёт, нужен {name: “RSA-PSS”, saltLength: N}. saltLength задаётся в БАЙТАХ («desired length of the random salt in bytes»), тогда как соседние длины в API считаются в битах. При verify saltLength должен совпадать с использованным при подписи. Для generateKey/importKey у RSA-PSS используются другие словари — RsaHashedKeyGenParams и RsaHashedImportParams.

SubtleCrypto

[SecureContext,Exposed=(Window,Worker)] interface SubtleCrypto { … };

Интерфейс с 12 методами: encrypt, decrypt, sign, verify, digest, generateKey, deriveKey, deriveBits, importKey, exportKey, wrapKey, unwrapKey. Все возвращают Promise.⚠️ Конструктора нет — экземпляр получают только через crypto.subtle. Весь интерфейс [SecureContext]: в Worker он есть, но только если сам worker загружен из защищённого контекста.

SubtleCrypto.decrypt()

Promise decrypt(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);знач.: Зарегистрирован для: RSA-OAEP, AES-CTR, AES-CBC, AES-GCM

Расшифровывает data. Сигнатура симметрична encrypt: три позиционных аргумента.⚠️ Требует usage “decrypt” — отдельный от “encrypt”: ключ, сгенерированный только с [“encrypt”], расшифровать не сможет. Параметры (iv, counter, additionalData) должны быть теми же, что при шифровании, иначе OperationError.

SubtleCrypto.deriveBits()

Promise deriveBits(AlgorithmIdentifier algorithm, CryptoKey baseKey, optional unsigned long? length = null);знач.: Зарегистрирован для: ECDH (EcdhKeyDeriveParams), X25519 (EcdhKeyDeriveParams), HKDF (HkdfParams), PBKDF2 (Pbkdf2Params)

length = null

Выводит сырые байты, а не CryptoKey. Именно поэтому здесь нет ни extractable, ни keyUsages.⚠️ length измеряется в БИТАХ, не байтах. В Level 2 аргумент стал необязательным и nullable со значением по умолчанию null (вернуть весь секрет целиком); в Level 1 он был обязательным, так что вызов без length ломается в старых реализациях. Результат — обычный ArrayBuffer вне защиты CryptoKey.

SubtleCrypto.deriveKey()

Promise deriveKey(AlgorithmIdentifier algorithm, CryptoKey baseKey, AlgorithmIdentifier derivedKeyType, boolean extractable, sequence keyUsages);

Выводит новый CryptoKey из baseKey. Пять позиционных аргументов; extractable — ЧЕТВЁРТЫЙ, keyUsages — ПЯТЫЙ, и относятся они к ВЫВЕДЕННОМУ ключу, а не к baseKey.⚠️ Двух AlgorithmIdentifier легко перепутать: algorithm описывает KDF (PBKDF2/HKDF/ECDH), derivedKeyType — тип получаемого ключа (например AES-GCM). Внутри метод нормализует algorithm с операцией “deriveBits” и затем импортирует результат как “raw” — поэтому ни HKDF, ни PBKDF2 не регистрируют операцию deriveKey отдельно, и работать это будет только для derivedKeyType, умеющего importKey формата “raw”.

SubtleCrypto.digest()

Promise digest(AlgorithmIdentifier algorithm, BufferSource data);знач.: “SHA-1”, “SHA-256”, “SHA-384”, “SHA-512”

Считает хеш. Единственный метод SubtleCrypto, который не принимает CryptoKey.⚠️ Parameters = None, поэтому передаётся просто строка. SHA-256 без “-” (“SHA256”) не распознаётся. SHA-3 и BLAKE в спецификации отсутствуют. Потоковый/инкрементальный digest не предусмотрен вовсе — только цельный буфер.

SubtleCrypto.encrypt()

Promise encrypt(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);знач.: Зарегистрирован для: RSA-OAEP (RsaOaepParams), AES-CTR (AesCtrParams), AES-CBC (AesCbcParams), AES-GCM (AesGcmParams)

Шифрует data ключом key по алгоритму algorithm. Ровно три позиционных аргумента, ни extractable, ни keyUsages здесь нет.⚠️ AES-KW НЕ регистрирует encrypt/decrypt — только wrapKey/unwrapKey, вызов encrypt с ним даст NotSupportedError. Ключ обязан содержать usage “encrypt”, иначе InvalidAccessError. Для AES-GCM iv обязателен и повторно использовать его с тем же ключом нельзя.

SubtleCrypto.exportKey()

Promise<(ArrayBuffer or JsonWebKey)> exportKey(KeyFormat format, CryptoKey key);знач.: format: “raw” | “spki” | “pkcs8” | “jwk”

Экспортирует ключ. Возвращаемый тип — объединение: JsonWebKey для “jwk”, ArrayBuffer для остальных форматов.⚠️ Два независимых условия отказа. Первое: если [[extractable]] равен false — InvalidAccessError (это невозможно обойти, ключ сгенерирован неизвлекаемым навсегда). Второе: если алгоритм не поддерживает запрошенный format — NotSupportedError. Набор форматов задаёт каждый алгоритм отдельно, а не сам метод.

SubtleCrypto.generateKey()

Promise<(CryptoKey or CryptoKeyPair)> generateKey(AlgorithmIdentifier algorithm, boolean extractable, sequence keyUsages);

Генерирует ключ или пару ключей. extractable — ВТОРОЙ позиционный аргумент, keyUsages — ТРЕТИЙ. Ни тот, ни другой не являются членами словаря алгоритма.⚠️ Классическая ошибка — писать {name:“AES-GCM”, length:256, extractable:true, keyUsages:[…]}: лишние члены словаря молча игнорируются при нормализации, ключ получается неизвлекаемым и без usages. Возвращаемый тип — объединение: CryptoKey для AES/HMAC, CryptoKeyPair для RSA/EC/Ed25519/X25519. Пустой usages у secret/private ключа → SyntaxError.

SubtleCrypto.importKey()

Promise importKey(KeyFormat format, (BufferSource or JsonWebKey) keyData, AlgorithmIdentifier algorithm, boolean extractable, sequence keyUsages);знач.: format: “raw” | “spki” | “pkcs8” | “jwk”

Импортирует внешний материал ключа. extractable — ЧЕТВЁРТЫЙ позиционный аргумент, keyUsages — ПЯТЫЙ.⚠️ Тип keyData зависит от format: для “jwk” это объект JsonWebKey, для остальных — BufferSource; передача ArrayBuffer при format “jwk” даёт TypeError на уровне WebIDL. Для PBKDF2 и HKDF ограничения жёстче: format обязан быть “raw”, а extractable обязан быть false, иначе SyntaxError.

SubtleCrypto.sign()

Promise sign(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);знач.: Зарегистрирован для: RSASSA-PKCS1-v1_5 (None), RSA-PSS (RsaPssParams), ECDSA (EcdsaParams), HMAC (None), Ed25519 (None)

Подписывает data приватным (или секретным для HMAC) ключом.⚠️ У трёх из пяти алгоритмов Parameters = None, то есть достаточно строки “HMAC”/“Ed25519”/“RSASSA-PKCS1-v1_5”. Но RSA-PSS требует RsaPssParams с обязательным saltLength, а ECDSA — EcdsaParams с обязательным hash: строка вместо словаря там приведёт к ошибке нормализации.

SubtleCrypto.unwrapKey()

Promise unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence keyUsages);

Самый длинный метод API: семь позиционных аргументов. extractable — ШЕСТОЙ, keyUsages — СЕДЬМОЙ; оба описывают распакованный ключ, а не unwrappingKey.⚠️ Два AlgorithmIdentifier идут подряд: unwrapAlgorithm — чем расшифровывать обёртку, unwrappedKeyAlgorithm — чем будет являться полученный ключ. Именно здесь чаще всего и путают позиции, а поскольку оба аргумента одного IDL-типа, ошибка проявится как невнятный OperationError/NotSupportedError. Это же единственный способ ввести ключ в систему как неизвлекаемый (extractable = false), не раскрывая его в JS.

SubtleCrypto.verify()

Promise verify(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource signature, BufferSource data);

Проверяет подпись. Единственный метод с ЧЕТЫРЬМЯ аргументами и единственный, чей Promise разрешается в boolean.⚠️ Порядок аргументов: signature ИДЁТ ПЕРЕД data. Перепутанные местами буферы не бросают исключение — Promise просто разрешается в false, и ошибка выглядит как «неверная подпись». Promise отклоняется только при ошибках API, а не при невалидной подписи.

SubtleCrypto.wrapKey()

Promise wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm);

Экспортирует key в формате format и тут же шифрует его на wrappingKey. Первым аргументом идёт KeyFormat, а не алгоритм.⚠️ Подряд идут два CryptoKey — оборачиваемый и оборачивающий; перепутать их легко, а диагностика будет невнятной. wrappingKey обязан иметь usage “wrapKey”. И главное: обернуть можно только extractable-ключ, потому что wrapKey фактически является экспортом.

X25519

Registration §26.2: deriveBits | EcdhKeyDeriveParams | byte sequence; generateKey | None | CryptoKeyPair; importKey | None | CryptoKey; exportKey | None | objectзнач.: Зарегистрированы четыре операции: deriveBits, generateKey, importKey, exportKey

Согласование ключей по RFC 7748. Парный к Ed25519 алгоритм: тот подписывает, этот выводит общий секрет.⚠️ Операции sign/verify для X25519 НЕ зарегистрированы — путать его с Ed25519 нельзя. deriveKey тоже не зарегистрирован напрямую, он работает через deriveBits. Спецификация требует constant-time проверки нулевого секрета: «If secret is the all-zero value, then throw a OperationError».

exportKey("raw", key) — ограничение по алгоритмам

exportKey(“raw”, key) -> зависит от алгоритма; иначе NotSupportedErrorзнач.: Спецификация (шаги Export Key): “raw” поддерживают ECDSA, ECDH, Ed25519, X25519, AES-CTR, AES-CBC, AES-GCM, AES-KW, HMAC; НЕ поддерживают RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP (у них только “spki”, “pkcs8”, “jwk”)

Утверждение «raw работает для любого ключа» неверно. Шаги Export Key определены отдельно для каждого алгоритма, и ветка для неподдерживаемого формата заканчивается словами «Otherwise: throw a NotSupportedError».⚠️ RSA-ключи в “raw” не экспортируются вообще: правильные форматы — “spki” для публичного и “pkcs8” для приватного. MDN формулирует ограничение ещё уже, чем Level 2: «You can use this format to import or export AES or HMAC secret keys, or Elliptic Curve public keys (ECDSA or ECDH)» — про Ed25519/X25519 там не сказано, хотя Level 2 их “raw”-ветку определяет. Приватный EC-ключ через “raw” тоже не выгружается.

extractable / keyUsages — сводка позиций

generateKey(alg, extractable, keyUsages) | importKey(format, keyData, alg, extractable, keyUsages) | deriveKey(alg, baseKey, derivedKeyType, extractable, keyUsages) | unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlg, unwrappedKeyAlg, extractable, keyUsages)знач.: Ровно четыре метода: generateKey (позиции 2, 3), importKey (4, 5), deriveKey (4, 5), unwrapKey (6, 7)

extractable и keyUsages — всегда ПОЗИЦИОННЫЕ аргументы методов и всегда два последних. Они не являются и никогда не являлись членами словарей алгоритмов (Algorithm, RsaHashedKeyGenParams, EcKeyGenParams и прочих).⚠️ Их принимают только те четыре метода, которые СОЗДАЮТ новый CryptoKey. У encrypt/decrypt/sign/verify/digest/deriveBits/exportKey/wrapKey их нет вовсе. Если положить extractable внутрь объекта алгоритма, WebIDL-нормализация отбросит неизвестный член без ошибки, а недостающий boolean-аргумент приведёт к extractable = false.

Документ-первоисточник

https://www.w3.org/TR/WebCryptoAPI/ -> 301 -> https://www.w3.org/TR/webcrypto/

URL из задания отдаёт 301 на /TR/webcrypto/, где сейчас лежит документ «Web Cryptography Level 2» (W3C First Public Working Draft 22 April 2025, редактор Daniel Huigens). Именно его IDL приведён во всех строках ниже.⚠️ Это НЕ Recommendation 2017 года. Level 1 (REC 26 January 2017, /TR/2017/REC-WebCryptoAPI-20170126/) не содержит ни Ed25519, ни X25519, ни randomUUID, и в нём deriveBits(length) — обязательный ненулевой аргумент. Ссылаясь на «WebCryptoAPI», уточняйте уровень: /TR/webcrypto/ теперь показывает Level 2.

Версии и подводные камни:

1.2. Главная опасность этой кнопки

Она не в криптографии. Она в отличительном имени.

Движок сопоставляет предъявленный сертификат с учётной записью по DN. Значит, если позволить заявителю самому назвать имя, он выпишет себе сертификат с DN администратора — и войдёт администратором. Совершенно легальным, настоящим, подписанным вашим же УЦ сертификатом.

Поэтому имя в форме показано, но недоступно для правки, а сервер не верит тому, что пришло в запросе. Субъект вычисляется из уже установленной личности и передаётся openssl ключом -subj, который перекрывает subject запроса целиком. Что бы браузер ни положил в CSR, до сертификата доедет только открытый ключ.

Тест на это выглядит буквально так: отправить запрос с CN=Совершенно Другой Человек и убедиться, что в выпущенном сертификате стоит имя учётной записи.

Здесь всплывает тонкость, которую легко пропустить. Предварительная проверка политики у подписанта разбирала subject из запроса — а в сертификат-то попадёт переопределённый. Пока имена совпадали, расхождение было незаметно; стоило начать переопределять — и выпуск стал падать на emailAddress: правило supplied, хотя почта в переопределённом имени была. Проверять надо то, что попадёт в сертификат:

// Если субъект переопределяется ключом -subj, в сертификат попадёт именно он —
// значит и политику проверяем против него, а не против того, что написал заявитель.
subject := csr.Subject
if strings.TrimSpace(p.Subject) != "" {
    subject = SubjectFromString(p.Subject)
}

Остальные ограничения кнопки — прямым текстом из разбора атак:

  • Только для вошедших. Аноним сертификат себе не выпустит: сначала вход по паролю, потом переход на сертификат.

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

  • Проверка стойкости ключа на сервере. Браузерная проверка не гарантирует ничего: запрос может прийти откуда угодно.

  • Ограничение частоты. Пять сертификатов в час на учётную запись: иначе кнопка превращается в способ раздуть index.txt и CRL до состояния, когда проверка отзыва перестаёт работать у всех.

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

И ещё одна проверка, не столько про безопасность, сколько про уважение к человеку. Политика УЦ из Части 1 требует emailAddress = supplied, поэтому у учётной записи без почты выпуск обречён — но узнать об этом можно двумя способами. Либо после форка openssl, сообщением «The emailAddress field needed to be supplied and was missing», из которого непонятно ни что сделать, ни кому. Либо заранее, прямо на странице: «в вашей учётной записи не указана почта, попросите администратора её заполнить». Движок выбирает второе — политика читается из конфига до того, как что-либо запускается.


1.3. То же самое в терминале

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

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out my.key.pem
openssl req -new -key my.key.pem -out my.csr.pem -sha256 -utf8 \
  -subj "/C=RU/O=CertService/CN=Фамилия Имя/emailAddress=you@certservice.info"

Расширенный справочник: openssl genpkey

Показать таблицу — 64 параметра (openssl genpkey)

Параметр

Синтаксис / значения

По умолч.

Описание и нюансы

-algorithm

-algorithm algзнач.: RSA, RSA-PSS, EC, X25519, X448, ED25519, ED448, ML-DSA, ML-KEM, DSA, DH, DHX и др.

Задаёт алгоритм открытого ключа. Должна идти ДО любых -pkeyopt. Взаимоисключающая с -paramfile.⚠️ Порядок аргументов значим — это не GNU-style парсер: -pkeyopt rsa_keygen_bits:4096 -algorithm RSA молча не сработает или упадёт, потому что на момент разбора -pkeyopt алгоритм ещё не известен. Всегда ставьте -algorithm первым.

-cipher

-cipher (имя шифра как опция, например -aes-128-cbc или -aes-256-cbc)знач.: любой шифр из openssl list -cipher-algorithms

без шифрования

Шифрует закрытый ключ указанным шифром. Имя шифра само является опцией с дефисом впереди, а не аргументом отдельного флага.⚠️ Синтаксис нетипичный: пишется -aes-256-cbc, а не -cipher aes-256-cbc — вторая форма приведёт к ошибке разбора. До OpenSSL 4.0 шифрование применялось только к PEM: с -outform DER ключ выходил открытым, несмотря на заданный шифр и пароль.

-config

-config configfileзнач.: путь к файлу конфигурации

Указывает файл конфигурации OpenSSL. Подробности — в разделе «Configuration Option» страницы openssl(1).⚠️ Конфиг может активировать провайдеры и алгоритмы, поэтому результат genpkey зависит от него даже там, где вы этого не ждёте. Если сборочный сервер и прод отличаются openssl.cnf, один и тот же вызов может дать ключ, сгенерированный разными реализациями.

-encopt

-encopt opt:valueзнач.: зависит от кодировщика

Устанавливает параметр opt кодировщика закрытого ключа в значение value. Может указываться несколько раз. Применяется в первую очередь к кодированию ML-DSA и ML-KEM в PKCS#8.⚠️ Не путайте с -pkeyopt: -pkeyopt влияет на генерацию ключа, -encopt — только на то, как готовый ключ сериализуется. Опция появилась в OpenSSL 4.0, поэтому скрипты с ней не запустятся на 3.x-сборках, которые сейчас стоят в большинстве LTS-дистрибутивов.

-engine

-engine

Опция выбора ENGINE, удалённая из команды в OpenSSL 4.0. Механизм ENGINE вытеснен архитектурой провайдеров.⚠️ Старые скрипты для HSM/PKCS#11 с -engine pkcs11 перестанут работать при обновлении до OpenSSL 4.0 и потребуют перевода на -provider. Планируйте миграцию заранее — это не предупреждение о депрекации, а полное удаление.

-genparam

-genparam

Генерирует набор параметров вместо закрытого ключа. Должна идти ДО любых -algorithm, -paramfile или -pkeyopt.⚠️ Двойное ограничение порядка: -genparam раньше -algorithm, а -algorithm раньше -pkeyopt. И помните, что при -genparam флаг -outform игнорируется — параметры всегда выйдут в PEM.

-help

-help

Печатает справку по использованию команды. В связке с -algorithm выводит список -pkeyopt, поддерживаемых конкретным алгоритмом.⚠️ Это единственный документированный способ узнать реальный список -pkeyopt для вашей сборки: страница man описывает алгоритмы default-провайдера, а FIPS-провайдер или сторонний провайдер могут принимать другой набор. Перед копированием опции в боевой конфиг выполните openssl genpkey -algorithm XXX -help на целевой машине.

-out

-out filenameзнач.: путь к файлу

стандартный вывод

Записывает закрытый ключ в указанный файл. Если аргумент не задан, ключ уходит в stdout.⚠️ openssl создаёт файл с правами по umask процесса — никакого 0600 автоматически не выставляется. В скриптах ставьте umask 077 перед вызовом либо chmod 600 сразу после; иначе свежесгенерированный ключ окажется world-readable. Вывод в stdout плюс редирект — та же проблема плюс риск попадания ключа в логи pipeline.

-outform

-outform DER|PEMзнач.: DER, PEM

PEM

Формат вывода ключа. По умолчанию PEM. При использовании -genparam опция игнорируется.⚠️ Молчаливое игнорирование при -genparam — классическая ловушка: вы просите DER-параметры, получаете PEM и без единого предупреждения. Кроме того, шифрование DER-ключа паролем заработало только в OpenSSL 4.0 — в более ранних версиях -pass и -aes-256-cbc с -outform DER просто ничего не делали, отдавая незашифрованный ключ.

-outpubkey

-outpubkey filenameзнач.: путь к файлу

открытый ключ не выводится

Дополнительно записывает открытый ключ в указанный файл. Если опция не задана, открытый ключ не выводится вовсе.⚠️ Экономит отдельный вызов openssl pkey -pubout, но многие готовые скрипты и Ansible-роли написаны под старый двухшаговый вариант и про эту опцию не знают. Формат открытого ключа подчиняется тому же -outform, что и закрытый — раздельно их задать нельзя.

-paramfile

-paramfile filenameзнач.: путь к файлу параметров

Подставляет заранее сгенерированные параметры из файла — некоторые алгоритмы строят закрытый ключ на их основе. Должна идти до любых -pkeyopt. Взаимоисключающая с -algorithm.⚠️ Алгоритм берётся из самого файла параметров, поэтому попытка добавить -algorithm рядом — ошибка, а не уточнение. Для DH именованную группу проще задать через -pkeyopt group:name, тогда -paramfile вообще не нужен.

-pass

-pass argзнач.: pass:, env:, file:, fd:, stdin (см. openssl-passphrase-options(1))

Источник пароля для выходного файла. Формат arg описан в openssl-passphrase-options(1).⚠️ pass:hello из примеров документации годится только для демо: пароль виден в ps и попадает в history. В продакшене используйте file: или fd:, но не env: — переменные окружения читаются через /proc//environ. Сам по себе -pass не шифрует: без указания шифра (-aes-256-cbc) он не даст защищённого ключа.

-pkeyopt

-pkeyopt opt:valueзнач.: зависит от алгоритма и реализации

Устанавливает опцию opt алгоритма открытого ключа в значение value. Опцию можно указывать многократно для разных параметров.⚠️ Разделитель — двоеточие, а не знак равенства: rsa_keygen_bits=4096 не пройдёт. Неизвестное имя опции даёт ошибку, но неверное ЗНАЧЕНИЕ иногда молча отбрасывается провайдером — обязательно проверяйте результат через openssl pkey -in key.pem -text -noout.

-propquery

-propquery propqзнач.: строка запроса свойств, например “provider=fips”

Строка запроса свойств, по которой выбирается реализация алгоритма среди загруженных провайдеров. Подробности — в property(7).⚠️ Разница между «?» и «=» в запросе принципиальна: ?provider=fips — предпочтение с откатом на другой провайдер, provider=fips — жёсткое требование. Первый вариант тихо сгенерирует ключ не-FIPS реализацией, если FIPS-провайдер не загружен.

-provider

-provider nameзнач.: имя провайдера (default, fips, legacy, base и др.)

Подгружает указанный провайдер. Подробности — в разделе «Provider Options» страницы openssl(1), а также в provider(7).⚠️ Явное указание -provider fips ОТКЛЮЧАЕТ default-провайдер, если тот не указан рядом — часть алгоритмов и кодировщиков внезапно перестанет находиться. На практике почти всегда нужна пара -provider fips -provider base.

-provider-path

-provider-path pathзнач.: путь к каталогу

Каталог, в котором искать модули провайдеров. Подробности — в разделе «Provider Options» страницы openssl(1).⚠️ Опция должна предшествовать -provider в командной строке, иначе провайдер уже будет искаться по старому пути. Задание пути не отменяет требования корректной подписи/самопроверки для FIPS-модуля.

-provparam

-provparam [name:]key=valueзнач.: зависит от провайдера

Передаёт параметр конфигурации провайдеру. Необязательный префикс name: адресует параметр конкретному провайдеру.⚠️ Здесь синтаксис key=value через знак равенства, а не opt:value через двоеточие, как у -pkeyopt — разные разделители в соседних опциях одной команды. Двоеточие тут используется только как разделитель имени провайдера.

-quiet

-quiet

Подавляет вывод «точек статуса» во время генерации ключа.⚠️ -quiet глушит только индикатор прогресса, но не ошибки и не -text. Не рассчитывайте на него как на способ сделать команду полностью беззвучной.

-rand

-rand filesзнач.: список файлов (разделитель зависит от ОС)

Задаёт файлы-источники энтропии для инициализации ГПСЧ. Подробности — в разделе «Random State Options» страницы openssl(1).⚠️ На современных Linux/BSD опция практически бессмысленна: OpenSSL 3.x берёт энтропию из getrandom(2). Указание «своего» файла энтропии не улучшает, а в худшем случае маскирует реальную проблему ГПСЧ во встраиваемых системах.

-text

-text

Дополнительно печатает незашифрованное текстовое представление закрытого и открытого ключей и параметров рядом со структурой PEM или DER.⚠️ Ключевое слово — «unencrypted»: -text выводит компоненты ЗАКРЫТОГО ключа открытым текстом, даже если сам файл вы шифруете паролем. В CI-логах это равносильно утечке ключа. Для проверки уже созданного ключа безопаснее openssl pkey -pubout -text.

-verbose

-verbose

Печатает «точки статуса» в процессе генерации ключа, показывая прогресс подбора простых чисел.⚠️ Точки идут в stderr, поэтому «чистый» вывод в файл они не портят, но замусоривают журналы CI. В неинтерактивных пайплайнах явно ставьте -quiet, а не полагайтесь на умолчание.

-writerand

-writerand fileзнач.: путь к файлу

Записывает состояние ГПСЧ в указанный файл после работы команды. Подробности — в разделе «Random State Options» страницы openssl(1).⚠️ Файл состояния ГПСЧ — секрет: доступ к нему на предсказуемой системе помогает атаковать последующие генерации. Не кладите его в общий каталог и не коммитьте.

bits

“bits” (OSSL_PKEY_PARAM_RSA_BITS)знач.: unsigned integer

Криптографическая длина RSA-криптосистемы в битах. Это имя параметра на уровне EVP/провайдера; в командной строке genpkey ему соответствует rsa_keygen_bits.⚠️ Не подставляйте это имя в -pkeyopt по аналогии — на странице openssl-genpkey(1) документировано именно rsa_keygen_bits. Имена уровня провайдера пригодятся, если вы вызываете EVP_PKEY_CTX_set_params() из кода или используете -provparam.

dh_paramgen_type

-pkeyopt dh_paramgen_type:valueзнач.: 0, 1, 2, 3 (соответствуют type: “generator”, “fips186_2”, “fips186_4”, “group”)

Числовая форма выбора типа генерируемых DH-параметров. Значения 0, 1, 2, 3 соответствуют значениям опции type — “generator”, “fips186_2”, “fips186_4” и “group”.⚠️ Числовой порядок НЕ совпадает с порядком перечисления строковых типов на странице: 1 — это fips186_2, а 2 — fips186_4, легко перепутать и получить генерацию по устаревшему стандарту. Если не обязывает старый скрипт, пишите строковую форму type: — она самодокументирована.

dh_rfc5114

-pkeyopt dh_rfc5114:numзнач.: 1, 2 или 3 (эквивалент group:“dh_1024_160”, “dh_2048_224”, “dh_2048_256”)

Использует параметры RFC5114 вместо генерации новых. Значения 1, 2, 3 соответствуют группам dh_1024_160, dh_2048_224 и dh_2048_256. При задании этой опции все остальные игнорируются.⚠️ Голая цифра ничего не говорит о размере группы: dh_rfc5114:1 — это всего 1024 бита, чего сегодня недостаточно. Читаемее и безопаснее писать group:dh_2048_256. Группы RFC 5114 при этом критикуются за неизвестное происхождение параметров — если нет требования интеропа, берите ffdhe* из RFC 7919 с алгоритмом DH.

dhkem-ikm

“dhkem-ikm” (OSSL_PKEY_PARAM_DHKEM_IKM) знач.: октетная строка

DHKEM требует генерации пары ключей на основе входного ключевого материала (seed); этот параметр его и задаёт.⚠️ Как и любое семя, определяющее ключ детерминированно, IKM эквивалентен закрытому ключу по чувствительности. Используется в HPKE (RFC 9180), а не в обычной генерации EC-ключа.

digest (DH)

-pkeyopt digest:digestзнач.: sha1, sha224, sha256

хеш подбирается под длину q

Хеш, применяемый при генерации параметров. Должен быть sha1, sha224 или sha256. Если задан, длина qbits подстраивается под размер вывода хеша, а сам qbits игнорируется. Используется только генерацией типов “fips186_4” и “fips186_2”.⚠️ В примерах документации значение пишется как SHA256 (верхним регистром), а в описании допустимых значений — sha256 (нижним); имена хешей в OpenSSL регистронезависимы, но при переносе в конфиги придерживайтесь одного стиля. С обычным DH (тип generator/group) опция игнорируется.

dsa_paramgen_bits

-pkeyopt dsa_paramgen_bits:numbitsзнач.: целое число бит

2048

Число бит генерируемого простого числа p. Если не указано, используется 2048.⚠️ У DSA параметры и ключ создаются в два приёма: сначала -genparam в файл, потом -paramfile для самого ключа. Указывать эту опцию без -genparam бессмысленно. В примерах документации для DSA используется альтернативное имя pbits — оба написания встречаются в реальных скриптах.

dsa_paramgen_md / digest

-pkeyopt dsa_paramgen_md:digest или -pkeyopt digest:digestзнач.: sha1, sha224, sha256

хеш подбирается под длину q (sha1/sha224/sha256)

Хеш, применяемый при генерации параметров. Должен быть sha1, sha224 или sha256. Если задан, длина q подстраивается под размер вывода хеша, а dsa_paramgen_q_bits игнорируется.⚠️ Список допустимых значений закрыт тремя алгоритмами — SHA-384/SHA-512 здесь недоступны, это ограничение FIPS 186-4. sha1 оставлен только ради совместимости со старыми 1024/160-битными параметрами; для новых генераций берите sha256.

dsa_paramgen_q_bits / qbits

-pkeyopt dsa_paramgen_q_bits:numbits или -pkeyopt qbits:numbitsзнач.: 160, 224 или 256

224

Число бит параметра q. Должно быть одним из 160, 224 или 256. Если не указано, используется 224.⚠️ Задание digest переопределяет этот параметр: при установленном dsa_paramgen_md длина q подгоняется под размер вывода хеша, а dsa_paramgen_q_bits ИГНОРИРУЕТСЯ — без всякого предупреждения. Указывайте либо одно, либо другое, но следите, чтобы они не противоречили друг другу.

e

“e” (OSSL_PKEY_PARAM_RSA_E)знач.: любое нечётное число не меньше 65537

65537

Значение открытой экспоненты RSA «e». Может быть любым нечётным числом, большим или равным 65537. Значение по умолчанию 65537. CLI-эквивалент — rsa_keygen_pubexp.⚠️ Важное расхождение с CLI: на уровне провайдера действует ограничение «нечётное и >= 65537», то есть маленькие экспоненты вроде 3 здесь запрещены, тогда как пример из openssl-genpkey(1) показывает rsa_keygen_pubexp:3. В FIPS-режиме короткая экспонента будет отвергнута — ещё одна причина никогда не отходить от 65537.

ec_param_enc

-pkeyopt ec_param_enc:encodingзнач.: named_curve, explicit

named_curve

Способ кодирования параметров кривой. Значение должно быть named_curve или explicit. Значение по умолчанию — named_curve.⚠️ Практический смысл такой: named_curve записывает в ключ и сертификат только OID кривой, explicit — все её коэффициенты целиком. Explicit-кодирование ломает совместимость на каждом шагу: RFC 5480 требует named_curve для PKIX, TLS 1.3 (RFC 8446) поддерживает только именованные группы, Go crypto/x509, многие HSM и мобильные стеки такой ключ просто не примут, а сертификат распухает на сотни байт. Кроме того, explicit-параметры исторически были вектором атак с подменой кривой на невалидную. Умолчание named_curve верное — менять его нужно только для унаследованных систем, которые иначе не работают; в скриптах его часто пишут явно как страховку от чужой конфигурации.

ec_paramgen_curve

-pkeyopt ec_paramgen_curve:curveзнач.: имя кривой, например P-256, P-384, secp384r1

Задаёт эллиптическую кривую. OpenSSL понимает имена NIST-кривых вроде “P-256”. Список доступных кривых даёт openssl ecparam -list_curves.⚠️ Обязательный параметр: без него -algorithm EC завершится ошибкой, в отличие от RSA, где есть умолчание 2048. Одна и та же кривая имеет несколько имён (P-256 = prime256v1 = secp256r1) — в примерах документации используются оба стиля, и оба принимаются, но в конфигах команды лучше держать единое написание. Для X25519/ED25519 эта опция не применяется — там кривая заложена в само имя алгоритма.

encoding

“encoding” (OSSL_PKEY_PARAM_EC_ENCODING) знач.: explicit, named_curve

named_curve

Задаёт формат сериализации параметров EC-группы. Допустимые значения — “explicit” или “named_curve”, значение по умолчанию “named_curve”. CLI-эквивалент — ec_param_enc.⚠️ Умолчание named_curve подтверждено на обеих страницах — и в openssl-genpkey(1), и в EVP_PKEY-EC(7); это не случайность реализации, а зафиксированное поведение. Менять его на explicit стоит только под конкретное унаследованное требование.

genpkey vs genrsa / ecparam

openssl genpkey -algorithm RSA -out key.pem (вместо openssl genrsa)openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve -out eckey.pem (вместо openssl ecparam -genkey)

genpkey — единая команда генерации ключей для всех алгоритмов. Документация прямо рекомендует её вместо алгоритмо-специфичных утилит, поскольку так доступны дополнительные опции алгоритмов и алгоритмы, предоставляемые провайдерами.⚠️ Практическая разница в трёх вещах. Первое — охват: genrsa умеет только RSA, ecparam только EC, а genpkey закрывает и RSA-PSS, X25519/Ed25519, и постквантовые ML-DSA/ML-KEM (с OpenSSL 3.5), которых у старых утилит нет и не будет. Второе — формат вывода: genpkey пишет PKCS#8 (BEGIN PRIVATE KEY), тогда как genrsa и ecparam -genkey исторически дают PKCS#1/SEC1 (BEGIN RSA PRIVATE KEY / BEGIN EC PRIVATE KEY) — если ваш парсер или приложение ждёт конкретный заголовок, подмена команды сломает загрузку ключа. Третье — ecparam -genkey за один вызов выдаёт и параметры, и ключ в один файл, а с genpkey EC-ключ генерируется напрямую, без промежуточного файла параметров: именно поэтому старые инструкции с двухшаговой схемой ecparam + genkey сегодня избыточны.

gindex (DH)

-pkeyopt gindex:indexзнач.: 0…255 (используется только младший байт)

-1

Индекс для канонической генерации и верификации генератора g; положительное значение 0…255 включает этот режим. Тот же индекс нужно переиспользовать при валидации ключа. Если не задан, g не поддаётся проверке; умолчание -1. Используется только типами “fips186_4” и “fips186_2”.⚠️ Как и в DSA, gindex вместе с seed печатается только при -text и в PEM не сохраняется — без ручного сохранения возможность верифицировать g теряется навсегда. Значения больше 255 обрезаются до младшего байта, а не отвергаются.

gindex (DSA)

-pkeyopt gindex:indexзнач.: 0…255 (положительное значение включает режим; используется только младший байт)

-1

Индекс для канонической генерации и верификации генератора g. Положительное значение из диапазона 0…255 включает этот режим; тот же индекс потребуется при валидации ключа для проверки g. Если не задан, g не поддаётся проверке; значение по умолчанию -1.⚠️ Индекс и seed выводятся на экран при -text, но НЕ сохраняются в выходном PEM-файле — документация предупреждает об этом прямо. Если они нужны для последующей валидации, сохраняйте их отдельно в момент генерации, второй раз получить их будет неоткуда. Значения больше 255 не отвергаются, а обрезаются до младшего байта.

group (DH keygen)

-pkeyopt group:nameзнач.: для DH: ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192, modp_1536, modp_2048, modp_3072, modp_4096, modp_6144, modp_8192; для DHX: dh_1024_160, dh_2048_224, dh_2048_256

Выбирает именованную DH-группу непосредственно при генерации ключа. При использовании этой опции файл параметров (-paramfile) не требуется.⚠️ Это самый быстрый и правильный путь для DH: генерация собственных параметров занимает минуты и не даёт выигрыша в безопасности, тогда как ffdhe-группы из RFC 7919 проверены и совместимы. Не берите modp_1536 и dh_1024_160 — 1024/1536-битные группы уязвимы к прекомпиляционным атакам класса Logjam.

group (EC)

“group” (OSSL_PKEY_PARAM_GROUP_NAME) знач.: имя кривой

Имя кривой на уровне параметров провайдера. В командной строке genpkey тому же смыслу соответствует ec_paramgen_curve.⚠️ То же имя group в genpkey означает совсем другое — именованную DH-группу. Один и тот же идентификатор в разных слоях API относится к разным сущностям; ориентируйтесь на страницу того интерфейса, который используете.

group / dh_param

-pkeyopt group:name или -pkeyopt dh_param:nameзнач.: DH: “ffdhe2048”, “ffdhe3072”, “ffdhe4096”, “ffdhe6144”, “ffdhe8192”, “modp_1536”, “modp_2048”, “modp_3072”, “modp_4096”, “modp_6144”, “modp_8192”; DHX (имена RFC5114): “dh_1024_160”, “dh_2048_224”, “dh_2048_256”

по умолчанию именованная группа не используется

Выбирает именованную DH-группу с константными значениями параметров. Если значение задано, ВСЕ остальные опции игнорируются. Документация рекомендует использовать group вместо опций type для большинства сценариев.⚠️ «All other options will be ignored» — буквально: pbits, qbits, generator рядом с group не выдадут ошибку, а просто не сработают. Именованная группа не подставляется по умолчанию — если не указать ни одной опции генерации параметров, group не применится. Наборы значений для DH и DHX не пересекаются: ffdhe* только для DH, dh_* только для DHX.

hexseed (DH)

-pkeyopt hexseed:seedзнач.: шестнадцатеричная строка

случайное семя генерируется внутренне

Данные семени вместо внутренне генерируемого случайного значения. Предназначено только для тестирования. Используется только типами “fips186_4” и “fips186_2”.⚠️ Одно и то же имя hexseed на этой странице означает совершенно разные вещи в разных контекстах: у ML-DSA/ML-KEM это семя САМОГО КЛЮЧА (утечка = компрометация ключа), здесь — семя генерации ПАРАМЕТРОВ (публичной величины). Не переносите практики между этими случаями механически.

hexseed (DSA)

-pkeyopt hexseed:seedзнач.: шестнадцатеричная строка

случайное семя генерируется внутренне

Данные семени вместо генерируемого внутренне случайного значения. Документация прямо указывает, что использовать это следует только в целях тестирования.⚠️ Поведение бинарное: либо получите фиксированные параметры, либо команда упадёт, если из семени не выводятся валидные простые числа. Промежуточного «почти сработало» нет, поэтому автоматические ретраи со случайным seed бессмысленны.

hexseed (ML-DSA)

-pkeyopt hexseed:seedзнач.: 32 байта = 64 шестнадцатеричные цифры

случайное значение

Задаёт необязательное семя ML-DSA в шестнадцатеричном виде. Ключ, порождённый из явного семени, полностью им определяется.⚠️ Полная детерминированность означает, что семя — это и есть закрытый ключ: кто увидит его в командной строке (а её видно в ps и в истории оболочки), получит ключ целиком. В документации об этом сказано прямо. Используйте только для векторов тестирования и воспроизводимых прогонов, никогда для боевых ключей.

hexseed (ML-KEM)

-pkeyopt hexseed:seedзнач.: 64 байта = 128 шестнадцатеричных цифр

случайное значение

Задаёт необязательное семя ML-KEM в шестнадцатеричном виде. Ключ, порождённый из явного семени, полностью им определяется.⚠️ Обратите внимание на разную длину: у ML-DSA семя 32 байта (64 hex-символа), у ML-KEM — 64 байта (128 hex-символов). Перепутанная длина даст ошибку, а не молчаливое дополнение нулями. Те же риски раскрытия через командную строку, что и у ML-DSA.

include-public

“include-public” (OSSL_PKEY_PARAM_EC_INCLUDE_PUBLIC) знач.: 0 или 1

1

Значение 0 исключает открытый ключ при кодировании закрытого. По умолчанию 1 — открытый ключ включается.⚠️ Ключ без встроенной открытой части формально валиден по RFC 5915, но часть инструментов не сможет из него получить публичный ключ без пересчёта. Не выключайте, если нет требования минимизировать размер.

pbits / dh_paramgen_prime_len

-pkeyopt pbits:numbits или -pkeyopt dh_paramgen_prime_len:numbitsзнач.: целое число бит

2048

Число бит простого параметра p. По умолчанию 2048.⚠️ Два имени для одной опции — короткое pbits (новое) и dh_paramgen_prime_len (старое), в примерах документации встречаются оба. Самостоятельная генерация 2048-битного p занимает минуты и не даёт преимущества перед ffdhe2048: если нет требования уникальных параметров, используйте group.

point-format

“point-format” (OSSL_PKEY_PARAM_EC_POINT_CONVERSION_FORMAT) знач.: uncompressed, compressed

uncompressed

Устанавливает или возвращает point_conversion_form для ключа. Допустимые значения — “uncompressed” или “compressed”, по умолчанию “uncompressed”.⚠️ Сжатая форма экономит около половины байт открытой точки, но не все старые стеки и HSM её принимают. В openssl-genpkey(1) соответствующей -pkeyopt-опции нет — задать формат точки при генерации из командной строки напрямую нельзя.

primes

“primes” (OSSL_PKEY_PARAM_RSA_PRIMES)знач.: unsigned integer (до 10; провайдеры могут поддерживать только 2)

2

Количество простых множителей генерируемого RSA-ключа, по умолчанию 2. CLI-эквивалент в genpkey — rsa_keygen_primes.⚠️ Документация прямо оговаривает, что при заявленном максимуме в 10 множителей конкретный провайдер может поддерживать только 2 — запрос большего числа упрётся в реализацию, а не в стандарт.

properties (DH)

-pkeyopt properties:queryзнач.: строка запроса свойств

Строка запроса свойств при получении хеш-функции от провайдера. Используется только генерацией типов “fips186_4” и “fips186_2”.⚠️ Вне типов fips186_* опция не даёт эффекта и об этом не сообщает. Если вы задали её при type:generator и ждали переключения на FIPS-реализацию хеша — этого не произошло.

properties (DSA)

-pkeyopt properties:queryзнач.: строка запроса свойств

Строка запроса свойств, используемая при получении хеш-функции от провайдера.⚠️ Не путайте с общей опцией -propquery: эта строка влияет только на выбор реализации хеша внутри генерации параметров DSA, а не на выбор реализации самого алгоритма DSA.

qbits / dh_paramgen_subprime_len

-pkeyopt qbits:numbits или -pkeyopt dh_paramgen_subprime_len:numbitsзнач.: целое число бит

224

Число бит подпростого параметра q. По умолчанию 224. Имеет значение только совместно с опцией dh_paramgen_type при генерации параметров DHX.⚠️ Для обычного DH этот параметр не работает вовсе — он относится к X9.42/DHX. И, как и у DSA, заданный digest переопределяет qbits: длина q подгонится под вывод хеша, а qbits будет проигнорирован.

rsa-a

“rsa-a” (OSSL_PKEY_PARAM_RSA_A)знач.: 1, 3, 5 или 7

Установка значения в 1, 3, 5 или 7 накладывает дополнительное ограничение: вычисленное простое p должно быть сравнимо с этим значением по модулю 8.⚠️ Узкоспециальный параметр для соответствия отдельным профилям генерации, а не для общего применения. Дополнительное ограничение сужает пространство поиска и удлиняет генерацию.

rsa-b

“rsa-b” (OSSL_PKEY_PARAM_RSA_B)знач.: 1, 3, 5 или 7

Установка значения в 1, 3, 5 или 7 накладывает дополнительное ограничение: вычисленное простое q должно быть сравнимо с этим значением по модулю 8.⚠️ Парный к rsa-a параметр для q. Задавать их вслепую не стоит: неудачная комбинация ограничений заметно замедлит подбор простых чисел.

rsa-derive-from-pq

“rsa-derive-from-pq” (OSSL_PKEY_PARAM_RSA_DERIVE_FROM_PQ)знач.: ненулевое значение включает

При ненулевом значении недостающие компоненты ключа выводятся автоматически, если не были заданы. Требуется как минимум модуль и первые два простых множителя.⚠️ Относится к импорту/восстановлению ключа, а не к генерации: удобно для «урезанных» ключей из HSM или чужих форматов, но при нехватке p и q восстановление не состоится.

rsa_keygen_bits

-pkeyopt rsa_keygen_bits:numbitsзнач.: целое число бит

2048

Число бит в генерируемом ключе RSA. Если не указано, используется 2048.⚠️ По состоянию на сегодня 2048 бит — это минимально приемлемый уровень, а не «хороший»: NIST SP 800-57 отводит ему стойкость до конца десятилетия, публичные CA и CA/Browser Forum ключи короче 2048 бит уже не принимают. Для долгоживущих корневых и промежуточных CA берите 3072 или 4096; 4096 даёт заметно более медленную подпись и вчетверо более медленную генерацию, поэтому для листовых сертификатов и mTLS чаще разумнее ECDSA P-256, чем RSA-4096. Значения меньше 512 бит современные сборки просто отклоняют.

rsa_keygen_primes

-pkeyopt rsa_keygen_primes:numprimesзнач.: целое число простых множителей

2

Количество простых множителей в генерируемом ключе (multi-prime RSA). Если не указано, используется 2.⚠️ Multi-prime ускоряет расшифрование через CRT, но снижает эффективную стойкость при том же размере модуля и почти не поддерживается чужими стеками: TLS-библиотеки, HSM и аппаратные ускорители часто отвергают такой ключ. Не трогайте эту опцию, если у вас нет конкретного требования RFC 8017 multi-prime.

rsa_keygen_pubexp

-pkeyopt rsa_keygen_pubexp:valueзнач.: большое десятичное или шестнадцатеричное число (hex — с префиксом 0x)

65537

Значение открытой экспоненты RSA. Может быть большим десятичным числом или шестнадцатеричным при префиксе 0x. Значение по умолчанию — 65537.⚠️ В документации есть пример с экспонентой 3 — не копируйте его в прод: e=3 исторически связана с атаками Блейхенбахера на неправильно реализованную проверку PKCS#1 v1.5 и отвергается многими валидаторами сертификатов. Оставляйте 65537 (0x10001): это единственное значение, которое гарантированно примут все стеки и требования CA/Browser Forum.

rsa_pss_keygen_md

-pkeyopt rsa_pss_keygen_md:digestзнач.: имя хеш-функции

без ограничений

Если задано, ключ становится ограниченным (restricted) и может использоваться для подписи только с указанным хешем.⚠️ «Ограниченный» ключ — свойство, зашитое в сам ключ и сертификат: потом сменить хеш нельзя, придётся перевыпускать. По умолчанию у RSA-PSS-ключа ограничений нет, и в большинстве случаев это правильный выбор — жёсткая фиксация нужна лишь там, где её требует профиль (например, некоторые национальные или FIPS-профили).

rsa_pss_keygen_mgf1_md

-pkeyopt rsa_pss_keygen_mgf1_md:digestзнач.: имя хеш-функции

без ограничений

Если задано, ключ становится ограниченным и может использовать указанный хеш только в качестве параметра MGF1.⚠️ Задавать MGF1-хеш отличным от основного хеша подписи технически можно, но многие верификаторы такую комбинацию не поддерживают. Если уж фиксируете — держите rsa_pss_keygen_md и rsa_pss_keygen_mgf1_md одинаковыми.

rsa_pss_keygen_saltlen

-pkeyopt rsa_pss_keygen_saltlen:lenзнач.: длина соли в байтах

без ограничений

Если задано, ключ становится ограниченным, а len задаёт МИНИМАЛЬНУЮ длину соли.⚠️ Это именно минимум, а не точное значение — не рассчитывайте на побайтовое совпадение подписей. Слишком большая заданная соль сделает подпись невозможной для маленьких модулей: длина соли ограничена сверху размером модуля минус размер хеша минус 2 байта.

safeprime-generator / dh_paramgen_generator

-pkeyopt safeprime-generator:value или -pkeyopt dh_paramgen_generator:valueзнач.: целое число (обычно 2 или 5)

2

Значение генератора g. По умолчанию 2. Опция работает только когда алгоритм — “DH”.⚠️ Осторожно с написанием: сама опция пишется через дефис (safeprime-generator), а связанное с ней значение type ссылается на неё как safeprime_generator — через подчёркивание. С алгоритмом DHX опция молча не применится.

type (DH)

-pkeyopt type:stringзнач.: generator, fips186_4, fips186_2, group, default

“default” (для DH — “generator”, для DHX — “fips186_2”)

Имя типа генерации DH-параметров. generator — безопасное простое с опцией safeprime_generator (только DH); fips186_4 и fips186_2 — генерация по соответствующим стандартам (только DHX); group — вместе с pbits выбирает одну из ffdhe-групп (только DH); default — тип по алгоритму.⚠️ Каждое значение жёстко связано с алгоритмом: fips186_* требуют DHX, generator и group требуют DH — несовпадение даст ошибку. Значение “default” для DHX выбирает fips186_2, то есть УСТАРЕВШИЙ стандарт, ради обратной совместимости default-провайдера: для новых DHX-параметров задавайте fips186_4 явно.

type (DSA)

-pkeyopt type:typeзнач.: 0 — FIPS186-4, 1 — legacy FIPS186-2

0 (FIPS186-4)

Тип генерации параметров. 1 — устаревшая генерация по FIPS186-2, умолчание 0 — генерация по FIPS186-4.⚠️ Имя опции type совпадает с DH-опцией type, но принимает ЧИСЛО, а у DH это СТРОКА («generator», «fips186_4»). Перенос куска команды с DH на DSA даст ошибку. Значение 1 нужно только для воспроизведения старых артефактов.

use-cofactor-flag

“use-cofactor-flag” (OSSL_PKEY_PARAM_USE_COFACTOR_ECDH) знач.: 0 или 1

При значении 1 включает Cofactor Diffie-Hellman (ECC CDH), при 0 используется обычный EC DH.⚠️ Для кривых с кофактором 1 (все NIST prime-кривые, включая P-256/P-384) флаг ничего не меняет. Он существенен для кривых с кофактором больше единицы и требуется рядом профилей SP 800-56A.

Версии и подводные камни:

  • genpkey — современная замена genrsa и связки ecparam + genpkey: один интерфейс на все алгоритмы, параметры задаются через -pkeyopt.

  • Для эллиптических кривых почти всегда нужен ec_param_enc:named_curve: при кодировании явными параметрами многие клиенты сертификат не примут.

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

  • Сверено с: https://docs.openssl.org/master/man1/openssl-genpkey/, https://docs.openssl.org/master/man7/EVP_PKEY-RSA/, https://docs.openssl.org/master/man7/EVP_PKEY-EC/

Расширенный справочник: openssl req

Показать таблицу — 55 опций (openssl req)

Параметр

Синтаксис / значения

По умолч.

Описание и нюансы

-CA

-CA filename|uri

Задаёт сертификат «CA» для подписания нового сертификата и подразумевает использование -x509. Работает как «micro CA»: subject указанного сертификата становится issuer нового.⚠️ Наличие -CA меняет умолчание -in (стандартный ввод больше не подразумевается) и перекрывает -key при подписании сертификата.

-CAkey

-CAkey filename|uri

ключ берётся из входа -CA, если опция не задана

Задаёт приватный ключ «CA», которым подписывается сертификат.⚠️ Ключ обязан соответствовать открытому ключу сертификата из -CA; если опция не указана, ключ должен присутствовать во входных данных -CA.

-addext

-addext extзнач.: пара key=value в синтаксисе конфигурационного файла

Добавляет конкретное расширение в сертификат (при -x509) или в запрос. Аргумент должен иметь вид key=value, как в конфигурационном файле.⚠️ Каждое расширение может встречаться в объекте не более одного раза, и через -addext задаваться не более одного раза, хотя саму опцию можно повторять. Значения из командной строки перекрывают одноимённые расширения из конфига. Чтобы убрать нежелательные, используются -addext subjectKeyIdentifier=none и -addext authorityKeyIdentifier=none — они работают корректно, даже если расширений изначально не было.

-batch

-batch

Неинтерактивный режим.⚠️ Страница описывает опцию единственной фразой «Non-interactive mode». Отказ от запроса полей DN настраивается отдельно — параметром prompt = no в конфигурационном файле, который к тому же меняет ожидаемый формат секций distinguished_name и attributes.

-cipher

-cipher nameзнач.: любое валидное имя шифра OpenSSL

AES-256-CBC

Задаёт шифр для шифрования приватного ключа. Если шифр не указан, используется AES-256-CBC.⚠️ HISTORY отмечает смену умолчания с 3DES на AES-256 в OpenSSL 3.5 — один и тот же вызов на разных версиях даст ключ, зашифрованный по-разному.

-config

-config filename

см. «COMMAND SUMMARY» в openssl(1)

Позволяет указать альтернативный конфигурационный файл.⚠️ Опция необязательна, но без найденного конфига генерация запроса/сертификата падает с «unable to find ‘distinguished_name’ in config», тогда как просмотр готового запроса работает и без него — сама документация называет это поведение похожим на баг.

-copy_extensions

-copy_extensions argзнач.: none, copy, copyall

none (расширения игнорируются)

Определяет, как обрабатывать расширения X.509 из запроса сертификата, когда используется -x509. При copy или copyall все расширения запроса копируются в сертификат.⚠️ Работает только совместно с -x509. Если опция отсутствует или arg = none, расширения запроса молча игнорируются. Документация называет основным назначением возможность запроса задать значения вроде subjectAltName.

-days

-days nзнач.: положительное целое

30 days

При использовании -x509 задаёт срок действия сертификата в днях от сегодняшнего дня.⚠️ Без -x509 игнорируется. Дни всегда считаются от сегодня, независимо от -not_before; при совместном использовании с -not_after приоритет у явной даты окончания.

-digest

-digestзнач.: любой дайджест, поддерживаемый командой dgst

default_md из конфигурационного файла

Задаёт хеш-функцию для подписи запроса; подставляется имя дайджеста (в SYNOPSIS — -digest). Перекрывает алгоритм, указанный в конфигурационном файле.⚠️ Некоторые алгоритмы открытого ключа игнорируют выбор: DSA всегда использует SHA1, GOST R 34.10 — GOST R 34.11-94 (-md_gost94), а Ed25519 и Ed448 не используют дайджест вовсе.

-extensions

-extensions section

req_extensions (для CSR) или x509_extensions (для сертификата) из секции req

Переопределяет имя секции конфигурационного файла, из которой берутся расширения X.509 для сертификата (при -x509) или запроса.⚠️ Если ни -extensions, ни его алиас -reqexts не заданы, имя секции берётся из переменных req_extensions/x509_extensions секции req; при отсутствии этих переменных расширения не добавляются вовсе. Заменить саму секцию req можно опцией -section. В OpenSSL 4.0 убрана встроенная генерация subjectKeyIdentifier и authorityKeyIdentifier — их нужно перечислять явно.

-help

-help

Печатает сообщение об использовании команды.⚠️ Документация не описывает никаких особенностей: вывод ограничен usage-сообщением.

-in

-in filename

standard input unless -x509 or -CA is specified

Файл, из которого читается запрос сертификата.⚠️ Запрос читается, только если не заданы опции создания: -new, -newkey или -precert. При -x509 или -CA умолчанием перестаёт быть стандартный ввод.

-inform

-inform DER|PEMзнач.: DER, PEM

by default PEM is tried first

Формат входного файла CSR. Подробности — в openssl-format-options(1).⚠️ Это не жёсткое умолчание, а порядок попыток разбора: сначала пробуется PEM. Формулировка «tried first» отличается от «unspecified by default» у -outform и -keyform.

-key

-key filename|uri

Задаёт приватный ключ для подписания нового сертификата или запроса. Если не задан -in, соответствующий открытый ключ помещается в новый сертификат/запрос, что даёт самоподпись.⚠️ При подписании сертификата эта опция перекрывается опцией -CA. Для PEM-файлов принимаются и ключи в формате PKCS#8. Принимает не только имя файла, но и uri.

-keyform

-keyform DER|PEM|P12знач.: DER, PEM, P12

unspecified by default

Формат приватного ключа. Подробности — в openssl-format-options(1).⚠️ Набор форматов на master-странице — ровно DER|PEM|P12. Значения ENGINE в нём нет (опция -engine удалена в OpenSSL 4.0), поэтому переносить сюда формулировки старых версий нельзя; внешние ключи задаются через uri в -key.

-keyout

-keyout filename

If neither the -keyout option nor the -key option are given then the filename specified in the configuration file with the default_keyfile option is used, if present. … If a new key is generated and no filename is specified the key is written to standard output.

Задаёт файл, в который записывается любой приватный ключ — вновь созданный или прочитанный через -key.⚠️ Если -key задан, default_keyfile из конфигурации не применяется: документация прямо требует указать -keyout явно, иначе ключ не будет записан. При генерации нового ключа без имени файла он уходит в стандартный вывод — вместе с остальным выводом команды.

-modulus

-modulus

Печатает значение модуля открытого ключа, содержащегося в запросе.⚠️ Сам по себе не отменяет вывод запроса; обычно требуется вместе с -noout.

-multivalue-rdn

-multivalue-rdn

Опция относится к разбору строки -subj (исторически — к трактовке многозначных RDN). В текущей документации объявлена устаревшей и не имеет никакого эффекта.⚠️ Опция не имеет отношения к генерации ключа. Указывать её бессмысленно: многозначные RDN сейчас задаются символом + прямо внутри значения -subj, а сама опция полностью проигнорируется.

-nameopt

-nameopt optionзнач.: значения из openssl-namedisplay-options(1)

Задаёт способ отображения имён subject и issuer.⚠️ Влияет на вывод -subject и -text, но не на разбор -subj: формат ввода имени задаётся отдельно.

-new

-new

при отсутствии -key и настроек — RSA 2048 бит

Создаёт новый запрос сертификата, запрашивая у пользователя значения полей. Набор полей и их минимальные/максимальные размеры задаются конфигурационным файлом.⚠️ Если -key не задан, ключ генерируется по настройкам конфигурации либо по -newkey/-pkeyopt, иначе — RSA 2048. Флаг подразумевается опциями -newkey и -precert, а также -x509, если не задан -in.

-newhdr

-newhdr

Добавляет слово NEW в строки заголовка и завершения PEM-файла выводимого запроса.⚠️ Нужно только специфичному ПО (Netscape certificate server) и некоторым CA; обычные парсеры такого заголовка не требуют.

-newkey

-newkey argзнач.: [rsa:]nbits | param:file | algname[:file]

default_bits из конфигурации, иначе 2048

Генерирует новый приватный ключ (если не задан -key) и далее использует его так, как если бы он был передан через -key. Форма algname:file берёт параметры из файла параметров или сертификата.⚠️ Подразумевает -new. Если параметры алгоритма не заданы файлом, их нужно передать через -pkeyopt. Ключи gost2001/gost2012_256/gost2012_512 требуют провайдера gostprov, а для gost2001 без файла — -pkeyopt paramset:X.

-nodes

-nodes

Устаревший синоним -noenc: не шифровать создаваемый приватный ключ. Документация объявляет опцию устаревшей начиная с OpenSSL 3.0 и предписывает использовать -noenc.⚠️ Опция всё ещё присутствует в SYNOPSIS и работает, но помечена как deprecated — в новых скриптах документация требует -noenc. Название читается как «no DES», хотя умолчанием давно является AES-256-CBC.

-noenc

-noenc

Если задана, создаваемый приватный ключ не шифруется.⚠️ Эквивалент настройки encrypt_key = no в конфигурации (совместимое имя — encrypt_rsa_key). Отменяет действие -cipher и -passout применительно к ключу.

-noout

-noout

Подавляет вывод закодированной версии запроса сертификата.⚠️ Подавляется только закодированный объект; -text, -subject, -pubkey, -modulus продолжают печатать своё.

-not_after

-not_after dateзнач.: YYMMDDHHMMSSZ | YYYYMMDDHHMMSSZ | today

При использовании -x509 явно задаёт дату окончания действия сертификата.⚠️ Перекрывает опцию -days. Без -x509 игнорируется; секунды и Z обязательны.

-not_before

-not_before dateзнач.: YYMMDDHHMMSSZ | YYYYMMDDHHMMSSZ | today

При использовании -x509 явно задаёт дату начала действия сертификата.⚠️ Без -x509 опция игнорируется. В обоих форматах обязательны секунды SS и таймзона Z. Отсчёт -days ведётся от сегодняшнего дня независимо от значения -not_before.

-out

-out filename

standard output

Файл для записи результата.⚠️ Без -noout в тот же поток попадает и закодированный запрос, и текстовый вывод -text.

-outform

-outform DER|PEMзнач.: DER, PEM

unspecified by default

Формат вывода. Выводимые данные — объект PKCS#10.⚠️ «Unspecified by default» — не синоним PEM. При -x509 или -CA той же опцией задаётся формат выводимого сертификата, а не запроса.

-passin

-passin argзнач.: форматы из openssl-passphrase-options(1)

Источник пароля для входного приватного ключа и сертификата.⚠️ Перекрывает значение input_password из секции req конфигурационного файла.

-passout

-passout argзнач.: форматы из openssl-passphrase-options(1)

Источник пароля для выходного файла.⚠️ Перекрывает значение output_password из конфигурационного файла. При -noenc пароль на созданный ключ не накладывается вовсе.

-pkeyopt

-pkeyopt opt:value

Устанавливает опцию opt алгоритма открытого ключа в значение value.⚠️ Точный набор опций зависит от алгоритма и его реализации; перечень — в разделе «KEY GENERATION OPTIONS» страницы openssl-genpkey(1).

-precert

-precert

Добавляет в сертификат poison-расширение, превращая его в «pre-certificate» (RFC 6962) для отправки в логи Certificate Transparency и получения SCT.⚠️ Подразумевает флаг -new, то есть отключает чтение запроса через -in. Полученные SCT затем встраиваются в pre-certificate, после чего poison удаляется и сертификат подписывается заново.

-propquery

-propquery propq

Задаёт property query clause, применяемый при выборке алгоритмов из загруженных провайдеров.⚠️ Версия появления в openssl-req(1) не указана. Синтаксис выражения описан не здесь, а в property(7).

-provider

-provider name

Загружает и инициализирует провайдера с именем name; name может быть также путём к модулю провайдера. Подробности — в «Provider Options» в openssl(1), provider(7) и property(7).⚠️ Страница openssl-req(1) не называет версию, в которой появилась опция, — версию проставлять неоткуда. В HISTORY отмечено, что опция -engine удалена в OpenSSL 4.0, и на master-странице её больше нет.

-provider-path

-provider-path path

Задаёт путь поиска модулей провайдеров. Эквивалентно установке переменной окружения OPENSSL_MODULES.⚠️ Версия появления в документации openssl-req(1) не указана. Порядок опций важен: путь должен быть задан до загрузки соответствующего провайдера.

-provparam

-provparam [name:]key=value

если name не указан — применяется ко всем загруженным провайдерам

Устанавливает параметр конфигурации key в значение value у провайдера name; имя провайдера необязательно.⚠️ Документация openssl-req(1) не называет версию OpenSSL, в которой опция появилась, — поле версии остаётся пустым. Без префикса name: настройка применяется сразу ко всем загруженным провайдерам, а не к одному.

-pubkey

-pubkey

Печатает открытый ключ.⚠️ Не отменяет вывод самого запроса — для чистого вывода ключа нужен ещё -noout.

-quiet

-quiet

Печатает меньше подробностей о выполняемых операциях, что удобно в пакетных скриптах и конвейерах.⚠️ Конкретно подавляются «progress dots» при генерации ключа — то есть скрывается прогресс долгой операции, а не ошибки.

-rand

-rand files

Файл или файлы со случайными данными для затравки генератора случайных чисел. Описание — в разделе «Random State Options» страницы openssl(1).⚠️ Несколько файлов разделяются символом, зависящим от ОС; страница не называет его явно. В конфигурационном файле есть родственный параметр RANDFILE, который загружается при старте.

-reqexts

-reqexts section

см. -extensions

Алиас опции -extensions: задаёт имя секции конфигурационного файла с расширениями.⚠️ С OpenSSL 3.2 это ровно тот же обработчик, что и -extensions; исторического разделения «reqexts только для запросов» на master-странице больше нет.

-reqopt

-reqopt optionзнач.: одна опция или несколько через запятую

Настраивает формат печати, используемый с -text.⚠️ Имеет смысл только вместе с -text. Перечень значений на этой странице не приводится: см. описание параметра -certopt в openssl-x509(1).

-section

-section name

req

Задаёт имя секции конфигурационного файла, которая используется командой.⚠️ Переопределяет не только базовые настройки, но и то, откуда берутся переменные req_extensions и x509_extensions. Если значения в указанной секции нет, дополнительно просматривается начальная безымянная (default) секция.

-set_serial

-set_serial nзнач.: десятичное число или hex с префиксом 0x

большое случайное число

Серийный номер для выводимого самоподписанного сертификата.⚠️ Если опция не задана, используется большое случайное число — воспроизводимость серийников придётся обеспечивать самому.

-sigopt

-sigopt nm:v

Передаёт опции алгоритму подписи при операциях подписания.⚠️ Имена и значения специфичны для алгоритма; страница openssl-req(1) их не перечисляет.

-subj

-subj argзнач.: /type0=value0/type1=value1/type2=…

Задаёт subject для нового запроса или заменяет subject при обработке существующего запроса сертификата.⚠️ Спецсимволы экранируются обратным слэшем, пробелы сохраняются. Пустые значения допустимы, но соответствующий тип в запрос не попадёт. Одиночный / даёт пустую последовательность RDN (NULL-DN). Многозначные RDN собираются символом + вместо / между AVA.

-subject

-subject

Печатает subject запроса (или subject сертификата, если используется -x509).⚠️ Способ отображения имени задаётся отдельной опцией -nameopt.

-text

-text

Печатает запрос сертификата в текстовом виде.⚠️ Если -verify завершился неудачей, программа выходит немедленно и -text уже не отрабатывает. Формат печати настраивается через -reqopt.

-utf8

-utf8

значения полей трактуются как ASCII

Значения полей интерпретируются как строки UTF-8 (по умолчанию — как ASCII).⚠️ Требование распространяется и на ввод с терминала, и на значения из конфигурационного файла — они должны быть валидным UTF-8. Аналог в конфигурации — utf8 = yes.

-verbose

-verbose

Печатает дополнительные подробности о выполняемых операциях.⚠️ Прямая противоположность -quiet; документация не оговаривает поведение при одновременном указании обеих опций.

-verify

-verify

Проверяет самоподпись запроса.⚠️ При неудачной проверке программа выходит немедленно, дальнейшая обработка опций (например -text) пропускается. Начиная с OpenSSL 3.3 код возврата при неудаче — 1.

-vfyopt

-vfyopt nm:v

Передаёт опции алгоритму подписи при операциях проверки подписи.⚠️ Парная к -sigopt: применяется только на проверке (-verify), а не на подписании.

-writerand

-writerand file

Записывает данные затравки в указанный файл при выходе; этот файл можно использовать в последующем вызове команды. Описан на той же странице openssl(1) в разделе «Random State Options», что и -rand.⚠️ Направление противоположно -rand: -rand читает случайные данные на старте, -writerand записывает файл состояния на выходе. Опции не взаимозаменяемы и обычно используются в паре. Похожее поведение даёт параметр конфигурации RANDFILE, в который при выходе пишется 256 байт.

-x509

-x509

Выводит сертификат вместо запроса сертификата; типично используется для тестовых сертификатов. Подразумевается опцией -CA.⚠️ Подразумевает -new, если не задан -in. Если запрос передан через -in, он конвертируется в сертификат; расширения из него НЕ копируются, пока не задан -copy_extensions. Без -set_serial серийный номер — большое случайное число. Без -x509v1 сертификат будет версии 3, даже если расширений нет.

-x509v1

-x509v1

Требует сгенерировать сертификат версии X.509 v1. Подразумевает -x509.⚠️ Если к сертификату добавляются расширения X.509, версия 3 используется независимо от этой опции.

Версии и подводные камни:

  • -addext добавляет расширение прямо в запрос и не требует правки конфига; -reqexts указывает секцию конфига. При одновременном использовании поведение зависит от версии — проверяйте результат командой openssl req -in ... -noout -text.

  • -subj перекрывает интерактивный ввод целиком. Слэш внутри значения экранируется обратным слэшем, иначе DN развалится на лишние компоненты.

  • Ключ -x509 превращает req из «сделать запрос» в «выпустить самоподписанный сертификат» — именно им в Части 1 создавался корень.

  • Сверено с: https://docs.openssl.org/master/man1/openssl-req/, https://docs.openssl.org/master/man1/openssl/


Шаг 2. PKIDesk: движок управления сертификатами

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

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

PKIDesk
PKIDesk

Движок называется PKIDesk и живёт здесь: github.com/DidenkoMS/pkidesk, Apache-2.0. Написан на Go, зависимостей нет ни одной — go.mod не содержит ни строчки require. Всё, что меняет состояние УЦ, выполняется теми же командами openssl ca, что и вручную, поверх тех же конфигов и той же текстовой базы index.txt.

Честная оговорка: движок написан в паре с Claude Code — я ставил задачу, выбирал развилки и вычитывал результат, модель писала код и тесты. Решения ниже мои, и отвечаю за них я, но сам код в изрядной части машинный, и относиться к нему нужно как к любому чужому коду: прочитать до того, как ставить в один контур с ключом УЦ. Отчасти поэтому движок и не реализует X.509 самостоятельно, а форкает openssl и пишет в журнал выполненную команду целиком — проверить, что он сделал, можно построчно и не читая его исходников.

Развилка первая: свой X.509 или обёртка над openssl

Соблазн реализовать выпуск на crypto/x509 велик: чище тестируется, не нужно форкать процесс, не нужно разбирать чужой вывод.

Отказался. Причина не в трудозатратах, а в том, что появился бы второй источник истины: сертификат, выпущенный движком, отличался бы от выпущенного командой из статьи — другой порядок полей, другая логика серийников, другой формат базы. В обёртке же результат неотличим от ручного: index.txt, serial, newcerts/ остаются прежними, и любой шаг можно повторить в терминале.

Разбор и показ готовых объектов, наоборот, сделан на crypto/x509 — форкать процесс, чтобы прочитать одно поле, незачем. Кнопка «а как это видит openssl» рядом осталась и вызывает openssl x509 -text.

Развилка вторая: где живёт ключ УЦ

Это единственное решение, которое я считаю по-настоящему обязательным.

Веб-приложение принимает запросы из интернета. Ключ удостоверяющего центра подписывает всё, чему доверяет организация. Совместить их в одном процессе — значит поставить между злоумышленником и корнем доверия ровно одну ошибку в разборе HTTP.

Поэтому движок разрезан по той же границе, что и стенд Части 2, где OCSP-ответчик живёт во внутреннем сегменте, а наружу смотрит DMZ-хост без ключей:

public-dmz            app-backend            signer-bridge          internal-ca
┌────────────┐        ┌──────────────┐        ┌──────────┐        ┌────────────────┐
│  app-web   │ mTLS   │pkidesk-portal│  JSON  │          │        │ pkidesk-signer │
│ nginx 443  ├───────>│    веб-UI    ├───────>│   мост   ├───────>│   ключи УЦ     │
│ /apache    │ X-SSL-*│  ключей нет  │ токен  │          │        │   index.txt    │
└────────────┘        └──────────────┘        └──────────┘        └────────────────┘

pkidesk-portal

pkidesk-signer

Сети

app-backend, signer-bridge

internal-ca, signer-bridge

Порты наружу

нет

нет вообще

Ключи УЦ

нет

да

Дерево УЦ, index.txt

нет

да

openssl в образе

нет

да

Портал умеет ровно то, что перечислено в API подписанта, — девятнадцать методов. Расширить его возможности, не тронув подписанта, нельзя, и это главное свойство конструкции. Компрометация веб-части даёт злоумышленнику ровно то, что даёт легальному оператору, — и ни байтом больше.

Корневой УЦ к подписанту не подключён вовсе. Он остаётся air-gapped, как в Части 2: смонтировать корневой ключ в постоянно работающий сетевой процесс — значит отменить ту самую изоляцию, ради которой всё и затевалось.

Развилка третья: где живёт роль

Роль можно зашить в сертификат — в OU или в certificatePolicies. Тогда внешнее хранилище не нужно вовсе, а сервер может разграничивать доступ сам, через тот же SSLRequire.

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

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

Развилка четвёртая: что делать с subjectAltName из запроса

Заявитель приносит CSR, в котором просит subjectAltName. Логично было бы перенести расширения из запроса в сертификат — в openssl для этого есть copy_extensions = copy.

Так делать нельзя. При copy_extensions = copy заявитель может попросить себе basicConstraints = CA:TRUE — и получить право подписывать сертификаты от имени вашего центра. Значение по умолчанию none стоит не случайно.

В движке SAN из запроса показывается оператору как пожелание, а в сертификат попадает отдельной строкой временного оверлея расширений — то есть ровно то, что оператор подтвердил. Оверлей живёт одну команду и удаляется:

# Временный оверлей, собранный движком на одну команду openssl ca.
# Основа — секция [ user_cert ], добавлена одна строка.
[ user_cert ]
basicConstraints       = CA:FALSE
keyUsage               = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage       = clientAuth, emailProtection
crlDistributionPoints  = URI:http://pki.certservice.info/crl/PersonIntermediateCA.crl
authorityInfoAccess    = OCSP;URI:http://ocsp.certservice.info/
subjectAltName         = email:user@certservice.info, DNS:user.certservice.info

Развилка пятая: первый вход

Классическое «яйцо и курица»: чтобы получить сертификат, нужно войти в движок, а чтобы войти — нужен сертификат.

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

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

Что движок умеет

  • Обзор — состояние всех УЦ: сколько сертификатов действует, сколько отозвано, когда истекает CRL и сам сертификат центра.

  • Список сертификатов — таблица index.txt с фильтрами; фактический статус считается по дате, а не только по букве V/R/E.

  • Карточка сертификата — разбор поле за полем, все расширения с пометкой critical и объяснением, на что влияет каждое; выгрузка в PEM, DER, PKCS#7, с цепочкой и в виде расшифровки openssl x509 -text; кнопки openssl verify и запроса к OCSP.

  • Выпуск — по готовому CSR либо с генерацией ключа на сервере (с честной оговоркой, что это разрушает неотказуемость).

  • Отзыв — все причины -crl_reason, включая certificateHold и указание момента компрометации через -crl_compromise, который попадает в запись CRL расширением invalidityDate.

  • Заявки — функция регистрационного центра: заявитель приносит CSR, оператор решает.

  • Журнал — кто, что, когда и какой именно командой.

Последнее стоит подчеркнуть. В журнал пишется выполненная команда целиком:

openssl ca -batch -config /root/ca/config/PersonIntermediateCA.cnf \
  -passin env:CA_PASS_PERSON -notext \
  -extfile /tmp/pkidesk-signer/ext-2676677601/overlay.cnf -extensions user_cert -days 90 \
  -in /tmp/casign-.../request.csr.pem -out /tmp/casign-.../issued.cert.pem

Через год это единственный способ ответить не только «кто отозвал», но и «чем именно выпущен вот этот спорный сертификат».

Пароль ключа УЦ, кстати, в командную строку не попадает никогда — только -passin env:CA_PASS_PERSON. В ps виден лишь env:CA_PASS_PERSON, а сама переменная ставится единственному дочернему процессу.


Шаг 3. Три дефекта, которые вылезли только на живом стенде

Всё вышеописанное проходило тесты локально. На боевом хосте вылезли три вещи, которых тесты не поймали. Каждая — готовая ловушка для того, кто повторит.

3.1. subjectAltName = supplied в политике

Первый же выпуск серверного сертификата упал:

Check that the request matches the signature
Signature ok
The subjectAltName field needed to be supplied and was missing

При том, что subjectAltName в расширениях запроса был.

Смотрим политику из Части 1:

[ policy_intermediate_server ]
organizationName = match
commonName       = supplied
emailAddress     = supplied
subjectAltName   = supplied

Разгадка в том, что openssl ca не различает «поле DN» и «расширение». Проверяя политику, он берёт имя из секции, переводит его в OID и ищет компонент subject с этим OID. У subjectAltName OID есть — 2.5.29.17, — а вот компонента subject с таким OID не бывает: это расширение. Правило не выполнится никогда, сколько бы альтернативных имён ни лежало в запросе.

Как это обошли в Части 1 — видно в базе:

V  181209120005Z  1B00000000000000  unknown
   /C=RU/ST=Krasnoyarskiy kray/L=Norilsk/O=CertService/OU=CertService. IT-Department./
   CN=certservice.info/emailAddress=web@certservice.info/
   subjectAltName=DNS:certservice.info, DNS:www.certservice.info

subjectAltName вписан прямо в subject. Для проверяющей стороны такой компонент бесполезен — имя хоста браузер сверяет только с расширением, — но политику он проходит.

Выхода два: вписывать SAN в -subj, как в Части 1, либо смягчить правило до optional и задавать SAN расширением, как и положено. Движок теперь делает первое автоматически и объясняет, почему.

3.2. Просроченный сертификат остаётся V

Следующий отказ:

There is already a certificate for /C=RU/.../CN=certservice.info

Серверный сертификат из Части 1 истёк в 2018 году. Казалось бы, истёкший не должен мешать перевыпуску.

Мешает. unique_subject = yes (файл index.txt.attr) строит индекс уникальности только по строкам со статусом V, а буква E появляется в базе не сама — её проставляет отдельная команда:

openssl ca -config ... -updatedb

Пока -updatedb не выполнен, истёкший сертификат числится действующим и занимает subject. Отсюда правило: перед перевыпуском с тем же DN всегда прогоняйте -updatedb. В движке это отдельная кнопка «Обновить статусы» на странице УЦ и шаг в скрипте выпуска серверного сертификата.

3.3. Гонка «выпустил — сразу зашёл»

Самый интересный.

Ответчик из состава openssl читает index.txt только при запуске. В Части 2 это решалось руками: отозвал — перезапусти. Но движок отзывает по кнопке, и держать в голове ручной шаг нельзя, поэтому ответчик работает под надзирателем, который следит за временем изменения базы и перезапускает процесс сам.

И вот тут всё сложилось неудачно. Выпуск сертификата меняет index.txt — значит надзиратель перезапускает ответчика. А владелец свежего сертификата первым делом идёт им пользоваться. Запрос nginx попадает ровно в окно перезапуска, получает отказ — и nginx запоминает неудачный результат в ssl_ocsp_cache. Дальше человек видит 400 The SSL certificate error и не может войти, пока запись не протухнет, хотя ответчик давно поднялся.

Лечится с двух сторон.

Со стороны надзирателя — ожидание фактической готовности, а не факта запуска процесса:

ready() {
  # Ответчик готов, когда принимает соединение на своём порту.
  # Проверяем именно это: между fork и listen проходит заметное время,
  # и запрос, попавший в промежуток, получит отказ.
  timeout 1 sh -c "exec 3<>/dev/tcp/127.0.0.1/$PORT" 2>/dev/null
}

плюс выдержка на серию изменений: одна операция движка правит index.txt несколько раз (openssl пишет .new и переименовывает), а отзыв с перевыпуском CRL — ещё и подряд. Без выдержки получилась бы серия перезапусков, каждый со своим окном отказа.

Со стороны nginx — отказ от кэша:

ssl_ocsp           leaf;
ssl_ocsp_responder http://ocsp.certservice.info/;
# ssl_ocsp_cache   shared:OCSP:2m;   ← намеренно выключен

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

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


Проверка: полный цикл на живом стенде

Так это выглядит целиком, от первого входа до отзыва. Вывод настоящий, с боевого стенда на nginx 1.29.8 и OpenSSL 3.3.7:

 1. Вход первичным паролем                          ok
 2. Выпуск сертификата через веб-интерфейс          ok, серийник 1A00000000000009
 3. Скачивание ключа и сертификата                  ok, 2167 и 1704 байт
 4. Шаги 1-2 входа по кнопке: сертификат принят     ok, пропуск выдан
 5. Шаг 3: сессия на публичном хосте                ok, вошли без модалки
 6. Статус через OCSP до отзыва                     good
 7. Отзыв keyCompromise + перевыпуск CRL            ok
 8. Серийный номер попал в CRL                      ok
 9. ocsp-watch перечитал index.txt сам              перезапусков за прогон: 5
10. Статус через OCSP после отзыва                  revoked (keyCompromise)
11. Вход отозванным сертификатом                    HTTP 302 — отклонён
12. Хост входа без сертификата                      HTTP 302 (перенаправление на страницу входа)
13. Раздача CRL через соседний прокси (порт 80)     HTTP 200

Три строки заслуживают отдельного взгляда.

Строка 9 — пять перезапусков ответчика за один прогон. Это не баг, а цена решения из раздела 3.3: одна операция движка меняет index.txt несколько раз (openssl пишет .new и переименовывает), а отзыв с перевыпуском CRL делает это дважды подряд. Выдержка в надзирателе схлопывает серию, но не в одно событие. Пока ответчик поднимается заново, проверки отзыва отвечают ошибкой — поэтому ssl_ocsp_cache и выключен.

Строки 11 и 12 — то, ради чего всё затевалось, и заодно результат перехвата внутренних кодов nginx из Части 3. Отозванный сертификат отсекается на уровне рукопожатия, до приложения, но человек видит не сырую страницу «400 Bad Request», а перенаправление на вход с объяснением. Отсутствие сертификата обрабатывается так же — и это правильно: обе ситуации для пользователя означают одно и то же, «войти не получилось, вот что делать».

Строка 13 — не про PKI, а про честность: стенд живёт на хосте, где уже работает посторонний сайт со своим обратным прокси на портах 80 и 443. Именно поэтому движок опубликован на 8443, а раздача CRL идёт через соседний прокси. Проверка подтверждает, что мы никому не наступили на ногу.


Итог

Цикл замкнулся. Выпуск (Часть 1), отзыв с публикацией статуса (Часть 2), вход по сертификату (Часть 3) и управление всем этим через интерфейс (эта часть). В последней мы:

  • собрали PKCS#10 прямо в браузере, на голом WebCrypto и без единой библиотеки, — и проверили результат настоящим openssl, а не «на глаз»;

  • поняли, почему кнопке выпуска нельзя доверять отличительное имя, и как выглядит защита: субъект задаёт сервер, а не заявитель;

  • разрезали движок по границе доверия — веб-часть не имеет ни ключей УЦ, ни дерева index.txt, ни даже бинарника openssl;

  • разобрали развилки, за которые придётся отвечать перед аудитом: обёртка над openssl против своей реализации X.509, роль в сертификате против роли в движке, copy_extensions и SAN, «яйцо и курица» первого входа;

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

Что осталось за рамками и куда двигаться дальше: дельта-CRL, собственный OCSP-ответчик вместо openssl ocsp, хранение ключей УЦ в HSM или PKCS#11 и автоматизация выпуска по ACME или EST.


Источники

Всё в статье сверено с официальной документацией; реферальных и сокращённых ссылок нет.

Документация OpenSSL (ветка master):

  • openssl-req — создание запроса на сертификат

  • openssl-genpkey — генерация закрытого ключа

  • openssl-ca — политика выпуска, -updatedb, copy_extensions, -subj

  • openssl-pkcs12 — контейнер для браузера

  • x509v3_config — расширения X.509v3

  • config — формат конфигурационного файла OpenSSL

Браузер:

Стандарты (RFC):

  • RFC 2986 — PKCS#10: структура запроса на сертификат

  • RFC 5280 — профиль X.509: отличительное имя, типы атрибутов, расширения

  • RFC 6960 — OCSP

  • RFC 7292 — PKCS#12

Код и предыдущие части: