Проверка сертификатов TLS вручную

Проверка TLS-сертификатов вручную в JavaScript с использованием Jsrsasign сводится к разбору X.509-структуры, построению цепочки доверия и криптографической проверке подписи каждого звена. В отличие от встроенной в браузеры и Node.js автоматической валидации, здесь все этапы выполняются явно, что позволяет контролировать процесс на уровне протокола и сертификатов.


TLS-сертификат в основе всегда является X.509-объектом, содержащим:

  • субъект (Subject) — кому выдан сертификат
  • издатель (Issuer) — кто подписал сертификат
  • публичный ключ
  • период действия (Not Before / Not After)
  • расширения (SAN, Key Usage, Basic Constraints)
  • цифровую подпись издателя

В Jsrsasign работа с такими объектами осуществляется через модуль X509.

import { X509 } from "jsrsasign";

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

После парсинга доступ к структуре осуществляется через внутренний ASN.1-разбор.


Разбор сертификата сервера

Первый шаг — извлечение базовых данных сертификата сервера.

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

const subject = cert.getSubjectString();
const issuer = cert.getIssuerString();
const notBefore = cert.getNotBefore();
const notAfter = cert.getNotAfter();

Критически важные проверки на этом этапе:

  • срок действия сертификата
  • соответствие доменного имени
  • наличие SAN (Subject Alternative Name)

Проверка соответствия домена (hostname verification)

Современные TLS-соединения игнорируют CN и используют SAN.

function checkHostname(cert, hostname) {
  const sans = cert.getExtSubjectAltName();
  if (sans && sans.includes(hostname)) return true;

  const cn = cert.getSubjectString().match(/CN=([^,]+)/);
  return cn && cn[1] === hostname;
}

В реальной криптографической проверке SAN обязателен, CN используется только как fallback.


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

TLS-сертификат редко является самоподписанным. Обычно цепочка выглядит так:

Server Certificate → Intermediate CA → Root CA

Каждое звено подписано предыдущим.

Для ручной проверки требуется набор доверенных корневых сертификатов:

const trustedRoots = [rootPem1, rootPem2];
const intermediates = [intermediatePem1, intermediatePem2];

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

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

Извлечение ключа издателя

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

const pubKey = issuerCert.getPublicKey();

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

const isValid = cert.verifySignature(pubKey);

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


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

Цепочка проверяется итеративно:

  1. серверный сертификат проверяется ключом intermediate CA
  2. intermediate CA проверяется root CA
  3. root CA проверяется как доверенный якорь
function verifyChain(certPem, intermediates, roots) {
  const cert = new X509();
  cert.readCertPEM(certPem);

  let current = cert;

  for (const interm of intermediates) {
    const issuer = new X509();
    issuer.readCertPEM(interm);

    const ok = current.verifySignature(issuer.getPublicKey());
    if (ok) {
      current = issuer;
    }
  }

  for (const root of roots) {
    const rootCert = new X509();
    rootCert.readCertPEM(root);

    if (current.verifySignature(rootCert.getPublicKey())) {
      return true;
    }
  }

  return false;
}

Проверка ключевых ограничений сертификата

Basic Constraints

Определяет, является ли сертификат CA.

const isCA = cert.getExtBasicConstraints().isCA;

Если серверный сертификат имеет CA=true, это нарушение структуры TLS.


Key Usage

Ограничивает назначение ключа:

  • digitalSignature
  • keyEncipherment
  • certificateSigning
const keyUsage = cert.getExtKeyUsage();

Для серверных сертификатов обязательно наличие digitalSignature или keyEncipherment.


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

const now = new Date();

const valid =
  now >= new Date(cert.getNotBefore()) &&
  now <= new Date(cert.getNotAfter());

Даже корректная цепочка становится недействительной при истёкшем сроке.


Криптографическая природа проверки подписи

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

  • RSA: PKCS#1 v1.5 или PSS
  • ECDSA: проверка через эллиптические кривые
  • SHA-256 / SHA-384 как хеш-функции

Пример RSA-валидации:

const pubKey = issuerCert.getPublicKey();
const verified = cert.verifySignature(pubKey);

Фактически происходит:

  1. вычисление хеша TBSCertificate
  2. расшифровка подписи публичным ключом
  3. сравнение результатов

Извлечение и проверка SAN (Subject Alternative Name)

SAN хранится в расширениях сертификата:

const sans = cert.getExtSubjectAltName();

Пример содержимого:

DNS:www.example.com
DNS:example.com

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

function matchSAN(sans, host) {
  return sans.split(",").some(s => s.includes(host));
}

Разбор полной проверки TLS вручную

Процесс в терминах шагов:

1. Парсинг сертификата сервера

const server = new X509();
server.readCertPEM(serverPem);

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

const now = Date.now();
const validTime =
  now >= Date.parse(server.getNotBefore()) &&
  now <= Date.parse(server.getNotAfter());

3. Проверка домена через SAN

const hostnameOK = checkHostname(server, "example.com");

4. Проверка подписи промежуточным CA

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

const sigOK = server.verifySignature(intermediate.getPublicKey());

5. Проверка цепочки до доверенного root

const root = new X509();
root.readCertPEM(rootPem);

const trusted = intermediate.verifySignature(root.getPublicKey());

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

Самоподписанный сертификат — это случай, когда Issuer == Subject.

const isSelfSigned =
  cert.getIssuerString() === cert.getSubjectString();

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

const ok = cert.verifySignature(cert.getPublicKey());

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


Типичные причины провала проверки

  • несовпадение hostname и SAN
  • истёкший срок действия
  • отсутствует промежуточный сертификат
  • неправильная цепочка доверия
  • неподдерживаемый алгоритм подписи
  • повреждённая PEM-структура

Особенности Jsrsasign при работе с TLS

  • работает полностью в JavaScript без нативных модулей
  • поддерживает X.509 v1–v3
  • позволяет вручную строить trust store
  • не выполняет автоматический OCSP/CRL чек (требует внешней реализации)
  • подходит для браузерных и Node.js сценариев

Минимальная схема ручной валидации

function validateTLS(serverPem, intermPem, rootPem, host) {
  const server = new X509();
  server.readCertPEM(serverPem);

  if (!checkHostname(server, host)) return false;

  if (!verifyChain(serverPem, intermPem, rootPem)) return false;

  const now = Date.now();
  if (
    now < Date.parse(server.getNotBefore()) ||
    now > Date.parse(server.getNotAfter())
  ) return false;

  return true;
}