跳到內容
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.
A high-volume signing run end to end: shared signing material is warmed once; each document is rendered and signed individually on a disposable unit; a correctly-signed result commits exactly once, while any failure is held for retry, never passed on as unsigned bytes; a crashed run resumes from its checkpoint.

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),讓一個大規模的封存維持數十年可驗證。

  • High-volume document generation ——本頁在其之上簽署的那套有界記憶體、佇列化的批次模型;先讀它,以了解吞吐量與量測的紀律。
  • PAdES baseline profiles——每個層級(B-B 到 B-LTA)各自新增了什麼,好讓你在義務所需的層級上簽署。
  • How signatures sit in a PDF——讓一道簽章成為逐份文件的那套位元組範圍與字典基礎。
  • HSM-backed signing——當簽署材料住在硬體中時,那條私鑰邊界坐落在何處。
  • Stream (Pro)——那套具持久、並行、恰好一次性質的算繪引擎,它把一個單一的簽署單元變成一次可續行的執行。
  • 批次簽署——依排程簽署許多份文件。一個排程上的概念;每一份文件仍在它自己的位元組之上個別簽署。
  • 失敗即關閉——在一個原本會產出未簽署或錯誤輸出的失敗上,管線扣住那份文件並回報,而非把它以純位元組的形式遞交出去。
  • 恰好一次提交——一項持久管線的性質,其中一個正確簽署的輸出只發布一次,並且不會在一個當機的執行續行時被重新發出。
  • 檢查點——逐份文件、具持久性的紀錄,載明什麼已提交,好讓一次執行能從它停下的地方繼續,而非把一切重新簽署。
  • CMS SignedData——用於存放對內容的簽章的密碼學容器(它可以攜帶多個簽署者);這套管線為每一份文件產出一個簽署者的 PDF 簽章,也就是一個批次逐份產出的那個單元。
  • PAdES——PDF Advanced Electronic Signatures(PDF 進階電子簽章),用於 PDF 簽署的 ETSI EN 319 142 設定檔系列;其基準層級從 B-B 到 B-LTA。