Signed attributes: contentType, messageDigest, signingTime

В структуре CMS/PKCS

Подписанные атрибуты всегда хэшируются вместе с содержимым косвенным образом: фактически подпись вычисляется не над исходными данными напрямую, а над DER-кодировкой набора атрибутов. Это делает их критически важной частью структуры SignedData.


Структура SignedAttributes в CMS

SignedAttributes представляют собой ASN.1 структуру:

  • тип: SET OF Attribute

  • каждый Attribute содержит:

    • attrType (OID)
    • attrValues (SET OF ANY)

В Jsrsasign они формируются как объект, который затем кодируется в DER перед вычислением подписи.


contentType

OID: 1.2.840.113549.1.9.3

Атрибут contentType указывает тип подписываемого содержимого. Он обязателен в SignedAttributes согласно RFC 5652.

В контексте CMS он определяет, что именно было подписано: данные, вложенный CMS-объект или другой тип структуры.

Типичные значения:

  • 1.2.840.113549.1.7.1 — data
  • 1.2.840.113549.1.7.2 — signedData
  • другие OID в зависимости от типа контента

В Jsrsasign значение задаётся как часть структуры атрибутов:

var signedAttrs = [
  {
    type: "1.2.840.113549.1.9.3",
    value: "1.2.840.113549.1.7.1"
  }
];

При формировании CMS библиотека преобразует это в DER SET и включает в хэш подписи.


messageDigest

OID: 1.2.840.113549.1.9.4

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

Алгоритм:

  1. берётся исходное сообщение
  2. применяется хэш-функция (SHA-256, SHA-1 и т.д.)
  3. результат помещается в атрибут messageDigest

В Jsrsasign это обычно выполняется автоматически, но логика эквивалентна следующей:

var md = new KJUR.crypto.MessageDigest({alg: "sha256"});
md.updateString("data to sign");
var digestHex = md.digest();

Далее digestHex включается в signedAttributes:

var signedAttrs = [
  {
    type: "1.2.840.113549.1.9.4",
    valueHex: digestHex
  }
];

Внутри CMS происходит важная операция: подпись вычисляется не над messageDigest напрямую, а над DER-кодировкой всего SignedAttributes SET, где messageDigest является одним из элементов.

Это защищает от подмены данных между этапом хэширования и подписания.


signingTime

OID: 1.2.840.113549.1.9.5

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

Формат значения — UTCTime или GeneralizedTime в ASN.1.

В Jsrsasign время обычно задаётся автоматически:

var signedAttrs = [
  {
    type: "1.2.840.113549.1.9.5",
    value: new Date()
  }
];

Библиотека сама преобразует JavaScript Date в ASN.1 формат:

  • UTCTime (до 2049 года)
  • GeneralizedTime (после 2049 года)

Пример DER-структуры:

SigningTime ::= Time
Time ::= CHOICE {
    utcTime        UTCTime,
    generalizedTime GeneralizedTime
}

Формирование SignedAttributes в Jsrsasign

В Jsrsasign работа с CMS реализуется через пространство имён KJUR.crypto.CMS.

При создании SignedData библиотека автоматически включает стандартные атрибуты, если используется режим signed attributes.

Пример базового формирования подписи:

var cmsSigned = new KJUR.crypto.CMS.CMSSignedData();

cmsSigned.addSigner({
  cert: certPEM,
  key: privateKeyPEM,
  pass: password,
  digestAlg: "sha256"
});

cmsSigned.updateData("Hello world");

var cms = cmsSigned.sign();

Внутри этого процесса формируется:

  • contentType
  • messageDigest
  • signingTime
  • (опционально) authenticatedAttributes
  • DER SET SignedAttributes

Внутренняя сериализация атрибутов

Перед вычислением подписи Jsrsasign выполняет:

  1. сортировку атрибутов по OID
  2. ASN.1 DER кодирование SET
  3. хэширование DER-блока

Важно, что изменение порядка атрибутов приводит к изменению подписи.

Это соответствует требованиям CMS:

  • SET OF Attribute должен кодироваться DER-канонически
  • порядок элементов влияет на итоговый байтовый поток

Влияние signed attributes на подпись

Подпись вычисляется по следующей модели:

signature = Sign( hash( DER(SignedAttributes) ) )

Таким образом:

  • messageDigest защищает данные
  • contentType фиксирует тип контента
  • signingTime добавляет временную метку
  • весь набор защищён криптографически как единое целое

Проверка signed attributes

При верификации Jsrsasign выполняет обратную процедуру:

  1. извлекает SignedAttributes
  2. повторно вычисляет messageDigest
  3. сравнивает digest с указанным значением
  4. проверяет подпись над DER(SignedAttributes)

Псевдологика:

var md2 = new KJUR.crypto.MessageDigest({alg: "sha256"});
md2.updateString(originalData);
var calcDigest = md2.digest();

if (calcDigest !== messageDigestFromAttr) {
  throw "Digest mismatch";
}

Типовые ошибки при работе с атрибутами

Некорректная работа часто связана с:

  • изменением исходного сообщения после вычисления messageDigest
  • неправильной кодировкой DER (особенно UTF-8 vs Latin1)
  • ручным изменением порядка атрибутов
  • использованием несогласованного алгоритма хэширования

В CMS это приводит к:

  • invalid signature
  • bad signed attributes
  • digest mismatch

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

Jsrsasign скрывает ASN.1 сложность за JavaScript-структурами, но внутри использует:

  • KJUR.asn1.cms.Attribute
  • KJUR.asn1.DERSet
  • KJUR.crypto.Util

Signed attributes формируются автоматически, но могут быть расширены пользовательскими OID.

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

signedAttrs.push({
  type: "1.2.3.4.5.6.7.8.1",
  valueHex: "0a0b0c0d"
});

Роль атрибутов в совместимости S/MIME и CMS

Signed attributes являются обязательным элементом в:

  • S/MIME подписи email
  • CMS Detached signatures
  • PKCS#7 SignedData

Без них многие реализации (OpenSSL, Java BouncyCastle) отклоняют подпись как некорректную.

Особенно критичны:

  • messageDigest (обязателен)
  • contentType (обязателен)
  • signingTime (рекомендуем, но не всегда обязателен)

Контроль целостности через messageDigest

messageDigest выполняет роль контрольной точки:

  • фиксирует состояние данных до подписи
  • предотвращает подмену payload
  • связывает подпись с конкретным хэш-значением

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