以 PAdES B-B 签署 PDF,再扩展为 PAdES B-T
本示例会生成一个 PAdES B-B 签名,也就是带有签署属性(content-type、message-digest、signing-time)的 CMS SignedData。随后,它会加入一个 RFC 3161 signature-time-stamp,将该签名扩展为 PAdES B-T。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(DSS 验证数据、归档时间戳循环)不在本示例范围内,也不属于此处涵盖的 Core/Pro 签名接口。
composer require nextpdf/core:^3ext-openssl 必须启用——CertificateInfo 通过 OpenSSL 解析密钥。B-T 还需要一个可连接的 RFC 3161 TSA 端点,以及一个用于连接该端点的 PSR-18 HTTP 客户端。
概念总览
标题为“概念总览”的章节PAdES B-B 签名会将一个 DER 编码的 CMS SignedData 存放在签名字典的 Contents 项中;Contents 的值是一个基于字节范围摘要并经过填充的十六进制字符串(ISO 32000-2 §12.8.1)。该摘要涵盖整个文件,但排除签名值本身(ISO 32000-2 §12.8.1)。
PAdES B-T 只加入一个 RFC 3161 signature-time-stamp。该时间戳的消息印记(message imprint)是 SignerInfo 签名值八位字节的哈希——不含任何 ASN.1 标签或长度前缀(ETSI EN 319 122-1 §5.3;RFC 3161 附录 A)。该令牌通过 id-aa-timeStampToken 未签名属性承载,OID 为 1.2.840.113549.1.9.16.2.14(RFC 3161 附录 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 需要一个非 null 的 TsaClient;如果构建 B-T 签名器时未提供该客户端,则会抛出 SignatureException。
高级 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');save()(以及同样的 output() / getPdfData())会把 /Contents 项写成 SubFilter ETSI.CAdES.detached 下的 DER 编码 CMS SignedData(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。
如需明确掌控算法、字节范围数据或 SignatureResult,请使用下方较低级的 DigitalSigner 分步流程。
代码示例——快速上手
标题为“代码示例——快速上手”的章节<?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";代码示例——生产环境
标题为“代码示例——生产环境”的章节这是一个自包含、可由测试工具执行的程序。它对应 示例 examples/36-sign-pades-b-b-and-b-t.php。该程序会构建一份文件,将其配置为进行 PAdES 签名,随后用 B-B 签署一次,再通过一个 TSA 客户端用 B-T 签署一次。在生产环境中,TsaClient 会通过一个经过强化的 PSR-18 客户端指向真实的 RFC 3161 端点——这个具备安全意识的 HTTP 客户端会对 TSA SPKI 进行固定(pin),并安全地 resolve(解析)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 配置文件并不是符合性或法律效力认证;该判定由独立验证器作出。
边界情况与陷阱
标题为“边界情况与陷阱”的章节- 没有 TSA 客户端的 B-T。 构建 B-T
DigitalSigner但未提供TsaClient时,会抛出SignatureException(B-T 必须有 TSA)。请在签署前检查好 TSA 配置。 - TSA 可连接性。 B-T 每次签署都会执行一次即时 RFC 3161 往返。TSA 中断就意味着无法生成 B-T 签名。请使用断路器(circuit breaker),并采用符合你吞吐量的 TSA SLA;
TsaClient可接受断路器。 - 强化 TSA HTTP 客户端。 将
TsaClient指向一个会对 TSA 的 SPKI 做固定(RFC 7469 格式)并安全解析 DNS 的 PSR-18 客户端;TsaClient::extractPublicKeyPin()会从 TSA 凭证推导出该固定值。 - B-T 不是 B-LT/B-LTA。 签名时间戳不会嵌入验证数据(凭证、OCSP、CRL)或归档时间戳。这些属于 B-LT/B-LTA 层级,本示例不会生成。
- 线性化冲突。
enableLinearization()与已配置的签名互斥——当一方已配置时,调用另一方会抛出InvalidConfigException。 - HSM 密钥。 对于保存在硬件中的密钥,请将
CertificateInfo与CertificateInfo::fromHsm()搭配使用来构建;私钥永远不会进入进程内存。PKCS#11 签名器合约属于 Core;可运行的提供者属于 Premium。
B-B 签名是一项本地 CMS 运算。B-T 每次签署都会额外执行一次同步 RFC 3161 HTTP 往返。在批量工作负载中,请将 TSA 延迟与速率限制纳入预算。建议采用受断路器保护的 TsaClient。
安全性备注
标题为“安全性备注”的章节已生成的签名并不等于可信签名。一个签名能否通过验证,取决于凭证、其信任锚以及验证端政策——这些都在本库之外。加密提供的是机密性,而非完整性;签名提供的是 integrity/authenticity,而非机密性。请把密钥保管视为首要风险:进程内存中的软件密钥,其安全程度最多等同于主机本身。
数据驻留与 PII 缓解
标题为“数据驻留与 PII 缓解”的章节签名运算在进程内进行;除 B-T 的 TSA 往返外,文件字节与私钥都不会离开主机,而该往返只会发送消息印记(签名值的哈希),绝不会发送文件内容(RFC 3161 §2.4.1 MessageImprint)。不会有任何文件文本或 PII 传输到 TSA。请选择其管辖区符合你数据驻留政策的 TSA。
安全遥测与日志清理
标题为“安全遥测与日志清理”的章节DigitalSigner 接受一个可选的 PSR-3 记录器;它只记录算法与层级,不记录密钥材料或签名字节。password 参数——位于 CertificateInfo 与 TsaClient 上——标注了 #[SensitiveParameter],因此密码短语会在堆栈跟踪中被遮蔽。请勿记录 SignatureResult::$cmsSignedData 或 $timestampToken。
威胁模型
标题为“威胁模型”的章节已纳入考虑:签署后遭篡改的输入(由字节范围摘要检测)、密钥泄露(不在库范围内——密钥保管是集成者的责任)、TSA 冒充(通过 TSA HTTP 客户端上的 SPKI 固定来缓解),以及层级间降级(层级枚举是明确的;引擎不会悄悄把 B-T 降级为 B-B)。未作声明:不存在漏洞,或任何生成的签名具有法律效力。
FIPS 模式行为
标题为“FIPS 模式行为”的章节签名的基础组件由 OpenSSL 提供。在经过 FIPS 验证的 OpenSSL 构建上,RSA/ECDSA 与 SHA-256 运算会通过 FIPS 提供者执行;NextPDF 本身并未主张具备 FIPS 验证。CryptoCapabilities 会报告主机可用的基础组件;请在你的部署环境中验证 OpenSSL 提供者链。
符合性
标题为“符合性”的章节| 陈述 | 规范 | 条款 | 参考 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 在不使用 SHA-1 的情况下识别 TSA 凭证。 | RFC 5816 | §1 |
本示例描述 NextPDF 如何生成一个 B-B 与一个 B-T 签名。它并未主张任何生成的签名具有法律效力,或已符合 PAdES 符合性;这些判定由独立验证器作出。
商业情境
标题为“商业情境”的章节PAdES B-LT 与 B-LTA(DSS 验证数据与归档时间戳循环)以及 PKCS#11 HSM 密钥保管,均随 Pro 与 Enterprise 版本交付。本示例刻意只涵盖 B-B 与 B-T;更高层级属于各自独立、单独验证的能力,不在此处范围内。