コンテンツにスキップ
getnextpdf.com

Enterprise エディション

バッチ署名検証

NextPDF Enterpriseは、多数のPDF文書にわたるデジタル署名を1回の呼び出しで検証します。NextPDF\Enterprise\Signature\BatchSignatureValidator::validate() は文書のリストを受け取り、BatchValidationReport を返します。すべての署名は同一のフェイルクローズのパイプラインを通過します。すなわち、署名対象のバイト範囲に対する暗号的なCMS認証、トラストアンカーによる証明書チェーンの検証、そしてOCSP/CRLによる失効確認です。レポートは文書ごと・署名ごとの詳細(CertChainStatusRevocationStatusTimestampStatus)を保持するため、コンプライアンスツールは記録された証跡からすべての判定を再導出できます。

判定モデルは意図的に厳格です。署名が Valid になるのは、すべての証跡が積極的に確立された場合に限られます。失効証跡が欠けている場合は Indeterminate となり、決して Valid にはなりません。このページはバッチオーケストレーターとその結果型を扱います。単一文書のAdES検証側は 署名検証 で説明しています。長期検証用マテリアルの埋め込みは アーカイブ で説明しています。

この機能は NextPDF Enterprisenextpdf/enterprise)で提供され、Enterpriseティアのライセンスエンベロープで有効化されます。その権限を持たないデプロイでは、この機能のクラスは読み込まれません。エディションを比較してライセンスを取得

Terminal window
composer require nextpdf/enterprise

nextpdf/premium メタパッケージからもEnterpriseパッケージが解決されます。有効化にはEnterpriseライセンスエンベロープを使用します。ライセンスと有効化 を参照してください。バッチ関連の型は NextPDF\Enterprise\Signature 配下でオートロードされます。エンジンのベースラインを超えるPHP拡張は不要です。

validate() の1回の呼び出しで、DocumentSignatureInput 値のリストを処理します。各入力は、文書識別子、生のPDFバイト列、そして任意のPEMエンコードされたトラストアンカーを保持します。バリデーターは各文書の署名辞書を抽出し、署名ごとに3つのステージを実行します。

ステージ1 — 暗号的認証。 /Contents からのデタッチCMS/PKCS#7ブロブは、/ByteRange が対象とするバイト列に対して検証されます。検証器はコンテンツダイジェストを自ら再計算し、それを messageDigest 署名属性と比較します。プロデューサーが供給したダイジェストを信頼することは決してありません(RFC 5652 §5.6)。署名値が検証に成功し、かつ署名証明書がCMSにバインドされていなければなりません。/Contents もしくは /ByteRange が存在しないか不正な場合、CMSが解析不能な場合、ダイジェストの不一致、または署名チェックの失敗は、いずれもフェイルクローズとなります。SHA-1で検証される署名は脆弱とみなされ、完全な合格になることは決してありません。

ステージ2 — チェーン検証とトラストアンカリング。 CMSから復元された署名者チェーンは、見込みの証明書パスとして検証されます。指定する trustedCerts は、RFC 5280 §6.1.1 の意味でのトラストアンカー 入力 です。すなわち、チェーンの終端がDER SHA-256フィンガープリントによって供給されたアンカーと一致しなければなりません。構造的に整合していても終端が設定済みアンカーでないチェーンは、信頼済みとして報告されることは決してありません。利用可能なアンカーがない場合は、構造的な判定のみが報告され、CertChainStatus::$trustedfalse のままです。

ステージ3 — 失効。 失効確認は認証後に復元されたチェーンに対して実行され、失効確認がパス検証の成功後に続くというETSI EN 319 102-1 のモデル(clause 5.2.6.2)を踏襲します。OCSPが主です。すなわち、暗号的に検証されたレスポンスのみが Good または Revoked として有効に扱われます。CRLパスはフォールバックであり、リストの鮮度を証明します。どちらのクライアントも設定されていない場合、状態は unavailable になります。

署名ごとの判定は SignatureValidationStatus です。この分類は、ETSI EN 319 102-1 のステータスモデル(TOTAL-PASSED / TOTAL-FAILED / INDETERMINATE)を署名ごとの粒度で踏襲します。

証跡判定
証明書が失効と確認されたInvalid(他のチェックに関わらず決定的)
CMS認証が失敗し、署名者マテリアルが復元されなかったError
CMS認証が失敗し、署名者マテリアルが存在するInvalid
認証済みだが、チェーンが検証されないInvalid(チェーンがなければ Error
認証済みかつチェーン有効だが、確認されたトラストアンカーがないIndeterminate
認証済み・チェーン有効・信頼済みだが、確定的な非失効がないIndeterminate
上記すべてが積極的に確立されたValid

確定的な非失効のルール。 「失効が証明されていない」ことは「失効していないことが証明された」ことと同じではありません。Valid の判定には、少なくとも1つの Good な失効結果が必要です。検証済みで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クライアントを設定しないデプロイでは、以前 Valid だった箇所で Indeterminate が見られることが一般的になりました。

2つの境界がこの機能を誠実に位置づけます。第一に、バッチバリデーターは埋め込まれたタイムスタンプトークンを評価しません。バッチ結果における TimestampStatus は常に不在の状態です。RFC 3161 タイムスタンプの評価は単一文書の検証側に属します。署名検証 を参照してください。第二に、このページは読み取り専用の検証です。長期有効性のためのDSS/VRIマテリアルの埋め込みは アーカイブ 機能です。

要となる設計判断は、フェイルクローズの判定生成器です。Valid は、暗号的認証、トラストアンカーによるチェーン、確定的な非失効という3つの軸すべてにおける積極的な証跡からのみ生成されます。確立されていないものは、合格を既定とするのではなく Indeterminate に格下げされます。これは、失効マテリアルが欠けている場合のEN 319 102-1 の姿勢です。バッチのスループットが厳密さを引き換えにすることは決してありません。バッチレイヤーは、単一文書に用いるのと同じ監査済みCMS検証器の上に構築されたオーケストレーションであるため、1,000件の文書を処理する実行でも同一の暗号処理が適用されます。またレポートは証跡と判定を分離します。すなわち、CertChainStatusRevocationStatus は各判定が依拠する入力を記録するため、監査者は後からそれを再導出できます。

設計の背景: 妥協なき大規模署名

以下のシンボルはすべて nextpdf/enterprise 3.1.0 の公開APIです。

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 であり、判定は凍結されたテストクロックのもとで決定的になります。

final readonly class DocumentSignatureInput
{
public string $documentId;
public function __construct(
string $documentId,
public string $pdfData,
public array $trustedCerts = [],
)
}

スロー/失敗条件: $documentId が空文字列の場合は \InvalidArgumentException$trustedCerts はPEMエンコードされたトラストアンカー証明書のリストです。

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 になるのは、署名が存在し、かつそのいずれもnon-validでない場合に限られます。既定では、toJson() はプライバシー既定の NextPDF\Enterprise\Signature\Eidas\CertPiiGuard を適用し、署名者名、ルート発行者、TSA名、およびチェーン問題の診断情報をマスクします。ガードのAPIについては eIDAS保証レベル を参照してください。

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';
}

スロー/失敗条件: なし。イミュータブルな値オブジェクトおよびバック付きenum。

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検証済み証明書のサブジェクト、それ以外は空文字列です。$levelSubFilter から導出されるラベル(例: ETSI.CAdES.detached に対する B-B)であり、AdES準拠の判定ではありません。

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 は、確認されたトラストアンカーの一致がある場合にのみ設定され、アンカーリストが空でないことから設定されることは決してありません。

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()true になるのは、非失効の状態で、かつ少なくとも1つのチェックが Good である場合に限られます。

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() の状態です。エッジケースと注意点 を参照してください。

1つの文書を検証し、レポートを読み取ります。このサンプルは署名のないPDFを使用するため、出力は決定的です。

batch-quick-start.php
<?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: 1
Signatures: 0
doc-001: no_signatures
All valid: no
Unsigned documents: yes

ここで allValid()no を報告する点に注意してください。これは少なくとも1つの署名とnon-validな結果がないことを要求するため、空の署名集合が黙って合格することは決してありません。

失効クライアント、トラストアンカー、バッチのチャンク分割、およびPII保護されたJSONレポートを用いて、署名済み契約書のディレクトリを検証します。

batch-validate-contracts.php
<?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、失効証跡が取得できなかった1つの文書についての例。他の行は入力によって異なります):

contract-0042.pdf: indeterminate (chain trusted: yes, revoked: no)

JSONレポートは署名者の識別情報フィールドを既定の CertPiiGuard を通じてシリアライズするため、署名ごとのエントリは次のようになります(抜粋、説明用):

{
"status": "indeterminate",
"signer": "[REDACTED]",
"level": "B-B",
"subFilter": "ETSI.CAdES.detached"
}
  • 空の入力リストは \InvalidArgumentException をスローします。1回の呼び出しで1,000件を超える文書は \OverflowException をスローします。より大きな実行は、本番サンプルのようにチャンク分割してください。
  • 以前のリリースからのアップグレード: OCSPまたはCRLクライアントを設定していない場合、失効は unavailable となるため、どの署名も Valid に到達できません。以前のリリースはここで Valid を報告していましたが、3.1.0 は Indeterminate を報告します(コンセプト概要 を参照)。
  • 文書レベルのカウンターは厳格です。validCount を増やすのは Valid のみです。InvalidIndeterminateError はいずれも invalidCount を増やします。したがって、唯一の署名が Indeterminate である文書は all_invalid を報告します。区別が重要な場合は、署名ごとの status で判断してください。
  • OCSPチェックは、クエリに発行者が必要であるため、復元されたチェーンに少なくとも2つの証明書がある場合にのみ実行されます。証明書が1つだけのチェーンは、CRLパスまたは unavailable に落ちます。
  • crlStatus はバッチ結果で revoked を報告することは決してありません。CRLフォールバックはリストの鮮度のみを証明します。権威ある失効結果はOCSPから得られます。
  • timestamp はバッチ結果では常に absent() です。バッチバリデーターは埋め込まれたRFC 3161 トークンを評価しません。タイムスタンプの評価には 署名検証 を使用してください。
  • signer は認証が失敗した場合は空です。設定される場合は、CMS検証済み証明書のサブジェクトCN(またはO)であり、署名辞書の未認証な /Name 文字列になることは決してありません。
  • trustedCerts のエントリはPEM証明書でなければなりません。空または不正なアンカーリストは、trusted: false を伴う構造的のみのチェーン判定を生じ、判定は Indeterminate に上限が設けられます。
  • PDFヘッダーで始まらないバイト列は、署名数0の文書ごとの error 状態を生成します。例外はスローされません。
  • toJson() は既定でPIIを秘匿します。署名者の識別情報を処理する文書化された適法な根拠を有する場合にのみ、new CertPiiGuard(disclosePii: true) を渡してください。
  • フェイルクローズの判定生成器。 Valid には次のすべてが必要です。/ByteRange ダイジェストに対する検証済みCMS認証、有効なチェーン、確認されたトラストアンカーへの所属、そして確定的な非失効の状態です。確立されていないチェックはいずれも判定を格下げし、合格が既定になるものはありません。
  • 識別情報のロンダリングなし。 報告される署名者は、暗号的にバインドされた証明書のサブジェクトです。/Name エントリは攻撃者が制御可能なメタデータであり、署名者として現れることは決してありません。
  • 脆弱なアルゴリズムは決して合格しない。 検証に成功するSHA-1署名も、依然としてnon-validとして報告されます。脆弱なダイジェストのもとでの暗号的な有効性が、完全な合格へとロンダリングされることはありません。
  • 信頼は入力であり、推論ではない。 供給するアンカーは、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(3値の検証ステータスモデルと、取得できない失効情報がINDETERMINATEを生じるというルール)、RFC 5652 §5.6(検証側でのダイジェスト再計算)、およびRFC 5280 §6.1(パス検証への依拠当事者の入力としてのトラストアンカー)に基づく挙動を実装しています。サポートは準拠ではなく、準拠は認証ではありません。NextPDFはいかなる認証も保有せず、いかなる認証も付与しません。バッチバリデーターは適格検証サービスではなく、そのステータスはEN 319 102-1 の分類に整合したエンジニアリング上の判定であり、完全なclause 5 の検証プロセスによるTOTAL-PASSED/TOTAL-FAILED/INDETERMINATE の表示ではありません。特に、バッチモードは存在証明やタイムスタンプ処理を行いません。単一文書の検証側がその領域をカバーします。

バッチバリデーターはFIPSモードのポリシーを参照せず、FIPSモードを有効にしてもバッチの判定は変わりません。その検証側のアルゴリズム処理は固定かつフェイルクローズです。すなわち、FIPSモードの有無にかかわらず、脆弱な(SHA-1)署名が Valid として報告されることは決してありません。EnterpriseのFIPSモードポリシーは署名/生成側を制御するもので、FIPS 140 — 詳細リファレンス で説明しています。FIPS 140 のサポートは能力の表明であり、検証または認証の主張ではありません。

  • validate() は、空のリストに対して \InvalidArgumentException を、1,000件を超える文書に対して \OverflowException をスローします。不正な文書がスローすることは決してなく、文書ごとの error 結果を生成します。
  • Valid には次の連言が必要です。CMSが暗号的に検証済み、チェーンが有効、トラストアンカーへの所属が確認済み、そして RevocationStatus::hasConclusiveGood() が true であること。
  • 失効が確認された証明書は決定的です。他のすべての証跡に関わらず、判定は Invalid になります。
  • 両方の失効チェックが Unknown/Unavailable の場合は Indeterminate となり、決して Valid にはなりません(3.1.0 の厳格化、後方互換性への影響)。
  • 確認されたトラストアンカーがない、認証済みかつチェーン有効な署名は Indeterminate です。真正であるが、信頼は未確立です。
  • signer はCMS検証済みのサブジェクトまたは空文字列です。/Name エントリが使われることは決してありません。
  • timestamp はバッチ結果では常に不在の状態です。
  • validCountValid のみを数えます。その他すべてのステータスは invalidCount に数えられ、文書ステータスはこれらのカウンターから集計されます。
  • toJson() は、ガードが明示的に渡されない限り、プライバシー既定の CertPiiGuard を適用します。
  • レポートの合計値は文書ごとの結果にわたる正確な総和です。durationMs はバッチについて計測された実時間です。

NextPDF Coreの セキュリティ/署名 モジュールはプロデューサー側です。CMS署名を作成し、RFC 3161 タイムスタンプを適用し、署名時に埋め込むマテリアルについてチェーンと失効を検証します。Coreには検証側のバッチオーケストレーターは付属しません。すなわち、複数文書レポート、集約されたステータス分類、第三者文書に対するOCSP/CRL失効判定、およびPII保護されたレポートのシリアライズはありません。Coreのみでは、各署名を自分で抽出・検証し、独自のレポートを構築することになります。Enterpriseの単一文書検証側(署名検証)とこのバッチオーケストレーターが、そのレイヤーを提供します。

このページは、外部から観測可能な挙動とサポートされる公開API表面のみを説明します。内部の名前空間パス、ヘルパークラス、メカニズムの表、ランブックのファイル名、およびチケットの接頭辞は対象外です。