Skip to content
getnextpdf.com

Enterprise edition

ASiC trust binding

An ASiC container bundles signed files with the signatures that protect them. The hard question is not “does the signature compute?” but “who stands behind the signer?”. NextPDF\Enterprise\Security\Asic\AsicTrustBinder answers exactly that question. You hand it the signing certificate from the container signature, a trusted list, and a validation time. It answers with an AsicTrustBindingResult: a trusted/untrusted verdict, the anchor bundle version it decided against, and machine-readable reasons. Every rejection names its cause, so audit evidence writes itself.

One boundary is deliberate and worth stating up front. This API does not parse ASiC containers. Your tooling opens the container and extracts the signing certificate; NextPDF owns the trust decision.

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.

Terminal window
composer require nextpdf/enterprise

Activation requires your Enterprise license envelope. See Install and authenticate. The classes on this page live under NextPDF\Enterprise\Security\Asic and NextPDF\Enterprise\Security\Tsl.

ASiC (Associated Signature Containers, ETSI EN 319 162-1) packages data files and signatures in one archive. A baseline ASiC container embeds only CAdES or XAdES baseline signatures. A CAdES baseline signature carries its signing certificate inside SignedData.certificates, so a verifier is expected to extract it when the signature is well-formed and supported by the container tooling from the container’s signature. That extracted certificate is this API’s input.

The trust source is an ETSI TS 119 612 trusted list (TSL): a signed XML document that enumerates trust service providers and their service certificates. NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider converts a parsed TslDocument into an anchor bundle. Only services that are both in granted status and of the CA/QC service type seed the anchor set. The bundle carries a version string derived from the TSL sequence number and territory, plus a SHA-256 integrity digest.

Two fail-closed gates run before any anchor comparison:

  1. TSL freshness. A trusted list whose NextUpdate instant has passed must be discarded as expired. AsicTrustBinder::verify() asserts freshness at the supplied validation time before deriving a single anchor. A stale list, or a NextUpdate value without an explicit UTC designator, throws TslParseException.
  2. Signer validity period. RFC 5280 path validation requires the certificate validity period to include the validation time. A cryptographically intact signature whose certificate was expired, or not yet valid, at that time is rejected with a precise reason code.

Only then does the binder test the signing certificate against each anchor. A match yields trusted: true with reason anchor_signature_match. No match yields trusted: false with reason no_anchor_chain.

The load-bearing design decision is a strict separation between container mechanics and the trust decision, with the trust decision forced to be explicit about time. Container formats vary (ASiC-S, ASiC-E, CAdES or XAdES payloads), but the trust question is one invariant kernel: does this certificate chain to an anchor from a fresh trusted list at a stated instant? Keeping that kernel free of ZIP and XML parsing keeps it small enough to test exhaustively and to fail closed at every gate. The same reasoning forbids a silent now default: validation time changes the verdict, so the caller must own it. Freshness is asserted inside the anchor-derivation path itself, not in an optional collaborator, so no producer path can skip it.

Design background: How a digital signature proves who signed.

Construction takes the anchor provider that turns trusted lists into anchor bundles.

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

The primary entry point verifies a signer certificate against a trusted list:

public function verify(
string $signerCertPem,
TslDocument $tsl,
DateTimeInterface $validationTime,
): AsicTrustBindingResult
  • $signerCertPem — non-empty PEM string: the signing certificate from the ASiC signature.
  • $tsl — the parsed, authenticated trusted list.
  • $validationTime — the instant the signer certificate’s validity period must include. There is no default.

Throws or fails with: NextPDF\Enterprise\Security\Tsl\TslParseException when the TSL is stale (NextUpdate passed), when NextUpdate is not a canonical UTC value, or when the list contains no active CA/QC services. Untrusted signers do not throw; they return a result with trusted: false and a reason code.

For batch workloads, verify against a pre-built bundle:

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

Throws or fails with: no exceptions of its own; every outcome is an AsicTrustBindingResult. Obtain the bundle from TslTrustAnchorProvider::buildBundle() — do not construct it by hand.

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

Throws or fails with: TslParseException if the TSL is stale, its NextUpdate is not a canonical UTC value, or it has no active CA/QC services.

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

$reasons is a list<non-empty-string> of machine-readable codes. $anchorBundleVersion records the anchor set used, in the form tsl-<territory>-seq<N> (for example tsl-eu-seq42).

Reason codeMeaning
anchor_signature_matchThe signer certificate verifies against a TSL-derived anchor. Trusted.
no_anchor_chainNo anchor in the bundle verifies the signer certificate. Untrusted.
signer_cert_expiredThe validation time falls after the certificate’s notAfter. Untrusted.
signer_cert_not_yet_validThe validation time falls before the certificate’s notBefore. Untrusted.
cannot_parse_signer_certThe supplied PEM does not parse as an X.509 certificate. Untrusted.

Your container tooling has already extracted the signing certificate. Bind it to a member-state trusted list you have fetched and authenticated (see Trusted lists).

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

Expected output for a signer issued by a listed CA/QC service:

TRUSTED
Anchors: tsl-eu-seq42
Reasons: anchor_signature_match

Derive the anchor bundle once per trusted list, then verify many container signers against it. One stale or unusable TSL fails the whole batch closed; individual signer problems surface per container.

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

Expected output when one signer certificate has expired:

invoice-2026-06.asice => trusted (anchor_signature_match; anchors tsl-eu-seq42)
tender-2019.asice => rejected (signer_cert_expired; anchors tsl-eu-seq42)
  • Validation time is mandatory and decisive. There is no silent now default. A signature that verified in 2019 reports signer_cert_expired when you validate at a 2026 instant past notAfter. For historical material, pass the time your evidence supports (for example a proof-of-existence time), not the wall clock.
  • A stale TSL throws; it is not an “untrusted” verdict. TslParseException from verify() or buildBundle() means the trust source is unusable. Treat it as an operational failure: refresh the list, do not record it as a signer rejection.
  • Anchors are tested as direct issuers. Each anchor is tried as the certificate that signed the signer certificate. EU member-state TSLs list the issuing CA/QC service certificates, so end-entity qualified certificates typically match directly. A signer issued by an intermediate CA that is not itself a listed active CA/QC service yields no_anchor_chain.
  • Anchor derivation filters hard. Services that are withdrawn, or of any type other than CA/QC, never become anchors. A list whose active CA/QC set is empty throws rather than producing an empty bundle.
  • NextUpdate must be canonical UTC. A value without an explicit Z or numeric offset designator is rejected fail-closed, never reinterpreted in the server’s local timezone.
  • Malformed input degrades precisely. A PEM that does not parse returns cannot_parse_signer_cert; a not-yet-valid certificate is distinguished from an expired one.
  • Record anchorBundleVersion. It names the exact anchor set (tsl-<territory>-seq<N>) behind each verdict, which is what an auditor will ask for.
  • Fail-closed by construction. Freshness is asserted before any anchor is derived. The signer validity gate runs before any anchor comparison. Unusable trust material throws; questionable signers are rejected with reasons. No path degrades to a silent pass.
  • Trust binding is one layer, not the whole validation. This API does not verify the CAdES signature value over the container content, does not check revocation (no CRL or OCSP lookup), and does not authenticate the TSL document itself. Authenticate the list through the trusted-list pipeline first (see Trusted lists), verify the signature cryptographically with your signature tooling, and add revocation checking per your policy.
  • Choose the validation time deliberately. The verdict is a function of the time you pass. Derive it from trustworthy evidence (a qualified timestamp, an archival record), not from an attacker-influenceable clock.
  • Evidence outputs are deterministic. trusted, anchorBundleVersion, and reasons are stable, machine-readable values suitable for signed audit logs.

AsicTrustBinder supports workflows aligned with ETSI EN 319 162-1 (ASiC baseline containers), ETSI EN 319 122-1 (CAdES baseline signatures), and ETSI TS 119 612 (trusted lists), and applies the RFC 5280 validity-period gate at the supplied validation time.

NextPDF implements the checks this page describes. Whether a complete validation process meets a given legal or procurement requirement is a determination for your assessors.

The trust binding performs X.509 certificate-signature checks in-process; it is not routed through the Enterprise FIPS-mode runtime guard, and enabling FIPS mode does not change its behavior. Deployments with FIPS obligations should scope this API accordingly and see FIPS 140-2/3 cryptographic policy.

  • verify() derives anchors only from a TSL that is fresh at the supplied validation time; a stale or malformed list throws TslParseException before any anchor exists.
  • Anchors derive exclusively from TSL services in granted status with the CA/QC service type; an empty active set throws.
  • The signer certificate’s validity period must include the validation time; violations return signer_cert_expired or signer_cert_not_yet_valid.
  • Every outcome is an AsicTrustBindingResult carrying trusted, anchorBundleVersion, and at least one reason code; there is no reasonless verdict.
  • Untrusted signers are returned, never thrown; unusable trust material is thrown, never returned as a verdict.
  • Container parsing never occurs inside this API; inputs are the extracted PEM, the trusted list, and the validation time.

NextPDF Core validates PDF (CMS/PAdES) signatures against trust anchors you pin explicitly through its CaTrustAnchorBundle contract — see Core security. Core has no trusted-list (TSL) ingestion and no ASiC-specific trust binding. With Core alone, you can maintain your own anchor set for PDF signature validation; deriving anchors from an ETSI TS 119 612 trusted list and binding ASiC container signers to them requires NextPDF Enterprise.

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.