檢視既有簽章,並理解信任邊界
這則 recipe(範例)使用 Core 檢視器偵測 PDF 是否帶有簽章字典。檢視器會離線執行,不會用到 Spectrum sidecar。這則範例也把界線劃分清楚:偵測到簽章和驗證簽章不是同一回事。密碼學驗證、信任路徑驗證與撤銷檢查,屬於 Premium 或外部驗證器的範疇。
composer require nextpdf/core:^3概念說明
標題為「概念說明」的區段在 PDF 中,簽章是一個值為簽章字典的簽章欄位(ISO 32000-2 §12.7.4)。該字典的 Contents 項目存放 DER 編碼的 CMS SignedData(ISO 32000-2 §12.8.1)。Inspector 的 Quick 後援機制會掃描簽章標記,用來偵測這類結構是否存在。它不會剖析 CMS、重新計算位元組範圍摘要(該摘要不含簽章值——ISO 32000-2 §12.8.1)、驗證憑證鏈,也不會檢查撤銷狀態。
API 介面
標題為「API 介面」的區段先呼叫 new Inspector(),再呼叫 ->inspect(string $pdfData, InspectConfig $config)。離線 PHP 後援請使用 InspectConfig::quick()。InspectDepth::Standard/Full 需要 Spectrum sidecar;當 sidecar 不存在時,會以故障安全方式失敗(INSPECT-SIDECAR-001)。結果是一個 InspectResult 值物件。這裡相關的欄位是 $hasSigned(簽章是否存在)、$isEncrypted 與 $pdfVersion。
程式碼範例 — 快速開始
標題為「程式碼範例 — 快速開始」的區段<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Inspect\InspectConfig;use NextPDF\Inspect\Inspector;
$pdfData = file_get_contents(__DIR__ . '/incoming.pdf');if ($pdfData === false || $pdfData === '') { fwrite(STDERR, "Cannot read incoming.pdf\n"); exit(1);}
$result = (new Inspector())->inspect($pdfData, InspectConfig::quick());
// hasSigned reports the PRESENCE of a signature dictionary.// It does NOT mean the signature verifies.echo $result->hasSigned ? "A signature is present — NOT verified.\n" : "No signature found.\n";程式碼範例 — 正式環境
標題為「程式碼範例 — 正式環境」的區段這是一支可獨立執行,也能由測試載具執行的程式。它對應 範例 examples/37-inspect-existing-signature.php。這支程式會檢視一個已知帶有簽章的語料樣本,以及一份剛建立的未簽章文件,因此存在旗標的兩種分支都能觀察到。接著它會把判定結果往下游傳遞。簽章是否存在只是路由的輸入依據,絕不是信任判定。檔案會交給密碼學驗證器(Pro 或外部)處理;在這裡並不會信任它。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Inspect\InspectConfig;use NextPDF\Inspect\Inspector;
$inspector = new Inspector();
// --- A known-signed input ---// The repository corpus carries synthetic PAdES samples. In your// application this is simply the incoming PDF you received.$signedPath = __DIR__ . '/tests/Corpus/pades/pades-b-b-bytepattern-synthetic.pdf';if (is_file($signedPath)) { $signed = (string) file_get_contents($signedPath); $r = $inspector->inspect($signed, InspectConfig::quick());
echo "Signed sample:\n"; echo ' Signature present : ' . ($r->hasSigned ? 'yes' : 'no') . "\n"; echo ' Encrypted : ' . ($r->isEncrypted ? 'yes' : 'no') . "\n"; echo ' PDF version : ' . ($r->pdfVersion ?? 'unknown') . "\n"; echo " Verdict : presence detected — NOT verified.\n";
if ($r->hasSigned) { // Presence detected. This is routing input, not a trust verdict. // Hand the file to a cryptographic verifier (Pro or external) // before relying on it. (Pseudo-queue shown; wire your own.) // $verifierQueue->enqueue($signed); echo " Next step : run a cryptographic verifier before trusting it.\n"; }} else { echo "Signed corpus sample absent; skipping the signed branch.\n";}
// --- A known-unsigned input ---$unsigned = Document::createStandalone();$unsigned->setTitle('Unsigned sample');$unsigned->addPage();$unsigned->setFont('helvetica', '', 12);$unsigned->cell(0, 10, 'This document carries no signature.', newLine: true);$unsignedBytes = $unsigned->getPdfData();
$ru = $inspector->inspect($unsignedBytes, InspectConfig::quick());echo "Unsigned sample:\n";echo ' Signature present : ' . ($ru->hasSigned ? 'yes' : 'no') . "\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the// semantic profile; emit the unsigned document to the side-channel.$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/inspected.pdf', $unsignedBytes);預期的 STDOUT(若語料樣本不存在,會略過已簽章分支):
Signed sample: Signature present : yes Encrypted : no PDF version : <version> Verdict : presence detected — NOT verified. Next step : run a cryptographic verifier before trusting it.Unsigned sample: Signature present : no邊角案例與注意事項
標題為「邊角案例與注意事項」的區段- 存在不等於有效。
$hasSigned回報的是簽章字典存在。它並不會檢查 CMS 結構、位元組範圍摘要、簽署憑證、憑證鏈或撤銷狀態。遭竄改的檔案仍然可能回報hasSigned = true。絕對不要把存在當成完整性或作者身分的證明。 - 完整驗證需要什麼。 完整的判定會重新計算位元組範圍摘要(ISO 32000-2 §12.8.1)、驗證 CMS SignedData、建立 X.509 路徑並檢查其是否通往受信任錨點,並透過 OCSP 或 CRL 檢查撤銷狀態。若存在簽章時間戳記,也會驗證其本身涵蓋簽章值八位元組的印記(ETSI EN 319 122-1 §5.3)。這些作業會在簽署合約介面背後執行。正式環境實作隨 Pro 與 Enterprise 版本一同出貨。外部驗證器則是另一條受支援的路徑。
- 檢視深度。
InspectConfig::quick()是唯一不需 Spectrum sidecar 即可執行的深度。當 sidecar 無法使用時,Standard/Full會擲出INSPECT-SIDECAR-001。 - 空輸入。 空字串會擲出檢視例外,訊息為「PDF data must not be empty」。請替讀取流程加上防護。
- 多重簽章/時間戳記。 存在旗標不會計算簽章數量,也不會區分核可簽章與文件時間戳記(後者依 RFC 5652 §5.3 同樣承載於
unsignedAttrs中)。當數量或個別簽章的判定重要時,請改用專屬的驗證器。
Quick 後援會對文件位元組做一次有界掃描。它不會剖析完整的物件圖。這很適合在把傳入檔案路由給較重的驗證器之前,先做快速的初步分流。
安全性注意事項
標題為「安全性注意事項」的區段檢視器是初步分流工具,不是信任邊界。hasSigned 為真時,絕對不能單憑它就放行任何信任判定。
資料落地與 PII 緩解
標題為「資料落地與 PII 緩解」的區段檢視完全在行程內進行。沒有任何文件位元組離開主機。Quick 後援只讀取結構性標記,不讀文件內文,因此不會擷取或傳出任何 PII。
安全遙測與日誌清洗
標題為「安全遙測與日誌清洗」的區段Inspector 可接受選用的 PSR-3 記錄器。它只記錄採用的路徑(「Spectrum unavailable, using PHP fallback」),不記錄文件內容。如果文件包含敏感資料,請不要原樣記錄被檢視的 PDF 位元組或 InspectResult。
威脅模型
標題為「威脅模型」的區段已納入考量:一份帶有語法上有效簽章字典、但已遭竄改的檔案(檢視器回報存在,但明確不主張完整性),以及一份沒有簽章的檔案(正確回報為不存在)。未主張的部分:任何偵測到的簽章在密碼學上有效、受信任或未被撤銷——這些是驗證器的職責。
FIPS 模式行為
標題為「FIPS 模式行為」的區段Quick 後援不執行任何密碼學運算,因此 FIPS 模式與這則範例無關。只有密碼學驗證(Premium/外部)才會涉及 FIPS 提供者鏈。
符合性
標題為「符合性」的區段| 陳述 | 規範 | 條款 | 參考 ID |
|---|---|---|---|
| 簽章欄位的值為簽章字典。 | ISO 32000-2 | §12.7.4 | |
Contents 存放 DER 編碼的 CMS SignedData;文件時間戳記的 Contents 則存放 TimeStampToken。 | ISO 32000-2 | §12.8.1 | |
| 驗證會在位元組範圍上重新計算摘要,但排除簽章值。 | ISO 32000-2 | §12.8.1 | |
| 簽章時間戳記的印記涵蓋 SignerInfo 簽章值八位元組。 | ETSI EN 319 122-1 | §5.3 | |
| 時間戳記承載於 SignerInfo 的 unsignedAttrs 中。 | RFC 5652 | §5.3 |
這則範例會偵測簽章。它並不主張任何簽章有效、受信任或未被撤銷。這項判定屬於密碼學驗證器的職責。
商業情境
標題為「商業情境」的區段密碼學 CMS 驗證、X.509 路徑驗證,以及 OCSP/CRL 撤銷檢查,均透過簽署合約介面隨 Pro 與 Enterprise 版本出貨。Core 檢視器只涵蓋存在偵測。