Skip to content
getnextpdf.com

Enterprise edition

Signature verification — Deep Reference

This page is the deep reference for the AdES verify-side surface in NextPDF Enterprise. The entry point is NextPDF\Enterprise\Security\Validation\AdESValidationEngine. It implements NextPDF’s ETSI-modelled validation flows for basic, with-time, long-term, and archival timestamp checks: basic validation, validation with time, validation with long-term data, and archival DocTimeStamp coverage-chain validation. Outcomes are ValidationReport values carrying MainIndication and SubIndication enum cases with ETSI URN string values. Supporting surfaces documented here: the SignatureDataExtractor SPI and its CmsSignatureDataExtractor implementation, the PdfSignatureDictionaryScanner byte-level scanner, the NextPDF\Enterprise\Security\Pki path-validation surface, and BatchSignatureValidator. For workflow-level guidance, see Signature verification: AdES / PAdES cryptographic verify-side.

This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
AdESValidationEngine::__construct11 optional parameters: ?PathValidatorInterface $chainValidator, ?SignatureDataExtractor $extractor, ClockInterface $clock, ?LoggerInterface $logger, string $defaultPolicy, NetworkPolicy $networkPolicy, and five optional verifier collaboratorsAll defaults are fail-closed: Pki path validator over the engine clock, no extractor, no TSA trust storeNew engineDoes not throwWith no trust store, TSA-chain evaluation reports untrusted; that maps to INDETERMINATE, never a pass
AdESValidationEngine::validateBasicstring $signedData, string $signatureBasic validation: format, digest, crypto, weak-algorithm, chain, provenance-gated revocationValidationReportDoes not throw; extraction and path failures map to fail-closed reportsWithout an extractor, guard checks only; see edge cases
AdESValidationEngine::validateWithTimestring $signedData, string $signature, DateTimeImmutable $claimedTimeBasic validation first; certificate window and revocation compared against the claimed timeValidationReportDoes not throwStrict signature-timestamp gate when the attribute is present; $claimedTime stays the time anchor
AdESValidationEngine::validateWithLongTermDatastring $signedData, string $signature, array $dssData (certs/ocsps/crls)Basic pass required; signature-timestamp gate armed with TSA-at-genTime; POE, DSS revocation, and archival gatesValidationReportDoes not throwNetworkPolicy::STRICT_OFFLINE with insufficient embedded data yields INDETERMINATE / TRY_LATER
AdESValidationEngine::validateArchivalTimestampChainstring $pdfBytes, array $dssData = [], ?TrustAnchorStoreInterface $anchors = nullEvidence-based DocTimeStamp coverage chain over the exact ByteRange bytesValidationReportDoes not throw on hostile bytesTOTAL_PASSED only for a trusted, EOF-covering chain
MainIndicationString-backed enum, three casesETSI URN values; see the case list below
SubIndicationString-backed enum, fifteen casesETSI URN values; see the case list below
ValidationReport::__constructMainIndication $mainIndication, ?SubIndication $subIndication, DiagnosticData $diagnosticData, DateTimeImmutable $validationTime, string $validationPolicy = ''Immutable (final readonly) validation outcomeNew reportDoes not throwisPassed(), isFailed(), isIndeterminate(), toArray()
DiagnosticData::__constructarray $certificateChain, array $timestamps, array $revocationData, string $validationPolicy, string $signatureFormat, array $warnings (all defaulted)Immutable evidence container; audit trail onlyNew valueDoes not throwtoArray() serializes references for reporting
SignatureDataExtractor::extractstring $signedData, string $signatureSPI: parse the CMS and extract validation componentsExtractedSignatureDataSignatureExtractionException when the signature cannot be parsedInterface; decouples ASN.1 parsing from the engine
CmsSignatureDataExtractor::extractstring $signedData, string $signatureExtract plus cryptographically verify a detached PAdES basic signatureExtractedSignatureDataSignatureExtractionException only when the CMS cannot be parsed at allA crypto or binding failure returns data with cryptoValid / hashValid false; it never throws for that
PdfSignatureDictionaryScanner::scanstring $pdfBytesByte-level scan for /ByteRange + /Contents dictionaries with precise-fit anti-spoof cross-checkslist<PdfSignatureOccurrence>Total; never throws; malformed candidates are skippedOrdered by coverage end, earliest first
PathValidatorInterface::validatearray $chain, ?DateTimeImmutable $validationTime = null, array $initialPolicies = []RFC 5280 §6.1.4 path validation with policy processingPathValidationResultPathValidationException on a structurally invalid chain or a breached adversarial limitChain is end-entity first, anchor last
PathValidatorInterface::validateWithAiaChasingarray $chain, ?DateTimeImmutable $validationTime = nullAIA resolution of missing intermediates, then validationPathValidationResultPathValidationExceptionFetches are capped by timeout and byte limits
CertificateChainValidatorConstructor: engine, PathValidationOptions, clock, logger; static withDefaults()The SPI implementation with the default adversarial capsPathValidationResult from both methodsPathValidationExceptionAlso thrown when an OpenSSLCertificate cannot be exported to PEM
PathValidationOptions::__constructCaps (maxDepth, maxPolicyFanout, fetchTimeoutSeconds, fetchSizeCapBytes) plus policy flags, ?TrustAnchorStoreInterface $trustAnchors, bool $requireTrustedAnchorDepth 32, fanout 64, 5 s per fetch, 10 MiB per fetch; all flags falseNew optionsDoes not throwFactories: defaults(), strict(), withTrustAnchors()
PathValidationResult::__constructbool $valid, string $trustAnchorFingerprint, DateTimeImmutable $validatedAt, array $validPolicies, ?RevocationCheckResult $revocation, bool $trustAnchorTrusted, array $fetchedCertificates, array $failureReasonsImmutable outcome; trustAnchorTrusted defaults false (fail-closed)New valueDoes not throwTrust membership is distinct from structural validity
PolicyProcessorConstructor: PolicyTreeState $state, PathValidationOptions $options; processCertificate(string $certDer, int $depth, bool $selfIssued), finalizeWrapUp(), tree()RFC 5280 §6.1.4 policy-tree expansion, mapping, and wrap-upvoid / list<non-empty-string> / PolicyTreePathValidationException on any policy-processing failure (fail-closed)Wrap-up returns surviving policy OIDs, excluding anyPolicy
PolicyTreeattach(PolicyTreeNode $node, PathValidationOptions $options), enforceFanout(...), remove(...), plus read queriesThe valid_policy_tree state with a depth indexVaries per methodPathValidationException when the live leaf count exceeds the fanout capExposes ANY_POLICY_OID (2.5.29.32.0)
NameConstraintsChecker::processCertificatestring $certDer, bool $applyNameCheckAccumulates and enforces permitted / excluded subtrees per RFC 5280 §6.1.4(g)voidPathValidationException on a violated subtree, an unsupported GeneralName form in a constraint, or a breached capNon-comparable names are handled fail-closed
TrustAnchorStoreInterface::containsFingerprintstring $anchorDerSha256HexMembership by lowercase hex SHA-256 over the anchor’s DER certificateboolDoes not throwThe trust seam consulted by the path validator
BatchSignatureValidator::validatearray $inputs (list<DocumentSignatureInput>)Multi-document signature validation with per-batch revocation cachingBatchValidationReportInvalidArgumentException on an empty list; a resource guard rejects batches above 1000 documentsLives in NextPDF\Enterprise\Signature
final class AdESValidationEngine
public function validateBasic(string $signedData, string $signature): ValidationReport
public function validateWithTime(
string $signedData,
string $signature,
DateTimeImmutable $claimedTime,
): ValidationReport
public function validateWithLongTermData(
string $signedData,
string $signature,
array $dssData,
): ValidationReport
public function validateArchivalTimestampChain(
string $pdfBytes,
array $dssData = [],
?TrustAnchorStoreInterface $anchors = null,
): ValidationReport
public function validate(
array $chain,
?DateTimeImmutable $validationTime = null,
array $initialPolicies = [],
): PathValidationResult;
public function validateWithAiaChasing(
array $chain,
?DateTimeImmutable $validationTime = null,
): PathValidationResult;
public static function withDefaults(
?ClockInterface $clock = null,
?AiaChaser $aiaChaser = null,
?LoggerInterface $logger = null,
): self
public function containsFingerprint(string $anchorDerSha256Hex): bool;
public function extract(string $signedData, string $signature): ExtractedSignatureData;
public function scan(string $pdfBytes): array
public function validate(array $inputs): BatchValidationReport

Indication enums. MainIndication cases: TOTAL_PASSED, TOTAL_FAILED, INDETERMINATE. Backing values follow the pattern urn:etsi:019102:mainindication:total-passed (lowercase, hyphenated). SubIndication cases: HASH_FAILURE, SIG_CRYPTO_FAILURE, REVOKED, EXPIRED, NOT_YET_VALID, NO_POE, TRY_LATER, CERTIFICATE_CHAIN_GENERAL_FAILURE, FORMAT_FAILURE, REVOKED_CA_NO_POE, CRYPTO_CONSTRAINTS_FAILURE, POLICY_PROCESSING_FAILURE, REVOCATION_OUT_OF_BOUNDS_NO_POE, NO_SIGNING_CERTIFICATE_FOUND, TIMESTAMP_ORDER_FAILURE. Each is backed by urn:etsi:019102:subindication:<CASE_NAME> with the exact case name.

  • Reports in, reports out. The four engine entry points return a ValidationReport for hostile input instead of throwing. A caught SignatureExtractionException routes to the guard path; a caught PathValidationException maps to TOTAL_FAILED / CERTIFICATE_CHAIN_GENERAL_FAILURE.
  • Basic validation order. Format check first; a non-parseable structure is TOTAL_FAILED / FORMAT_FAILURE (EN 319 102-1 §5.3.4). Then digest (HASH_FAILURE) and cryptographic verification (SIG_CRYPTO_FAILURE), matching the EN 319 102-1 §5.2.7.4 building-block outcomes. The digest is recomputed by the verifier and compared to the messageDigest signed attribute (RFC 5652 §5.6); producer-supplied digests are never trusted.
  • Weak algorithms degrade. A signature that verifies under SHA-1, or with a weak signing-certificate binding, returns INDETERMINATE / CRYPTO_CONSTRAINTS_FAILURE, never TOTAL_PASSED. The time path re-asserts this so a weak signature is never laundered into a time-valid pass.
  • Revocation provenance gate. Extractor revocation flags are consulted only when the extractor actually performed a revocation check (revocationChecked true). An unchecked default is neither “verified not revoked” nor a REVOKED trigger. Revocation evidence is established by the DSS path.
  • Non-pass propagation. The time and long-term paths never upgrade a non-pass basic result. One carve-out exists: a basic INDETERMINATE / REVOKED is resolved against $claimedTime; revocation at or before the claimed time is TOTAL_FAILED / REVOKED. This mirrors the EN 319 102-1 §5.3.4 pattern of resolving a revocation-related indeterminate with time evidence. When the comparison cannot be performed, the unresolved basic report is propagated verbatim.
  • Strict signature-timestamp binding (fail-closed; BC break). When the CMS carries an id-aa-timeStampToken unsigned attribute, its presence triggers enforcement in both the time and long-term paths; there is no warn-only mode. Cardinality must be exactly one attribute with exactly one value (EN 319 122-1 §5.3); any other shape is TOTAL_FAILED / FORMAT_FAILURE. The token must verify cryptographically end to end; an unverifiable token, a parser-differential conflict, or an imprint mismatch is INDETERMINATE / TIMESTAMP_ORDER_FAILURE. An unsupported or SHA-1 imprint algorithm is INDETERMINATE / CRYPTO_CONSTRAINTS_FAILURE. The binding rule is RFC 3161 Appendix A: the token’s messageImprint must equal the hash of the SignerInfo signature value octets, compared in constant time.
  • Long-term path gates. In the clause-5.4-annotated path the bound signature timestamp additionally receives TSA-certificate evaluation at the token’s genTime; an untrusted anchor is INDETERMINATE / CERTIFICATE_CHAIN_GENERAL_FAILURE, never a pass. NetworkPolicy::STRICT_OFFLINE with insufficient embedded DSS material returns INDETERMINATE / TRY_LATER. Proof-of-existence, DSS revocation, and archival-chain findings each short-circuit to INDETERMINATE with a mapped sub-indication.
  • Archival chain gates. No DocTimeStamp present is INDETERMINATE / NO_POE. A structurally non-conforming ByteRange is TOTAL_FAILED / FORMAT_FAILURE. Each token must verify, bind its imprint to the exact ByteRange-covered bytes, and pass TSA-at-genTime facet mapping (EXPIRED, NOT_YET_VALID, REVOKED_CA_NO_POE, CERTIFICATE_CHAIN_GENERAL_FAILURE, or TRY_LATER under strict-offline). Ordering is enforced: non-decreasing genTime, strictly progressing coverage, and later tokens containing the prior token’s /Contents hole. The latest token must cover the final byte; trailing bytes are TIMESTAMP_ORDER_FAILURE. A genTime more than 300 seconds ahead of the verifier clock is TIMESTAMP_ORDER_FAILURE.
  • Diagnostics never decide. DiagnosticData::$timestamps proof-of-existence entries are audit-trail only. They never change an indication, and the accumulator resets at every entry point.
  • Pki limits precede crypto. PathValidationOptions caps (depth 32, policy fanout 64, 5 s and 10 MiB per fetch) are checked before expensive work. PathValidationResult::$trustAnchorTrusted is distinct from $valid; requireTrustedAnchor makes an unaffirmed terminus invalid. strict() enables requireExplicitPolicy, hard-fail revocation transport, and requireTrustedAnchor. Path validity is anchor-relative per RFC 5280 §6.1: a valid path begins at a trust anchor supplied as input.
  • Batch surface. BatchSignatureValidator::validate() throws InvalidArgumentException for an empty list and rejects batches above 1000 documents through a resource guard. PHP owns all cryptographic validation in that pipeline.
  • Default engine has no extractor. new AdESValidationEngine() performs guard checks only: empty signature or signed data is TOTAL_FAILED; any non-empty pair resolves to INDETERMINATE / NO_SIGNING_CERTIFICATE_FOUND, never TOTAL_PASSED. Inject NextPDF\Enterprise\Security\Validation\CmsSignatureDataExtractor to obtain cryptographic verification.
  • Default TSA trust check has no store. Every TSA chain then reports untrusted, so archival and long-term signature-timestamp outcomes stay INDETERMINATE. Supply anchors via validateArchivalTimestampChain(..., $anchors) or a configured TsaCertificateAtGenTimeCheck.
  • Empty $pdfBytes. validateArchivalTimestampChain('') returns TOTAL_FAILED / FORMAT_FAILURE.
  • Pre-fix signature timestamps cannot pass. Tokens produced by NextPDF versions before the strict-binding fix imprinted a different input. They fail the Appendix A binding permanently; re-sign and re-timestamp to restore a positive result. This is a deliberate, documented BC break.
  • Duplicate or overlapping DocTimeStamps. A same-revision duplicate, equal or overlapping coverage, or a later token that does not contain the prior token’s signature hole fails the ordering gate.
  • Scanner is total and byte-level. scan() skips malformed or spoofed candidates silently; a decoy /ByteRange inside a content stream is rejected. It does not resolve indirect objects or walk the cross-reference table.
  • Coverage, not reachability. validateArchivalTimestampChain() proves cryptographic byte-range coverage to end-of-file. Object-level reachability analysis (for example, a re-pointed document root inside a covered revision) is declared out of scope.
  • Direct Pki usage throws. Calling PathValidatorInterface implementations directly surfaces PathValidationException for structurally invalid chains, breached caps, unsupported constraint forms, and failed PEM export of an OpenSSLCertificate handle. The engine catches this class; your own callers must handle it.

The verify-side accepts RSA PKCS#1 v1.5 with SHA-2 and ECDSA on P-256/P-384/P-521. RSASSA-PSS, EdDSA, and SHA-3 tokens fail closed as unsupported; SHA-1 degrades to CRYPTO_CONSTRAINTS_FAILURE. Under the Enterprise FIPS 140-3 crypto-policy profile (documented with the security module), the constraint applies to which algorithms are accepted; the validation flow itself — digest recomputation, signature checks, binding, path validation — is unchanged.

ClaimStandardClause
Basic Signature validation is a reusable building block for time-stamp and with-time validation.ETSI EN 319 102-1§5.3.1
Integrity failure maps to HASH_FAILURE; a failed signature check maps to SIG_CRYPTO_FAILURE.ETSI EN 319 102-1§5.2.7.4
Format checking runs first and a non-pass stops the process.ETSI EN 319 102-1§5.3.4
A revocation-related indeterminate can be resolved with time evidence.ETSI EN 319 102-1§5.3.4
A valid certification path begins at a trust anchor supplied as input.RFC 5280§6.1
The verifier recomputes the content digest; it must equal the messageDigest signed attribute.RFC 5652§5.6
The signature timestamp’s messageImprint hashes the SignerInfo signature field value.RFC 3161Appendix A
The signature-time-stamp attribute carries exactly one AttributeValue.ETSI EN 319 122-1§5.3

All clauses are paraphrased. The engine implements the cited validation procedures as capability; a TOTAL_PASSED report is a cryptographic statement. The enum values reuse the ETSI URN identifier pattern for interoperability of report data.

  • Clause-label mapping. The package source annotates the entry points as EN 319 102-1 clauses 5.2, 5.3, and 5.4. The compliance corpus places the Basic Signature validation process itself at clause 5.3, with the cryptographic building block at 5.2.7.4. This page cites the retrieved clause numbers; the behavior contract, not the label, is authoritative.
  • Deterministic tests. Every time comparison flows through the injected PSR-20 ClockInterface. Inject a frozen clock to test window checks, the 300-second genTime skew bound, and CRL freshness decisions.
  • Composition. All engine collaborators are constructor-injected and optional, with fail-closed defaults. The default path validator is CertificateChainValidator::withDefaults() over the engine clock; default options keep policy and name-constraint processing a no-op for conformant, unconstrained inputs.
  • Namespaces. The engine surface lives in NextPDF\Enterprise\Security\Validation, the path-validation surface in NextPDF\Enterprise\Security\Pki, and the batch orchestrator in NextPDF\Enterprise\Signature.
  • Report hygiene. Reports are immutable and serializable via toArray(). Diagnostic context resets at each entry point, so a report never carries evidence from a prior run on the same engine instance.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.