Перейти к содержимому
getnextpdf.com

Enterprise редакция

Доверенные списки (TSL)

Проверка подписей в ЕС начинается с опубликованного факта: какие поставщики имеют квалифицированный статус. Этот факт находится в доверенных списках (TSL) — подписанных XML-документах, которые публикует каждое государство-член и которые индексируются списком доверенных списков ЕС (LOTL). NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer превращает URL TSL или необработанный XML в TslDocument, на который можно положиться. Он загружает данные по защищённому HTTPS, проверяет подпись XMLDSig по закреплённым вами якорям, разбирает усиленный XML и отклоняет устаревшие списки. Ещё один вызов, TslTrustAnchorProvider::buildBundle(), преобразует активные службы CA/QC в версионированный набор якорей доверия. Каждая проверка работает по принципу отказоустойчивости; каждый отказ — типизированное исключение.

Эта страница отвечает за загрузку списков и вывод якорей. Проверка пути сертификации описана в разделе Проверка подписи. Сопоставление уровней доверия eIDAS описано в разделе Уровни доверия eIDAS. Привязка доверия для контейнеров описана в разделе Привязка доверия ASiC.

Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без такого права доступа не загружает классы этой возможности. Сравните редакции и получите лицензию.

Окно терминала
composer require nextpdf/enterprise

Для активации требуется ваш лицензионный конверт Enterprise. См. Установка и аутентификация. Классы на этой странице находятся в пространстве имён NextPDF\Enterprise\Security\Tsl; типы сетевой политики находятся в пространстве имён NextPDF\Enterprise\Security. Для онлайн-загрузки дополнительно нужны любой клиент PSR-18 и фабрика PSR-17 (например, guzzlehttp/guzzle).

Согласно статье 22 eIDAS, каждое государство-член публикует доверенный список своих квалифицированных поставщиков услуг доверия, подписанный или заверенный печатью для автоматизированной обработки. ETSI TS 119 612 определяет формат XML. Список настолько же надёжен, насколько надёжны три проверки: его подпись, его структура и его свежесть. NextPDF выполняет их именно в этом порядке, как единый конвейер:

  1. ЗагрузкаTslFetcher получает XML только по HTTPS. Защита от SSRF проверяет хост перед любым исходящим запросом. Ответы ограничены по размеру, а кэш PSR-16 обеспечивает повторную проверку ETag и чтение в изолированной среде.
  2. ПроверкаTslSignatureVerifier проверяет вложенную подпись XMLDSig. Подписывающий сертификат должен выстраивать цепочку до якоря доверия, закреплённого вами вне канала; ничему внутри документа не доверяют самому по себе.
  3. РазборTslXmlParser извлекает информацию о схеме и каждую службу TSP в неизменяемый TslDocument. Документы с DOCTYPE отклоняются до построения любой таблицы сущностей.
  4. Применение — момент NextUpdate списка не должен уже пройти. Устаревший список отбрасывается, а не потребляется.

TslPolicyEnforcer объединяет все четыре шага; полученный от него TslDocument прошёл каждую проверку. Далее TslTrustAnchorProvider::buildBundle() отбирает службы, которые одновременно имеют статус granted и тип CA/QC, и выдаёт EnterpriseCaTrustAnchorBundle: закреплённые якоря PEM, версию tsl-<territory>-seq<N> и дайджест целостности SHA-256. Именно этот набор потребляют проверка пути и привязка доверия ASiC.

Тот же механизм охватывает процесс LOTL. Проверьте LOTL по вручную закреплённому якорю; затем проверьте каждый TSL государства-члена по подписывающим сертификатам, которые LOTL для него объявляет.

Ключевое решение — фиксированный минимальный профиль проверки вместо общего XMLDSig. Гибкая обработка XML-подписей — произвольные цепочки преобразований, ссылки на ID, объявленные атакующим, гибкость алгоритмов — это как раз то, на чём исторически ломаются верификаторы. Поэтому верификатор принимает ровно одну модель обработки: exclusive C14N, ссылку, покрывающую корень, и конвейер из двух преобразований [enveloped-signature, exclusive-C14N], а всё остальное отклоняет по принципу отказоустойчивости. Доверие никогда не выстраивается из самого документа: сертификаты KeyInfo выстраивают цепочку только до настроенных вами якорей. Свежесть находится на самом TslDocument, поэтому её применяет каждый потребитель, а не один необязательный участник. В результате получается небольшое ядро — тестируемое, детерминированное и честное в том, что оно отвергает.

Проектный фон: Квалифицированные подписи: разъяснение.

Оркестрированная точка входа: загрузка, проверка, разбор и контроль свежести в одном вызове.

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

Выбрасывает или завершается с ошибкой: TslFetchException и NextPDF\Enterprise\Security\NetworkPolicyViolation на этапе загрузки; TslSignatureException при проверке подписи; TslParseException при разборе, при неканоническом значении NextUpdate или при устаревшем списке. Оба метода возвращают TslDocument только тогда, когда пройдена каждая проверка. Здесь проверка устаревания сравнивает NextUpdate с текущими системными часами.

HTTP-загрузчик с кэшированием на основе ETag и проверкой сетевой политики.

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

Выбрасывает или завершается с ошибкой: TslFetchException при URL без HTTPS, отклонённом (SSRF) хосте, статусе ошибки HTTP, слишком большом ответе или пустом теле; NetworkPolicyViolation, когда активна NetworkPolicy::STRICT_OFFLINE и нет кэшированного тела. Кэшированные тела удовлетворяют повторной проверке 304 Not Modified и являются единственными телами, обслуживаемыми при STRICT_OFFLINE. Записи кэша живут в течение $defaultTtlSeconds.

Верификатор XMLDSig для подписанных доверенных списков.

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

verify() возвращает PEM подписывающего сертификата, для которого доказано выстраивание цепочки до одного из $trustAnchorsPem. Конструктор выбрасывает InvalidArgumentException, когда список якорей пуст. $clockTolerance симметрично расширяет окно действия сертификата, в секундах.

Принимаемый профиль фиксирован. Алгоритмы подписи: список разрешённых ALLOWED_SIG_ALG (rsa-sha256/384/512, ecdsa-sha256/384/512). Дайджесты: список разрешённых ALLOWED_DIGEST_ALG (SHA-256, SHA-384, SHA-512). Канонизация: только exclusive C14N 1.0. SHA-1 и MD5 отклоняются как unsupported_algorithm.

Выбрасывает или завершается с ошибкой: TslSignatureException с машиночитаемым reason:

Код причиныЗначение
missing_signatureВ документе нет элемента ds:Signature.
untrusted_signerСертификат KeyInfo не выстраивает цепочку до настроенного якоря.
invalid_signatureСтруктурный дефект либо провалилась проверка RSA/ECDSA.
digest_mismatchДайджест ссылки не совпадает с канонизированным документом.
unsupported_algorithmАлгоритм подписи или дайджеста вне списка разрешённых.
unsupported_transformКанонизация или конвейер преобразований вне фиксированного профиля.
expired_anchorСертификат в цепочке вне своего окна действия либо его срок действия не поддаётся разбору.

Структурный разборщик, не зависящий от подписи. Вызывающие ОБЯЗАНЫ проверить подпись, прежде чем доверять его выводу; TslPolicyEnforcer обеспечивает этот порядок за вас.

public function parse(string $xml): TslDocument

Выбрасывает или завершается с ошибкой: TslParseException, когда XML объявляет DOCTYPE (защита от XXE и раскрытия сущностей), не поддаётся разбору, не содержит корня TrustServiceStatusList или несёт недопустимый TSLSequenceNumber. Класс предоставляет константы пространств имён NS_TSL, NS_DSIG и NS_TSL_X.

TslDocument — неизменяемый объект-значение: schemeTerritory, schemeOperatorName, tslType, sequenceNumber, issueDateTime, nextUpdate, tspServices и rawXmlSha256 (хеш-доказательство по необработанным байтам).

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

Выбрасывает или завершается с ошибкой: isStale() и assertFresh() выбрасывают TslParseException, когда nextUpdate не является каноническим dateTime в UTC с явным Z или числовым смещением; устаревший список заставляет assertFresh() выбросить исключение. activeServices() возвращает только службы в статусе granted. servicesOfType() фильтрует по URI типа службы ETSI.

Каждая запись TspService предоставляет tspName, serviceName, serviceTypeIdentifier, serviceStatus, statusStartingTime, serviceCertificatePem, qualifiers и additionalServiceInformation, а также:

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

Полезные константы: TspService::STATUS_GRANTED, TspService::STATUS_WITHDRAWN, TspService::TYPE_CA_QC, TspService::TYPE_OCSP_QC, TspService::TYPE_TSA_QTST. URI квалификаторов (например, TspServiceQualifier::FOR_ESIG, FOR_ESEAL, QSCD_STATEMENT, NO_QSCD) доступны на TspServiceQualifier для слоя сопоставления eIDAS.

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

Выбрасывает или завершается с ошибкой: TslParseException, когда TSL устарел на момент $now, когда nextUpdate не является каноническим значением UTC или когда список не содержит активных служб CA/QC.

Замечание о совместимости — правило свежести buildBundle($now). buildBundle() требует момент проверки и вызывает TslDocument::assertFresh($now) перед извлечением хотя бы одного якоря. Более ранние версии могли выводить якоря из TslDocument, полученного разборщиком, без какой-либо проверки свежести. Вызывающие, которые подавали кэшированные или архивные списки, теперь должны передавать момент, в который выполняется их проверка; список, устаревший на этот момент, выбрасывает исключение вместо молчаливого создания якорей доверия.

Возвращаемый EnterpriseCaTrustAnchorBundle — объект-значение только для чтения: anchorsPem (якоря PEM), bundleVersion (tsl-<territory>-seq<N>) и bundleSha256 (дайджест целостности по канонизированной конкатенации PEM). Получайте его из buildBundle(); не конструируйте вручную — конструктор выбрасывает InvalidArgumentException при несовпадении дайджеста или некорректном PEM.

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

Аутентифицируйте и потребляйте локально зеркалированный доверенный список. Для этого пути не нужна зависимость от HTTP: проверьте, разберите, затем примените проверку свежести на момент вашей проверки.

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

Ожидаемый вывод (значения зависят от списка):

Territory: DE
Sequence: 127
Active services: 143

Свяжите полный онлайн-конвейер: защищённая загрузка с кэшированием, проверка подписи, разбор, свежесть, затем вывод набора якорей. Каждый класс отказа перехватывается и сообщается отдельно.

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

Ожидаемый вывод (значения зависят от списка):

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

Записывайте bundleVersion и bundleSha256 при каждой проверке, которую вы выполняете против набора. Они называют точный набор якорей, стоящий за каждым вердиктом.

  • Проверка свежести в enforcer использует текущие часы. fetchAndVerify() и verifyXml() отклоняют список, у которого NextUpdate уже прошёл. Для исторической проверки против архивного списка управляйте TslSignatureVerifier и TslXmlParser напрямую, затем вызовите assertFresh() с прошлым моментом, который подтверждают ваши доказательства.
  • buildBundle() повторно проверяет свежесть на ваш $now. Список, прошедший enforcer, всё равно может быть отклонён здесь, если ваш момент проверки более поздний. См. замечание о совместимости выше.
  • Никогда не заполняйте trustAnchorsPem из списка, который вы проверяете. Якорь должен поступать из закреплённого вне канала источника (для LOTL) или из уже проверенного родительского списка (для TSL государств-членов). Что угодно другое делает проверку циклической.
  • DOCTYPE где угодно фатален. Соответствующие требованиям TSL никогда не несут DTD, поэтому разборщик отклоняет любой DOCTYPE до того, как libxml построит таблицу сущностей. Это намеренное усиление, а не ограничение разборщика.
  • Отсутствующие структурные поля деградируют безопасно. Служба без читаемого статуса считается отозванной, поэтому она никогда не может стать якорем. Отсутствующая территория схемы разбирается как unknown. Отказоустойчивые значения по умолчанию не пускают некорректные записи в материал доверия.
  • Промежуточные сертификаты должны быть настоящими CA. При построении цепочки кандидат-издатель без basicConstraints cA=TRUE (или с keyUsage без keyCertSign) пропускается. Конечный сертификат, тайком подсунутый в KeyInfo, не может служить промежуточным звеном пути. Цепочки ограничены глубиной 8.
  • NextUpdate должен быть каноническим UTC. Значение без явного Z или числового смещения выбрасывает TslParseException. Оно никогда не переинтерпретируется в локальном часовом поясе сервера.
  • Большие списки и ограничение по байтам. Ответы читаются вплоть до $maxBytes (по умолчанию 16 MiB). Поднимите ограничение в конструкторе, если список вашей схемы больше; усечение проявляется как отказ подписи, а не как молчаливое принятие.
  • clockTolerance только расширяет. Он добавляет симметричный запас к проверкам действия сертификата. Он не ослабляет проверку свежести на уровне списка.
  • Проверяйте перед разбором, всегда. TslXmlParser по замыслу не зависит от подписи. TslPolicyEnforcer ставит проверку первой; если вы собираете части сами, сохраняйте этот порядок.
  • Эшелонированная защита от SSRF. fetch() требует https:// и проверяет хост против диапазонов приватных, петлевых, link-local, CGN и облачных метаданных, с DNS-разрешением A и AAAA для смягчения rebinding. Отклонённый URL выбрасывает исключение до любого исходящего запроса.
  • Усиление против XXE и раскрытия сущностей. Документы с DOCTYPE отклоняются до того, как таблица сущностей существует, и ещё раз после загрузки. Сетевая загрузка сущностей отключена; внешние сущности никогда не подставляются.
  • Строгий профиль XMLDSig. Только exclusive C14N; ровно пара преобразований [enveloped-signature, exclusive-C14N]; проверяемая ссылка должна покрывать корень документа; enveloped-преобразование удаляет только проверяемую подпись, сохраняя соседние подписи. Устаревшие алгоритмы (SHA-1, MD5) отклоняются.
  • Дисциплина цепочки. Каждое звено цепочки — подписант, промежуточные сертификаты и случай прямого якоря — проверяется на временную действительность, с отказоустойчивостью при неразбираемых границах действия. Циклы обнаруживаются; глубина ограничена.
  • Изолированный режим. При NetworkPolicy::STRICT_OFFLINE путь загрузки вообще не выполняет исходящих запросов; может быть обслужено только ранее кэшированное тело, а всё остальное выбрасывает NetworkPolicyViolation с быстрым отказом.
  • Дайджесты набора обнаруживают повреждение, а не подделку. bundleSha256 проверяется при конструировании и обнаруживает дрейф при переписывании. Когда дайджест выводится из тех же якорей, которые он защищает, он не является независимым доказательством отсутствия подделки. Закрепляйте дайджесты вне канала при переносе наборов между системами.

Конвейер потребляет доверенные списки так, как их определяет ETSI TS 119 612: он аутентифицирует подпись оператора схемы (§5.7), разбирает информацию о схеме и структуры списка поставщиков (§5.3, §5.4, §5.5), применяет правила dateTime в UTC (§5.1.3) и отбрасывает списки, у которых NextUpdate прошёл (§5.3.15). Это поддерживает модель eIDAS статьи 22 подписанных, машинообрабатываемых доверенных списков. Построение цепочки применяет проверки basic-constraints и key-usage RFC 5280 к кандидатам-издателям.

Поддержка — не соответствие, а соответствие — не сертификация. NextPDF реализует проверки, описанные на этой странице; он не был сертифицирован против ETSI TS 119 612, eIDAS или любого другого стандарта каким-либо органом, и NextPDF не имеет сертификации и не предоставляет её. Потребление доверенного списка через этот API само по себе не делает подпись «квалифицированной» или юридически действительной. Соответствует ли ваш полный процесс проверки юридическому или закупочному требованию — это определение, которое выносят ваши оценщики.

Проверка подписи TSL выполняет проверки RSA и ECDSA внутри процесса через встроенную криптографическую библиотеку. Она не маршрутизируется через защитный барьер режима FIPS в Enterprise, и включение режима FIPS не меняет её поведение. Это не FIPS-валидированная криптографическая служба, и никакая сертификация FIPS 140 не заявляется. Развёртывания с обязательствами FIPS должны соответствующим образом ограничивать этот API и обратиться к разделу Криптографическая политика FIPS 140-2/3.

  • fetch() выполняет исходящий запрос только для HTTPS-URL, проходящих проверку SSRF, читает не более $maxBytes и соблюдает настроенную NetworkPolicy; при STRICT_OFFLINE всегда возвращается только кэшированное тело.
  • Ни один вывод разборщика не становится материалом доверия до успешного verify(); TslPolicyEnforcer гарантирует этот порядок.
  • verify() возвращает PEM подписанта только тогда, когда дайджест и подпись проходят проверку по фиксированному профилю и подписант выстраивает цепочку — в пределах глубины 8 и с временной действительностью каждого звена — до настроенного якоря.
  • Enforcer отклоняет любой список, у которого NextUpdate прошёл по текущим часам; buildBundle() повторно проверяет свежесть на момент, переданный вызывающим, перед выводом якорей.
  • Якоря выводятся исключительно из служб в статусе granted с типом службы CA/QC; пустой активный набор выбрасывает исключение, а не выдаёт пустой набор.
  • Каждый отказ — типизированное исключение (TslFetchException, NetworkPolicyViolation, TslSignatureException с кодом причины, TslParseException); ни один метод не возвращает частичный или непроверенный документ.

NextPDF Core проверяет подписи PDF против якорей доверия, которые вы явно закрепляете через его контракт CaTrustAnchorBundle — см. Безопасность Core. У Core нет возможности работы с доверенными списками: ни загрузки TSL, ни аутентификации списка XMLDSig, ни разбора ETSI TS 119 612, ни вывода якорей из записей квалифицированных служб. С одним лишь Core вы поддерживаете свой набор якорей вручную; вывод его из аутентифицированных доверенных списков ЕС требует NextPDF Enterprise.

Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов выходят за рамки.