Практика: проверка CMS-подписи

CMS (Cryptographic Message Syntax, RFC 5652) в контексте JavaScript-библиотеки jsrsasign представляется в виде структуры PKCS

Структура CMS SignedData

CMS-подпись в формате PKCS#7 включает несколько ключевых компонентов:

  • Signed Content — исходные данные или их хэш
  • SignerInfo — информация о подписанте
  • Certificates — цепочка X.509 сертификатов
  • Signature Value — криптографическая подпись
  • Digest Algorithms — алгоритмы хэширования

В jsrsasign эта структура обрабатывается через ASN.1 парсинг и объектные модели PKCS#7/CMS.

Подготовка окружения jsrsasign

Для работы с CMS используется пакет jsrsasign:

import * as KJUR from 'jsrsasign';

или в браузерной среде:

<script src="jsrsasign-all-min.js"></script>

Основные пространства имён:

  • KJUR.crypto
  • KJUR.asn1.cms
  • KEYUTIL
  • X509

Разбор CMS/PKCS#7 структуры

CMS-подпись обычно поступает в формате PEM или DER. В jsrsasign она декодируется следующим образом:

const cmsData = "-----BEGIN PKCS7-----...-----END PKCS7-----";

const p7 = new KJUR.crypto.PKCS7({ data: cmsData });

При необходимости можно работать с DER:

const p7 = new KJUR.crypto.PKCS7({ hex: cmsHex });

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

  • p7.type — тип контейнера
  • p7.signerInfo — информация о подписи
  • p7.certificates — список сертификатов
  • p7.content — подписанные данные

Извлечение подписанных данных

CMS может содержать либо встроенные данные, либо detached signature.

const content = p7.getContent();

Если подпись detached, исходные данные должны быть переданы отдельно:

const data = "original message";

Проверка подписи CMS

Основной этап — криптографическая верификация подписи.

const result = p7.verify({ data: data });

Возвращаемое значение:

  • true — подпись корректна
  • false — подпись недействительна

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

Подпись CMS не имеет смысла без проверки сертификата.

Извлечение сертификата:

const certs = p7.certificates;
const signerCert = certs[0];

Парсинг X.509:

const x509 = new X509();
x509.readCertHex(signerCert);

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

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

Проверка цепочки доверия

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

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

const isValid = x509.verifySignature(caCert);

В реальных CMS-потоках цепочка может содержать несколько промежуточных сертификатов:

p7.certificates.forEach(certHex => {
    const cert = new X509();
    cert.readCertHex(certHex);
});

Работа с алгоритмами подписи

CMS поддерживает различные алгоритмы:

  • SHA-256 with RSA
  • SHA-384 with ECDSA
  • SHA-512 with RSA

Определение алгоритма:

const alg = p7.signerInfo.signatureAlgorithm.name;

Проверка подписанных атрибутов

В CMS часто используются authenticated attributes:

const attrs = p7.signerInfo.authenticatedAttributes;

Ключевые атрибуты:

  • messageDigest
  • signingTime
  • contentType

Проверка digest:

const md = KJUR.crypto.Util.hashString(data, "sha256");

Полная валидация CMS-подписи

Процесс включает несколько этапов:

1. Парсинг структуры

const p7 = new KJUR.crypto.PKCS7({ data: cmsPem });

2. Извлечение данных

const data = p7.getContent();

3. Проверка подписи

const ok = p7.verify({ data: data });

4. Проверка сертификата

const cert = new X509();
cert.readCertHex(p7.certificates[0]);

5. Проверка цепочки доверия

const trusted = cert.verifySignature(rootCert);

Обработка detached CMS-подписей

Detached CMS не содержит данных внутри структуры:

const p7 = new KJUR.crypto.PKCS7({ data: cmsSignature });

const ok = p7.verify({ data: externalData });

Ошибка в передаче данных приводит к провалу верификации даже при корректной подписи.

Работа с бинарным DER форматом

CMS часто приходит в бинарном виде:

const p7 = new KJUR.crypto.PKCS7({ hex: cmsDerHex });

Конвертация PEM → HEX:

const hex = KJUR.asn1.ASN1Util.pemToHex(pem);

Извлечение информации о подписанте

SignerInfo содержит идентификатор подписанта:

const signer = p7.signerInfo;
const issuer = signer.issuer;
const serial = signer.serialNumber;

Эти данные используются для сопоставления с сертификатом.

Диагностика ошибок верификации

Типичные причины отказа проверки:

  • Несовпадение данных (detached signature mismatch)
  • Повреждённый CMS контейнер
  • Недоверенный сертификат
  • Просроченный сертификат
  • Несоответствие алгоритма подписи

Получение диагностической информации:

try {
    p7.verify({ data: data });
} catch (e) {
    console.log(e.message);
}

Работа с несколькими подписантами

CMS поддерживает множественные подписи:

p7.signerInfos.forEach(si => {
    const alg = si.signatureAlgorithm.name;
});

Каждая подпись проверяется независимо.

Извлечение оригинального содержимого

Если CMS содержит embedded content:

const content = p7.contentInfo.content;

Данные могут быть в формате OCTET STRING и требовать дополнительной декодировки.

Взаимодействие с внешними сертификатами

Добавление доверенного корневого сертификата:

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

Используется при построении trust chain.

Особенности реализации CMS в jsrsasign

  • Полностью JavaScript ASN.1 парсер
  • Поддержка DER и PEM
  • Поддержка PKCS#7/CMS SignedData
  • Работа без нативных криптобиблиотек
  • Кросс-платформенность (браузер / Node.js)

CMS-верификация опирается на:

  • WebCrypto (в некоторых конфигурациях)
  • JS-реализацию RSA/ECDSA
  • ASN.1 декодирование

Пример полного сценария проверки

const cms = new KJUR.crypto.PKCS7({ data: cmsPem });

const content = cms.getContent();

const validSignature = cms.verify({ data: content });

const signerCertHex = cms.certificates[0];

const cert = new X509();
cert.readCertHex(signerCertHex);

const certValid = cert.verifySignature(rootCert);

Каждый этап верификации должен завершаться успешно для признания CMS-подписи валидной.