Enterprise edition
Signature verification — Deep Reference
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Public API surface
Section titled “Public API surface”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
AdESValidationEngine::__construct | 11 optional parameters: ?PathValidatorInterface $chainValidator, ?SignatureDataExtractor $extractor, ClockInterface $clock, ?LoggerInterface $logger, string $defaultPolicy, NetworkPolicy $networkPolicy, and five optional verifier collaborators | All defaults are fail-closed: Pki path validator over the engine clock, no extractor, no TSA trust store | New engine | Does not throw | With no trust store, TSA-chain evaluation reports untrusted; that maps to INDETERMINATE, never a pass |
AdESValidationEngine::validateBasic | string $signedData, string $signature | Basic validation: format, digest, crypto, weak-algorithm, chain, provenance-gated revocation | ValidationReport | Does not throw; extraction and path failures map to fail-closed reports | Without an extractor, guard checks only; see edge cases |
AdESValidationEngine::validateWithTime | string $signedData, string $signature, DateTimeImmutable $claimedTime | Basic validation first; certificate window and revocation compared against the claimed time | ValidationReport | Does not throw | Strict signature-timestamp gate when the attribute is present; $claimedTime stays the time anchor |
AdESValidationEngine::validateWithLongTermData | string $signedData, string $signature, array $dssData (certs/ocsps/crls) | Basic pass required; signature-timestamp gate armed with TSA-at-genTime; POE, DSS revocation, and archival gates | ValidationReport | Does not throw | NetworkPolicy::STRICT_OFFLINE with insufficient embedded data yields INDETERMINATE / TRY_LATER |
AdESValidationEngine::validateArchivalTimestampChain | string $pdfBytes, array $dssData = [], ?TrustAnchorStoreInterface $anchors = null | Evidence-based DocTimeStamp coverage chain over the exact ByteRange bytes | ValidationReport | Does not throw on hostile bytes | TOTAL_PASSED only for a trusted, EOF-covering chain |
MainIndication | — | String-backed enum, three cases | — | — | ETSI URN values; see the case list below |
SubIndication | — | String-backed enum, fifteen cases | — | — | ETSI URN values; see the case list below |
ValidationReport::__construct | MainIndication $mainIndication, ?SubIndication $subIndication, DiagnosticData $diagnosticData, DateTimeImmutable $validationTime, string $validationPolicy = '' | Immutable (final readonly) validation outcome | New report | Does not throw | isPassed(), isFailed(), isIndeterminate(), toArray() |
DiagnosticData::__construct | array $certificateChain, array $timestamps, array $revocationData, string $validationPolicy, string $signatureFormat, array $warnings (all defaulted) | Immutable evidence container; audit trail only | New value | Does not throw | toArray() serializes references for reporting |
SignatureDataExtractor::extract | string $signedData, string $signature | SPI: parse the CMS and extract validation components | ExtractedSignatureData | SignatureExtractionException when the signature cannot be parsed | Interface; decouples ASN.1 parsing from the engine |
CmsSignatureDataExtractor::extract | string $signedData, string $signature | Extract plus cryptographically verify a detached PAdES basic signature | ExtractedSignatureData | SignatureExtractionException only when the CMS cannot be parsed at all | A crypto or binding failure returns data with cryptoValid / hashValid false; it never throws for that |
PdfSignatureDictionaryScanner::scan | string $pdfBytes | Byte-level scan for /ByteRange + /Contents dictionaries with precise-fit anti-spoof cross-checks | list<PdfSignatureOccurrence> | Total; never throws; malformed candidates are skipped | Ordered by coverage end, earliest first |
PathValidatorInterface::validate | array $chain, ?DateTimeImmutable $validationTime = null, array $initialPolicies = [] | RFC 5280 §6.1.4 path validation with policy processing | PathValidationResult | PathValidationException on a structurally invalid chain or a breached adversarial limit | Chain is end-entity first, anchor last |
PathValidatorInterface::validateWithAiaChasing | array $chain, ?DateTimeImmutable $validationTime = null | AIA resolution of missing intermediates, then validation | PathValidationResult | PathValidationException | Fetches are capped by timeout and byte limits |
CertificateChainValidator | Constructor: engine, PathValidationOptions, clock, logger; static withDefaults() | The SPI implementation with the default adversarial caps | PathValidationResult from both methods | PathValidationException | Also thrown when an OpenSSLCertificate cannot be exported to PEM |
PathValidationOptions::__construct | Caps (maxDepth, maxPolicyFanout, fetchTimeoutSeconds, fetchSizeCapBytes) plus policy flags, ?TrustAnchorStoreInterface $trustAnchors, bool $requireTrustedAnchor | Depth 32, fanout 64, 5 s per fetch, 10 MiB per fetch; all flags false | New options | Does not throw | Factories: defaults(), strict(), withTrustAnchors() |
PathValidationResult::__construct | bool $valid, string $trustAnchorFingerprint, DateTimeImmutable $validatedAt, array $validPolicies, ?RevocationCheckResult $revocation, bool $trustAnchorTrusted, array $fetchedCertificates, array $failureReasons | Immutable outcome; trustAnchorTrusted defaults false (fail-closed) | New value | Does not throw | Trust membership is distinct from structural validity |
PolicyProcessor | Constructor: PolicyTreeState $state, PathValidationOptions $options; processCertificate(string $certDer, int $depth, bool $selfIssued), finalizeWrapUp(), tree() | RFC 5280 §6.1.4 policy-tree expansion, mapping, and wrap-up | void / list<non-empty-string> / PolicyTree | PathValidationException on any policy-processing failure (fail-closed) | Wrap-up returns surviving policy OIDs, excluding anyPolicy |
PolicyTree | attach(PolicyTreeNode $node, PathValidationOptions $options), enforceFanout(...), remove(...), plus read queries | The valid_policy_tree state with a depth index | Varies per method | PathValidationException when the live leaf count exceeds the fanout cap | Exposes ANY_POLICY_OID (2.5.29.32.0) |
NameConstraintsChecker::processCertificate | string $certDer, bool $applyNameCheck | Accumulates and enforces permitted / excluded subtrees per RFC 5280 §6.1.4(g) | void | PathValidationException on a violated subtree, an unsupported GeneralName form in a constraint, or a breached cap | Non-comparable names are handled fail-closed |
TrustAnchorStoreInterface::containsFingerprint | string $anchorDerSha256Hex | Membership by lowercase hex SHA-256 over the anchor’s DER certificate | bool | Does not throw | The trust seam consulted by the path validator |
BatchSignatureValidator::validate | array $inputs (list<DocumentSignatureInput>) | Multi-document signature validation with per-batch revocation caching | BatchValidationReport | InvalidArgumentException on an empty list; a resource guard rejects batches above 1000 documents | Lives in NextPDF\Enterprise\Signature |
final class AdESValidationEnginepublic function validateBasic(string $signedData, string $signature): ValidationReportpublic function validateWithTime( string $signedData, string $signature, DateTimeImmutable $claimedTime,): ValidationReportpublic function validateWithLongTermData( string $signedData, string $signature, array $dssData,): ValidationReportpublic function validateArchivalTimestampChain( string $pdfBytes, array $dssData = [], ?TrustAnchorStoreInterface $anchors = null,): ValidationReportpublic 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,): selfpublic function containsFingerprint(string $anchorDerSha256Hex): bool;public function extract(string $signedData, string $signature): ExtractedSignatureData;public function scan(string $pdfBytes): arraypublic function validate(array $inputs): BatchValidationReportIndication 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.
Behavior contract
Section titled “Behavior contract”- Reports in, reports out. The four engine entry points return a
ValidationReportfor hostile input instead of throwing. A caughtSignatureExtractionExceptionroutes to the guard path; a caughtPathValidationExceptionmaps toTOTAL_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 themessageDigestsigned 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, neverTOTAL_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 (
revocationCheckedtrue). An unchecked default is neither “verified not revoked” nor aREVOKEDtrigger. 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/REVOKEDis resolved against$claimedTime; revocation at or before the claimed time isTOTAL_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-timeStampTokenunsigned 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 isTOTAL_FAILED/FORMAT_FAILURE. The token must verify cryptographically end to end; an unverifiable token, a parser-differential conflict, or an imprint mismatch isINDETERMINATE/TIMESTAMP_ORDER_FAILURE. An unsupported or SHA-1 imprint algorithm isINDETERMINATE/CRYPTO_CONSTRAINTS_FAILURE. The binding rule is RFC 3161 Appendix A: the token’smessageImprintmust equal the hash of the SignerInfosignaturevalue 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 isINDETERMINATE/CERTIFICATE_CHAIN_GENERAL_FAILURE, never a pass.NetworkPolicy::STRICT_OFFLINEwith insufficient embedded DSS material returnsINDETERMINATE/TRY_LATER. Proof-of-existence, DSS revocation, and archival-chain findings each short-circuit toINDETERMINATEwith a mapped sub-indication. - Archival chain gates. No DocTimeStamp present is
INDETERMINATE/NO_POE. A structurally non-conforming ByteRange isTOTAL_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, orTRY_LATERunder strict-offline). Ordering is enforced: non-decreasinggenTime, strictly progressing coverage, and later tokens containing the prior token’s/Contentshole. The latest token must cover the final byte; trailing bytes areTIMESTAMP_ORDER_FAILURE. AgenTimemore than 300 seconds ahead of the verifier clock isTIMESTAMP_ORDER_FAILURE. - Diagnostics never decide.
DiagnosticData::$timestampsproof-of-existence entries are audit-trail only. They never change an indication, and the accumulator resets at every entry point. - Pki limits precede crypto.
PathValidationOptionscaps (depth 32, policy fanout 64, 5 s and 10 MiB per fetch) are checked before expensive work.PathValidationResult::$trustAnchorTrustedis distinct from$valid;requireTrustedAnchormakes an unaffirmed terminus invalid.strict()enablesrequireExplicitPolicy, hard-fail revocation transport, andrequireTrustedAnchor. 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()throwsInvalidArgumentExceptionfor an empty list and rejects batches above 1000 documents through a resource guard. PHP owns all cryptographic validation in that pipeline.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- Default engine has no extractor.
new AdESValidationEngine()performs guard checks only: empty signature or signed data isTOTAL_FAILED; any non-empty pair resolves toINDETERMINATE/NO_SIGNING_CERTIFICATE_FOUND, neverTOTAL_PASSED. InjectNextPDF\Enterprise\Security\Validation\CmsSignatureDataExtractorto 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 viavalidateArchivalTimestampChain(..., $anchors)or a configuredTsaCertificateAtGenTimeCheck. - Empty
$pdfBytes.validateArchivalTimestampChain('')returnsTOTAL_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/ByteRangeinside 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
PathValidatorInterfaceimplementations directly surfacesPathValidationExceptionfor structurally invalid chains, breached caps, unsupported constraint forms, and failed PEM export of anOpenSSLCertificatehandle. The engine catches this class; your own callers must handle it.
FIPS-mode behavior
Section titled “FIPS-mode behavior”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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| 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 3161 | Appendix 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.
Development notes
Section titled “Development notes”- 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 inNextPDF\Enterprise\Security\Pki, and the batch orchestrator inNextPDF\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.
See also
Section titled “See also”- Signature verification: AdES / PAdES cryptographic verify-side — the capability page: workflow, algorithm table, upgrade notes.
- Signature — Deep Reference — the PAdES B-LT / B-LTA producer side.
- Validation — Deep Reference — structural policy checks without cryptography.
- Security — Deep Reference — the combined Enterprise security surface, including the FIPS profile.
- PAdES clause map — B-B, B-T, B-LT, B-LTA across editions.
Publication boundary
Section titled “Publication boundary”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.