规模化签署,不打折扣
Spec: ISO 32000-2, §12.8ISO 32000-2 §12.8Spec: ETSI EN 319 142-1ETSI EN 319 142-1Spec: RFC 5652, §5.1RFC 5652 §5.1
签署一份文件是一次密码学操作。在截止期限内签署十万份,是同一次操作的重复,而其中危险的失败,不再是“它很慢”,而是“其中一份未经签署就发了出去,却没人注意到”。本页讲的就是在做第二件事的同时不放弃第一件:批量且并发的签署,其中每个签名仍然正确,执行过程会拒绝发出一个它无法签署的文件,而一个大任务能够续跑而不是重头开始。
为什么这很重要
标题为“为什么这很重要”的章节一个签名是一项逐文件的事实。它的摘要在一个所声明、且排除签名值本身的字节范围上计算(Spec: ISO 32000-2, §12.8ISO 32000-2 §12.8),因此没有任何诚实的方式能“作为一个批次”一举签署一千份文件——每一份都在自己的字节上携带自己的 CMS SignedData(Spec: RFC 5652, §5.1RFC 5652 §5.1)。于是规模放大了恰好一件事悄然出错的机会:一个短暂失效的密钥句柄、一个超时的时间戳权威机构、一个握着半写文件就死掉的工作进程。
代价高昂的结局不是崩溃。崩溃是吵闹的,你会重试它。代价高昂的结局是一个无声的结局——一个看起来已完成、实则未签署的 PDF,静静躺在某个归档里,数月之后才被某位审计师的验证器发现。在规模之下,“大致都签了”与“都签了”无从区分,直到那一份真正要紧的被检查为止。规模化签署的全部要点,就是让那个结局在结构上成为不可能,而不是在统计上成为罕见。
精简版
标题为“精简版”的章节- 每一份文件都被单独签署,覆盖它自己的字节范围。 批量是一个调度上的词,而不是一个密码学上的词。不存在共享的签名。
- 级别是一份合约,而不是一个提示。 你指定一个 PAdES 基线级别,引擎就为每一份文件生产出恰好那个级别,否则就让那份文件大声失败(Spec: ETSI EN 319 142-1ETSI EN 319 142-1)。
- 流水线是失败即关闭的。 一份无法被正确签署的文件,不会以普通字节的形式蒙混过关。它被扣住,而不是被传递下去。
- 并发是逐文件的,并且在构造上是安全的。 各个签署单元不共享可变状态,因此两个工作进程无法损坏彼此的输出。
- 大型执行是持久的。 已提交的输出在续跑时不会被重新发出;一次崩溃的执行从它最后一个检查点继续,而不是重新签署一切。
NextPDF 如何处理它
标题为“NextPDF 如何处理它”的章节这套设计建立在一个分离之上:生产签名是一个小的、确定性的、逐文件的步骤;安全地运行成千上万次,则是一个编排步骤。 把这两者分开,正是让每一边都能保持简单的关键。
签署步骤是那个绝不能打折扣的步骤。你请求一个级别——一个 SignatureLevel 枚举分支,而绝不是一个引擎还得去解释的字符串——而那个级别被当作那份文件的一份合约来对待。引擎要么生产出所请求的级别,要么带着一个可据以行动的错误停下;它不会悄悄地以一个更低的级别签署,却让一条记录声称了一个更高的级别。正确性不会因为后面还排着更多文件就放松。第十万个签名,会被计算得和第一个一样仔细。
失败即关闭这条规则,正是让这件事在规模之下值得信任的原因。NextPDF 的签署路径拒绝用一个看似可信、实则未签署的产物,去顶替你所请求的那一个。受支持的应用路线是高层 Document API:你用 Document::setSignature() 配置签名,然后用 Document::getPdfData()(或 save() / output())索取字节,而那一趟写入要么发出一个正确签署的 PDF,要么在交回字节之前抛出异常——绝不会交出一个调用方误以为已签署的未签署文件。在一个批次上施加这一点,正是这条规则把“有一份未签署地溜了过去”从一个无声的潜伏缺陷,转化为一个单独失败、可重试的任务。
- Warm the signing material onceOn worker boot, open the key/certificate source and the timestamp client. This cost is paid once per worker, not once per document.
- Enqueue the documentsA queue holds the per-document jobs. The queue is the throughput dial — signing workers scale horizontally behind it.
- Render and sign one documentA disposable unit renders the document, then signs it over its own byte range at the requested PAdES level. Nothing is shared with the next document.
- Commit on success, hold on failureA correctly-signed file commits once. A document that could not be signed is failed and retried — never emitted as unsigned bytes.
- Checkpoint, and resume on crashA durable run records what has committed. After a crash it continues from the last checkpoint instead of re-signing the whole batch.
Core 给你的是密码学正确性:软件 CMS 签署以及 PAdES B-B(通过时间戳客户端可达 B-T),其中每一份文件都被单独且失败即关闭地签署。那种让一次大型执行变得持久、并发且恰好一次的编排——无副作用的渲染引擎,加上提交器、检查点、幂等性和死信存储——是高级版本中的 Stream 模块;通过 HSM 或云端 KMS 实现的硬件支撑签署,同样是一个高级版本的衔接面。Core 证明每一个签名都正确;高级版本让其中的一百万个变得可生存。
实践示例
标题为“实践示例”的章节下面这个形态,是一个批次循环内部的逐文件签署单元。每一次迭代以一个指定的级别签署一份文件,要么产出一个正确签署的结果,要么让那一个任务失败——它绝不会返回一份伪装成结果的未签署字节。
<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\SignatureLevel;use NextPDF\Exception\SignatureException;use Psr\Log\LoggerInterface;
/** * One signing-batch iteration: render, sign at a named level, commit or fail. * * The factory and the certificate source ($certInfo, the warmed signing * material) are process-lifetime singletons; the document is disposable. A * document that cannot be signed at the requested level fails this job loudly — * it is never committed unsigned. * * @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs */function signBatch( DocumentFactoryInterface $factory, CertificateInfo $certInfo, LoggerInterface $logger, iterable $jobs,): void { // The level is an explicit, ordered contract — not a flag we hope is honoured. $level = SignatureLevel::PAdES_B_T;
foreach ($jobs as $jobId => $build) { // Fresh, disposable unit — shares the warmed signing material only. $doc = $factory->create(); $doc = $build($doc);
try { // Sign over this document's own byte range, at exactly $level, // or throw. There is no "signed lower, reported higher" path. $doc->setSignature(certInfo: $certInfo, level: $level); $signed = $doc->getPdfData(); } catch (SignatureException $e) { // Fail-closed: this document does NOT continue as unsigned bytes. // The job is failed and left for retry / dead-letter handling. $logger->error('pdf.sign.failed', ['job_id' => $jobId, 'reason' => $e->getMessage()]); continue; }
// Only a correctly-signed result reaches the commit step. commitSignedOutput($jobId, $signed); unset($doc, $signed); // release per-document state before the next iteration
$logger->info('pdf.sign.committed', ['job_id' => $jobId, 'level' => $level->value]); }}那个 catch 是那条承重的行。它是一次执行“扣下它无法签署的文件”与一次执行“照样把它们发出去”之间的差别。那个 continue 不是把失败粉饰过去——任务被记录下来并留待重试,于是这个批次结束时,会有一份关于哪些签了、哪些没签的、已知且完整的清单,而绝不会留下一道无声的缺口。
常见误解
标题为“常见误解”的章节第一个误解是,以为“批量签署”意味着把一个签名施加到许多文件上。它不是,而任何声称如此的系统都不是在生产有效的 PAdES 签名——每一份文件的摘要都绑定到它自己的字节(Spec: ISO 32000-2, §12.8ISO 32000-2 §12.8)。批量纯粹关乎有多少以及有多快,而绝不关乎共享那个密码学单元。
第二个是,以为并发意味着为了速度而放松正确性——以为一个快速的签署器,必定要走一条仔细的那个不走的捷径。它不必。因为各个签署单元不共享可变状态,并行运行它们改变的是调度,而不是字节。每一个并行的签名,都以与单个签名相同的严谨来计算;并行性存在于围绕它们的编排之中。
第三个是,以为持久性是你在第一次失败的过夜执行之后才补上去的东西。到那时,你已经丢失了那次执行。一个可续跑的流水线必须在崩溃之前就逐文件地知道,哪些已提交、哪些没有——而这恰恰是检查点和幂等性存储存在的目的。
限制与边界
标题为“限制与边界”的章节- 每一个签名都是逐文件且受标准约束的;不存在批量捷径。 规模改变的是调度,而不是那个密码学单元。NextPDF 在每一份文件自己的字节范围上签署。
- Core 做软件 CMS 签署以及 PAdES B-B(经由时间戳客户端达到 B-T)。 那个持久、并发、恰好一次的渲染与签署引擎,是高级版本中的 Stream 模块;由 HSM/KMS 支撑的密钥托管,是一个高级版本的衔接面。本页并不把那种编排声称为 Core 的能力。
- 失败即关闭是引擎的行为,而不是对你接线方式的保证。 NextPDF 拒绝发出一个未签署却被误以为已签署的文件,并把受支持的签署路线呈现出来。一个捕获了由此产生的错误、却照样提交的流水线,是选择了去击败这项保证——这正是示例中那个
catch/continue所要防止的表述。 - PAdES 级别是逐文件强制执行的,而不是为整次执行作出认证。 引擎要么生产出所请求的基线级别,要么失败;那是一种结构性的强制执行,而不是对所生产文件作出的第三方合规裁决。级别本身的递进,涵盖于 PAdES 基线配置文件。
- 队列、密钥托管、时间戳权威机构和对象存储,都是你的。 NextPDF 提供逐文件的签署正确性,并在高级版本中提供持久的编排基础组件。它不会运行你的基础设施,也不会为你的 TSA 担保。
| Edition | Availability |
|---|---|
| Core | 逐文件的软件 CMS 签署、PAdES B-B(经由时间戳客户端达到 B-T), 在每一份文件自己的字节范围上单独签署,失败即关闭以对抗悄无声息未签署的输出。纯粹的逐文件签署不需要任何商业层级。 |
| Pro | 增加 Stream 模块:一个无副作用的渲染引擎,加上持久的提交器、检查点、幂等性和死信存储——并发、抗崩溃、 恰好一次的批量执行,能够续跑而不是重头再来。 |
| Enterprise | 增加硬件支撑的密钥托管(经由 PKCS#11 的 HSM,或一个云端 KMS), 使私钥永不离开设备,并增加长期 PAdES 级别 (B-LT、B-LTA),让一个大批量归档在数十年内保持可验证。 |
相关文件
标题为“相关文件”的章节- 大批量文件生成 ——本页在其之上进行签署的、有界内存且队列化的批量模型;先读它,以了解吞吐量与测量纪律。
- PAdES 基线配置文件——每个级别(B-B 到 B-LTA)各增加了什么,好让你按义务所需的级别去签署。
- PDF 中签名的存在方式——让一个签名成为逐文件的那套字节范围与字典基础。
- HSM 支撑的签署——当签署材料存在于硬件中时,那条私钥边界落在何处。
- Stream(Pro)——那个持久、并发、恰好一次的渲染引擎,它把一个单一的签署单元变成一次可续跑的执行。
词汇表
标题为“词汇表”的章节- 批量签署——按计划签署许多文件。一个调度概念;每一份文件仍然在它自己的字节上被单独签署。
- 失败即关闭——在一个本会产生未签署或错误输出的失败上,流水线扣住文件并报告,而不是把它当作普通字节传递下去。
- 恰好一次提交——一种持久流水线的属性,其中一个正确签署的输出被发布一次,且在一次崩溃的执行续跑时不会被重新发出。
- 检查点——关于哪些已提交的、持久的逐文件记录,使一次执行能够从它停下的地方继续,而不是重新签署一切。
- CMS SignedData——用于对内容签署的密码学容器(它可以携带多个签署者);本流水线为每一份文件生产一个签署者的 PDF 签名,即一个批次逐文件生产出的那个单元。
- PAdES——PDF 高级电子签名,用于 PDF 签署的 ETSI EN 319 142 配置文件系列;其基线级别从 B-B 到 B-LTA。