Skip to content

Inspect an existing signature and understand the trust boundary

Use the Core inspector to detect whether a PDF has a signature, and keep the trust boundary clear. Detection is not verification. Premium editions or an external verifier handle cryptographic verification, trust-path validation, and revocation checking.

  • Core installed: composer require nextpdf/core:^3.
  • A PDF file to inspect.
  1. Read the PDF bytes.
  2. Create an Inspector and call inspect().
  3. Read InspectResult::$hasSigned. true means the file contains a signature dictionary.
  4. Read InspectResult::$isEncrypted and the risk flags for surrounding context.
  5. Route the file to a verifier for the cryptographic decision. The inspector reports presence, not validity.

The diagram below shows the boundary: presence is not validity. It also shows the checks a real verifier must still perform.

false

true

Yes

No

Yes

No / unknown

Incoming PDF

Inspector.inspect — Quick

hasSigned?

No signature present

Signature dictionary PRESENT

NOT yet validity

Recompute byte-range digest

ISO 32000-2 §12.8.1

Validate CMS SignedData

Build & check X.509 path

to a chosen trust anchor

Check revocation OCSP / CRL

Long-term input?

Read DSS validation material

All checks evaluated

Every check passed?

Trustworthy

Not trusted — do not act

Diagram
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Inspect\Inspector;
use NextPDF\Inspect\InspectConfig;
$pdfData = file_get_contents(__DIR__ . '/incoming.pdf');
if ($pdfData === false || $pdfData === '') {
fwrite(STDERR, "Cannot read incoming.pdf\n");
exit(1);
}
$inspector = new Inspector();
// InspectConfig::quick() selects InspectDepth::Quick. This is the only
// depth that runs offline: a default Inspector has no Spectrum sidecar,
// and without a sidecar Quick is the sole path that returns a result.
// Standard and Full require the sidecar (see the SIDECAR-001 edge case).
$result = $inspector->inspect(
$pdfData,
InspectConfig::quick(),
);
// hasSigned reports the PRESENCE of a signature dictionary.
// It does NOT mean the signature verifies.
if ($result->hasSigned) {
echo "A signature is present in incoming.pdf\n";
echo "Encrypted: " . ($result->isEncrypted ? 'yes' : 'no') . "\n";
echo "Next step: run a cryptographic verifier before trusting it.\n";
} else {
echo "No signature found in incoming.pdf\n";
}

For a signed input:

A signature is present in incoming.pdf
Encrypted: no
Next step: run a cryptographic verifier before trusting it.

For an unsigned input:

No signature found in incoming.pdf
  • Presence is not validity — boundary. InspectResult::$hasSigned reports that a signature dictionary exists in the file. It does not check the Cryptographic Message Syntax (CMS) structure, the byte-range digest, the signing certificate, the certificate chain, or revocation status. A tampered file can still report hasSigned = true. Never treat presence as proof of integrity or authorship.
  • What full verification needs. A complete decision recomputes the byte-range digest and compares it (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). For long-term inputs, validation data lives in the Document Security Store (DSS) (ETSI EN 319 142-2 §6.3.1). These operations run behind the SignerInterface and LtvManagerInterface contracts; the production implementations ship in the Pro and Enterprise editions. An external validator is the other supported path.
  • Inspection depth and the sidecar (SIDECAR-001). A default new Inspector() has no Spectrum sidecar configured. Without a sidecar, only InspectDepth::Quick returns a result. InspectDepth::Quick uses the in-process PHP fallback. InspectDepth::Standard and InspectDepth::Full both require the sidecar. If no sidecar is available, they throw InspectException with code INSPECT-SIDECAR-001 (“Spectrum sidecar is required for Standard/Full depth inspection”, retryable = true). The InspectConfig constructor’s default depth is Standard. Therefore, a bare new InspectConfig() (or new InspectConfig(depth: InspectDepth::Standard)) is not offline-usable. It throws SIDECAR-001 before hasSigned is ever read. Use InspectConfig::quick() for offline presence detection, as this recipe does. No depth performs cryptographic signature verification in Core.
  • Empty input. An empty string throws InspectException with code INSPECT-INPUT-001. Guard the file read.
  • Multiple signatures. The presence flag does not count signatures or distinguish an approval signature from a document timestamp. Use a dedicated verifier when the count or per-signature verdict matters.
StatementSpecClausereference_id
The signature value is stored in the signature dictionary Contents entry.ISO 32000-2§12.8.1
Verification recomputes the digest over the ByteRange, excluding the signature value.ISO 32000-2§12.8.1
Long-term validation data lives in the DSS.ETSI EN 319 142-2§6.3.1

This recipe detects whether a signature is present. It does not assert that any signature is valid, trusted, or unrevoked. That decision belongs to a cryptographic verifier.