跳到內容

檢視既有簽章,並理解信任邊界

這則 recipe(範例)使用 Core 檢視器偵測 PDF 是否帶有簽章字典。檢視器會離線執行,不會用到 Spectrum sidecar。這則範例也把界線劃分清楚:偵測到簽章和驗證簽章不是同一回事。密碼學驗證、信任路徑驗證與撤銷檢查,屬於 Premium 或外部驗證器的範疇。

Terminal window
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)、驗證憑證鏈,也不會檢查撤銷狀態。

先呼叫 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 為真時,絕對不能單憑它就放行任何信任判定。

檢視完全在行程內進行。沒有任何文件位元組離開主機。Quick 後援只讀取結構性標記,不讀文件內文,因此不會擷取或傳出任何 PII。

Inspector 可接受選用的 PSR-3 記錄器。它只記錄採用的路徑(「Spectrum unavailable, using PHP fallback」),不記錄文件內容。如果文件包含敏感資料,請不要原樣記錄被檢視的 PDF 位元組或 InspectResult

已納入考量:一份帶有語法上有效簽章字典、但已遭竄改的檔案(檢視器回報存在,但明確不主張完整性),以及一份沒有簽章的檔案(正確回報為不存在)。未主張的部分:任何偵測到的簽章在密碼學上有效、受信任或未被撤銷——這些是驗證器的職責。

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 檢視器只涵蓋存在偵測。