大規模簽署,不打折扣
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),讓一個大規模的封存維持數十年可驗證。 |
相關文件
標題為「相關文件」的區段- 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。