Enterprise edition
Trusted lists — Deep Reference
At a glance
Section titled “At a glance”This page is the deep reference for the trusted-list surface in NextPDF Enterprise. The surface is the twelve public classes of the NextPDF\Enterprise\Security\Tsl namespace. NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer is the orchestrated entry point: it returns a TslDocument only when HTTP fetch, XMLDSig verification, structural parse, and the nextUpdate staleness gate all pass. TslTrustAnchorProvider::buildBundle() then derives a trust-anchor bundle from active CA/QC services, re-asserting freshness at a caller-supplied instant before any anchor is extracted. Every failure raises a typed exception; no stage degrades silently. The pipeline supports verifying EU member-state trusted lists and LOTL (List of Trusted Lists)-sourced trust anchors when supplied by the caller; automatic LOTL discovery, polling, and pivot processing are out of scope.
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 |
|---|---|---|---|---|---|
TslPolicyEnforcer | TslFetcher $fetcher, TslSignatureVerifier $verifier, TslXmlParser $parser | Combines fetch, signature-verify, parse, and staleness gate into one entry point | — | Propagates the pipeline exceptions below | final; fail-closed by construction |
TslPolicyEnforcer::fetchAndVerify | string $url | Fetches one TSL, then runs verifyXml() on the bytes | TslDocument | TslFetchException, NetworkPolicyViolation, TslSignatureException, TslParseException | Returns only when all four stages pass |
TslPolicyEnforcer::verifyXml | string $xml | Verifies the signature, parses, and rejects a stale list | TslDocument | TslSignatureException, TslParseException | Staleness is evaluated against current system time |
TslFetcher::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, ?CacheInterface $cache = null, int $defaultTtlSeconds = 3600, int $maxBytes = 16_777_216, NetworkPolicy $networkPolicy = NetworkPolicy::ONLINE | HTTPS-only TSL/LOTL retrieval with ETag-based caching | — | — | final; SSRF guard blocks private, loopback, link-local, and metadata hosts with DNS-rebinding mitigation |
TslFetcher::fetch | string $url | GET with If-None-Match revalidation; caches body plus ETag under the configured TTL | string (raw XML bytes) | TslFetchException, NetworkPolicyViolation | Reads at most $maxBytes bytes; under STRICT_OFFLINE only a cached body is served |
TslSignatureVerifier::__construct | array $trustAnchorsPem, int $clockTolerance = 0 | XMLDSig verifier pinned to configured trust anchors | — | InvalidArgumentException when the anchor list is empty | final; allowlists in ALLOWED_SIG_ALG and ALLOWED_DIGEST_ALG |
TslSignatureVerifier::verify | string $xml | Verifies the enveloped XMLDSig signature fail-closed | string (signer-certificate PEM) | TslSignatureException with a machine-readable reason code | KeyInfo certificates are never trusted on their own; the signer must chain to a configured anchor |
TslXmlParser::parse | string $xml | Structural parse into a TslDocument; signature-agnostic | TslDocument | TslParseException | Rejects any DOCTYPE fail-closed before parsing; loads with LIBXML_NONET; callers must verify before trusting the result |
TslTrustAnchorProvider::buildBundle | TslDocument $tsl, DateTimeImmutable $now | Asserts freshness first, then collects the certificates of active CA/QC services | EnterpriseCaTrustAnchorBundle | TslParseException | The freshness gate precedes any anchor extraction; empty result set throws |
TslDocument::__construct | Eight promoted readonly properties (see constructor fence) | Immutable parsed TSL value object | — | — | final readonly; source-annotated @api |
TslDocument::isStale | DateTimeImmutable $now | Compares nextUpdate against $now after a fail-closed UTC parse | bool | TslParseException | Requires an explicit Z or numeric-offset designator |
TslDocument::assertFresh | DateTimeImmutable $now | Throws when the list is stale or nextUpdate is unparseable | void | TslParseException | The consumer-boundary freshness gate |
TslDocument::servicesOfType | string $serviceTypeIdentifier | Filters services by ETSI service-type URI | list<TspService> | Does not throw | — |
TslDocument::activeServices | — | Returns services in granted status only | list<TspService> | Does not throw | Granted means TspService::STATUS_GRANTED |
TspService::__construct | Eight promoted readonly properties | One trust-service entry inside a TSL | — | — | final readonly; constants for status and service-type URIs |
TspService::isGranted | — | Status equality against the granted URI | bool | Does not throw | — |
TspService::isQualifiedCa | — | Type equality against the CA/QC URI | bool | Does not throw | — |
TspServiceQualifier::__construct | string $qualifierUri, string $criteriaListAssert = 'all', array $policyOidConditions = [], array $keyUsageConditions = [] | One ETSI service qualifier with optional criteria | — | — | final readonly; constants FOR_ESIG, FOR_ESEAL, FOR_WSA, QSCD_STATEMENT, NO_QSCD |
EnterpriseCaTrustAnchorBundle::__construct | array $anchorsPem, string $bundleVersion, string $bundleSha256 | Pinned-anchor bundle; validates the supplied digest against the supplied anchors at construction | — | InvalidArgumentException | Obtain from buildBundle(); do not construct by hand; implements TrustAnchorStoreInterface |
EnterpriseCaTrustAnchorBundle::containsFingerprint | string $anchorDerSha256Hex | Anchor membership by hex SHA-256 over the DER body | bool | Does not throw | — |
EnterpriseCaTrustAnchorBundle::computeBundleSha256 | array $anchorsPem | Canonical SHA-256 over newline-normalized PEM concatenation | string | Does not throw | static |
TslFetchException | — | Signals a failed TSL retrieval | — | — | final; extends RuntimeException |
TslParseException | — | Signals a structural or freshness failure | — | — | final; extends RuntimeException |
TslSignatureException::__construct | string $reason, string $message | Signals XMLDSig verification failure with a reason code | — | — | final; public readonly $reason (see reason codes below) |
TslPolicyEnforcer
public function fetchAndVerify(string $url): TslDocumentpublic function verifyXml(string $xml): TslDocumentTslFetcher
public function __construct( private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly ?CacheInterface $cache = null, private readonly int $defaultTtlSeconds = 3600, private readonly int $maxBytes = 16_777_216, private readonly NetworkPolicy $networkPolicy = NetworkPolicy::ONLINE,) {}
public function fetch(string $url): stringTslSignatureVerifier
public function __construct(private readonly array $trustAnchorsPem, private readonly int $clockTolerance = 0)
public function verify(string $xml): stringTslXmlParser
public function parse(string $xml): TslDocumentTslTrustAnchorProvider
public function buildBundle(TslDocument $tsl, DateTimeImmutable $now): EnterpriseCaTrustAnchorBundleTslDocument
public function __construct( public string $schemeTerritory, public string $schemeOperatorName, public string $tslType, public int $sequenceNumber, public string $issueDateTime, public string $nextUpdate, public array $tspServices, public string $rawXmlSha256,) {}
public function isStale(DateTimeImmutable $now): boolpublic function assertFresh(DateTimeImmutable $now): voidpublic function servicesOfType(string $serviceTypeIdentifier): arraypublic function activeServices(): arrayTspService
public function __construct(public string $tspName, public string $serviceName, public string $serviceTypeIdentifier, public string $serviceStatus, public string $statusStartingTime, public string $serviceCertificatePem, public array $qualifiers, public array $additionalServiceInformation) {}
public function isGranted(): boolpublic function isQualifiedCa(): boolTspServiceQualifier
public function __construct(public string $qualifierUri, public string $criteriaListAssert = 'all', public array $policyOidConditions = [], public array $keyUsageConditions = []) {}EnterpriseCaTrustAnchorBundle
public function __construct(public array $anchorsPem, public string $bundleVersion, public string $bundleSha256)
public function containsFingerprint(string $anchorDerSha256Hex): boolpublic static function computeBundleSha256(array $anchorsPem): stringTslSignatureException
public function __construct(public readonly string $reason, string $message)TslSignatureException reason codes: missing_signature, untrusted_signer, invalid_signature, digest_mismatch, unsupported_algorithm, unsupported_transform, expired_anchor.
Behavior contract
Section titled “Behavior contract”- The pipeline order is fixed: fetch, XMLDSig verification, structural parse, staleness gate.
TslPolicyEnforcerreturns aTslDocumentonly when all four succeed. A trusted list is signed by its scheme operator so relying parties can check authenticity and integrity — ETSI TS 119 612 §5.7.1. TslXmlParseris signature-agnostic by design. Callers must verify the signature before trusting any parsed field.TslPolicyEnforcer::verifyXml()enforces that ordering.- The freshness invariant is enforced at every consumer boundary. A list whose
nextUpdatehas passed is expired and is refused — ETSI TS 119 612 §5.3.15.verifyXml()gates against current system time;TslDocument::assertFresh()andbuildBundle()gate against a caller-supplied instant. - The freshness parse is fail-closed. Date-time fields are ISO 8601 UTC values with an explicit designator — ETSI TS 119 612 §5.1.3. A
nextUpdatewithout an explicitZor numeric offset raisesTslParseException; the value is never reinterpreted in the server’s local timezone. buildBundle()callsassertFresh($now)before extracting any anchor, then admits only services that are both granted and CA/QC. Granted and withdrawn are the qualified-service status URIs — ETSI TS 119 612 §5.5.4. CA/QC is the qualified-CA service-type URI — ETSI TS 119 612 §5.5.1.1.- The bundle version is derived from the scheme territory and the TSL sequence number. The sequence number is monotonic across releases — ETSI TS 119 612 §5.3.2. The bundle digest is a canonical SHA-256 over the anchor PEMs, and
containsFingerprint()answers membership by DER SHA-256. - The verifier trusts only configured anchors. Certificates found in
KeyInfoserve as the signer leaf and candidate intermediates; the chain must reach a configured anchor within depth 8, every link must be temporally valid, and an issuing certificate must carrybasicConstraintscA=TRUE(pluskeyCertSignwhenkeyUsageis present). - The verification profile is an allowlist: RSA or ECDSA with SHA-256, SHA-384, or SHA-512; digest methods SHA-256, SHA-384, or SHA-512; exclusive canonicalization only; and exactly the enveloped-signature plus exclusive-C14N transform pair on the list-covering
ds:Reference. Anything else fails withunsupported_algorithmorunsupported_transform. TslFetcherrefuses non-HTTPS URLs and applies an SSRF guard before any egress. UnderNetworkPolicy::STRICT_OFFLINEit serves a previously cached body or raisesNetworkPolicyViolation; no outbound request is ever sent.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- Stale list.
TslParseExceptionfromverifyXml(),assertFresh(), orbuildBundle()means the trust source is unusable. Treat it as an operational refresh failure, not a signature verdict. - Non-canonical
nextUpdate. A value without an explicitZor numeric offset throws instead of parsing leniently. ETSI TS 119 612 §5.1.3 mandates the UTCZform; the gate also accepts an explicit numeric offset and rejects everything else. - Time-of-use drift.
verifyXml()gates at verification time; a document held in memory pastnextUpdatestill fails the laterbuildBundle($tsl, $now)gate. - Empty anchor configuration.
TslSignatureVerifierrefuses construction with an empty anchor list (InvalidArgumentException). - No usable services. A fresh list with no granted CA/QC services raises
TslParseExceptionfrombuildBundle(); an empty bundle is never produced. - Offline posture.
STRICT_OFFLINEwith no cached body raisesNetworkPolicyViolation. The cache lookup precedes the policy check, so a cached list keeps air-gapped validation working. - Oversized or empty response.
fetch()reads at most$maxBytesbytes (default 16 MiB); a truncated list then fails digest verification downstream. An empty body raisesTslFetchException. - DOCTYPE in the XML. Any DOCTYPE is rejected before libxml builds an entity table, and again after load. This closes XXE and entity-expansion (billion-laughs) input classes.
- Multiple signatures. Only the verified enveloped
ds:Signatureis removed before digest computation; sibling signatures and counter-signatures are preserved. Additional XAdES references are permitted, but exactly oneds:Referencemust cover the document root. - Expired chain material. An expired or not-yet-valid signer, intermediate, or anchor fails with reason
expired_anchor.clockTolerancewidens the acceptance window symmetrically and defaults to0.
FIPS-mode behavior
Section titled “FIPS-mode behavior”The verifier’s allowlist is fixed to RSA and ECDSA with the SHA-2 family; SHA-1 and MD5 are structurally excluded. Signature arithmetic runs in bundled software cryptography (phpseclib). NextPDF makes no FIPS 140-3 validation claim for that arithmetic. The Enterprise FIPS 140-3 crypto-policy profile is documented with the security module; it constrains algorithm selection and does not change trusted-list structures or this module’s fail-closed behavior.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| A trusted list whose Next update has passed is discarded as expired. | ETSI TS 119 612 | §5.3.15 |
Date-time fields are ISO 8601 strings in UTC with the Z designator. | ETSI TS 119 612 | §5.1.3 |
| The scheme operator signs the trusted list for authenticity and integrity. | ETSI TS 119 612 | §5.7.1 |
| Qualified-service status is the granted or withdrawn status URI. | ETSI TS 119 612 | §5.5.4 |
A qualified CA is identified by the Svctype/CA/QC service-type URI. | ETSI TS 119 612 | §5.5.1.1 |
| The TSL sequence number starts at 1 and increments on each release. | ETSI TS 119 612 | §5.3.2 |
All clauses are paraphrased; NextPDF does not reproduce normative text. NextPDF makes no ETSI TS 119 612 conformance claim and no eIDAS certification claim. Consuming a trusted list does not make a signature, a certificate, or a NextPDF output “qualified”; qualification belongs to the trust service provider under member-state supervision, and legal effect is outside this module. The XMLDSig processing-model constraints (enveloped-signature transform, exclusive canonicalization, root-covering reference) are documented from the product’s verification profile; the W3C XML Signature specification is outside the cited evidence set. This module decides only whether a list is acceptable as trust input; certificate-path validation against the resulting anchors belongs to the certificate-validation layer.
Development notes
Section titled “Development notes”- Dependencies are PSR interfaces: a PSR-18 client, a PSR-17 request factory, and an optional PSR-16 cache. Inject in-memory doubles in tests; no stage requires live network access except a cold
fetch(). - Pin the top anchor out of band. For member-state lists, the LOTL anchor authorizes list signers; the verifier never bootstraps trust from
KeyInfocontent. - Background polling, pivot-LOTL processing, and mutual-TLS or proxy authentication are outside the fetcher’s scope in this version. Schedule refresh externally and re-fetch before each
nextUpdate. - Pass the validation instant, not the construction instant, to
buildBundle(). Rebuild the bundle after each refresh; never cache a bundle past the source list’snextUpdate. bundleVersionhas the observable shapetsl-<territory>-seq<sequenceNumber>;rawXmlSha256onTslDocumentsupports evidence records and replay detection.- Malformed service entries parse with defensive placeholder values; a malformed digital identity that reaches bundle construction fails closed with
InvalidArgumentException. - The classes carry package
@since 1.10.0source annotations (TslFetchException:3.2.0).TslDocument,TspService, andTspServiceQualifierare source-annotated@api.
See also
Section titled “See also”- eIDAS assurance levels — the capability page that maps trusted-list evidence to Levels of Assurance.
- ASiC containers — a consumer of
TslTrustAnchorProvider::buildBundle()for container trust binding. - Signature verification — the AdES/PAdES verify-side that consumes trust anchors.
- Security — Deep Reference — the combined Enterprise security surface.
- Signature — Deep Reference — the PAdES B-LT and B-LTA long-term producer.
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.