Подпись PDF через PKCS#7 и внешние инструменты

PKCS

PDF-подпись в формате PKCS#7 обычно используется в режиме detached signature: хэш документа вычисляется отдельно, затем подписывается приватным ключом, после чего формируется CMS-структура, которая встраивается в PDF или передаётся отдельно как внешняя подпись.

Структура PKCS#7 в контексте PDF

PKCS#7 (CMS SignedData) включает несколько ключевых компонентов:

  • SignedData

    • version
    • digestAlgorithms
    • encapContentInfo (обычно отсутствует содержимое — detached)
    • certificates (цепочка сертификатов)
    • signerInfos (подписи)

В PDF-контексте важно, что поле encapContentInfo.eContent чаще всего отсутствует, поскольку документ уже существует, и подписывается его хэш.

Jsrsasign предоставляет низкоуровневые средства для формирования CMS через:

  • KJUR.asn1.cms.SignedData
  • KJUR.crypto.CMS

Однако в реальных сценариях чаще используется ручная сборка через KJUR.crypto.Signature + ASN.1-структуры.

Подготовка PDF к подписи: ByteRange

PDF не подписывается целиком. Формируется специальная структура:

  • В документ вставляется placeholder для подписи (обычно /Contents)
  • Определяется массив /ByteRange, например:
[0 123456 234567 89012]

Это означает:

  • от байта 0 до начала подписи
  • пропуск области подписи
  • продолжение после неё

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

PDF_part1 + PDF_part2

Вычисление хэша

Jsrsasign работает с бинарными строками и hex-данными:

const sha256 = new KJUR.crypto.MessageDigest({alg: "sha256", prov: "cryptojs"});
sha256.updateHex(part1Hex);
sha256.updateHex(part2Hex);
const digest = sha256.digest();

В PDF-подписи важно, что хэш считается не от текста, а от байтового представления файла.

Формирование CMS/PKCS#7 через Jsrsasign

После получения хэша формируется структура SignedData.

Базовый пример создания CMS

const cms = new KJUR.asn1.cms.SignedData({
  version: 1,
  digestAlg: ["sha256"],
  contentInfo: {
    type: "data",
    content: null
  },
  certs: [certPem],
  signerInfo: [{
    version: 1,
    sid: {type: "issuerAndSerialNumber", cert: certObj},
    digestAlg: "sha256",
    signAlg: "SHA256withRSA",
    signature: signatureHex
  }]
});

const cmsHex = cms.getContentInfoEncodedHex();

В реальной PDF-подписи signatureHex не вычисляется напрямую из CMS — он получается отдельно через криптографическую подпись хэша.

Подписание хэша приватным ключом

Jsrsasign предоставляет универсальный интерфейс KJUR.crypto.Signature:

const sig = new KJUR.crypto.Signature({alg: "SHA256withRSA"});
sig.init(privateKeyPem);
sig.updateHex(digest);
const signatureHex = sig.sign();

Этот шаг является критическим: PDF требует подпись именно хэша ByteRange, а не всей структуры CMS.

Сборка PKCS#7 для PDF

После получения:

  • digest (SHA-256)
  • signatureHex
  • certificate chain

формируется PKCS#7 контейнер.

Пример через CMS SignedData:

const sd = new KJUR.asn1.cms.SignedData({
  version: 1,
  digestAlg: ["sha256"],
  contentInfo: {type: "data"},
  certs: [certPem],
  signerInfo: [{
    version: 1,
    sid: {type: "issuerAndSerialNumber", cert: certObj},
    digestAlg: "sha256",
    signAlg: "SHA256withRSA",
    signature: signatureHex
  }]
});

const pkcs7Hex = sd.getContentInfoEncodedHex();

Результат — DER-кодированный PKCS#7, который может быть вставлен в PDF как /Contents.

Встраивание подписи в PDF

PDF требует фиксированного размера поля подписи. Обычно:

  • создаётся placeholder (например, 8192 байт hex-заполнителя)
  • после генерации PKCS#7 результат заменяет placeholder

Пример логики:

pdf.replaceSignature({
  byteRange: [0, offset1, offset2, offset3],
  signature: pkcs7Hex
});

Важно, что PKCS#7 часто кодируется в HEX и дополняется нулями до размера поля /Contents.

Использование внешних инструментов

Jsrsasign редко используется изолированно в PDF-подписании. Чаще применяется связка с внешними инструментами для подготовки или валидации.

OpenSSL: генерация сертификатов и подписи

Создание ключей:

openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out req.csr
openssl x509 -req -in req.csr -signkey key.pem -out cert.pem

Генерация PKCS#7:

openssl smime -sign -binary -in hash.bin -signer cert.pem -inkey key.pem -outform DER -nodetach -out signature.p7s

Этот вариант часто используется как эталонный для сравнения с Jsrsasign.

Adobe-совместимость

PDF-экосистема ожидает строгую структуру:

  • DER-encoded CMS
  • правильный порядок сертификатов
  • корректный digest algorithm OID
  • отсутствие лишних encapsulated content

Jsrsasign позволяет формировать совместимые структуры, но требует точного соответствия ByteRange и encoding.

Особенности работы с SHA-2 и RSA

Современные PDF требуют:

  • SHA-256 или выше
  • RSA 2048+ или ECDSA P-256

Jsrsasign поддерживает алгоритмы:

  • SHA256withRSA
  • SHA384withRSA
  • SHA256withECDSA

Пример выбора алгоритма:

const sig = new KJUR.crypto.Signature({alg: "SHA256withRSA"});

Частые проблемы интеграции

Несовпадение ByteRange

Любое смещение байтов делает подпись недействительной. Даже изменение пробела ломает проверку.

Неверное заполнение /Contents

PDF требует фиксированного размера поля. Если PKCS#7 меньше, остаток заполняется 0x00 или 0xFF.

Кодировка DER vs PEM

PKCS#7 должен быть DER (hex), а не PEM. Jsrsasign часто возвращает hex-строку, которую нужно корректно вставлять в PDF.

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

Некоторые валидаторы требуют полный chain:

  • end-entity cert
  • intermediate CA
  • root (опционально)

Jsrsasign позволяет передать массив certs.

Расширенный сценарий: внешняя подпись

В распределённых системах PDF подписывается внешним сервисом:

  1. Клиент формирует ByteRange
  2. Отправляет хэш на сервер подписи
  3. Сервер (Jsrsasign + HSM или OpenSSL) возвращает PKCS#7
  4. Клиент встраивает подпись в PDF

Пример серверной подписи:

const sig = new KJUR.crypto.Signature({alg: "SHA256withRSA"});
sig.init(privateKey);
sig.updateHex(hash);
const signed = sig.sign();

Возвращается:

{
  "pkcs7": "308203..."
}

CMS SignedData vs простая PKCS#7 подпись

Jsrsasign поддерживает два подхода:

  • низкоуровневый Signature + ручная сборка CMS
  • высокоуровневый KJUR.asn1.cms.SignedData

CMS предпочтительнее, так как:

  • поддерживает multiple signers
  • корректно формирует структуры ASN.1
  • совместим с Adobe Acrobat

Контроль корректности подписи

Проверка включает:

  • валидацию DER структуры
  • проверку цепочки сертификатов
  • сверку digestAlgorithm
  • контроль ByteRange

Jsrsasign может использоваться и для проверки:

const cms = new KJUR.asn1.cms.SignedData({hex: pkcs7Hex});
const result = cms.verify();

Совместное использование Jsrsasign и внешних HSM

В корпоративных сценариях приватный ключ не используется напрямую:

  • Jsrsasign формирует хэш
  • HSM подписывает его
  • возвращается raw signature
  • Jsrsasign собирает CMS

Такая архитектура повышает безопасность и соответствует требованиям PKI-инфраструктур.

Практическая модель потока подписи

Общий процесс выглядит как последовательность:

  1. Парсинг PDF
  2. Вставка placeholder подписи
  3. Вычисление ByteRange
  4. Хэширование фрагментов
  5. Подпись хэша (Jsrsasign или внешняя система)
  6. Формирование PKCS#7 (CMS SignedData)
  7. Встраивание DER в /Contents
  8. Финализация PDF

Каждый шаг критичен, но наибольшая чувствительность приходится на согласованность ByteRange и CMS-структуры.