Подписание PDF по профилю PAdES B-B с последующим расширением до PAdES B-T
Используйте этот рецепт, чтобы создать подпись Portable Document Format (PDF) Advanced Electronic Signatures (PAdES) B-B: объект Cryptographic Message Syntax (CMS) SignedData с подписанными атрибутами (content-type, message-digest, signing-time). Затем расширьте эту подпись до PAdES B-T, добавив одну метку времени подписи RFC 3161. B-T — это B-B плюс одна метка времени; это не отдельный класс подписи. Граница доверия описана явно: создать подпись — не то же самое, что признать её действительной на стороне проверки.
Оговорка U-1. NextPDF не заявляет о какой-либо независимой сертификации ETSI EN 319 142-1 для PAdES B-T. EN 319 142-1 не входит в проверочный корпус; требование к B-T
signature-time-stampбыло проверено по ETSI EN 319 122-1 §5.3 совместно с RFC 3161, RFC 5652, RFC 5816 и ISO 32000-2 §12.8. Поддержка профиля B-T не является сертификацией соответствия или юридической действительности; такое заключение выносит независимый валидатор.
B-LT и B-LTA (материал проверки Document Security Store (DSS), цикл архивных меток времени) выходят за рамки этого рецепта и не входят в поверхность подписания Core/Pro, рассматриваемую здесь.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3ext-openssl должно быть включено, потому что CertificateInfo разбирает ключи через OpenSSL. Для B-T также нужна доступная конечная точка Time Stamping Authority (TSA) RFC 3161 и HTTP-клиент PHP Standards Recommendation (PSR)-18.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Подпись PAdES B-B хранит объект CMS SignedData в кодировке Distinguished Encoding Rules (DER) в записи Contents словаря подписи; значение Contents — шестнадцатеричная строка, дополненная по дайджесту диапазона байтов (ISO 32000-2 §12.8.1). Дайджест охватывает файл и исключает само значение подписи (ISO 32000-2 §12.8.1).
PAdES B-T добавляет ровно одну метку времени подписи RFC 3161. Отпечаток сообщения для этой метки времени — хеш октетов значения подписи SignerInfo без тега или префикса длины Abstract Syntax Notation One (ASN.1) (ETSI EN 319 122-1 §5.3; RFC 3161 Appendix A). Токен передаётся как несподписанный атрибут id-aa-timeStampToken, object identifier (OID) 1.2.840.113549.1.9.16.2.14 (RFC 3161 Appendix A), размещённый в SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Поскольку несподписанные атрибуты не защищены подписью (RFC 5652 §5.4), подписанный дайджест B-B, /ByteRange и байты подписи B-B остаются неизменными — B-T только дописывает метку времени. Сертификат TSA идентифицируется через ESSCertIDv2 (RFC 5816 обновляет RFC 3161).
Оговорка U-1 (повторена при заявлении о B-T). NextPDF не заявляет о какой-либо независимой сертификации ETSI EN 319 142-1 для PAdES B-T. EN 319 142-1 не входит в проверочный корпус; требование к B-T
signature-time-stampбыло проверено по ETSI EN 319 122-1 §5.3 совместно с RFC 3161, RFC 5652, RFC 5816 и ISO 32000-2 §12.8. Поддержка профиля B-T не является сертификацией соответствия или юридической действительности; независимый валидатор выносит такое заключение.
SignatureLevel::PAdES_B_T доступен в Core: SignatureLevel::PAdES_B_T->requiresTimestamp() равно true, ->isAvailableInEnvironment() равно true, а ->requiresDss() равно false — B-T не подключает Document Security Store. B-T ≠ B-LT ≠ B-LTA: метка времени подписи не добавляет материал проверки или архивную метку времени; это отдельные более высокие уровни, которые здесь не создаются.
Схема ниже показывает поток B-B, а затем B-T в том порядке, в котором его выполняет движок. ByteRange вычисляется только после записи всего файла, поэтому итоговые смещения не могут изменить хешируемые байты. Затем B-T дописывает один токен RFC 3161 как несподписанный атрибут, не меняя подписанный дайджест B-B.
Поверхность API
Заголовок раздела «Поверхность API»Точка входа для конфигурации — Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Этот вызов фиксирует намерение подписать документ. Криптографическую подпись создаёт движок подписания Core PAdES (NextPDF\Security\Signature\DigitalSigner). Поскольку интеграционный набор тестов задействует этот движок, а исполняемый пример использует его напрямую, результат — реальный, разбираемый объект CMS. Для SignatureLevel::PAdES_B_T нужен ненулевой TsaClient; создание подписывающего объекта B-T без него выбрасывает SignatureException.
Высокоуровневый API — один вызов, подписанный результат
Заголовок раздела «Высокоуровневый API — один вызов, подписанный результат»Самый быстрый путь — высокоуровневый API: настройте подпись в документе, затем сериализуйте его. Под капотом используется тот же движок Core PAdES (DigitalSigner). Это лишь тонкая обёртка над более низкоуровневым разбором ниже, а не отдельный путь кода.
<?php
declare(strict_types=1);
use NextPDF\Core\Document;use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\SignatureLevel;use NextPDF\Security\Timestamp\TsaClient;
$certInfo = CertificateInfo::fromPkcs12( p12Path: __DIR__ . '/signer.p12', password: 'p12-passphrase',);
// PAdES B-B end to end: configure, then serialise.$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', '', 12);$doc->cell(0, 10, 'Signed end to end.', newLine: true);$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);$doc->save(__DIR__ . '/signed.pdf'); // or output() to stream, getPdfData() for bytes
// PAdES B-T: pass a TsaClient on the same call — one RFC 3161// signature-time-stamp is added (see the TsaClient hardening notes below).$doc->setSignature( certInfo: $certInfo, level: SignatureLevel::PAdES_B_T, tsaClient: $tsa,);$doc->save(__DIR__ . '/signed-bt.pdf');Как и output() и getPdfData(), save() записывает запись /Contents как CMS SignedData в кодировке DER под SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). Результат проверяем на уровне CMS: это корректно сформированный объект CMS SignedData, который может прочитать парсер CMS. Это не то же самое, что соответствие базовому профилю ETSI EN 319 142-1 или юридическая действительность; такие заключения выносит независимый валидатор (см. оговорку U-1 выше). Для B-T высокоуровневый вызов добавляет ровно ту единственную метку времени подписи RFC 3161 signature-time-stamp, которая описана в концептуальном обзоре; единственное отличие от B-B — передача TsaClient.
Используйте более низкоуровневый разбор DigitalSigner ниже, когда вам нужен прямой контроль над алгоритмом, данными диапазона байтов или SignatureResult.
Пример кода — Быстрый старт
Заголовок раздела «Пример кода — Быстрый старт»<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\DigitalSigner;use NextPDF\Security\Signature\SignatureAlgorithm;use NextPDF\Security\Signature\SignatureLevel;
$certInfo = CertificateInfo::fromPkcs12( p12Path: __DIR__ . '/signer.p12', password: 'p12-passphrase',);
// PAdES B-B — a CMS SignedData, no timestamp.$signer = new DigitalSigner( certInfo: $certInfo, level: SignatureLevel::PAdES_B_B, algorithm: SignatureAlgorithm::Pkcs1v15,);$result = $signer->sign($byteRangeData);
echo $result->hasTimestamp() ? "B-T\n" : "B-B (no timestamp)\n";Пример кода — Рабочая среда
Заголовок раздела «Пример кода — Рабочая среда»Эта автономная программа выполняется в среде cookbook. Она повторяет examples/36-sign-pades-b-b-and-b-t.php. Программа создаёт документ, настраивает для него подпись PAdES, затем подписывает на уровне B-B и ещё раз на B-T с клиентом TSA. В рабочей среде направьте TsaClient на реальную конечную точку RFC 3161 через защищённый клиент PSR-18: HTTP-клиент с учётом безопасности, который закрепляет SubjectPublicKeyInfo (SPKI) TSA и безопасно разрешает Domain Name System (DNS). Чтобы программа оставалась автономной и детерминированной, она внедряет тестовый фиктивный клиент TSA из репозитория. Фиктивный клиент TSA возвращает структурно корректный RFC 3161 TimeStampResp.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\DigitalSigner;use NextPDF\Security\Signature\SignatureAlgorithm;use NextPDF\Security\Signature\SignatureLevel;use NextPDF\Security\Timestamp\TsaClient;use NextPDF\Tests\Support\FakeTsaHttpClient;
// In your application, build CertificateInfo from your own signing material:// CertificateInfo::fromPkcs12($p12Path, $passphrase) — a .p12/.pfx bundle// CertificateInfo::fromFiles($certPem, $keyPem, $pass) — separate PEM files// This program uses the repository RSA-2048 test fixtures so it is offline.$certDir = __DIR__ . '/tests/Fixtures/Certificates';$certPath = $certDir . '/test-rsa-2048-cert.pem';$keyPath = $certDir . '/test-rsa-2048-key.pem';
if (!is_file($certPath) || !is_file($keyPath)) { fwrite(STDERR, "Certificate fixtures absent. Run tests/Fixtures/Certificates/generate.sh\n"); exit(1);}
$certInfo = new CertificateInfo( certificate: (string) file_get_contents($certPath), privateKey: (string) file_get_contents($keyPath),);
// Build the document and record the signing intent on it. The ByteRange// digest input is the document bytes with the /Contents placeholder// excluded (ISO 32000-2 §12.8); getPdfData() yields the bytes to hash.$doc = Document::createStandalone();$doc->setTitle('Signed Invoice 2026-0042');$doc->setAuthor('NextPDF Cookbook');$doc->addPage();$doc->setFont('helvetica', '', 12);$doc->cell(0, 10, 'This document is configured for a PAdES signature.', newLine: true);$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$byteRangeData = $doc->getPdfData();
// --- PAdES B-B: a CMS SignedData, no timestamp ---$bb = (new DigitalSigner( certInfo: $certInfo, level: SignatureLevel::PAdES_B_B, algorithm: SignatureAlgorithm::Pkcs1v15,))->sign($byteRangeData);
// --- PAdES B-T: B-B + one RFC 3161 signature-time-stamp ---// In production, build the TsaClient with your TSA endpoint and a hardened// PSR-18 client (use the security-aware HTTP client for SSRF/DNS pinning):// $tsa = new TsaClient(// tsaUrl: 'https://tsa.example.com/timestamp',// httpClient: $hardenedPsr18Client,// );// Here the offline fake TSA client keeps the program network-free.$tsa = new TsaClient( tsaUrl: 'https://tsa.example.com/timestamp', httpClient: new FakeTsaHttpClient(),);$bt = (new DigitalSigner( certInfo: $certInfo, tsaClient: $tsa, level: SignatureLevel::PAdES_B_T, algorithm: SignatureAlgorithm::Pkcs1v15,))->sign($byteRangeData);
// B-T = B-B + a single timestamp token. The B-B signed digest is unchanged;// $bt->timestampToken holds the DER-encoded RFC 3161 token.printf("PAdES B-B CMS: %d bytes, timestamp=%s\n", $bb->getSize(), $bb->hasTimestamp() ? 'yes' : 'no');printf( "PAdES B-T CMS: %d bytes, timestamp=%s (%d-byte RFC 3161 token)\n", $bt->getSize(), $bt->hasTimestamp() ? 'yes' : 'no', strlen($bt->timestampToken),);echo "B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the// semantic profile (the signed CMS/timestamp bytes are inherently// non-reproducible and are asserted by the PHPUnit harness, not a byte hash).$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/signed-invoice.pdf', $byteRangeData);Ожидаемый STDOUT (размеры зависят от сертификата и токена TSA):
PAdES B-B CMS: <n> bytes, timestamp=noPAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).Оговорка U-1 (размещена рядом с заявлением о B-T в рабочей среде). NextPDF не заявляет о какой-либо независимой сертификации ETSI EN 319 142-1 для PAdES B-T. EN 319 142-1 не входит в проверочный корпус; требование к B-T
signature-time-stampбыло проверено по ETSI EN 319 122-1 §5.3 совместно с RFC 3161, RFC 5652, RFC 5816 и ISO 32000-2 §12.8. Поддержка профиля B-T не является сертификацией соответствия или юридической действительности; такое заключение выносит независимый валидатор.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- B-T без клиента TSA. Создание B-T
DigitalSignerбезTsaClientвыбрасываетSignatureException(для B-T требуется TSA). Защитите конфигурацию TSA перед подписанием. - Доступность TSA. Для каждой подписи B-T выполняет живой обмен RFC 3161. Простой TSA означает, что подпись B-T не будет создана. Используйте предохранитель (circuit breaker) и соглашение об уровне обслуживания (SLA) TSA, соответствующее вашей пропускной способности;
TsaClientпринимает предохранитель. - Защита HTTP-клиента TSA. Направьте
TsaClientна клиент PSR-18, который закрепляет SubjectPublicKeyInfo (SPKI, формат RFC 7469) сертификата TSA и безопасно разрешает Domain Name System (DNS);TsaClient::extractPublicKeyPin()получает закрепление из сертификата TSA. - B-T — это не B-LT/B-LTA. Метка времени подписи не встраивает материал проверки (сертификаты, Online Certificate Status Protocol (OCSP), список отзыва сертификатов (CRL)) или архивную метку времени. Это уровни B-LT/B-LTA; этот рецепт их не создаёт.
- Конфликт линеаризации.
enableLinearization()и настроенная подпись взаимоисключают друг друга — любой из вызовов выбрасываетInvalidConfigException, когда другой уже задан. - Ключи HSM. Для ключа, хранящегося в аппаратном модуле безопасности (HSM), создавайте
CertificateInfoчерезCertificateInfo::fromHsm(); закрытый ключ никогда не попадает в память процесса. Контракт подписывающего объекта PKCS#11 относится к Core; рабочий провайдер — Premium.
Производительность
Заголовок раздела «Производительность»Подпись B-B — локальная операция CMS. B-T добавляет один синхронный HTTP-обмен RFC 3161 с TSA на каждую подпись. Для пакетных нагрузок закладывайте задержку TSA и ограничения по частоте. Используйте защищённый предохранителем TsaClient.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»Созданная подпись — ещё не доверенная подпись. Пройдёт ли подпись проверку, зависит от сертификата, его якоря доверия и политики проверяющей стороны; всё это находится вне этой библиотеки. Шифрование защищает конфиденциальность, а не целостность; подписание защищает целостность и подлинность, а не конфиденциальность. Считайте хранение ключа основным риском: программный ключ в памяти процесса настолько же безопасен, насколько безопасен хост.
Резидентность данных и меры по защите ПДн
Заголовок раздела «Резидентность данных и меры по защите ПДн»Операция подписания выполняется внутри процесса; байты документа и закрытый ключ не покидают хост, за исключением обмена с TSA для B-T. В этом обмене отправляется только отпечаток сообщения (хеш значения подписи), но никогда содержимое документа (RFC 3161 §2.4.1 MessageImprint). Текст документа и персональные данные (PII) не передаются в TSA. Выбирайте TSA в юрисдикции, которая соответствует вашей политике резидентности данных.
Безопасная телеметрия и очистка журналов
Заголовок раздела «Безопасная телеметрия и очистка журналов»DigitalSigner принимает необязательный логгер PSR-3. Он записывает в журнал алгоритм и уровень, но не ключевой материал и не байты подписи. Параметры password в CertificateInfo и TsaClient помечены #[SensitiveParameter], поэтому парольные фразы скрываются в трассировках стека. Не записывайте в журнал SignatureResult::$cmsSignedData или $timestampToken.
Модель угроз
Заголовок раздела «Модель угроз»Учтено: подмена входных данных после подписания (обнаруживается дайджестом диапазона байтов), компрометация ключа (вне рамок библиотеки, поскольку хранение ключа — ответственность интегратора), выдача себя за TSA (смягчается закреплением SPKI на HTTP-клиенте TSA) и понижение уровня между уровнями (перечисление уровней явное; движок не понижает B-T до B-B молча). Не утверждается, что уязвимости отсутствуют или что любая полученная подпись юридически действительна.
Поведение в режиме FIPS
Заголовок раздела «Поведение в режиме FIPS»Примитивы подписания предоставляет OpenSSL. В сборке OpenSSL, прошедшей валидацию Federal Information Processing Standards (FIPS), операции RSA/ECDSA и SHA-256 выполняются через провайдер FIPS; NextPDF сам по себе не заявляет о валидации FIPS. CryptoCapabilities сообщает о доступных на хосте примитивах; проверьте цепочку провайдеров OpenSSL в вашем развёртывании.
Соответствие
Заголовок раздела «Соответствие»| Утверждение | Спецификация | Пункт | reference_id (ссылочный идентификатор) |
|---|---|---|---|
| Дайджест диапазона байтов охватывает файл и исключает значение подписи. | ISO 32000-2 | §12.8.1 | |
Contents содержит DER CMS SignedData; Contents метки времени документа содержит TimeStampToken. | ISO 32000-2 | §12.8.1 | |
Contents — шестнадцатеричная строка, дополненная по дайджесту диапазона байтов. | ISO 32000-2 | §12.8.1 | |
| Отпечаток signature-time-stamp — это хеш октетов значения подписи SignerInfo (без тега/длины ASN.1 tag/length). | ETSI EN 319 122-1 | §5.3 | |
| Значение signature-time-stamp — это SignatureTimeStampToken. | ETSI EN 319 122-1 | §6 | |
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }. | RFC 3161 | §2.4.1 | |
Отпечаток метки времени подписи — это хеш поля подписи SignerInfo; SignatureTimeStampToken ::= TimeStampToken. | RFC 3161 | Прил. A | |
id-aa-timeStampToken — это OID, равный 1.2.840.113549.1.9.16.2.14. | RFC 3161 | Прил. A | |
SignerInfo содержит unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL. | RFC 5652 | §5.3 | |
| Несподписанные атрибуты не защищены подписью; подписанный дайджест B-B остаётся неизменным. | RFC 5652 | §5.4 | |
| RFC 5816 обновляет RFC 3161; ESSCertIDv2 идентифицирует сертификат TSA без SHA-1. | RFC 5816 | §1 |
Этот рецепт описывает, как NextPDF создаёт подпись B-B и B-T. Он не утверждает, что любая полученная подпись юридически действительна или что достигнуто соответствие PAdES; такие заключения выносит независимый валидатор.
Коммерческий контекст
Заголовок раздела «Коммерческий контекст»PAdES B-LT и B-LTA (материал проверки DSS и цикл архивных меток времени), а также хранение ключей PKCS#11 HSM поставляются в редакциях Pro и Enterprise. Этот рецепт охватывает только B-B и B-T; более высокие уровни — отдельные возможности, проверяемые по отдельности, и они выходят за рамки этой страницы.