Enterprise 版本
批次簽章驗證
NextPDF Enterprise 能在單一呼叫中跨多份 PDF 文件驗證數位簽章。NextPDF\Enterprise\Signature\BatchSignatureValidator::validate() 接收一份文件清單並回傳一個 BatchValidationReport。每個簽章都會通過相同的 fail-closed 管線:在簽署位元組範圍上進行密碼學 CMS 認證、信任錨定的憑證鏈驗證,以及 OCSP/CRL 撤銷檢查。報告會攜帶每份文件與每個簽章的細節 — CertChainStatus、RevocationStatus、TimestampStatus — 讓合規工具能從其記錄的證據重新推導出每一個裁決。
裁決模型刻意採取嚴格立場。只有在所有證據都被積極確立時,簽章才會是 Valid。缺少撤銷證據會產生 Indeterminate,絕不會是 Valid。本頁涵蓋批次協調器及其結果型別。單一文件的 AdES 驗證側記載於 簽章驗證。嵌入長期驗證材料的說明則記載於 Archive。
供應情況與授權
標題為「供應情況與授權」的區段此能力隨 NextPDF Enterprise(nextpdf/enterprise)出貨,並透過 Enterprise 等級的授權封裝啟用。未持有該權益的部署不會載入此能力的類別。比較各版本並取得授權。
composer require nextpdf/enterprisenextpdf/premium metapackage 也會解析出 Enterprise 套件。啟用時使用你的 Enterprise 授權封裝;請參閱 授權與啟用。批次型別自動載入於 NextPDF\Enterprise\Signature 之下。除引擎基準之外,不需要任何額外的 PHP 擴充。
概念概覽
標題為「概念概覽」的區段一次對 validate() 的呼叫會處理一份 DocumentSignatureInput 值的清單。每個輸入攜帶一個文件識別碼、原始 PDF 位元組,以及選用的 PEM 編碼信任錨。驗證器會擷取每份文件的簽章字典,並對每個簽章執行三個階段。
階段 1 — 密碼學認證。 來自 /Contents 的分離式 CMS/PKCS#7 blob 會在 /ByteRange 所涵蓋的位元組上進行驗證。驗證器自行重新計算內容摘要,並將其與 messageDigest 簽署屬性比對。它絕不信任產生者提供的摘要(RFC 5652 §5.6)。簽章值必須通過驗證,且簽署憑證必須綁定至 CMS。缺少或格式錯誤的 /Contents 或 /ByteRange、無法解析的 CMS、摘要不符,或簽章檢查失敗,全都會 fail closed。在 SHA-1 下通過驗證的簽章會被視為弱簽章,絕不構成完整通過。
階段 2 — 鏈驗證與信任錨定。 從 CMS 復原的簽署者鏈會作為候選憑證路徑進行驗證。你提供的 trustedCerts 就是信任錨輸入,符合 RFC 5280 §6.1.1 的意義:鏈的終端必須以 DER SHA-256 指紋比對到某個提供的錨。若一條結構一致的鏈其終端並非已設定的錨,則絕不會被回報為受信任。在沒有可用錨的情況下,只會回報結構裁決,而 CertChainStatus::$trusted 維持為 false。
階段 3 — 撤銷。 撤銷會在認證之後於復原的鏈上執行,呼應 ETSI EN 319 102-1 模型中撤銷檢查接續於成功路徑驗證之後的做法(clause 5.2.6.2)。OCSP 為主:只有經密碼學驗證的回應才算數,判為 Good 或 Revoked。CRL 路徑為後備,並證明清單的新鮮度。當兩個 client 皆未設定時,狀態為 unavailable。
每個簽章的裁決是一個 SignatureValidationStatus。此分類法在每個簽章的粒度上呼應 ETSI EN 319 102-1 的狀態模型(TOTAL-PASSED / TOTAL-FAILED / INDETERMINATE):
| 證據 | 裁決 |
|---|---|
| 憑證確認已撤銷 | Invalid(決定性,無論其他檢查為何) |
| CMS 認證失敗,未復原任何簽署者材料 | Error |
| CMS 認證失敗,存在簽署者材料 | Invalid |
| 已認證,但鏈未通過驗證 | Invalid(若無鏈則為 Error) |
| 已認證且鏈有效,但無已確認的信任錨 | Indeterminate |
| 已認證、鏈有效、受信任,但無確定的非撤銷 | Indeterminate |
| 以上全部皆積極確立 | Valid |
確定非撤銷規則。「未被證明已撤銷」並不等同於「被證明未撤銷」。Valid 裁決至少需要一個 Good 撤銷結果。經驗證為良好的 OCSP 回應是確定的形式:它斷言簽署者憑證自身的狀態。一份經密碼學接受、新鮮的 CRL 在本實作中也能滿足此門檻,但僅作為新鮮度與完整性的證明 — 該路徑並不逐序號解析條目,因此它不提供任何逐序號的撤銷保證,也絕不會產生肯定的 revoked 裁決。凡是肯定撤銷偵測重要之處,都請設定 OCSP:僅使用 CRL 的部署不會將已撤銷的憑證呈現為 Invalid。當 OCSP 與 CRL 結果皆為 Unknown 或 Unavailable 時,撤銷狀態無法確定,裁決為 Indeterminate。這遵循 ETSI EN 319 102-1:無法取得的撤銷狀態資訊會導致 INDETERMINATE,絕不通過(clause 5.1.3,TRY_LATER)。這是 3.1.0 中具向後相容性影響的行為強化:較早的版本可能在沒有確定撤銷證據的情況下回報 Valid。未設定任何 OCSP 或 CRL client 的部署,如今在過去看到 Valid 之處通常會看到 Indeterminate。
有兩條邊界誠實地界定了此能力。第一,批次驗證器不評估嵌入的時間戳記符記:批次結果中的 TimestampStatus 永遠是缺席狀態。RFC 3161 時間戳評估屬於單一文件驗證側;請參閱 簽章驗證。第二,本頁是唯讀驗證。嵌入 DSS/VRI 材料以達成長期有效性屬於 Archive 能力。
為何如此運作
標題為「為何如此運作」的區段承載重量的決策是一個 fail-closed 的裁決產生器。Valid 只會由三個軸線上的積極證據鑄造:密碼學認證、一條信任錨定的鏈,以及確定的非撤銷。任何未確立者都會退化為 Indeterminate,而非預設為通過,這正是 EN 319 102-1 對於缺少撤銷材料的立場。批次吞吐量絕不換回嚴謹:批次層是在同一份經稽核的 CMS 驗證器(單一文件也使用該驗證器)之上的協調,因此一次 1,000 份文件的執行套用的是相同的密碼學。報告也將證據與裁決分離 — CertChainStatus 與 RevocationStatus 記錄每個裁決所依據的輸入,讓稽核者日後能重新推導出來。
設計背景:大規模簽署,不妥協。
API 介面
標題為「API 介面」的區段以下所有符號皆為 nextpdf/enterprise 3.1.0 的公開 API。
BatchSignatureValidator
標題為「BatchSignatureValidator」的區段final class BatchSignatureValidator{ public function __construct( ?SignatureExtractor $extractor = null, ?CertificateChainValidator $chainValidator = null, private readonly ?OcspClient $ocspClient = null, private readonly ?CrlFetcher $crlFetcher = null, ?CmsSignatureDataExtractor $cmsExtractor = null, private readonly ClockInterface $clock = new SystemClock(), )
public function validate(array $inputs): BatchValidationReport}擲出或失敗於:若輸入清單為空,validate() 會擲出 \InvalidArgumentException;當批次超過 1,000 份文件時擲出 \OverflowException。無法解析為 PDF 的文件不會擲出例外;它會成為一個每份文件的 Error 結果。$clock 是用於 CRL 新鮮度決策的 PSR-20 Psr\Clock\ClockInterface,因此在凍結的測試時鐘下裁決具有確定性。
DocumentSignatureInput
標題為「DocumentSignatureInput」的區段final readonly class DocumentSignatureInput{ public string $documentId;
public function __construct( string $documentId, public string $pdfData, public array $trustedCerts = [], )}擲出或失敗於:若 $documentId 為空字串則擲出 \InvalidArgumentException。$trustedCerts 是一份 PEM 編碼信任錨憑證的清單。
BatchValidationReport
標題為「BatchValidationReport」的區段final readonly class BatchValidationReport{ public function __construct( public array $documents, public int $totalDocuments, public int $totalSignatures, public int $totalValid, public int $totalInvalid, public float $durationMs, )
public function allValid(): bool
public function hasDocumentsWithoutSignatures(): bool
public function toJson(?CertPiiGuard $piiGuard = null): string}擲出或失敗於:若編碼失敗,toJson() 會擲出 \JsonException。只有在存在簽章且沒有任何一個非有效時,allValid() 才為 true。toJson() 預設套用一個 privacy-by-default 的 NextPDF\Enterprise\Signature\Eidas\CertPiiGuard,遮罩簽署者名稱、根簽發者、TSA 名稱與鏈問題診斷;關於該守衛的 API,請參閱 eIDAS 保證等級。
DocumentValidationResult 與 DocumentValidationStatus
標題為「DocumentValidationResult 與 DocumentValidationStatus」的區段final readonly class DocumentValidationResult{ public function __construct( public string $documentId, public DocumentValidationStatus $status, public array $signatures, public int $validCount, public int $invalidCount, )
public function hasSignatures(): bool
public function totalSignatures(): int}enum DocumentValidationStatus: string{ case AllValid = 'all_valid'; case SomeInvalid = 'some_invalid'; case AllInvalid = 'all_invalid'; case NoSignatures = 'no_signatures'; case Error = 'error';}擲出或失敗於:無。不可變值物件與後端列舉。
SignatureValidationResult 與 SignatureValidationStatus
標題為「SignatureValidationResult 與 SignatureValidationStatus」的區段final readonly class SignatureValidationResult{ public function __construct( public SignatureValidationStatus $status, public CertChainStatus $certChain, public TimestampStatus $timestamp, public RevocationStatus $revocation, public string $signer, public string $level = '', public string $subFilter = '', public string $reason = '', )
public function isValid(): bool}enum SignatureValidationStatus: string{ case Valid = 'valid'; case Invalid = 'invalid'; case Indeterminate = 'indeterminate'; case Error = 'error';}擲出或失敗於:無。當認證通過時,$signer 是經 CMS 驗證的憑證主體,否則為空字串。$level 是一個由 SubFilter 衍生的標籤(例如 ETSI.CAdES.detached 對應 B-B),而非 AdES 一致性判定。
CertChainStatus
標題為「CertChainStatus」的區段final readonly class CertChainStatus{ public function __construct( public bool $valid, public bool $trusted, public int $chainLength, public string $rootIssuer, public array $issues = [], )
public function hasIssues(): bool}擲出或失敗於:無。$trusted 只在確認信任錨成員命中時才設定,絕不會因錨清單非空而設定。
RevocationStatus 與 RevocationCheckResult
標題為「RevocationStatus 與 RevocationCheckResult」的區段final readonly class RevocationStatus{ public function __construct( public RevocationCheckResult $ocspStatus, public RevocationCheckResult $crlStatus, public bool $isRevoked, public ?DateTimeImmutable $revocationDate = null, )
public static function unavailable(): self
public function hasConclusiveGood(): bool}enum RevocationCheckResult: string{ case Good = 'good'; case Revoked = 'revoked'; case Unknown = 'unknown'; case Unavailable = 'unavailable';}擲出或失敗於:所示成員皆不擲出。此類別也提供經證據檢查的靜態工廠(good()、revoked()、fromResults()),當所主張的狀態與 OCSP/CRL 證據矛盾時會擲出 \InvalidArgumentException — 已撤銷的結果絕不能鑄造為未撤銷,反之亦然。hasConclusiveGood() 只在未撤銷狀態且至少一項檢查為 Good 時才為 true。
TimestampStatus
標題為「TimestampStatus」的區段final readonly class TimestampStatus{ public function __construct( public bool $present, public bool $valid, public ?DateTimeImmutable $timestampTime = null, public string $tsaName = '', public array $issues = [], )
public static function absent(): self}擲出或失敗於:無。在批次結果中這永遠是 absent() 狀態;請參閱 邊界情況與陷阱。
程式碼範例 — 快速上手
標題為「程式碼範例 — 快速上手」的區段驗證一份文件並讀取報告。此範例使用一份未簽署的 PDF,因此輸出具有確定性。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Signature\BatchSignatureValidator;use NextPDF\Enterprise\Signature\DocumentSignatureInput;
// A minimal, unsigned PDF: the validator reports it as no_signatures.$unsigned = "%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n";
$validator = new BatchSignatureValidator();
try { $report = $validator->validate([ new DocumentSignatureInput(documentId: 'doc-001', pdfData: $unsigned), ]);} catch (\InvalidArgumentException $e) { // Empty input list, or an empty documentId. echo 'Rejected: ' . $e->getMessage() . "\n"; exit(1);}
echo 'Documents: ' . $report->totalDocuments . "\n";echo 'Signatures: ' . $report->totalSignatures . "\n";
foreach ($report->documents as $doc) { echo $doc->documentId . ': ' . $doc->status->value . "\n";}
echo 'All valid: ' . ($report->allValid() ? 'yes' : 'no') . "\n";echo 'Unsigned documents: ' . ($report->hasDocumentsWithoutSignatures() ? 'yes' : 'no') . "\n";預期輸出:
Documents: 1Signatures: 0doc-001: no_signaturesAll valid: noUnsigned documents: yes請注意此處 allValid() 回報 no:它需要至少一個簽章且沒有任何非有效結果,因此空的簽章集合絕不會靜默通過。
程式碼範例 — 正式環境
標題為「程式碼範例 — 正式環境」的區段以撤銷 client、信任錨、批次分塊,以及一份受 PII 保護的 JSON 報告,驗證一整個目錄的已簽署合約。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Ltv\CrlFetcher;use NextPDF\Enterprise\Security\Ltv\OcspClient;use NextPDF\Enterprise\Security\Ltv\OcspResponseCache;use NextPDF\Enterprise\Signature\BatchSignatureValidator;use NextPDF\Enterprise\Signature\DocumentSignatureInput;use NextPDF\Enterprise\Signature\SignatureValidationStatus;
// Any PSR-18 client works; Guzzle shown here.$httpClient = new \GuzzleHttp\Client(['timeout' => 10]);
// Revocation clients make a conclusive non-revoked (Good) result reachable.// Without them, every verdict tops out at Indeterminate. The response cache// lets repeat signers across the batch resolve without extra network calls.$validator = new BatchSignatureValidator( ocspClient: new OcspClient($httpClient, cache: new OcspResponseCache()), crlFetcher: new CrlFetcher($httpClient),);
// Trust anchors are an input: the chain terminus must match one of these.$anchors = [(string) file_get_contents('/etc/nextpdf/trust/enterprise-root.pem')];
$inputs = [];foreach (glob('/var/contracts/signed/*.pdf') ?: [] as $path) { $inputs[] = new DocumentSignatureInput( documentId: basename($path), pdfData: (string) file_get_contents($path), trustedCerts: $anchors, );}
$exit = 0;
// One call is capped at 1,000 documents; chunk larger runs.foreach (array_chunk($inputs, 1000) as $batch) { try { $report = $validator->validate($batch); // Signer PII is redacted by default in the serialized report. file_put_contents('/var/log/nextpdf/batch-report.jsonl', $report->toJson() . PHP_EOL, FILE_APPEND); // one JSON document per line } catch (\InvalidArgumentException | \OverflowException $e) { fwrite(STDERR, 'Batch rejected: ' . $e->getMessage() . "\n"); exit(2); } catch (\JsonException $e) { fwrite(STDERR, 'Report encoding failed: ' . $e->getMessage() . "\n"); exit(3); }
foreach ($report->documents as $doc) { foreach ($doc->signatures as $sig) { if ($sig->status !== SignatureValidationStatus::Valid) { $exit = 1; fwrite(STDERR, sprintf( "%s: %s (chain trusted: %s, revoked: %s)\n", $doc->documentId, $sig->status->value, $sig->certChain->trusted ? 'yes' : 'no', $sig->revocation->isRevoked ? 'yes' : 'no', )); } } }}
exit($exit);預期輸出(stderr,針對一份撤銷證據無法取得的文件;其他行會隨你的輸入而不同):
contract-0042.pdf: indeterminate (chain trusted: yes, revoked: no)JSON 報告會透過預設的 CertPiiGuard 序列化簽署者身分欄位,因此每個簽章的條目看起來像這樣(節錄、示意):
{ "status": "indeterminate", "signer": "[REDACTED]", "level": "B-B", "subFilter": "ETSI.CAdES.detached"}邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 空的輸入清單會擲出
\InvalidArgumentException;一次呼叫超過 1,000 份文件會擲出\OverflowException。請如正式環境範例那樣分塊處理較大的執行。 - 從較早版本升級: 在未設定任何 OCSP 或 CRL client 時,撤銷為
unavailable,因此沒有任何簽章能達到Valid。較早版本在此回報Valid;3.1.0 回報Indeterminate(請參閱 概念概覽)。 - 文件層級的計數器很嚴格:只有
Valid會遞增validCount。Invalid、Indeterminate與Error全都遞增invalidCount。因此一份唯一簽章為Indeterminate的文件會回報all_invalid。當這個區別重要時,請以每個簽章的status作為判斷依據。 - OCSP 檢查只在復原的鏈至少有兩張憑證時才執行,因為查詢需要簽發者。單一憑證的鏈會落至 CRL 路徑或
unavailable。 - 在批次結果中,
crlStatus絕不回報revoked。CRL 後備僅證明清單新鮮度;具權威性的已撤銷結果來自 OCSP。 - 在批次結果中,
timestamp永遠是absent()。批次驗證器不評估嵌入的 RFC 3161 符記;時間戳評估請使用 簽章驗證。 - 當認證失敗時
signer為空。當有值時,它是經 CMS 驗證之憑證的主體 CN(或 O)— 絕不是來自簽章字典中未經認證的/Name字串。 trustedCerts條目必須是 PEM 憑證。空的或格式錯誤的錨清單會產生僅結構的鏈裁決,其trusted: false,將裁決封頂於Indeterminate。- 不以 PDF 標頭開頭的位元組會產生一個每份文件的
error狀態且零簽章 — 不會有例外。 toJson()預設遮罩 PII。只在你握有記載於文件的合法處理簽署者身分之依據時,才傳入new CertPiiGuard(disclosePii: true)。
安全性註記
標題為「安全性註記」的區段- Fail-closed 的裁決產生器。
Valid需要以下全部:在/ByteRange摘要上經驗證的 CMS 認證、一條有效的鏈、確認的信任錨成員資格,以及確定的非撤銷狀態。每一項未確立的檢查都會使裁決退化;沒有任何東西預設為通過。 - 不容身分洗白。 回報的簽署者是經密碼學綁定的憑證主體。
/Name條目是攻擊者可控的中繼資料,絕不會被當作簽署者呈現。 - 弱演算法絕不通過。 通過驗證的 SHA-1 簽章仍會被回報為非有效;弱摘要下的密碼學有效性不會被洗成完整通過。
- 信任是輸入,不是推論。 你提供的錨會以 DER SHA-256 指紋比對鏈的終端(RFC 5280 §6.1.1)。一條鏈的自我一致性,或僅是一份非空的錨清單,絕不建立信任。
- 撤銷具決定性。 一份經驗證的已撤銷聲明會強制
Invalid,無論其他所有檢查為何;無法取得的證據會強制Indeterminate。 - 序列化輸出中的預設隱私。 除非你選擇退出,否則
toJson()會遮罩簽署者 CN、根簽發者 DN、TSA 名稱與鏈問題診斷,在序列化邊界上落實 GDPR Article 5(1)(c) 的資料最小化。 - 確定性時間。 CRL 新鮮度決策讀取注入的 PSR-20 時鐘,而非主機的實際時鐘,因此撤銷裁決在測試下可重現。
一致性
標題為「一致性」的區段NextPDF Enterprise 實作了受 ETSI EN 319 102-1(三值驗證狀態模型,以及無法取得撤銷資訊即產生 INDETERMINATE 的規則)、RFC 5652 §5.6(驗證側的摘要重新計算)與 RFC 5280 §6.1(信任錨作為信賴方輸入至路徑驗證)啟發的行為。支援不等於一致性,而一致性不等於認證。NextPDF 未持有任何認證,也不授予任何認證。批次驗證器不是合格驗證服務,其狀態是與 EN 319 102-1 分類法對齊的工程裁決 — 而非來自完整 clause 5 驗證流程的 TOTAL-PASSED/TOTAL-FAILED/INDETERMINATE 指示。尤其批次模式不執行任何存在證明或時間戳處理;那片領域由單一文件驗證側涵蓋。
FIPS 模式行為
標題為「FIPS 模式行為」的區段批次驗證器不諮詢任何 FIPS 模式政策,且啟用 FIPS 模式不會改變批次裁決。其驗證側的演算法處理是固定且 fail-closed 的:弱(SHA-1)簽章絕不會被回報為 Valid,無論有無 FIPS 模式。Enterprise 的 FIPS 模式政策把關的是簽署/產生側,記載於 FIPS 140 — 深度參考。FIPS 140 支援是一項能力聲明,而非驗證或認證主張。
行為契約
標題為「行為契約」的區段validate()對空清單擲出\InvalidArgumentException,對超過 1,000 份文件擲出\OverflowException。格式錯誤的文件絕不擲出例外;它們產生每份文件的error結果。Valid需要以下的連言:CMS 經密碼學驗證、鏈有效、確認信任錨成員資格,且RevocationStatus::hasConclusiveGood()為真。- 一張確認已撤銷的憑證具決定性:無論所有其他證據為何,裁決都是
Invalid。 - 兩項撤銷檢查皆為
Unknown/Unavailable表示Indeterminate,絕不是Valid(3.1.0 強化,具向後相容性影響)。 - 一個已認證、鏈有效但沒有確認信任錨的簽章是
Indeterminate— 真實,但信任未確立。 signer是經 CMS 驗證的主體或空字串;/Name條目絕不使用。- 在批次結果中,
timestamp永遠是缺席狀態。 validCount只計入Valid;所有其他狀態都計入invalidCount,而文件狀態由這些計數器彙整。- 除非明確傳入一個守衛,否則
toJson()套用 privacy-by-default 的CertPiiGuard。 - 報告總計是對每份文件結果的精確加總;
durationMs是批次所測得的實際耗時。
Core 後備
標題為「Core 後備」的區段NextPDF Core 的 Security / Signing 模組是產生側:它建立 CMS 簽章、套用 RFC 3161 時間戳,並針對其在簽署時嵌入的材料驗證鏈與撤銷。Core 未出貨任何驗證側批次協調器:沒有多文件報告、沒有彙總狀態分類法、沒有針對第三方文件的 OCSP/CRL 撤銷裁決,也沒有受 PII 保護的報告序列化。僅靠 Core,你得自行擷取並驗證每個簽章,並建構自己的報告。Enterprise 的單一文件驗證側(簽章驗證)與此批次協調器提供了那一層。
出版邊界
標題為「出版邊界」的區段本頁僅記載外部可觀察的行為與受支援的公開 API 介面。內部命名空間路徑、輔助類別、機制表、操作手冊檔名與工單前綴皆不在範圍內。
另請參閱
標題為「另請參閱」的區段- 簽章驗證 — 單一文件的 AdES/PAdES 密碼學驗證側,包含時間戳與封存鏈驗證
- Archive — 為長期有效性嵌入 DSS/VRI 材料與文件時間戳
- Validation — 唯讀的結構性政策檢查,不含密碼學
- Signature — 深度參考 — Signature 模組的深度參考
- eIDAS 保證等級 —
CertPiiGuardAPI 與保證等級對應 - 大規模簽署,不妥協 — 關於大量簽署與驗證設計的 Insider 專文
- 正確地驗證簽章 — 關於 fail-closed 驗證為何重要的 Insider 專文