跳转到内容

检视现有签章并理解信任边界

本 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 检视器只涵盖存在检测。