Inspect an existing signature and understand the trust boundary
At a glance
Section titled “At a glance”Use the Core inspector to detect whether a PDF carries a signature dictionary. The inspector runs offline and does not use the Spectrum sidecar. This recipe also makes the trust boundary clear: detecting a signature is not the same as verifying it. Cryptographic verification, trust-path validation, and revocation checking are Premium or external.
Install
Section titled “Install”composer require nextpdf/core:^3Conceptual overview
Section titled “Conceptual overview”A PDF signature is a signature field whose value is a signature dictionary
(ISO 32000-2 §12.7.4). The dictionary’s Contents entry holds DER-encoded
Cryptographic Message Syntax (CMS) SignedData (ISO 32000-2 §12.8.1). The
Inspector Quick fallback detects the presence of that structure by scanning
for signature markers. It does not parse the CMS, recompute the byte-range
digest (which excludes the signature value — ISO 32000-2 §12.8.1), validate
the certificate chain, or check revocation.
API surface
Section titled “API surface”Call new Inspector(), then ->inspect(string $pdfData, InspectConfig $config).
Use InspectConfig::quick() for the offline PHP fallback.
InspectDepth::Standard/Full require the Spectrum sidecar and fail closed
(INSPECT-SIDECAR-001) when it is absent. The result is an InspectResult
value object. For this workflow, use $hasSigned for signature presence,
plus $isEncrypted and $pdfVersion.
Code sample — Quick start
Section titled “Code sample — Quick start”<?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";Code sample — Production
Section titled “Code sample — Production”This self-contained program runs in the cookbook harness. It mirrors
examples/37-inspect-existing-signature.php.
It inspects a known-signed corpus sample and a freshly built unsigned document,
so you can observe both branches of the presence flag. It then routes the
verdict onward. Presence is routing input, never a trust verdict. The file is
handed to a cryptographic verifier (Pro or external). It is not trusted here.
<?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);Expected STDOUT (the signed branch is skipped if the corpus sample is absent):
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 : noEdge cases & gotchas
Section titled “Edge cases & gotchas”- Presence is not validity.
$hasSignedreports that a signature dictionary exists. It does not check the CMS structure, the byte-range digest, the signing certificate, the chain, or revocation. A tampered file can still reporthasSigned = true. Never treat presence as proof of integrity or authorship. - What full verification needs. A complete decision recomputes the byte-range digest (ISO 32000-2 §12.8.1), validates the CMS SignedData, builds and checks the X.509 path to a trusted anchor, and checks revocation through Online Certificate Status Protocol (OCSP) or a certificate revocation list (CRL). A signature timestamp, when present, is itself verified against its own imprint over the signature value octets (ETSI EN 319 122-1 §5.3). These operations run behind the signing contracts. Production implementations ship in Pro and Enterprise. An external validator is the other supported path.
- Inspection depth.
InspectConfig::quick()is the only depth that runs without the Spectrum sidecar.Standard/FullthrowINSPECT-SIDECAR-001when the sidecar is unavailable. - Empty input. An empty string throws an inspect exception with “PDF data must not be empty”. Guard the read.
- Multiple signatures / timestamps. The presence flag does not count
signatures or distinguish an approval signature from a document timestamp
(which is also carried in
unsignedAttrsper RFC 5652 §5.3). Use a dedicated verifier when the count or per-signature verdict matters.
Performance
Section titled “Performance”The Quick fallback performs a bounded scan over the document bytes. It does not parse the full object graph. Use it for fast triage of incoming files before you route them to a heavier verifier.
Security notes
Section titled “Security notes”The inspector is a triage tool, not a trust boundary. A positive hasSigned
must never gate a trust decision on its own.
Data Residency & PII Mitigations
Section titled “Data Residency & PII Mitigations”Inspection runs fully in-process. No document bytes leave the host. The Quick fallback reads only structural markers, not document text, so it does not extract or transmit personally identifiable information (PII).
Safe Telemetry & Log Scrubbing
Section titled “Safe Telemetry & Log Scrubbing”Inspector accepts an optional PSR-3 logger. It logs the chosen path
(“Spectrum unavailable, using PHP fallback”), not document content. Do not log
the inspected PDF bytes or the InspectResult verbatim if the document is
sensitive.
Threat model
Section titled “Threat model”Considered: a tampered file that presents a syntactically valid signature dictionary (the inspector reports presence; it explicitly does not assert integrity), and a file with no signature (correctly reported absent). Not asserted: that any detected signature is cryptographically valid, trusted, or unrevoked — those are the verifier’s job.
FIPS-mode behavior
Section titled “FIPS-mode behavior”The Quick fallback performs no cryptography, so Federal Information Processing Standards (FIPS) mode is not relevant to this recipe. Cryptographic verification (Premium/external) is where the FIPS provider chain matters.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| A signature field’s value is a signature dictionary. | ISO 32000-2 | §12.7.4 | |
Contents holds DER CMS SignedData; a document-timestamp Contents holds a TimeStampToken. | ISO 32000-2 | §12.8.1 | |
| Verification recomputes the digest over the byte range, excluding the signature value. | ISO 32000-2 | §12.8.1 | |
| A signature timestamp imprint is over the SignerInfo signature value octets. | ETSI EN 319 122-1 | §5.3 | |
| A timestamp is carried in SignerInfo unsignedAttrs. | RFC 5652 | §5.3 |
This recipe detects a signature. It does not assert that any signature is valid, trusted, or unrevoked. A cryptographic verifier makes that decision.
Commercial context
Section titled “Commercial context”Cryptographic CMS verification, X.509 path validation, and OCSP/CRL revocation checking ship behind the signing contracts in the Pro and Enterprise editions. The Core inspector covers presence detection only.