Верификация SignedData

SignedData в CMS/PKCS


Структура SignedData и логика проверки

SignedData содержит несколько ключевых компонентов:

  • исходные данные (encapContentInfo или detached content)
  • алгоритм хеширования
  • алгоритм подписи
  • значение цифровой подписи
  • сертификат(ы) подписанта
  • дополнительные атрибуты (timestamp, policy и др.)

Верификация заключается в том, чтобы:

  • извлечь подписанные данные
  • вычислить хеш по указанному алгоритму
  • проверить подпись с использованием публичного ключа сертификата
  • убедиться в валидности сертификата

Разбор SignedData в Jsrsasign

Jsrsasign предоставляет объект KJUR.asn1.cms.SignedData, который позволяет декодировать CMS-структуру из PEM или DER.

Основной сценарий начинается с преобразования входных данных:

const cms = new KJUR.asn1.cms.SignedData({ 
  schema: "-----BEGIN CMS----- ... -----END CMS-----"
});

Либо при работе с PKCS#7:

const p7 = new KJUR.asn1.cms.CMSProcessor();
const parsed = p7.parseCMS(signedDataPem);

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

  • signerInfo
  • certificates
  • signedContent
  • digestAlgorithm

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

Каждый объект signerInfo содержит идентификатор подписанта и параметры подписи:

const signerInfo = parsed.signerInfos[0];
const certs = parsed.certificates;

Для сопоставления сертификата используется issuer и serialNumber:

const signerCert = certs.find(cert => {
  return cert.serialNumber === signerInfo.sid.serialNumber &&
         cert.issuer.getString() === signerInfo.sid.issuer.getString();
});

Верификация цифровой подписи

Jsrsasign использует криптографический модуль KJUR.crypto.Signature для проверки подписи.

Алгоритм проверки:

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

Пример:

const sig = new KJUR.crypto.Signature({
  alg: signerInfo.signatureAlgorithm,
  prov: "cryptojs/jsrsa"
});

sig.init(signerCert.getPublicKey());
sig.updateHex(parsed.signedContent.hex);

const isValid = sig.verify(signerInfo.signature);

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


Работа с хеш-алгоритмами

SignedData может использовать различные алгоритмы:

  • SHA-1
  • SHA-256
  • SHA-384
  • SHA-512

Jsrsasign автоматически сопоставляет OID алгоритма с реализацией:

KJUR.crypto.Util.hashString("SHA256", data);

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


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

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

В Jsrsasign можно использовать:

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

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

const isChainValid = x509.verifySignature(issuerCert.getPublicKey());

Цепочка строится рекурсивно до корневого сертификата.


Проверка подписанных данных (detached и embedded)

SignedData бывает двух типов:

  • detached — данные отдельно от подписи
  • embedded — данные включены в контейнер

При detached-сценарии необходимо вручную передать исходный контент:

sig.updateString(originalData);

При embedded:

const content = parsed.signedContent.content;
sig.updateHex(content.hex);

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


Атрибуты подписанта и timestamp

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

  • signingTime
  • messageDigest
  • contentType

Jsrsasign позволяет извлекать атрибуты:

const attrs = signerInfo.signedAttrs;
const signingTime = attrs["signingTime"];

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


Типовые ошибки при верификации SignedData

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

Несовпадение алгоритма хеширования

Подпись может быть создана с SHA-1, а проверка выполняется с SHA-256.

Неверный контент

При detached подписи даже один байт отличия делает подпись недействительной.

Неправильный сертификат

Использование промежуточного или просроченного сертификата приводит к ошибке проверки цепочки.

Повреждённая CMS структура

Некорректное PEM-кодирование или обрезанные данные ломают парсинг ASN.1.


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

const cms = new KJUR.asn1.cms.SignedData({
  schema: signedDataPem
});

const signerInfo = cms.signerInfos[0];
const signerCert = cms.certificates.find(cert =>
  cert.serialNumber === signerInfo.sid.serialNumber
);

const sig = new KJUR.crypto.Signature({
  alg: signerInfo.signatureAlgorithm,
  prov: "cryptojs/jsrsa"
});

sig.init(signerCert.getPublicKey());

if (cms.signedContent.type === "detached") {
  sig.updateString(originalContent);
} else {
  sig.updateHex(cms.signedContent.hex);
}

const verified = sig.verify(signerInfo.signature);

Криптографическая интерпретация результата

Факт успешной проверки подписи означает:

  • данные не изменялись после подписания
  • подпись создана владельцем соответствующего приватного ключа
  • структура CMS корректно интерпретирована

Однако это не гарантирует доверие к субъекту подписи без проверки сертификатной инфраструктуры.


Особенности реализации Jsrsasign

Jsrsasign работает полностью в JavaScript, что накладывает особенности:

  • вычисления выполняются через BigInteger и CryptoJS
  • производительность ниже нативных библиотек
  • гибкость выше за счёт прямого доступа к ASN.1 структурам

При работе с большими CMS контейнерами важно учитывать нагрузку на парсинг ASN.1 и операции хеширования.