Ir al contenido
getnextpdf.com

Enterprise edición

Listas de confianza (TSL)

La validación de firmas de la UE parte de un hecho publicado: qué proveedores tienen estatus cualificado. Ese hecho reside en las listas de confianza (TSL): documentos XML firmados que cada Estado miembro publica, indexados por la lista de listas de confianza (LOTL) de la UE. NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer convierte una URL de TSL o un XML en bruto en un TslDocument en el que se puede confiar. Realiza la obtención sobre HTTPS protegido, verifica la firma XMLDSig frente a las anclas que se fijan, analiza XML endurecido y rechaza listas caducadas. Una llamada adicional, TslTrustAnchorProvider::buildBundle(), convierte los servicios CA/QC activos en un paquete de anclas de confianza versionado. Todos los controles fallan de forma cerrada; todo rechazo es una excepción tipada.

Esta página cubre la ingesta de listas y la derivación de anclas. La validación de rutas de certificación reside en Verificación de firmas. El mapeo de niveles de garantía eIDAS reside en Niveles de garantía eIDAS. La vinculación de confianza en contenedores reside en Vinculación de confianza ASiC.

Esta capacidad se incluye en NextPDF Enterprise (nextpdf/enterprise) y se activa con un sobre de licencia de nivel Enterprise. Un despliegue sin esa habilitación no carga las clases de la capacidad. Compara ediciones y obtén una licencia.

Ventana de terminal
composer require nextpdf/enterprise

La activación requiere tu sobre de licencia Enterprise. Consulta Instalar y autenticar. Las clases de esta página residen bajo NextPDF\Enterprise\Security\Tsl; los tipos de política de red residen bajo NextPDF\Enterprise\Security. La obtención en línea necesita además cualquier cliente PSR-18 y fábrica PSR-17 (por ejemplo, guzzlehttp/guzzle).

Bajo el Artículo 22 de eIDAS, cada Estado miembro publica una lista de confianza de sus proveedores cualificados de servicios de confianza, firmada o sellada para el procesamiento automatizado. ETSI TS 119 612 define el formato XML. La lista solo es tan fiable como la hacen tres comprobaciones: su firma, su estructura y su vigencia. NextPDF las ejecuta en ese orden, como una única canalización:

  1. ObtenciónTslFetcher recupera el XML únicamente sobre HTTPS. Una protección contra SSRF valida el host antes de cualquier salida. Las respuestas tienen un tope de tamaño, y una caché PSR-16 habilita la revalidación por ETag y las lecturas en entornos aislados.
  2. VerificaciónTslSignatureVerifier comprueba la firma XMLDSig envuelta. El certificado de firma debe encadenar a un ancla de confianza que hayas fijado fuera de banda; nada dentro del documento se confía por sí mismo.
  3. AnálisisTslXmlParser extrae la información del esquema y cada servicio TSP en un TslDocument inmutable. Los documentos que portan DOCTYPE se rechazan antes de construir cualquier tabla de entidades.
  4. Aplicación — el instante NextUpdate de la lista no debe haber pasado. Una lista caducada se descarta, nunca se consume.

TslPolicyEnforcer compone las cuatro fases; un TslDocument procedente de él ha pasado todos los controles. A partir de ahí, TslTrustAnchorProvider::buildBundle() filtra los servicios que están a la vez en estado granted y del tipo CA/QC, y emite un EnterpriseCaTrustAnchorBundle: anclas PEM fijadas, una versión tsl-<territory>-seq<N> y un compendio de integridad SHA-256. Ese paquete es lo que consumen la validación de rutas y la vinculación de confianza ASiC.

La misma maquinaria cubre el flujo de trabajo de la LOTL. Verifica la LOTL frente a un ancla fijada manualmente; luego verifica cada TSL de Estado miembro frente a los certificados de firma que la LOTL declara para ella.

La decisión de mayor peso es un perfil de verificación fijo y mínimo en lugar de XMLDSig general. El procesamiento flexible de firmas XML —cadenas de transformación arbitrarias, referencias de ID declaradas por el atacante, agilidad de algoritmos— es donde históricamente fallan los verificadores. Por eso el verificador acepta exactamente un modelo de procesamiento: C14N exclusivo, una referencia que cubre la raíz y la canalización de dos transformaciones [enveloped-signature, exclusive-C14N], con todo lo demás rechazado de forma cerrada. La confianza nunca arranca a partir del propio documento: los certificados de KeyInfo solo encadenan a las anclas que hayas configurado. La vigencia reside en el propio TslDocument, de modo que cada ruta de consumo la aplica en lugar de un colaborador opcional. El resultado es un núcleo pequeño que es comprobable, determinista y honesto sobre lo que rechaza.

Trasfondo de diseño: Firmas cualificadas, explicadas.

El punto de entrada orquestado: obtención, verificación, análisis y comprobación de vigencia en una sola llamada.

public function __construct(
private readonly TslFetcher $fetcher,
private readonly TslSignatureVerifier $verifier,
private readonly TslXmlParser $parser,
) {}
public function fetchAndVerify(string $url): TslDocument
public function verifyXml(string $xml): TslDocument

Lanza o falla con: TslFetchException y NextPDF\Enterprise\Security\NetworkPolicyViolation de la etapa de obtención; TslSignatureException de la verificación de firma; TslParseException del análisis, de un valor NextUpdate no canónico o de una lista caducada. Ambos métodos devuelven un TslDocument solo cuando todos los controles han pasado. El control de caducidad aquí compara NextUpdate con el reloj del sistema actual.

Obtenedor HTTP con caché basada en ETag y un control de política de red.

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

Lanza o falla con: TslFetchException ante una URL no HTTPS, un host rechazado (SSRF), un estado de error HTTP, una respuesta de tamaño excesivo o un cuerpo vacío; NetworkPolicyViolation cuando NetworkPolicy::STRICT_OFFLINE está activo y no existe ningún cuerpo en caché. Los cuerpos en caché satisfacen la revalidación 304 Not Modified y son los únicos cuerpos servidos bajo STRICT_OFFLINE. Las entradas de caché viven durante $defaultTtlSeconds.

Verificador XMLDSig para listas de confianza firmadas.

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

verify() devuelve el PEM del certificado de firma, probado que encadena a uno de $trustAnchorsPem. El constructor lanza InvalidArgumentException cuando la lista de anclas está vacía. $clockTolerance ensancha simétricamente la ventana de validez del certificado, en segundos.

El perfil aceptado es fijo. Algoritmos de firma: la lista de permitidos ALLOWED_SIG_ALG (rsa-sha256/384/512, ecdsa-sha256/384/512). Compendios: la lista de permitidos ALLOWED_DIGEST_ALG (SHA-256, SHA-384, SHA-512). Canonicalización: únicamente C14N exclusivo 1.0. SHA-1 y MD5 se rechazan como unsupported_algorithm.

Lanza o falla con: TslSignatureException, que porta un reason legible por máquina:

Código de razónSignificado
missing_signatureEl documento no tiene ningún elemento ds:Signature.
untrusted_signerEl certificado de KeyInfo no encadena a un ancla configurada.
invalid_signatureDefecto estructural, o la comprobación RSA/ECDSA falló.
digest_mismatchEl compendio de la referencia no coincide con el documento canonicalizado.
unsupported_algorithmAlgoritmo de firma o compendio fuera de la lista de permitidos.
unsupported_transformCanonicalización o canalización de transformación fuera del perfil fijo.
expired_anchorUn certificado de la cadena está fuera de su ventana de validez, o su validez no es analizable.

Analizador estructural agnóstico a la firma. Quienes lo invoquen DEBEN verificar antes de confiar en su salida; TslPolicyEnforcer aplica ese orden por ti.

public function parse(string $xml): TslDocument

Lanza o falla con: TslParseException cuando el XML declara un DOCTYPE (endurecimiento contra XXE y expansión de entidades), no se puede analizar, carece de la raíz TrustServiceStatusList o porta un TSLSequenceNumber inválido. La clase expone las constantes de espacio de nombres NS_TSL, NS_DSIG y NS_TSL_X.

TslDocument es un objeto de valor inmutable: schemeTerritory, schemeOperatorName, tslType, sequenceNumber, issueDateTime, nextUpdate, tspServices y rawXmlSha256 (hash de evidencia sobre los bytes en bruto).

public function isStale(DateTimeImmutable $now): bool
public function assertFresh(DateTimeImmutable $now): void
public function servicesOfType(string $serviceTypeIdentifier): array
public function activeServices(): array

Lanza o falla con: isStale() y assertFresh() lanzan TslParseException cuando nextUpdate no es un dateTime UTC canónico con una Z explícita o un desplazamiento numérico; una lista caducada hace que assertFresh() lance. activeServices() devuelve solo los servicios en estado granted. servicesOfType() filtra por la URI de tipo de servicio de ETSI.

Cada entrada TspService expone tspName, serviceName, serviceTypeIdentifier, serviceStatus, statusStartingTime, serviceCertificatePem, qualifiers y additionalServiceInformation, además de:

public function isGranted(): bool
public function isQualifiedCa(): bool

Constantes útiles: TspService::STATUS_GRANTED, TspService::STATUS_WITHDRAWN, TspService::TYPE_CA_QC, TspService::TYPE_OCSP_QC, TspService::TYPE_TSA_QTST. Las URI de calificador (por ejemplo TspServiceQualifier::FOR_ESIG, FOR_ESEAL, QSCD_STATEMENT, NO_QSCD) afloran en TspServiceQualifier para la capa de mapeo eIDAS.

TslTrustAnchorProvider y el paquete de anclas

Sección titulada «TslTrustAnchorProvider y el paquete de anclas»
public function buildBundle(TslDocument $tsl, DateTimeImmutable $now): EnterpriseCaTrustAnchorBundle

Lanza o falla con: TslParseException cuando la TSL está caducada en $now, cuando nextUpdate no es un valor UTC canónico, o cuando la lista no contiene ningún servicio CA/QC activo.

Nota de compatibilidad (BC) — la regla de vigencia de buildBundle($now). buildBundle() requiere el instante de validación y llama a TslDocument::assertFresh($now) antes de extraer una sola ancla. Revisiones anteriores podían derivar anclas de un TslDocument producido por el analizador sin ninguna comprobación de vigencia. Quienes invocaban alimentando listas en caché o archivadas deben ahora pasar el instante en que se ejecuta su validación; una lista caducada en ese instante lanza en lugar de sembrar silenciosamente anclas de confianza.

El EnterpriseCaTrustAnchorBundle devuelto es un objeto de valor de solo lectura: anchorsPem (las anclas PEM), bundleVersion (tsl-<territory>-seq<N>) y bundleSha256 (compendio de integridad sobre la concatenación PEM canonicalizada). Obténlo de buildBundle(); no lo construyas a mano: el constructor lanza InvalidArgumentException ante una discrepancia de compendio o un PEM malformado.

public function containsFingerprint(string $anchorDerSha256Hex): bool
public static function computeBundleSha256(array $anchorsPem): string

Autentica y consume una lista de confianza reflejada localmente. No se necesita ninguna dependencia HTTP para esta ruta: verifica, analiza y luego controla la vigencia en tu instante de validación.

tsl-verify-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureVerifier;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
// The list-signing certificate, pinned OUT-OF-BAND. Never take it from the list itself.
$pinnedAnchorPem = (string) file_get_contents(__DIR__ . '/tsl-signer-anchor.pem');
// A trusted-list XML document you mirrored locally.
$tslXml = (string) file_get_contents(__DIR__ . '/member-state-tsl.xml');
try {
// 1. Authenticate: XMLDSig must verify AND the signer must chain to the pinned anchor.
(new TslSignatureVerifier(trustAnchorsPem: [$pinnedAnchorPem]))->verify($tslXml);
// 2. Parse the now-authenticated bytes.
$tsl = (new TslXmlParser())->parse($tslXml);
// 3. Freshness: refuse a list whose NextUpdate has passed.
$tsl->assertFresh(new DateTimeImmutable('now', new DateTimeZone('UTC')));
} catch (TslSignatureException $e) {
fwrite(STDERR, "TSL rejected ({$e->reason}): {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (TslParseException $e) {
fwrite(STDERR, 'TSL unusable: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
echo "Territory: {$tsl->schemeTerritory}\n";
echo "Sequence: {$tsl->sequenceNumber}\n";
echo 'Active services: ' . count($tsl->activeServices()) . "\n";

Salida esperada (los valores varían según la lista):

Territory: DE
Sequence: 127
Active services: 143

Cablea la canalización completa en línea: obtención protegida con caché, verificación de firma, análisis, vigencia y luego derivación del paquete de anclas. Cada clase de fallo se captura y se reporta de forma diferenciada.

tsl-anchor-bundle-production.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use NextPDF\Enterprise\Security\NetworkPolicy;
use NextPDF\Enterprise\Security\NetworkPolicyViolation;
use NextPDF\Enterprise\Security\Tsl\TslFetchException;
use NextPDF\Enterprise\Security\Tsl\TslFetcher;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer;
use NextPDF\Enterprise\Security\Tsl\TslSignatureException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureVerifier;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
// Any PSR-18 client, PSR-17 factory, and PSR-16 cache work; these are examples.
$enforcer = new TslPolicyEnforcer(
fetcher: new TslFetcher(
httpClient: new Client(),
requestFactory: new HttpFactory(),
cache: new Psr16Cache(new FilesystemAdapter('tsl')),
defaultTtlSeconds: 3600,
maxBytes: 16_777_216,
networkPolicy: NetworkPolicy::ONLINE,
),
verifier: new TslSignatureVerifier(
trustAnchorsPem: [(string) file_get_contents(__DIR__ . '/tsl-signer-anchor.pem')],
clockTolerance: 300,
),
parser: new TslXmlParser(),
);
// Use the official publication URL for your scheme territory (HTTPS required).
$tslUrl = 'https://trusted-lists.example.eu/member-state-tsl.xml';
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
try {
$tsl = $enforcer->fetchAndVerify($tslUrl);
$bundle = (new TslTrustAnchorProvider())->buildBundle($tsl, $now);
} catch (NetworkPolicyViolation $e) {
// Air-gapped posture: egress forbidden and no cached body available.
fwrite(STDERR, 'Network policy: ' . $e->getMessage() . PHP_EOL);
exit(75);
} catch (TslFetchException $e) {
// Transport layer: SSRF-rejected URL, HTTP error, oversized or empty body.
fwrite(STDERR, 'Fetch failed: ' . $e->getMessage() . PHP_EOL);
exit(1);
} catch (TslSignatureException $e) {
// Authentication layer: treat as a potential attack, not a retry case.
fwrite(STDERR, "Signature rejected ({$e->reason}): {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (TslParseException $e) {
// Structure or freshness: stale list, malformed NextUpdate, no active CA/QC services.
fwrite(STDERR, 'List unusable: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
printf(
"Anchor bundle %s: %d anchors (sha256 %s...)\n",
$bundle->bundleVersion,
count($bundle->anchorsPem),
substr($bundle->bundleSha256, 0, 12),
);

Salida esperada (los valores varían según la lista):

Anchor bundle tsl-de-seq127: 96 anchors (sha256 4b0e2a9f31c8...)

Registra bundleVersion y bundleSha256 con cada validación que realices frente al paquete. Nombran el conjunto exacto de anclas detrás de cada veredicto.

  • El control de vigencia del ejecutor usa el reloj actual. fetchAndVerify() y verifyXml() rechazan una lista cuyo NextUpdate ya ha pasado. Para la validación histórica frente a una lista archivada, opera directamente con TslSignatureVerifier y TslXmlParser, y luego llama a assertFresh() con el instante pasado que tu evidencia respalde.
  • buildBundle() reafirma la vigencia en tu $now. Una lista que pasó el ejecutor todavía puede rechazarse aquí si tu instante de validación es posterior. Consulta la nota de compatibilidad (BC) anterior.
  • Nunca siembres trustAnchorsPem a partir de la lista que estás verificando. El ancla debe provenir de una fuente fijada fuera de banda (para la LOTL) o de una lista superior ya verificada (para las TSL de Estado miembro). Cualquier otra cosa hace circular la verificación.
  • Un DOCTYPE en cualquier lugar es fatal. Las TSL conformes nunca portan un DTD, así que el analizador rechaza cualquier DOCTYPE antes de que libxml construya una tabla de entidades. Esto es un endurecimiento intencionado, no una limitación del analizador.
  • Los campos estructurales que faltan degradan de forma segura. Un servicio sin un estado legible se trata como retirado, de modo que nunca puede convertirse en ancla. Un territorio de esquema ausente se analiza como unknown. Los valores predeterminados fail-closed mantienen las entradas malformadas fuera del material de confianza.
  • Los intermedios deben ser CA reales. Durante la construcción de la cadena, un emisor candidato sin basicConstraints cA=TRUE (o que afirma keyUsage sin keyCertSign) se omite. Un certificado de entidad final introducido de contrabando en KeyInfo no puede servir como intermedio de la ruta. Las cadenas tienen un tope de profundidad 8.
  • NextUpdate debe ser UTC canónico. Un valor sin una Z explícita o un desplazamiento numérico lanza TslParseException. Nunca se reinterpreta en la zona horaria local del servidor.
  • Listas grandes y el tope de bytes. Las respuestas se leen hasta $maxBytes (16 MiB por defecto). Eleva el tope en el constructor si la lista de tu esquema es mayor; el truncamiento aflora como un fallo de firma, nunca como una aceptación silenciosa.
  • clockTolerance solo ensancha. Añade holgura simétrica a las comprobaciones de validez del certificado. No relaja el control de vigencia a nivel de lista.
  • Verifica antes de analizar, siempre. TslXmlParser es agnóstico a la firma por diseño. TslPolicyEnforcer ordena la verificación primero; si compones las piezas por tu cuenta, mantén ese orden.
  • Defensa en profundidad contra SSRF. fetch() requiere https:// y valida el host frente a rangos privados, de bucle invertido, de enlace local, CGN y de metadatos de nube, con resolución DNS A y AAAA para mitigar el rebinding. Una URL rechazada lanza antes de cualquier salida.
  • Endurecimiento contra XXE y expansión de entidades. Los documentos que portan DOCTYPE se rechazan antes de que exista la tabla de entidades y de nuevo tras la carga. La carga de entidades de red está deshabilitada; las entidades externas nunca se sustituyen.
  • Perfil XMLDSig estricto. Únicamente C14N exclusivo; exactamente el par de transformaciones [enveloped-signature, exclusive-C14N]; la referencia verificada debe cubrir la raíz del documento; la transformación envuelta elimina solo la firma verificada, preservando las firmas hermanas. Los algoritmos obsoletos (SHA-1, MD5) se rechazan.
  • Disciplina de cadena. Cada eslabón de la cadena —firmante, intermedios y el caso de ancla directa— se comprueba por su validez temporal, con fallo cerrado ante límites de validez no analizables. Los bucles se detectan; la profundidad tiene un tope.
  • Postura de aislamiento (air-gap). Bajo NetworkPolicy::STRICT_OFFLINE, la ruta de obtención no realiza ninguna salida saliente en absoluto; solo puede servirse un cuerpo previamente en caché, y cualquier otra cosa lanza NetworkPolicyViolation con fallo rápido.
  • Los compendios de paquete detectan corrupción, no manipulación. bundleSha256 se valida en la construcción y detecta la deriva de transcripción. Cuando el compendio se deriva de las mismas anclas que protege, no es evidencia independiente de manipulación. Fija los compendios fuera de banda al transportar paquetes entre sistemas.

La canalización consume las listas de confianza tal como las define ETSI TS 119 612: autentica la firma del operador del esquema (§5.7), analiza las estructuras de información del esquema y de la lista de proveedores (§5.3, §5.4, §5.5), aplica las reglas de dateTime UTC (§5.1.3) y descarta las listas cuyo NextUpdate ha pasado (§5.3.15). Esto respalda el modelo del Artículo 22 de eIDAS de listas de confianza firmadas y procesables por máquina. La construcción de la cadena aplica los controles de restricciones básicas y de uso de clave de RFC 5280 a los emisores candidatos.

El soporte no es conformidad, y la conformidad no es certificación. NextPDF implementa las comprobaciones que esta página describe; no ha sido certificado frente a ETSI TS 119 612, eIDAS ni ningún otro estándar por ningún organismo, y NextPDF no posee certificación alguna ni la otorga. Consumir una lista de confianza a través de esta API no hace por sí mismo que una firma sea «cualificada» ni jurídicamente eficaz. Si tu proceso de validación completo cumple un requisito legal o de contratación es una determinación que corresponde a tus evaluadores.

La verificación de firmas TSL ejecuta las comprobaciones RSA y ECDSA en proceso a través de la biblioteca criptográfica incluida. No se enruta a través de la protección del entorno de ejecución en modo FIPS de Enterprise, y habilitar el modo FIPS no cambia su comportamiento. No es un servicio criptográfico validado por FIPS, y no se reclama ninguna certificación FIPS 140. Los despliegues con obligaciones FIPS deben acotar esta API en consecuencia y consultar Política criptográfica FIPS 140-2/3.

  • fetch() realiza salida solo para URL HTTPS que pasan la validación SSRF, lee como máximo $maxBytes y respeta la NetworkPolicy configurada; bajo STRICT_OFFLINE solo se devuelve un cuerpo en caché.
  • Ninguna salida del analizador se convierte en material de confianza antes de que verify() tenga éxito; TslPolicyEnforcer garantiza ese orden.
  • verify() devuelve el PEM del firmante solo cuando el compendio y la firma cuadran bajo el perfil fijo y el firmante encadena, dentro de la profundidad 8 y con cada eslabón temporalmente válido, a un ancla configurada.
  • El ejecutor rechaza cualquier lista cuyo NextUpdate haya pasado en el reloj actual; buildBundle() reafirma la vigencia en el instante suministrado por quien invoca antes de derivar anclas.
  • Las anclas se derivan exclusivamente de los servicios en estado granted con el tipo de servicio CA/QC; un conjunto activo vacío lanza en lugar de producir un paquete vacío.
  • Todo fallo es una excepción tipada (TslFetchException, NetworkPolicyViolation, TslSignatureException con un código de razón, TslParseException); ningún método devuelve un documento parcial o no verificado.

NextPDF Core valida firmas PDF frente a anclas de confianza que fijas explícitamente a través de su contrato CaTrustAnchorBundle — consulta Seguridad de Core. Core no tiene capacidad de listas de confianza: sin obtención de TSL, sin autenticación XMLDSig de listas, sin análisis de ETSI TS 119 612 y sin derivación de anclas a partir de entradas de servicios cualificados. Con Core por sí solo mantienes tu conjunto de anclas a mano; derivarlo de listas de confianza de la UE autenticadas requiere NextPDF Enterprise.

Esta página documenta únicamente el comportamiento observable externamente y la superficie pública de la API soportada. Las rutas de espacios de nombres internas, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbooks y los prefijos de tickets quedan fuera de alcance.