Подпись сертификата закрытым ключом CA

Подпись сертификата в PKI-системе — это процесс создания цифровой подписи для X.509 сертификата с использованием закрытого ключа удостоверяющего центра (CA). В контексте JavaScript-библиотеки Jsrsasign этот процесс реализуется через формирование структуры TBSCertificate (To Be Signed Certificate), вычисление криптографической подписи и упаковку результата в полноценный X.509 объект.

Любой X.509 сертификат до подписи состоит из двух ключевых частей:

  • TBSCertificate (данные сертификата)
  • Signature Algorithm Identifier (алгоритм подписи)
  • Signature Value (сама подпись, формируется CA)

TBSCertificate включает:

  • версия сертификата
  • серийный номер
  • алгоритм подписи
  • имя субъекта (Subject)
  • имя издателя (Issuer)
  • срок действия (Validity)
  • публичный ключ субъекта
  • расширения (Extensions)

Именно TBSCertificate является объектом, который подписывается закрытым ключом CA.

Подготовка ключей CA в Jsrsasign

В Jsrsasign закрытый ключ CA обычно загружается через KEYUTIL.

const caPrivateKeyPEM = `
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`;

const caPrivateKey = KEYUTIL.getKey(caPrivateKeyPEM);

А сертификат CA (Issuer) может использоваться для получения Subject Distinguished Name:

const caCertPEM = `
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----`;

const caCert = new X509();
caCert.readCertPEM(caCertPEM);

const issuerDN = caCert.getSubjectString();

Важно понимать: именно issuerDN становится значением Issuer в подписываемом сертификате.

Формирование сертификата субъекта (end-entity)

Перед подписью необходимо сформировать структуру сертификата, включая публичный ключ субъекта:

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

const subjectKeyPEM = KEYUTIL.getPEM(subjectPublicKey);

Далее задаются основные поля сертификата:

  • Subject DN
  • Issuer DN (CA)
  • срок действия
  • серийный номер

Создание TBSCertificate

В Jsrsasign используется KJUR.asn1.x509.Certificate (или более низкоуровневые структуры ASN.1) для формирования сертификата.

const cert = new KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: 1 },
    sigalg: "SHA256withRSA",
    issuer: issuerDN,
    notbefore: "202601010000Z",
    notafter:  "202701010000Z",
    subject: "/C=KZ/O=Example Org/OU=IT/CN=example.com",
    pubkey: subjectKeyPEM,
    ext: [
        {
            extname: "basicConstraints",
            cA: false
        },
        {
            extname: "keyUsage",
            names: ["digitalSignature", "keyEncipherment"]
        }
    ]
});

На этом этапе создаётся структура, которая ещё не подписана.

Процесс подписи закрытым ключом CA

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

  1. Формирует DER-кодировку TBSCertificate
  2. Вычисляет хэш (например SHA-256)
  3. Подписывает хэш закрытым ключом CA
  4. Встраивает подпись в итоговый сертификат

Пример явного указания CA ключа:

const caKey = KEYUTIL.getKey(caPrivateKeyPEM);

cert.sign(caKey, "SHA256withRSA");

После этого объект содержит полностью сформированный X.509 сертификат.

Получение итогового сертификата

Результат можно экспортировать в PEM-формат:

const pem = cert.getPEM();
console.log(pem);

Или в DER (Base64):

const der = cert.getHex();

Внутренняя логика подписи в Jsrsasign

Под капотом происходит следующая последовательность:

1. Кодирование TBSCertificate

ASN.1 структура сериализуется:

  • DER encoding
  • фиксированный порядок полей
  • строгая совместимость с X.509

2. Вычисление хэша

Алгоритм определяется параметром sigalg:

  • SHA256withRSA → SHA-256
  • SHA384withECDSA → SHA-384

Результат:

hash = SHA256(DER(TBSCertificate))

3. Криптографическая подпись

Для RSA:

signature = RSA_sign(hash, CA_private_key)

Для ECDSA:

signature = ECDSA_sign(hash, CA_private_key)

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

Финальный объект:

  • TBSCertificate
  • AlgorithmIdentifier
  • SignatureValue

Подпись сертификата с расширенными параметрами CA

CA сертификат часто имеет критические ограничения:

{
    extname: "basicConstraints",
    cA: true,
    critical: true
}

Это важно, потому что только сертификаты с cA: true могут использоваться для подписи других сертификатов.

Цепочка доверия и роль CA подписи

Когда CA подписывает сертификат, создаётся доверительная цепочка:

Root CA → Intermediate CA → End Entity

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

Подпись промежуточным CA

Если используется intermediate CA:

const intermediateKey = KEYUTIL.getKey(intermediateCAKeyPEM);

cert.sign(intermediateKey, "SHA256withRSA");

Issuer DN в этом случае берётся из intermediate CA сертификата.

Проверка корректности подписи

После генерации можно проверить подпись:

const x509 = new X509();
x509.readCertPEM(pem);

const isValid = x509.verifySignature(caCert.getPublicKey());

Проверка включает:

  • пересчёт хэша TBSCertificate
  • проверку RSA/ECDSA подписи
  • сопоставление с публичным ключом CA

Типичные ошибки при подписи сертификатов

Несовпадение Issuer DN

Если Issuer DN не совпадает с Subject CA сертификата, цепочка доверия разрывается.

Неверный алгоритм подписи

CA ключ RSA нельзя использовать с ECDSA алгоритмом и наоборот.

Ошибки времени Validity

Если notbefore и notafter некорректны, сертификат может считаться недействительным сразу после создания.

Формирование корректного CA подписи в реальных системах

В production сценариях обычно применяются:

  • отдельный root CA ключ (офлайн)
  • intermediate CA для подписи конечных сертификатов
  • ограничение keyUsage на CA уровне
  • хранение приватного ключа в HSM или защищённом хранилище

Jsrsasign позволяет эмулировать такую архитектуру полностью в JavaScript, что удобно для тестирования и генерации сертификатов в dev-средах.

Использование кастомных расширений при подписи

CA может добавлять расширения:

ext: [
    {
        extname: "subjectAltName",
        array: [{ dns: "example.com" }]
    },
    {
        extname: "authorityKeyIdentifier",
        kid: caCert.getPublicKey().pubKeyHex
    }
]

Эти поля влияют на поведение TLS и проверку цепочек доверия.

Контроль криптографической стойкости подписи

Выбор алгоритма влияет на безопасность:

  • SHA-256 + RSA 2048 — минимально допустимый уровень
  • SHA-384 + RSA 3072 — повышенная стойкость
  • ECDSA P-256 — компактная альтернатива RSA

Jsrsasign поддерживает все основные схемы через единый интерфейс sign().

Итоговая модель процесса подписи

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

  1. Формирование ключевой пары субъекта
  2. Определение Issuer (CA)
  3. Создание TBSCertificate
  4. Хэширование структуры
  5. Подпись закрытым ключом CA
  6. Формирование X.509 сертификата
  7. Экспорт PEM/DER
  8. Проверка подписи через публичный ключ CA