Самоподписанный сертификат: пошаговое построение

Самоподписанный сертификат всегда начинается с асимметрической криптографии. В Jsrsasign ключевая пара создаётся на основе RSA или ECDSA, где приватный ключ используется для подписи, а публичный — для проверки.

В большинстве практических сценариев применяется RSA:

const kp = KEYUTIL.generateKeypair("RSA", 2048);

const privateKey = kp.prvKeyObj;
const publicKey = kp.pubKeyObj;

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

Формирование структуры X.509 сертификата

Самоподписанный сертификат отличается тем, что субъект и издатель совпадают. Это ключевая особенность всей конструкции.

В Jsrsasign работа ведётся через объект KJUR.asn1.x509.Certificate.

const cert = new KJUR.asn1.x509.Certificate({
  version: 3
});

Далее задаются основные поля, формирующие структуру X.509.

Определение субъекта и издателя

В самоподписанном сертификате оба поля идентичны:

const dn = "/C=RU/O=Example Org/OU=IT Department/CN=example.local";

cert.setSubject({ str: dn });
cert.setIssuer({ str: dn });

Структура Distinguished Name должна быть согласована с требованиями X.509. Обычно используются следующие атрибуты:

  • C — страна
  • O — организация
  • OU — подразделение
  • CN — общее имя (домен или хост)

Привязка публичного ключа

Публичный ключ внедряется в сертификат как основа для дальнейшей верификации:

cert.setPublicKey(publicKey);

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

Установка периода действия

Каждый сертификат обязан иметь ограниченный срок действия. Jsrsasign требует явного указания даты начала и окончания:

cert.setNotBefore("240101000000Z");
cert.setNotAfter("250101000000Z");

Формат времени соответствует ASN.1 UTCTime или GeneralizedTime в зависимости от диапазона.

Критически важно: истечение срока делает сертификат недействительным независимо от остальных параметров.

Серийный номер сертификата

Серийный номер должен быть уникальным для каждого сертификата:

cert.setSerialNumber({ hex: "01a3b5c7d9e2f1" });

В реальных системах серийный номер часто генерируется случайным образом или на основе времени и хэша.

Добавление расширений X.509 v3

Расширения определяют поведение сертификата в инфраструктуре.

Basic Constraints

Определяет, может ли сертификат выступать как удостоверяющий центр:

cert.addExtension(new KJUR.asn1.x509.BasicConstraints({
  cA: false
}));

Для самоподписанного пользовательского сертификата обычно устанавливается cA: false.

Key Usage

Определяет допустимые криптографические операции:

cert.addExtension(new KJUR.asn1.x509.KeyUsage({
  bin: "101000"
}));

Типичные флаги:

  • digitalSignature
  • keyEncipherment
  • dataEncipherment
  • keyCertSign

Subject Alternative Name

Используется для дополнительных доменов:

cert.addExtension(new KJUR.asn1.x509.SubjectAltName({
  critical: false,
  array: [{ dns: "example.local" }]
}));

Подписание сертификата приватным ключом

Ключевой этап, превращающий структуру в полноценный сертификат — цифровая подпись.

cert.sign(privateKey, "SHA256withRSA");

Алгоритм подписи должен соответствовать типу ключа. Для RSA чаще всего используется SHA-256.

После подписи объект становится неизменяемым с точки зрения криптографической целостности.

Генерация итогового PEM-представления

Финальная форма сертификата обычно требуется в PEM-формате:

const pem = cert.getPEM();

PEM представляет собой Base64-кодированный DER с заголовками:

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

Полный цикл построения самоподписанного сертификата

Процесс можно рассматривать как последовательность строго определённых этапов:

const kp = KEYUTIL.generateKeypair("RSA", 2048);

const cert = new KJUR.asn1.x509.Certificate({ version: 3 });

const dn = "/C=RU/O=Example Org/OU=IT/CN=example.local";

cert.setSubject({ str: dn });
cert.setIssuer({ str: dn });

cert.setPublicKey(kp.pubKeyObj);

cert.setNotBefore("240101000000Z");
cert.setNotAfter("250101000000Z");

cert.setSerialNumber({ hex: "01a3b5c7d9e2f1" });

cert.addExtension(new KJUR.asn1.x509.BasicConstraints({ cA: false }));
cert.addExtension(new KJUR.asn1.x509.KeyUsage({ bin: "101000" }));
cert.addExtension(new KJUR.asn1.x509.SubjectAltName({
  array: [{ dns: "example.local" }]
}));

cert.sign(kp.prvKeyObj, "SHA256withRSA");

const pemCert = cert.getPEM();
const pemPrivateKey = KEYUTIL.getPEM(kp.prvKeyObj, "PKCS8PRV");

Внутреннее устройство подписи

При вызове sign() библиотека выполняет несколько критических операций:

  • сериализация TBS (To Be Signed) структуры
  • вычисление хэша (SHA-256 или иной алгоритм)
  • шифрование хэша приватным ключом
  • упаковка результата в ASN.1 структуру

Эти шаги обеспечивают целостность и проверяемость сертификата.

Особенности самоподписанных сертификатов

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

  • отсутствие цепочки доверия
  • необходимость ручного добавления в доверенные хранилища
  • ограниченная применимость в публичных TLS-сценариях

При этом он полностью корректен с точки зрения X.509 и широко используется в:

  • локальных тестовых HTTPS-серверах
  • внутренней корпоративной инфраструктуре
  • разработке криптографических протоколов

Работа с форматами экспорта

Jsrsasign поддерживает несколько форматов представления ключей и сертификатов.

PEM

Человеко-читаемый формат, основной для обмена:

  • сертификаты
  • приватные ключи
  • цепочки доверия

DER

Бинарное представление ASN.1 структуры:

const der = cert.getEncodedHex();

DER используется в системных API и низкоуровневых протоколах.

Типичные ошибки при построении

Ошибки, возникающие при создании самоподписанного сертификата, чаще всего связаны с:

  • несовпадением subject и issuer при self-signed логике
  • неправильным форматом даты (не UTC Z-формат)
  • отсутствием приватного ключа при вызове sign()
  • несоответствием алгоритма подписи типу ключа

Каждая из этих ошибок приводит либо к невозможности подписи, либо к невалидному сертификату.

Криптографическая устойчивость

Уровень безопасности напрямую зависит от:

  • длины ключа (минимум 2048 бит для RSA)
  • алгоритма хэширования (SHA-256 и выше)
  • корректности генерации случайных чисел

Jsrsasign опирается на JavaScript-криптобэкенд, поэтому качество случайности критично для всей цепочки генерации.