检视现有签章并理解信任边界
本 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 检视器只涵盖存在检测。