コンテンツにスキップ

既存の署名を検査して信頼境界を理解する

このレシピでは、Core inspector を使って PDF に署名辞書が含まれるかどうかを検出します。この inspector はオフラインで動作し、Spectrum サイドカーは使用しません。また、このレシピでは境界も明確にします。署名を検出することは、その署名を検証することと同じではありません。暗号的検証、信頼パスの検証、および失効チェックは 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 サイドカーが必要で、利用できない場合はフェイルクローズします(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 サイドカーなしで動作する唯一の深さです。Standard/Full は、サイドカーが利用できない場合に INSPECT-SIDECAR-001 をスローします。
  • 空の入力。 空の文字列は、「PDF data must not be empty」という inspect 例外をスローします。読み取りをガードしてください。
  • 複数の署名 / タイムスタンプ。 存在フラグは署名数を数えず、承認署名とドキュメントタイムスタンプ(これは RFC 5652 §5.3 に従って unsignedAttrs でも運ばれます)を区別しません。署名数や署名ごとの判定が重要な場合は、専用のバリデーターを使用してください。

Quick フォールバックは、ドキュメントバイトに対する境界付きのスキャンです。オブジェクトグラフ全体は解析しません。受信したファイルをより重いバリデーターにルーティングする前の、高速なトリアージに適しています。

この inspector はトリアージツールであり、信頼境界ではありません。hasSigned が真であっても、それだけで信頼を判断してはいけません。

検査は完全にインプロセスで行われます。ドキュメントのバイトがホストの外に出ることはありません。Quick フォールバックはドキュメントテキストではなく構造マーカーのみを読み取るため、PII が抽出または送信されることはありません。

Inspector はオプションの PSR-3 ロガーを受け付けます。選択された経路(「Spectrum unavailable, using PHP fallback」)はログに記録しますが、ドキュメントの内容は記録しません。ドキュメントが機密である場合、検査した PDF のバイトや InspectResult をそのままログに記録しないでください。

考慮されているもの: 構文的に有効な署名辞書を提示する改ざんされたファイル(inspector は存在を報告しますが、完全性は明示的に主張しません)、および署名のないファイル(正しく存在しないと報告されます)。主張されないもの: 検出された署名が暗号的に有効、信頼済み、または失効していないということ。これらはバリデーターの役割です。

Quick フォールバックは暗号処理を行わないため、FIPS モードはこのレシピには関係ありません。FIPS プロバイダーチェーンが重要になるのは、暗号的検証(Premium/外部)です。

記述仕様箇条リファレンス 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 inspector は存在の検出のみをカバーします。