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

Enterprise редакция

Привязка доверия ASiC

Контейнер ASiC объединяет подписанные файлы с подписями, которые их защищают. Сложный вопрос не «вычисляется ли подпись?», а «кто стоит за подписантом?». NextPDF\Enterprise\Security\Asic\AsicTrustBinder отвечает именно на этот вопрос. Вы передаёте ему сертификат подписи из подписи контейнера, доверенный список и время валидации. Он отвечает объектом AsicTrustBindingResult: вердиктом доверенный/недоверенный, версией набора якорей, против которого было принято решение, и машиночитаемыми причинами. Каждый отказ называет свою причину, поэтому доказательства для аудита формируются сами собой.

Одна граница задана намеренно, и о ней стоит сказать заранее. Этот API не разбирает контейнеры ASiC. Ваши инструменты открывают контейнер и извлекают сертификат подписи; за решение о доверии отвечает NextPDF.

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

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

Активация требует вашего лицензионного конверта Enterprise. См. Установка и аутентификация. Классы на этой странице находятся в пространствах имён NextPDF\Enterprise\Security\Asic и NextPDF\Enterprise\Security\Tsl.

ASiC (Associated Signature Containers, ETSI EN 319 162-1) упаковывает файлы данных и подписи в один архив. Базовый контейнер ASiC встраивает только базовые подписи CAdES или XAdES. Базовая подпись CAdES несёт свой сертификат подписи внутри SignedData.certificates, поэтому предполагается, что верификатор извлечёт его из подписи контейнера, когда подпись корректно сформирована и поддерживается инструментами для контейнеров. Этот извлечённый сертификат и есть входные данные для данного API.

Источник доверия — доверенный список (TSL) ETSI TS 119 612: подписанный XML-документ, перечисляющий поставщиков услуг доверия и сертификаты их услуг. NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider преобразует разобранный TslDocument в набор якорей. Только услуги, находящиеся одновременно в статусе granted и относящиеся к типу услуги CA/QC, наполняют набор якорей. Набор несёт строку версии, полученную из порядкового номера TSL и территории, а также контрольную сумму целостности SHA-256.

Перед любым сравнением якорей выполняются два fail-closed контроля:

  1. Свежесть TSL. Доверенный список, у которого момент NextUpdate уже прошёл, должен быть отброшен как просроченный. AsicTrustBinder::verify() проверяет свежесть на переданное время валидации до вывода хотя бы одного якоря. Устаревший список или значение NextUpdate без явного обозначения UTC вызывает TslParseException.
  2. Срок действия подписанта. Валидация пути по RFC 5280 требует, чтобы срок действия сертификата включал время валидации. Криптографически целостная подпись, чей сертификат на тот момент был просрочен или ещё не действовал, отклоняется с точным кодом причины.

Только затем связыватель проверяет сертификат подписи против каждого якоря. Совпадение даёт trusted: true с причиной anchor_signature_match. Отсутствие совпадения даёт trusted: false с причиной no_anchor_chain.

Несущее проектное решение — строгое разделение механики контейнера и решения о доверии, при котором решение о доверии вынуждено быть явным в отношении времени. Форматы контейнеров различаются (ASiC-S, ASiC-E, полезные данные CAdES или XAdES), но вопрос доверия — это одно инвариантное ядро: связывается ли этот сертификат с якорем из свежего доверенного списка в заявленный момент? Освобождение этого ядра от разбора ZIP и XML сохраняет его достаточно малым, чтобы тестировать исчерпывающе и закрываться на отказ на каждом контроле. Та же логика запрещает молчаливый вариант по умолчанию now: время валидации меняет вердикт, поэтому вызывающая сторона должна владеть им. Свежесть проверяется внутри самого пути вывода якорей, а не в необязательном коллабораторе, поэтому ни один порождающий путь не может её пропустить.

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

Конструктор принимает поставщик якорей, превращающий доверенные списки в наборы якорей.

public function __construct(
private readonly TslTrustAnchorProvider $anchorProvider,
) {}

Основная точка входа проверяет сертификат подписанта против доверенного списка:

public function verify(
string $signerCertPem,
TslDocument $tsl,
DateTimeInterface $validationTime,
): AsicTrustBindingResult
  • $signerCertPem — непустая строка PEM: сертификат подписи из подписи ASiC.
  • $tsl — разобранный, аутентифицированный доверенный список.
  • $validationTime — момент, который должен включать срок действия сертификата подписанта. Значения по умолчанию нет.

Бросает или завершается с ошибкой: NextPDF\Enterprise\Security\Tsl\TslParseException, когда TSL устарел (NextUpdate прошёл), когда NextUpdate не является каноническим значением UTC или когда список не содержит активных услуг CA/QC. Недоверенные подписанты не вызывают исключений; они возвращают результат с trusted: false и кодом причины.

Для пакетных нагрузок проверяйте против заранее собранного набора:

public function verifyAgainstBundle(
string $signerCertPem,
EnterpriseCaTrustAnchorBundle $bundle,
DateTimeInterface $validationTime,
): AsicTrustBindingResult

Бросает или завершается с ошибкой: собственных исключений нет; каждый исход — это AsicTrustBindingResult. Получайте набор из TslTrustAnchorProvider::buildBundle() — не конструируйте его вручную.

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

Бросает или завершается с ошибкой: TslParseException, если TSL устарел, его NextUpdate не является каноническим значением UTC или в нём нет активных услуг CA/QC.

public function __construct(
public bool $trusted,
public string $anchorBundleVersion,
public array $reasons,
) {}

$reasons — это list<non-empty-string> машиночитаемых кодов. $anchorBundleVersion фиксирует использованный набор якорей в форме tsl-<territory>-seq<N> (например tsl-eu-seq42).

Код причиныЗначение
anchor_signature_matchСертификат подписанта проверяется против якоря, полученного из TSL. Доверенный.
no_anchor_chainНи один якорь в наборе не проверяет сертификат подписанта. Недоверенный.
signer_cert_expiredВремя валидации приходится после notAfter сертификата. Недоверенный.
signer_cert_not_yet_validВремя валидации приходится до notBefore сертификата. Недоверенный.
cannot_parse_signer_certПереданный PEM не разбирается как сертификат X.509. Недоверенный.

Ваши инструменты для контейнеров уже извлекли сертификат подписи. Привяжите его к доверенному списку государства-члена, который вы получили и аутентифицировали (см. Доверенные списки).

asic-trust-binding-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Asic\AsicTrustBinder;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
// Extracted by YOUR tooling from META-INF/signature.p7s or signatures.xml.
$signerCertPem = (string) file_get_contents(__DIR__ . '/asic-signer.pem');
// A trusted list you have already fetched and authenticated.
$tslXml = (string) file_get_contents(__DIR__ . '/member-state-tsl.xml');
$binder = new AsicTrustBinder(new TslTrustAnchorProvider());
try {
$tsl = (new TslXmlParser())->parse($tslXml);
$result = $binder->verify(
signerCertPem: $signerCertPem,
tsl: $tsl,
validationTime: new DateTimeImmutable('2026-07-03T12:00:00Z'),
);
} catch (TslParseException $e) {
// Fail closed: stale TSL, malformed NextUpdate, or no active CA/QC services.
fwrite(STDERR, 'Trusted list rejected: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
echo $result->trusted ? "TRUSTED\n" : "NOT TRUSTED\n";
echo 'Anchors: ' . $result->anchorBundleVersion . "\n";
echo 'Reasons: ' . implode(', ', $result->reasons) . "\n";

Ожидаемый вывод для подписанта, выпущенного перечисленной услугой CA/QC:

TRUSTED
Anchors: tsl-eu-seq42
Reasons: anchor_signature_match

Выведите набор якорей один раз на доверенный список, затем проверяйте против него множество подписантов контейнеров. Один устаревший или непригодный TSL закрывает на отказ всю пачку; проблемы отдельных подписантов проявляются по каждому контейнеру.

asic-trust-binding-batch.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Asic\AsicTrustBinder;
use NextPDF\Enterprise\Security\Asic\AsicTrustBindingResult;
use NextPDF\Enterprise\Security\Tsl\TslDocument;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
/**
* @param array<string, non-empty-string> $signerPemsByContainer PEM per container path.
* @return array<string, AsicTrustBindingResult>
* @throws TslParseException When no anchor set can be derived from the TSL.
*/
function bindBatch(
TslDocument $tsl,
array $signerPemsByContainer,
DateTimeImmutable $validationTime,
): array {
$provider = new TslTrustAnchorProvider();
// Derive the anchor set ONCE; a throw here means the trusted list itself
// is unusable at this validation time.
$bundle = $provider->buildBundle($tsl, $validationTime);
$binder = new AsicTrustBinder($provider);
$results = [];
foreach ($signerPemsByContainer as $container => $signerPem) {
$results[$container] = $binder->verifyAgainstBundle(
signerCertPem: $signerPem,
bundle: $bundle,
validationTime: $validationTime,
);
}
return $results;
}
$tsl = (new TslXmlParser())->parse(
(string) file_get_contents(__DIR__ . '/member-state-tsl.xml'),
);
$signerPems = [
'invoice-2026-06.asice' => (string) file_get_contents(__DIR__ . '/signer-a.pem'),
'tender-2019.asice' => (string) file_get_contents(__DIR__ . '/signer-b.pem'),
];
try {
$results = bindBatch(
tsl: $tsl,
signerPemsByContainer: $signerPems,
validationTime: new DateTimeImmutable('now', new DateTimeZone('UTC')),
);
} catch (TslParseException $e) {
// Fail closed for the WHOLE batch: no trustworthy anchor set exists.
fwrite(STDERR, 'Anchor derivation failed: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
foreach ($results as $container => $result) {
printf(
"%s => %s (%s; anchors %s)\n",
$container,
$result->trusted ? 'trusted' : 'rejected',
implode(',', $result->reasons),
$result->anchorBundleVersion,
);
}

Ожидаемый вывод, когда один сертификат подписанта просрочен:

invoice-2026-06.asice => trusted (anchor_signature_match; anchors tsl-eu-seq42)
tender-2019.asice => rejected (signer_cert_expired; anchors tsl-eu-seq42)
  • Время валидации обязательно и решающе. Молчаливого варианта по умолчанию now нет. Подпись, прошедшая проверку в 2019 году, сообщает signer_cert_expired, когда вы проводите валидацию в момент 2026 года, прошедший notAfter. Для исторических материалов передавайте время, которое подтверждают ваши доказательства (например, время подтверждения существования), а не стенные часы.
  • Устаревший TSL бросает исключение; это не вердикт «недоверенный». TslParseException из verify() или buildBundle() означает, что источник доверия непригоден. Трактуйте это как эксплуатационный сбой: обновите список, не записывайте это как отказ подписанту.
  • Якоря проверяются как прямые издатели. Каждый якорь пробуется как сертификат, подписавший сертификат подписанта. TSL государств-членов ЕС перечисляют выпускающие сертификаты услуг CA/QC, поэтому конечные квалифицированные сертификаты обычно совпадают напрямую. Подписант, выпущенный промежуточным CA, который сам не является перечисленной активной услугой CA/QC, даёт no_anchor_chain.
  • Вывод якорей фильтрует строго. Услуги, которые отозваны или относятся к любому типу, отличному от CA/QC, никогда не становятся якорями. Список, у которого набор активных CA/QC пуст, бросает исключение, а не создаёт пустой набор.
  • NextUpdate должен быть каноническим UTC. Значение без явного Z или числового обозначения смещения отклоняется fail-closed, а не переинтерпретируется в локальном часовом поясе сервера.
  • Некорректный ввод деградирует точно. PEM, который не разбирается, возвращает cannot_parse_signer_cert; ещё не действующий сертификат отличается от просроченного.
  • Записывайте anchorBundleVersion. Он называет точный набор якорей (tsl-<territory>-seq<N>) за каждым вердиктом — именно это спросит аудитор.
  • Закрытие на отказ по построению. Свежесть проверяется до вывода любого якоря. Контроль срока действия подписанта выполняется до любого сравнения якорей. Непригодный материал доверия бросает исключение; сомнительные подписанты отклоняются с причинами. Ни один путь не деградирует до молчаливого пропуска.
  • Привязка доверия — это один слой, а не вся валидация. Этот API не проверяет значение подписи CAdES над содержимым контейнера, не проверяет отзыв (без запросов CRL или OCSP) и не аутентифицирует сам документ TSL. Сначала аутентифицируйте список через конвейер доверенных списков (см. Доверенные списки), проверьте подпись криптографически своими инструментами подписи и добавьте проверку отзыва согласно вашей политике.
  • Выбирайте время валидации осознанно. Вердикт — это функция передаваемого вами времени. Выводите его из надёжных доказательств (квалифицированной метки времени, архивной записи), а не из часов, на которые может повлиять злоумышленник.
  • Выходные доказательства детерминированы. trusted, anchorBundleVersion и reasons — это стабильные, машиночитаемые значения, пригодные для подписанных журналов аудита.

AsicTrustBinder поддерживает рабочие процессы, согласованные с ETSI EN 319 162-1 (базовые контейнеры ASiC), ETSI EN 319 122-1 (базовые подписи CAdES) и ETSI TS 119 612 (доверенные списки), и применяет контроль срока действия по RFC 5280 на переданное время валидации.

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

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

  • verify() выводит якоря только из TSL, который свеж на переданное время валидации; устаревший или некорректный список бросает TslParseException до того, как появится хоть один якорь.
  • Якоря выводятся исключительно из услуг TSL в статусе granted с типом услуги CA/QC; пустой активный набор бросает исключение.
  • Срок действия сертификата подписанта должен включать время валидации; нарушения возвращают signer_cert_expired или signer_cert_not_yet_valid.
  • Каждый исход — это AsicTrustBindingResult, несущий trusted, anchorBundleVersion и хотя бы один код причины; вердикта без причины не бывает.
  • Недоверенные подписанты возвращаются, а не бросаются; непригодный материал доверия бросается, а не возвращается как вердикт.
  • Разбор контейнера никогда не происходит внутри этого API; входными данными являются извлечённый PEM, доверенный список и время валидации.

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

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