Ручная верификация цепочки

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

  • Конечного (end-entity) сертификата — принадлежит серверу или пользователю
  • Промежуточных (intermediate) сертификатов — используются для делегирования доверия
  • Корневого (root) сертификата — самоподписанный и заранее доверенный

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


Представление сертификатов в jsrsasign

Библиотека jsrsasign предоставляет класс X509 для работы с сертификатами. Сертификаты могут быть загружены в формате PEM:

const { X509 } = require('jsrsasign');

const cert = new X509();
cert.readCertPEM(pemString);

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

  • публичного ключа (getPublicKey)
  • издателя (getIssuerString)
  • субъекта (getSubjectString)
  • серийного номера и сроков действия

Извлечение публичного ключа

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

const pubKey = cert.getPublicKey();

Для цепочки:

  • ключ промежуточного сертификата проверяет конечный
  • ключ корневого проверяет промежуточный

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

Метод verifySignature выполняет криптографическую проверку:

const isValid = cert.verifySignature(pubKey);

Однако важно понимать:

  • cert — это проверяемый сертификат
  • pubKey — публичный ключ издателя (issuer)

Пример проверки цепочки из двух сертификатов:

const leaf = new X509();
leaf.readCertPEM(leafPem);

const intermediate = new X509();
intermediate.readCertPEM(intermediatePem);

const intermediateKey = intermediate.getPublicKey();

const valid = leaf.verifySignature(intermediateKey);

Последовательная проверка цепочки

Ручная верификация требует строгого порядка:

  1. Проверка подписи конечного сертификата ключом промежуточного
  2. Проверка подписи промежуточного ключом корневого
  3. Проверка самоподписи корневого сертификата

Пример:

function verifyChain(chain) {
    for (let i = 0; i < chain.length - 1; i++) {
        const current = new X509();
        current.readCertPEM(chain[i]);

        const issuer = new X509();
        issuer.readCertPEM(chain[i + 1]);

        const pubKey = issuer.getPublicKey();

        if (!current.verifySignature(pubKey)) {
            return false;
        }
    }

    // Проверка корневого сертификата
    const root = new X509();
    root.readCertPEM(chain[chain.length - 1]);

    return root.verifySignature(root.getPublicKey());
}

Проверка соответствия Subject и Issuer

Криптографическая проверка недостаточна без проверки логической связности цепочки. Требуется убедиться, что:

  • Issuer текущего сертификата совпадает с Subject следующего
const issuer = current.getIssuerString();
const subject = next.getSubjectString();

if (issuer !== subject) {
    throw new Error("Нарушена цепочка доверия");
}

Проверка срока действия

Каждый сертификат имеет поля:

  • notBefore
  • notAfter

Проверка:

const now = new Date();

const notBefore = cert.getNotBefore();
const notAfter = cert.getNotAfter();

if (now < new Date(notBefore) || now > new Date(notAfter)) {
    throw new Error("Сертификат недействителен по времени");
}

Работа с расширениями (Extensions)

Некоторые критически важные проверки:

Basic Constraints

Определяет, может ли сертификат выступать как CA:

const bc = cert.getExtBasicConstraints();

if (bc.cA !== true && isIntermediate) {
    throw new Error("Сертификат не является удостоверяющим центром");
}

Key Usage

Проверка наличия разрешения на подписание:

const ku = cert.getExtKeyUsageString();

if (!ku.includes("keyCertSign")) {
    throw new Error("Нет права подписи сертификатов");
}

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

Даже корректная цепочка не гарантирует доверия. Корневой сертификат должен:

  • находиться в списке доверенных
  • совпадать по отпечатку (fingerprint)
const trustedFingerprints = [
    "AB:CD:EF:..."
];

const fingerprint = root.getFingerprint();

if (!trustedFingerprints.includes(fingerprint)) {
    throw new Error("Недоверенный корневой сертификат");
}

Проверка CRL и OCSP (ограничения jsrsasign)

jsrsasign не предоставляет полноценной встроенной поддержки:

  • CRL (Certificate Revocation List)
  • OCSP (Online Certificate Status Protocol)

Ручная реализация возможна, но требует:

  • загрузки CRL
  • парсинга ASN.1
  • проверки подписи CRL

Для учебных целей чаще ограничиваются:

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

Полный пример ручной верификации

function validateCertificateChain(chain, trustedRoots) {
    for (let i = 0; i < chain.length - 1; i++) {
        const cert = new X509();
        cert.readCertPEM(chain[i]);

        const issuerCert = new X509();
        issuerCert.readCertPEM(chain[i + 1]);

        // Проверка подписи
        if (!cert.verifySignature(issuerCert.getPublicKey())) {
            return false;
        }

        // Проверка соответствия issuer/subject
        if (cert.getIssuerString() !== issuerCert.getSubjectString()) {
            return false;
        }

        // Проверка срока действия
        const now = new Date();
        if (now < new Date(cert.getNotBefore()) ||
            now > new Date(cert.getNotAfter())) {
            return false;
        }
    }

    // Проверка корневого сертификата
    const root = new X509();
    root.readCertPEM(chain[chain.length - 1]);

    const fingerprint = root.getFingerprint();

    return trustedRoots.includes(fingerprint);
}

Типичные ошибки при ручной верификации

  • Использование неправильного публичного ключа (не того issuer)
  • Игнорирование проверки Subject/Issuer
  • Отсутствие проверки срока действия
  • Принятие любого самоподписанного сертификата как доверенного
  • Пропуск проверки расширений CA

Практическое применение

Ручная верификация используется:

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

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