跳转到内容
getnextpdf.com

规模化签署,不打折扣

Spec: ISO 32000-2, §12.8Spec: ETSI EN 319 142-1Spec: RFC 5652, §5.1

签署一份文件是一次密码学操作。在截止期限内签署十万份,是同一次操作的重复,而其中危险的失败,不再是“它很慢”,而是“其中一份未经签署就发了出去,却没人注意到”。本页讲的就是在做第二件事的同时不放弃第一件:批量且并发的签署,其中每个签名仍然正确,执行过程会拒绝发出一个它无法签署的文件,而一个大任务能够续跑而不是重头开始。

一个签名是一项逐文件的事实。它的摘要在一个所声明、且排除签名值本身的字节范围上计算(Spec: ISO 32000-2, §12.8),因此没有任何诚实的方式能“作为一个批次”一举签署一千份文件——每一份都在自己的字节上携带自己的 CMS SignedData(Spec: RFC 5652, §5.1)。于是规模放大了恰好一件事悄然出错的机会:一个短暂失效的密钥句柄、一个超时的时间戳权威机构、一个握着半写文件就死掉的工作进程。

代价高昂的结局不是崩溃。崩溃是吵闹的,你会重试它。代价高昂的结局是一个无声的结局——一个看起来已完成、实则未签署的 PDF,静静躺在某个归档里,数月之后才被某位审计师的验证器发现。在规模之下,“大致都签了”与“都签了”无从区分,直到那一份真正要紧的被检查为止。规模化签署的全部要点,就是让那个结局在结构上成为不可能,而不是在统计上成为罕见。

  • 每一份文件都被单独签署,覆盖它自己的字节范围。 批量是一个调度上的词,而不是一个密码学上的词。不存在共享的签名。
  • 级别是一份合约,而不是一个提示。 你指定一个 PAdES 基线级别,引擎就为每一份文件生产出恰好那个级别,否则就让那份文件大声失败(Spec: ETSI EN 319 142-1)。
  • 流水线是失败即关闭的。 一份无法被正确签署的文件,不会以普通字节的形式蒙混过关。它被扣住,而不是被传递下去。
  • 并发是逐文件的,并且在构造上是安全的。 各个签署单元不共享可变状态,因此两个工作进程无法损坏彼此的输出。
  • 大型执行是持久的。 已提交的输出在续跑时不会被重新发出;一次崩溃的执行从它最后一个检查点继续,而不是重新签署一切。

这套设计建立在一个分离之上:生产签名是一个小的、确定性的、逐文件的步骤;安全地运行成千上万次,则是一个编排步骤。 把这两者分开,正是让每一边都能保持简单的关键。

签署步骤是那个绝不能打折扣的步骤。你请求一个级别——一个 SignatureLevel 枚举分支,而绝不是一个引擎还得去解释的字符串——而那个级别被当作那份文件的一份合约来对待。引擎要么生产出所请求的级别,要么带着一个可据以行动的错误停下;它不会悄悄地以一个更低的级别签署,却让一条记录声称了一个更高的级别。正确性不会因为后面还排着更多文件就放松。第十万个签名,会被计算得和第一个一样仔细。

失败即关闭这条规则,正是让这件事在规模之下值得信任的原因。NextPDF 的签署路径拒绝用一个看似可信、实则未签署的产物,去顶替你所请求的那一个。受支持的应用路线是高层 Document API:你用 Document::setSignature() 配置签名,然后用 Document::getPdfData()(或 save() / output())索取字节,而那一趟写入要么发出一个正确签署的 PDF,要么在交回字节之前抛出异常——绝不会交出一个调用方误以为已签署的未签署文件。在一个批次上施加这一点,正是这条规则把“有一份未签署地溜了过去”从一个无声的潜伏缺陷,转化为一个单独失败、可重试的任务。

  1. 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.
  2. Enqueue the documentsA queue holds the per-document jobs. The queue is the throughput dial — signing workers scale horizontally behind it.
  3. 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.
  4. 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.
  5. 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.8)。批量纯粹关乎有多少以及有多快,而绝不关乎共享那个密码学单元。

第二个是,以为并发意味着为了速度而放松正确性——以为一个快速的签署器,必定要走一条仔细的那个不走的捷径。它不必。因为各个签署单元不共享可变状态,并行运行它们改变的是调度,而不是字节。每一个并行的签名,都以与单个签名相同的严谨来计算;并行性存在于围绕它们的编排之中。

第三个是,以为持久性是你在第一次失败的过夜执行之后才补上去的东西。到那时,你已经丢失了那次执行。一个可续跑的流水线必须在崩溃之前就逐文件地知道,哪些已提交、哪些没有——而这恰恰是检查点和幂等性存储存在的目的。

  • 每一个签名都是逐文件且受标准约束的;不存在批量捷径。 规模改变的是调度,而不是那个密码学单元。NextPDF 在每一份文件自己的字节范围上签署。
  • Core 做软件 CMS 签署以及 PAdES B-B(经由时间戳客户端达到 B-T)。 那个持久、并发、恰好一次的渲染与签署引擎,是高级版本中的 Stream 模块;由 HSM/KMS 支撑的密钥托管,是一个高级版本的衔接面。本页并不把那种编排声称为 Core 的能力。
  • 失败即关闭是引擎的行为,而不是对你接线方式的保证。 NextPDF 拒绝发出一个未签署却被误以为已签署的文件,并把受支持的签署路线呈现出来。一个捕获了由此产生的错误、却照样提交的流水线,是选择了去击败这项保证——这正是示例中那个 catch/continue 所要防止的表述。
  • PAdES 级别是逐文件强制执行的,而不是为整次执行作出认证。 引擎要么生产出所请求的基线级别,要么失败;那是一种结构性的强制执行,而不是对所生产文件作出的第三方合规裁决。级别本身的递进,涵盖于 PAdES 基线配置文件
  • 队列、密钥托管、时间戳权威机构和对象存储,都是你的。 NextPDF 提供逐文件的签署正确性,并在高级版本中提供持久的编排基础组件。它不会运行你的基础设施,也不会为你的 TSA 担保。
High-volume and concurrent signing — edition availability
EditionAvailability
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。