Skip to content
getnextpdf.com

Enterprise edition

Trusted lists — Deep Reference

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.

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
TslPolicyEnforcerTslFetcher $fetcher, TslSignatureVerifier $verifier, TslXmlParser $parserCombines fetch, signature-verify, parse, and staleness gate into one entry pointPropagates the pipeline exceptions belowfinal; fail-closed by construction
TslPolicyEnforcer::fetchAndVerifystring $urlFetches one TSL, then runs verifyXml() on the bytesTslDocumentTslFetchException, NetworkPolicyViolation, TslSignatureException, TslParseExceptionReturns only when all four stages pass
TslPolicyEnforcer::verifyXmlstring $xmlVerifies the signature, parses, and rejects a stale listTslDocumentTslSignatureException, TslParseExceptionStaleness is evaluated against current system time
TslFetcher::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, ?CacheInterface $cache = null, int $defaultTtlSeconds = 3600, int $maxBytes = 16_777_216, NetworkPolicy $networkPolicy = NetworkPolicy::ONLINEHTTPS-only TSL/LOTL retrieval with ETag-based cachingfinal; SSRF guard blocks private, loopback, link-local, and metadata hosts with DNS-rebinding mitigation
TslFetcher::fetchstring $urlGET with If-None-Match revalidation; caches body plus ETag under the configured TTLstring (raw XML bytes)TslFetchException, NetworkPolicyViolationReads at most $maxBytes bytes; under STRICT_OFFLINE only a cached body is served
TslSignatureVerifier::__constructarray $trustAnchorsPem, int $clockTolerance = 0XMLDSig verifier pinned to configured trust anchorsInvalidArgumentException when the anchor list is emptyfinal; allowlists in ALLOWED_SIG_ALG and ALLOWED_DIGEST_ALG
TslSignatureVerifier::verifystring $xmlVerifies the enveloped XMLDSig signature fail-closedstring (signer-certificate PEM)TslSignatureException with a machine-readable reason codeKeyInfo certificates are never trusted on their own; the signer must chain to a configured anchor
TslXmlParser::parsestring $xmlStructural parse into a TslDocument; signature-agnosticTslDocumentTslParseExceptionRejects any DOCTYPE fail-closed before parsing; loads with LIBXML_NONET; callers must verify before trusting the result
TslTrustAnchorProvider::buildBundleTslDocument $tsl, DateTimeImmutable $nowAsserts freshness first, then collects the certificates of active CA/QC servicesEnterpriseCaTrustAnchorBundleTslParseExceptionThe freshness gate precedes any anchor extraction; empty result set throws
TslDocument::__constructEight promoted readonly properties (see constructor fence)Immutable parsed TSL value objectfinal readonly; source-annotated @api
TslDocument::isStaleDateTimeImmutable $nowCompares nextUpdate against $now after a fail-closed UTC parseboolTslParseExceptionRequires an explicit Z or numeric-offset designator
TslDocument::assertFreshDateTimeImmutable $nowThrows when the list is stale or nextUpdate is unparseablevoidTslParseExceptionThe consumer-boundary freshness gate
TslDocument::servicesOfTypestring $serviceTypeIdentifierFilters services by ETSI service-type URIlist<TspService>Does not throw
TslDocument::activeServicesReturns services in granted status onlylist<TspService>Does not throwGranted means TspService::STATUS_GRANTED
TspService::__constructEight promoted readonly propertiesOne trust-service entry inside a TSLfinal readonly; constants for status and service-type URIs
TspService::isGrantedStatus equality against the granted URIboolDoes not throw
TspService::isQualifiedCaType equality against the CA/QC URIboolDoes not throw
TspServiceQualifier::__constructstring $qualifierUri, string $criteriaListAssert = 'all', array $policyOidConditions = [], array $keyUsageConditions = []One ETSI service qualifier with optional criteriafinal readonly; constants FOR_ESIG, FOR_ESEAL, FOR_WSA, QSCD_STATEMENT, NO_QSCD
EnterpriseCaTrustAnchorBundle::__constructarray $anchorsPem, string $bundleVersion, string $bundleSha256Pinned-anchor bundle; validates the supplied digest against the supplied anchors at constructionInvalidArgumentExceptionObtain from buildBundle(); do not construct by hand; implements TrustAnchorStoreInterface
EnterpriseCaTrustAnchorBundle::containsFingerprintstring $anchorDerSha256HexAnchor membership by hex SHA-256 over the DER bodyboolDoes not throw
EnterpriseCaTrustAnchorBundle::computeBundleSha256array $anchorsPemCanonical SHA-256 over newline-normalized PEM concatenationstringDoes not throwstatic
TslFetchExceptionSignals a failed TSL retrievalfinal; extends RuntimeException
TslParseExceptionSignals a structural or freshness failurefinal; extends RuntimeException
TslSignatureException::__constructstring $reason, string $messageSignals XMLDSig verification failure with a reason codefinal; public readonly $reason (see reason codes below)

TslPolicyEnforcer

public function fetchAndVerify(string $url): TslDocument
public function verifyXml(string $xml): TslDocument

TslFetcher

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): string

TslSignatureVerifier

public function __construct(private readonly array $trustAnchorsPem, private readonly int $clockTolerance = 0)
public function verify(string $xml): string

TslXmlParser

public function parse(string $xml): TslDocument

TslTrustAnchorProvider

public function buildBundle(TslDocument $tsl, DateTimeImmutable $now): EnterpriseCaTrustAnchorBundle

TslDocument

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): bool
public function assertFresh(DateTimeImmutable $now): void
public function servicesOfType(string $serviceTypeIdentifier): array
public function activeServices(): array

TspService

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(): bool
public function isQualifiedCa(): bool

TspServiceQualifier

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): bool
public static function computeBundleSha256(array $anchorsPem): string

TslSignatureException

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.

  • The pipeline order is fixed: fetch, XMLDSig verification, structural parse, staleness gate. TslPolicyEnforcer returns a TslDocument only 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.
  • TslXmlParser is 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 nextUpdate has passed is expired and is refused — ETSI TS 119 612 §5.3.15. verifyXml() gates against current system time; TslDocument::assertFresh() and buildBundle() 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 nextUpdate without an explicit Z or numeric offset raises TslParseException; the value is never reinterpreted in the server’s local timezone.
  • buildBundle() calls assertFresh($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 KeyInfo serve 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 carry basicConstraints cA=TRUE (plus keyCertSign when keyUsage is 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 with unsupported_algorithm or unsupported_transform.
  • TslFetcher refuses non-HTTPS URLs and applies an SSRF guard before any egress. Under NetworkPolicy::STRICT_OFFLINE it serves a previously cached body or raises NetworkPolicyViolation; no outbound request is ever sent.
  • Stale list. TslParseException from verifyXml(), assertFresh(), or buildBundle() means the trust source is unusable. Treat it as an operational refresh failure, not a signature verdict.
  • Non-canonical nextUpdate. A value without an explicit Z or numeric offset throws instead of parsing leniently. ETSI TS 119 612 §5.1.3 mandates the UTC Z form; 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 past nextUpdate still fails the later buildBundle($tsl, $now) gate.
  • Empty anchor configuration. TslSignatureVerifier refuses construction with an empty anchor list (InvalidArgumentException).
  • No usable services. A fresh list with no granted CA/QC services raises TslParseException from buildBundle(); an empty bundle is never produced.
  • Offline posture. STRICT_OFFLINE with no cached body raises NetworkPolicyViolation. The cache lookup precedes the policy check, so a cached list keeps air-gapped validation working.
  • Oversized or empty response. fetch() reads at most $maxBytes bytes (default 16 MiB); a truncated list then fails digest verification downstream. An empty body raises TslFetchException.
  • 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:Signature is removed before digest computation; sibling signatures and counter-signatures are preserved. Additional XAdES references are permitted, but exactly one ds:Reference must cover the document root.
  • Expired chain material. An expired or not-yet-valid signer, intermediate, or anchor fails with reason expired_anchor. clockTolerance widens the acceptance window symmetrically and defaults to 0.

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.

ClaimStandardClause
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.

  • 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 KeyInfo content.
  • 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’s nextUpdate.
  • bundleVersion has the observable shape tsl-<territory>-seq<sequenceNumber>; rawXmlSha256 on TslDocument supports 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.0 source annotations (TslFetchException: 3.2.0). TslDocument, TspService, and TspServiceQualifier are source-annotated @api.

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.