Проверка CN и SAN

В инфраструктуре открытых ключей (PKI) идентификация субъекта сертификата осуществляется через поля Distinguished Name (DN) и расширения. Исторически основным полем для указания доменного имени выступал Common Name (CN), однако с развитием стандартов и требований безопасности ключевую роль взяло на себя расширение Subject Alternative Name (SAN).

  • CN (Common Name) — часть DN, традиционно содержащая основное доменное имя.
  • SAN (Subject Alternative Name) — расширение, позволяющее указать список альтернативных идентификаторов: домены, поддомены, IP-адреса, email.

Современные стандарты (например, RFC 6125) предписывают использовать SAN как основной источник проверки имени хоста, а CN рассматривается как устаревший fallback.


Структура сертификата и расположение CN/SAN

В библиотеке Jsrsasign сертификат представляется как ASN.1-структура, закодированная в DER и обычно передаваемая в PEM-формате.

  • CN находится внутри:

    tbsCertificate.subject
  • SAN располагается в расширениях:

    tbsCertificate.extensions.subjectAltName

Для извлечения этих данных используется класс X509.


Загрузка и парсинг сертификата в Jsrsasign

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

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

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

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


Извлечение CN

Метод getSubjectString() возвращает строковое представление DN:

const subject = x509.getSubjectString();

Пример результата:

/C=US/O=Example Corp/CN=www.example.com

Для извлечения CN:

const cnMatch = subject.match(/CN=([^\/]+)/);
const commonName = cnMatch ? cnMatch[1] : null;

Особенности:

  • Возможны множественные CN (редко, но допустимо)
  • Формат строки зависит от порядка атрибутов

Альтернативный способ — использование ASN.1 разбора:

const subjectObj = x509.getSubject();

Этот метод возвращает объект с атрибутами, где CN можно получить напрямую.


Извлечение SAN

Jsrsasign предоставляет метод:

const san = x509.getExtSubjectAltName();

Результат — массив объектов:

[
  { type: 2, value: 'example.com' },
  { type: 2, value: 'www.example.com' },
  { type: 7, ip: '192.168.1.1' }
]

Типы:

  • 2 — DNS имя
  • 7 — IP-адрес
  • 1 — email

Фильтрация DNS-имен:

const dnsNames = san
  .filter(entry => entry.type === 2)
  .map(entry => entry.value);

Приоритет SAN над CN

Современная логика проверки:

  1. Если SAN присутствует:

    • Проверка выполняется только по SAN
    • CN игнорируется
  2. Если SAN отсутствует:

    • Используется CN

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


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

Пример функции проверки:

function matchHostname(hostname, cert) {
  const x509 = new X509();
  x509.readCertPEM(cert);

  const san = x509.getExtSubjectAltName();

  if (san) {
    const dnsNames = san
      .filter(e => e.type === 2)
      .map(e => e.value);

    return dnsNames.some(name => matchPattern(hostname, name));
  }

  const subject = x509.getSubjectString();
  const cnMatch = subject.match(/CN=([^\/]+)/);
  const cn = cnMatch ? cnMatch[1] : null;

  return cn ? matchPattern(hostname, cn) : false;
}

Поддержка wildcard-доменов

Wildcard-сертификаты (*.example.com) требуют особой логики:

function matchPattern(hostname, pattern) {
  if (pattern.startsWith('*.')) {
    const domain = pattern.slice(2);
    return hostname.endsWith(domain) &&
           hostname.split('.').length === domain.split('.').length + 1;
  }
  return hostname === pattern;
}

Особенности:

  • *.example.com соответствует sub.example.com
  • Не соответствует deep.sub.example.com
  • Не соответствует example.com

Проверка IP-адресов

Если подключение осуществляется по IP, необходимо:

const ipEntries = san.filter(e => e.type === 7).map(e => e.ip);

const isValid = ipEntries.includes(targetIP);

CN в этом случае не используется.


Ошибки и уязвимости

Игнорирование SAN

  • Приводит к уязвимостям MITM
  • Нарушает современные стандарты

Неправильная обработка wildcard

  • Разрешение *.com или *.co.uk недопустимо
  • Требуется ограничение уровня домена

Сравнение без нормализации

  • Домены должны сравниваться в нижнем регистре
  • IDN-домены требуют преобразования (Punycode)

Нормализация доменных имен

function normalize(host) {
  return host.toLowerCase();
}

Для IDN:

const punycode = require('punycode/');
const ascii = punycode.toASCII(hostname);

Проверка наличия SAN

const hasSAN = !!x509.getExtSubjectAltName();

Если SAN отсутствует — сертификат считается устаревшим, но может использоваться в legacy-сценариях.


ASN.1 представление SAN

Расширение SAN в ASN.1:

SubjectAltName ::= GeneralNames

GeneralNames ::= SEQUENCE OF GeneralName

GeneralName ::= CHOICE {
  dNSName       [2] IA5String,
  iPAddress     [7] OCTET STRING,
  ...
}

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


Валидация цепочки и роль CN/SAN

Проверка CN/SAN — только часть полной валидации:

  1. Проверка подписи сертификата
  2. Проверка цепочки доверия
  3. Проверка срока действия
  4. Проверка отзыва (CRL/OCSP)
  5. Проверка имени (CN/SAN)

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


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

function validateCertificate(certPEM, hostname) {
  const x509 = new X509();
  x509.readCertPEM(certPEM);

  const san = x509.getExtSubjectAltName();

  let valid = false;

  if (san) {
    const dnsNames = san
      .filter(e => e.type === 2)
      .map(e => e.value.toLowerCase());

    valid = dnsNames.some(name => matchPattern(hostname, name));
  } else {
    const subject = x509.getSubjectString();
    const cnMatch = subject.match(/CN=([^\/]+)/);
    const cn = cnMatch ? cnMatch[1].toLowerCase() : null;

    valid = cn ? matchPattern(hostname, cn) : false;
  }

  return valid;
}

Особенности работы в браузере и Node.js

  • В Node.js часто используется встроенный TLS, где проверка уже реализована

  • Jsrsasign применяется для:

    • кастомной валидации
    • анализа сертификатов
    • работы с нестандартными протоколами

В браузере:

  • Используется для криптографии на клиенте
  • Проверка CN/SAN может применяться в WebPKI-подобных системах

Ограничения Jsrsasign

  • Нет встроенной функции проверки hostname
  • Нет полной реализации RFC 6125
  • Требуется ручная реализация wildcard и SAN-логики

Это дает гибкость, но требует строгого соблюдения стандартов при разработке.


Расширенные случаи SAN

SAN может содержать:

  • URI (type: 6)
  • email (type: 1)
  • directoryName (type: 4)

Jsrsasign возвращает их в общем массиве, что позволяет реализовать специфическую логику проверки.


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

  • Парсинг сертификата выполняется один раз
  • Повторное использование объекта X509 снижает нагрузку
  • Регулярные выражения для CN лучше компилировать заранее

Тестирование проверки CN/SAN

Рекомендуется использовать:

  • Сертификаты с:

    • только CN
    • только SAN
    • wildcard SAN
    • IP SAN
  • Негативные кейсы:

    • несовпадающий домен
    • вложенные поддомены
    • некорректный wildcard

Безопасные практики

  • Всегда проверять SAN в первую очередь
  • Не доверять CN при наличии SAN
  • Ограничивать wildcard
  • Нормализовать входные данные
  • Учитывать IDN

Эти правила критичны для предотвращения атак и соответствия современным стандартам TLS.