Pular para o conteúdo
getnextpdf.com

Enterprise edição

Listas confiáveis (TSL)

A validação de assinaturas na UE parte de um fato publicado: quais provedores possuem status qualificado. Esse fato vive nas listas confiáveis (TSLs) — documentos XML assinados que cada Estado-Membro publica, indexados pela lista de listas confiáveis (LOTL) da UE. O NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer transforma uma URL de TSL ou um XML bruto em um TslDocument em que você pode confiar. Ele faz o fetch sobre HTTPS protegido, verifica a assinatura XMLDSig contra âncoras que você fixa, faz o parsing de XML endurecido e rejeita listas obsoletas. Uma chamada adicional, TslTrustAnchorProvider::buildBundle(), converte serviços CA/QC ativos em um pacote de âncoras de confiança versionado. Cada gate falha em modo fechado; cada rejeição é uma exceção tipada.

Esta página trata da ingestão de listas e da derivação de âncoras. A validação do caminho de certificação vive em Verificação de assinaturas. O mapeamento de níveis de garantia eIDAS vive em Níveis de garantia eIDAS. A vinculação de confiança de contêineres vive em Vinculação de confiança ASiC.

Este recurso é fornecido no NextPDF Enterprise (nextpdf/enterprise) e é ativado com um envelope de licença de nível Enterprise. Uma implantação sem esse direito não carrega as classes do recurso. Compare edições e obtenha uma licença.

Terminal window
composer require nextpdf/enterprise

A ativação requer o seu envelope de licença Enterprise. Consulte Instalar e autenticar. As classes desta página vivem em NextPDF\Enterprise\Security\Tsl; os tipos de política de rede vivem em NextPDF\Enterprise\Security. A busca online adicionalmente requer qualquer cliente PSR-18 e fábrica PSR-17 (por exemplo, guzzlehttp/guzzle).

Sob o Artigo 22 do eIDAS, cada Estado-Membro publica uma lista confiável de seus provedores qualificados de serviços de confiança, assinada ou selada para processamento automatizado. A ETSI TS 119 612 define o formato XML. A lista só é tão confiável quanto três verificações a tornam: sua assinatura, sua estrutura e sua atualidade. O NextPDF as executa nessa ordem, como um único pipeline:

  1. Fetch — o TslFetcher recupera o XML somente sobre HTTPS. Uma proteção contra SSRF valida o host antes de qualquer saída. As respostas têm limite de tamanho, e um cache PSR-16 habilita a revalidação por ETag e leituras air-gapped.
  2. Verify — o TslSignatureVerifier verifica a assinatura XMLDSig envelopada. O certificado de assinatura precisa encadear até uma âncora de confiança que você fixou out-of-band; nada dentro do documento é confiável por si só.
  3. Parse — o TslXmlParser extrai as informações do esquema e cada serviço TSP em um TslDocument imutável. Documentos que carregam DOCTYPE são rejeitados antes que qualquer tabela de entidades seja construída.
  4. Enforce — o instante NextUpdate da lista não pode ter passado. Uma lista obsoleta é descartada, nunca consumida.

O TslPolicyEnforcer compõe as quatro etapas; um TslDocument produzido por ele passou por todos os gates. A partir daí, o TslTrustAnchorProvider::buildBundle() filtra serviços que estão tanto em status granted quanto do tipo CA/QC, e emite um EnterpriseCaTrustAnchorBundle: âncoras PEM fixadas, uma versão tsl-<territory>-seq<N> e um digest de integridade SHA-256. Esse pacote é o que a validação de caminho e a vinculação de confiança ASiC consomem.

A mesma maquinaria cobre o fluxo da LOTL. Verifique a LOTL contra uma âncora fixada manualmente; depois verifique cada TSL de Estado-Membro contra os certificados de assinatura que a LOTL declara para ela.

A decisão de sustentação é um perfil de verificação fixo e mínimo em vez de XMLDSig genérico. O processamento flexível de assinaturas XML — cadeias de transformação arbitrárias, referências de ID declaradas pelo atacante, agilidade de algoritmos — é onde os verificadores historicamente quebram. Por isso o verificador aceita exatamente um modelo de processamento: C14N exclusiva, uma referência que cobre a raiz e o pipeline de duas transformações [enveloped-signature, exclusive-C14N], com tudo o mais rejeitado em modo fechado. A confiança nunca se inicializa a partir do próprio documento: os certificados de KeyInfo só encadeiam até âncoras que você configurou. A atualidade vive no próprio TslDocument, de modo que todo caminho consumidor a impõe em vez de um único colaborador opcional. O resultado é um núcleo pequeno, testável, determinístico e honesto sobre o que ele recusa.

Contexto de projeto: Assinaturas qualificadas, explicadas.

O ponto de entrada orquestrado: fetch, verify, parse e verificação de atualidade em uma única chamada.

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

Lança ou falha com: TslFetchException e NextPDF\Enterprise\Security\NetworkPolicyViolation da etapa de fetch; TslSignatureException da verificação de assinatura; TslParseException do parsing, de um valor NextUpdate não canônico ou de uma lista obsoleta. Ambos os métodos retornam um TslDocument apenas quando todos os gates passaram. O gate de obsolescência aqui compara NextUpdate com o relógio atual do sistema.

Fetcher HTTP com cache baseado em ETag e um gate de política de rede.

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

Lança ou falha com: TslFetchException em uma URL não-HTTPS, um host rejeitado (SSRF), um status HTTP de erro, uma resposta grande demais ou um corpo vazio; NetworkPolicyViolation quando NetworkPolicy::STRICT_OFFLINE está ativo e não existe corpo em cache. Corpos em cache satisfazem a revalidação 304 Not Modified e são os únicos corpos servidos sob STRICT_OFFLINE. As entradas de cache vivem por $defaultTtlSeconds.

Verificador XMLDSig para listas confiáveis assinadas.

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

O verify() retorna o PEM do certificado de assinatura, comprovadamente encadeado até um de $trustAnchorsPem. O construtor lança InvalidArgumentException quando a lista de âncoras está vazia. O $clockTolerance amplia a janela de validade do certificado simetricamente, em segundos.

O perfil aceito é fixo. Algoritmos de assinatura: a allowlist ALLOWED_SIG_ALG (rsa-sha256/384/512, ecdsa-sha256/384/512). Digests: a allowlist ALLOWED_DIGEST_ALG (SHA-256, SHA-384, SHA-512). Canonicalização: somente C14N exclusiva 1.0. SHA-1 e MD5 são rejeitados como unsupported_algorithm.

Lança ou falha com: TslSignatureException, carregando um reason legível por máquina:

Código de motivoSignificado
missing_signatureO documento não tem elemento ds:Signature.
untrusted_signerO certificado de KeyInfo não encadeia até uma âncora configurada.
invalid_signatureDefeito estrutural, ou a verificação RSA/ECDSA falhou.
digest_mismatchO digest da referência não corresponde ao documento canonicalizado.
unsupported_algorithmAlgoritmo de assinatura ou digest fora da allowlist.
unsupported_transformPipeline de canonicalização ou transformação fora do perfil fixo.
expired_anchorUm certificado da cadeia está fora de sua janela de validade, ou sua validade é ilegível.

Parser estrutural agnóstico à assinatura. Os chamadores DEVEM verificar antes de confiar em sua saída; o TslPolicyEnforcer impõe essa ordem para você.

public function parse(string $xml): TslDocument

Lança ou falha com: TslParseException quando o XML declara um DOCTYPE (endurecimento contra XXE e expansão de entidades), não pode ser parseado, carece da raiz TrustServiceStatusList ou carrega um TSLSequenceNumber inválido. A classe expõe as constantes de namespace NS_TSL, NS_DSIG e NS_TSL_X.

O TslDocument é um value object imutável: schemeTerritory, schemeOperatorName, tslType, sequenceNumber, issueDateTime, nextUpdate, tspServices e rawXmlSha256 (hash de evidência sobre os bytes brutos).

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

Lança ou falha com: isStale() e assertFresh() lançam TslParseException quando nextUpdate não é um dateTime UTC canônico com um Z explícito ou offset numérico; uma lista obsoleta faz assertFresh() lançar. O activeServices() retorna apenas serviços em status granted. O servicesOfType() filtra pela URI de tipo de serviço ETSI.

Cada entrada TspService expõe tspName, serviceName, serviceTypeIdentifier, serviceStatus, statusStartingTime, serviceCertificatePem, qualifiers e additionalServiceInformation, além de:

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

Constantes úteis: TspService::STATUS_GRANTED, TspService::STATUS_WITHDRAWN, TspService::TYPE_CA_QC, TspService::TYPE_OCSP_QC, TspService::TYPE_TSA_QTST. URIs de qualificador (por exemplo TspServiceQualifier::FOR_ESIG, FOR_ESEAL, QSCD_STATEMENT, NO_QSCD) aparecem em TspServiceQualifier para a camada de mapeamento eIDAS.

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

Lança ou falha com: TslParseException quando a TSL está obsoleta em $now, quando nextUpdate não é um valor UTC canônico, ou quando a lista não contém serviços CA/QC ativos.

Nota de BC — a regra de atualidade de buildBundle($now). O buildBundle() requer o instante de validação e chama TslDocument::assertFresh($now) antes de extrair uma única âncora. Revisões anteriores podiam derivar âncoras de um TslDocument produzido pelo parser sem nenhuma verificação de atualidade. Chamadores que alimentavam listas em cache ou arquivadas agora precisam passar o instante em que a validação é executada; uma lista obsoleta nesse instante lança em vez de silenciosamente semear âncoras de confiança.

O EnterpriseCaTrustAnchorBundle retornado é um value object somente leitura: anchorsPem (as âncoras PEM), bundleVersion (tsl-<territory>-seq<N>) e bundleSha256 (digest de integridade sobre a concatenação canonicalizada dos PEM). Obtenha-o de buildBundle(); não o construa manualmente — o construtor lança InvalidArgumentException em uma divergência de digest ou PEM malformado.

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

Autentique e consuma uma lista confiável espelhada localmente. Nenhuma dependência HTTP é necessária para este caminho: verifique, faça o parsing e então avalie a atualidade no seu instante de validação.

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";

Saída esperada (os valores variam conforme a lista):

Territory: DE
Sequence: 127
Active services: 143

Conecte o pipeline online completo: fetch protegido com cache, verificação de assinatura, parse, atualidade e então derivação do pacote de âncoras. Cada classe de falha é capturada e reportada distintamente.

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),
);

Saída esperada (os valores variam conforme a lista):

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

Registre bundleVersion e bundleSha256 a cada validação que você realiza contra o pacote. Eles nomeiam o conjunto exato de âncoras por trás de cada veredito.

  • O gate de atualidade do enforcer usa o relógio atual. O fetchAndVerify() e o verifyXml() rejeitam uma lista cujo NextUpdate já passou. Para validação histórica contra uma lista arquivada, acione o TslSignatureVerifier e o TslXmlParser diretamente, depois chame assertFresh() com o instante passado que sua evidência sustenta.
  • O buildBundle() reafirma a atualidade no seu $now. Uma lista que passou pelo enforcer ainda pode ser rejeitada aqui se o seu instante de validação for posterior. Veja a nota de BC acima.
  • Nunca semeie trustAnchorsPem a partir da lista que você está verificando. A âncora precisa vir de uma fonte fixada out-of-band (para a LOTL) ou de uma lista pai já verificada (para as TSLs de Estado-Membro). Qualquer outra coisa torna a verificação circular.
  • Um DOCTYPE em qualquer lugar é fatal. TSLs conformes nunca carregam um DTD, então o parser rejeita qualquer DOCTYPE antes de o libxml construir uma tabela de entidades. Isso é endurecimento intencional, não uma limitação do parser.
  • Campos estruturais ausentes degradam de forma segura. Um serviço sem um status legível é tratado como retirado, de modo que nunca pode se tornar uma âncora. Um território de esquema ausente é parseado como unknown. Padrões fail-closed mantêm entradas malformadas fora do material de confiança.
  • Os intermediários precisam ser CAs reais. Durante a construção da cadeia, um emissor candidato sem basicConstraints cA=TRUE (ou que declara keyUsage sem keyCertSign) é ignorado. Um certificado de entidade final introduzido em KeyInfo não pode servir como intermediário de caminho. As cadeias são limitadas à profundidade 8.
  • O NextUpdate precisa ser UTC canônico. Um valor sem um Z explícito ou offset numérico lança TslParseException. Ele nunca é reinterpretado no fuso horário local do servidor.
  • Listas grandes e o limite de bytes. As respostas são lidas até $maxBytes (padrão 16 MiB). Aumente o limite no construtor se a lista do seu esquema for maior; a truncagem aparece como uma falha de assinatura, nunca como aceitação silenciosa.
  • O clockTolerance apenas amplia. Ele adiciona folga simétrica às verificações de validade de certificado. Ele não afrouxa o gate de atualidade no nível da lista.
  • Verifique antes de fazer o parsing, sempre. O TslXmlParser é agnóstico à assinatura por projeto. O TslPolicyEnforcer ordena a verificação primeiro; se você compor as peças por conta própria, mantenha essa ordem.
  • Defesa em profundidade contra SSRF. O fetch() exige https:// e valida o host contra faixas privadas, de loopback, link-local, CGN e de metadados de nuvem, com resolução DNS A e AAAA para mitigar rebinding. Uma URL rejeitada lança antes de qualquer saída.
  • Endurecimento contra XXE e expansão de entidades. Documentos que carregam DOCTYPE são rejeitados antes de a tabela de entidades existir e novamente após o carregamento. O carregamento de entidades de rede está desabilitado; entidades externas nunca são substituídas.
  • Perfil XMLDSig estrito. Somente C14N exclusiva; exatamente o par de transformações [enveloped-signature, exclusive-C14N]; a referência verificada precisa cobrir a raiz do documento; a transformação envelopada remove apenas a assinatura verificada, preservando assinaturas irmãs. Algoritmos obsoletos (SHA-1, MD5) são rejeitados.
  • Disciplina de cadeia. Cada elo da cadeia — signatário, intermediários e o caso de âncora direta — é verificado quanto à validade temporal, fail-closed em limites de validade ilegíveis. Loops são detectados; a profundidade é limitada.
  • Postura air-gap. Sob NetworkPolicy::STRICT_OFFLINE, o caminho de fetch não realiza nenhuma saída externa; apenas um corpo previamente em cache pode ser servido, e qualquer outra coisa lança NetworkPolicyViolation de forma fail-fast.
  • Digests de pacote detectam corrupção, não adulteração. O bundleSha256 é validado na construção e detecta desvio de transcrição. Quando o digest é derivado das mesmas âncoras que ele protege, ele não é evidência independente de adulteração. Fixe os digests out-of-band ao transportar pacotes entre sistemas.

O pipeline consome listas confiáveis conforme a ETSI TS 119 612 as define: ele autentica a assinatura do operador do esquema (§5.7), faz o parsing das estruturas de informações do esquema e da lista de provedores (§5.3, §5.4, §5.5), impõe as regras de dateTime UTC (§5.1.3) e descarta listas cujo NextUpdate passou (§5.3.15). Isso apoia o modelo do Artigo 22 do eIDAS de listas confiáveis assinadas e processáveis por máquina. A construção de cadeia aplica os gates de basic-constraints e key-usage da RFC 5280 aos emissores candidatos.

Suporte não é conformidade, e conformidade não é certificação. O NextPDF implementa as verificações que esta página descreve; ele não foi certificado contra ETSI TS 119 612, eIDAS ou qualquer outro padrão por nenhum órgão, e o NextPDF não detém nenhuma certificação e não concede nenhuma. Consumir uma lista confiável através desta API não torna, por si só, uma assinatura “qualificada” ou legalmente eficaz. Se o seu processo de validação completo atende a um requisito legal ou de aquisição é uma determinação para os seus avaliadores.

A verificação de assinatura de TSL executa as verificações RSA e ECDSA em processo, através da biblioteca de criptografia empacotada. Ela não é roteada através do guard de runtime do modo FIPS do Enterprise, e habilitar o modo FIPS não altera seu comportamento. Ela não é um serviço criptográfico validado por FIPS, e nenhuma certificação FIPS 140 é reivindicada. Implantações com obrigações de FIPS devem delimitar esta API de acordo e consultar Política criptográfica FIPS 140-2/3.

  • O fetch() realiza saída apenas para URLs HTTPS que passam na validação de SSRF, lê no máximo $maxBytes e honra a NetworkPolicy configurada; sob STRICT_OFFLINE apenas um corpo em cache é retornado.
  • Nenhuma saída do parser se torna material de confiança antes de o verify() ter sucesso; o TslPolicyEnforcer garante essa ordem.
  • O verify() retorna o PEM do signatário apenas quando o digest e a verificação de assinatura conferem sob o perfil fixo e o signatário encadeia, dentro da profundidade 8 e com cada elo temporalmente válido, até uma âncora configurada.
  • O enforcer rejeita qualquer lista cujo NextUpdate passou no relógio atual; o buildBundle() reafirma a atualidade no instante fornecido pelo chamador antes de derivar âncoras.
  • As âncoras derivam exclusivamente de serviços em status granted com o tipo de serviço CA/QC; um conjunto ativo vazio lança em vez de produzir um pacote vazio.
  • Cada falha é uma exceção tipada (TslFetchException, NetworkPolicyViolation, TslSignatureException com um código de motivo, TslParseException); nenhum método retorna um documento parcial ou não verificado.

O NextPDF Core valida assinaturas PDF contra âncoras de confiança que você fixa explicitamente através do seu contrato CaTrustAnchorBundle — veja Segurança do Core. O Core não tem capacidade de listas confiáveis: nenhum fetch de TSL, nenhuma autenticação XMLDSig de listas, nenhum parsing de ETSI TS 119 612 e nenhuma derivação de âncoras a partir de entradas de serviço qualificado. Somente com o Core você mantém seu conjunto de âncoras manualmente; derivá-lo de listas confiáveis da UE autenticadas requer o NextPDF Enterprise.

Esta página documenta apenas o comportamento observável externamente e a superfície pública de API suportada. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismos, nomes de arquivos de runbook e prefixos de ticket estão fora de escopo.