Skip to content
getnextpdf.com

Enterprise edition

eIDAS assurance levels

NextPDF Enterprise turns EU trusted-list evidence into an explicit eIDAS Level of Assurance (LoA). The NextPDF\Enterprise\Security\Eidas\LoaMapping service classifies one trust-service entry as Low, Substantial, or High. It returns a LoaAssertion carrying the level plus machine-readable reason codes. Your workflow can gate on assurance — “require High” — and archive the reasons as audit evidence. A companion guard, CertPiiGuard, redacts signer identity fields before audit records leave the process.

Two boundaries frame this capability. First, qualification always belongs to the trust service provider (TSP) under member-state supervision. NextPDF asserts a classification over published evidence. Second, this page covers LoA assertion and mapping only. The structural PAdES policy eidasQualified(), including its pass/fail criteria, is documented in Validation.

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

The nextpdf/premium metapackage also resolves the Enterprise package. Activation uses your Enterprise license envelope; see Licensing and activation. The eIDAS classes need no PHP extension beyond the engine baseline. They autoload under NextPDF\Enterprise\Security\Eidas and NextPDF\Enterprise\Signature\Eidas.

Regulation (EU) No 910/2014 (eIDAS) defines three assurance levels: low, substantial, and high (Article 8(1)). Each level expresses a degree of confidence in a claimed identity. Level high adds controls whose purpose is to prevent — not merely reduce — identity misuse or alteration (Article 8(2)(c)). Article 8 defines these levels for electronic identification schemes. NextPDF reuses the same vocabulary to classify the trust-service evidence behind a signing certificate. That reuse is an engineering convention for policy gating and audit.

The LoaLevel enum models the three levels. Its backing values are the eIDAS LoA URIs rather than bare labels, so a persisted assertion carries the full identifier. rank() gives a total order (Low = 1, Substantial = 2, High = 3), and meetsOrExceeds() compares against a required floor.

LoaMapping computes a level from one trusted-list entry — a TspService from the Enterprise trusted-list subsystem (NextPDF\Enterprise\Security\Tsl). The mapping is deterministic:

Trusted-list evidenceLevelReason codes
Service status is not grantedLowservice_not_granted
Service type is not CA/QCLowservice_not_qualified_ca
Granted CA/QC with QCWithQSCD and without QCNoQSCDHighca_qc_with_qscd plus esig_or_eseal or qc_default
Granted CA/QC otherwiseSubstantialca_qc_no_qscd_or_unspecified

The QSCD (qualified signature creation device) qualifier is the pivot. Under Article 3(12), a qualified electronic signature requires both a qualified certificate and a qualified creation device. A trusted-list statement that certificates are managed on a QSCD is therefore the evidence that supports a High assertion. Without that statement, a granted qualified CA still supports Substantial, never High.

The result is a LoaAssertion: the level plus a list of reason codes. The reasons let an audit consumer re-derive the classification from the same evidence later. Downstream policy evaluators can record the assertion alongside a signature-validation outcome.

One more piece ships in this module: CertPiiGuard. When validation artefacts are serialized into JSON audit bundles, the signer certificate carries personal data — the Subject CN, email attributes, and the serialNumber attribute, which can encode a national identifier for natural persons. GDPR Article 5(1)(c) requires that processing be limited to what is necessary. The guard therefore redacts those fields by default, replacing values with [REDACTED] while preserving the structural envelope (organization, country, chain and status fields). Consumers can still verify whether a signature passed without learning who signed.

The load-bearing decision is separating the assurance assertion from the validation verdict. Signature validation, per ETSI EN 319 102-1, ends in a status indication — TOTAL-PASSED, TOTAL-FAILED, or INDETERMINATE — and that verdict belongs to the validation layer. LoA mapping is a distinct, replayable classification over trusted-list evidence, with reason codes instead of a bare label. This keeps NextPDF from ever presenting an assurance claim as a validation result, or a validation result as a qualification grant. It also makes the mapping conservative by construction: absent or ambiguous evidence lowers the level, never raises it.

Design background: Qualified signatures, explained.

All symbols below are public API in nextpdf/enterprise 3.1.0.

enum LoaLevel: string
{
case Low = 'http://eidas.europa.eu/LoA/low';
case Substantial = 'http://eidas.europa.eu/LoA/substantial';
case High = 'http://eidas.europa.eu/LoA/high';
public function rank(): int
public function meetsOrExceeds(self $required): bool
}

Throws or fails with: nothing from rank() or meetsOrExceeds(). Native enum construction via LoaLevel::from() throws \ValueError on an unrecognized URI; LoaLevel::tryFrom() returns null instead.

final class LoaMapping
{
public function loaForService(TspService $service): LoaAssertion
}

Throws or fails with: nothing. The method is total — every TspService input yields a LoaAssertion.

The input DTOs NextPDF\Enterprise\Security\Tsl\TspService and NextPDF\Enterprise\Security\Tsl\TspServiceQualifier are stable public DTOs (@api). The mapping consults TspService::STATUS_GRANTED, TspService::TYPE_CA_QC, and the qualifier constants TspServiceQualifier::QSCD_STATEMENT (QCWithQSCD), TspServiceQualifier::NO_QSCD (QCNoQSCD), TspServiceQualifier::FOR_ESIG, and TspServiceQualifier::FOR_ESEAL.

final readonly class LoaAssertion
{
/**
* @param list<non-empty-string> $reasons Machine-readable reason codes for the assertion.
*/
public function __construct(
public LoaLevel $level,
public array $reasons,
) {}
}

Throws or fails with: nothing. Immutable value object.

final readonly class CertPiiGuard
{
public function __construct(
private bool $disclosePii = false,
) {}
public function disclosesPii(): bool
public function guardSignerCommonName(string $signer): string
public function guardDistinguishedName(string $dn): string
public function guardTsaName(string $tsaName): string
public function guardRootIssuer(string $issuer): string
public function guardChainIssue(string $issue): string
}

Throws or fails with: nothing. Guards are pure string transforms. On a DN component that cannot be confidently tokenized, the guard fails closed and collapses the component to [REDACTED] rather than emitting a partially masked value.

Parse a LoA URI and compare it against a required floor.

loa-quick-start.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Eidas\LoaLevel;
// A LoA URI as persisted in an audit record or received from a peer system.
$uri = 'http://eidas.europa.eu/LoA/substantial';
try {
$level = LoaLevel::from($uri);
} catch (\ValueError $e) {
// Unknown URI: refuse to classify. Never guess an assurance level.
echo "Unrecognized LoA URI: {$uri}\n";
exit(1);
}
echo 'Level: ' . $level->name . ' (rank ' . $level->rank() . ")\n";
echo 'Meets substantial: ' . ($level->meetsOrExceeds(LoaLevel::Substantial) ? 'yes' : 'no') . "\n";
echo 'Meets high: ' . ($level->meetsOrExceeds(LoaLevel::High) ? 'yes' : 'no') . "\n";

Expected output:

Level: Substantial (rank 2)
Meets substantial: yes
Meets high: no

Classify a trusted-list entry, gate on a required level, and emit a redacted audit record.

loa-audit-gate.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Eidas\LoaLevel;
use NextPDF\Enterprise\Security\Eidas\LoaMapping;
use NextPDF\Enterprise\Security\Tsl\TspService;
use NextPDF\Enterprise\Security\Tsl\TspServiceQualifier;
use NextPDF\Enterprise\Signature\Eidas\CertPiiGuard;
// Normally produced by the Enterprise trusted-list subsystem from a
// member-state TSL; constructed inline here for a self-contained example.
$caPem = (string) file_get_contents(__DIR__ . '/example-qc-ca.pem');
$service = new TspService(
tspName: 'Example Qualified TSP',
serviceName: 'Example Qualified CA G2',
serviceTypeIdentifier: TspService::TYPE_CA_QC,
serviceStatus: TspService::STATUS_GRANTED,
statusStartingTime: '2024-01-01T00:00:00Z',
serviceCertificatePem: $caPem,
qualifiers: [
new TspServiceQualifier(qualifierUri: TspServiceQualifier::QSCD_STATEMENT),
new TspServiceQualifier(qualifierUri: TspServiceQualifier::FOR_ESIG),
],
additionalServiceInformation: [],
);
try {
// Required floor from deployment configuration; defaults to High.
$required = LoaLevel::from(getenv('LOA_REQUIRED') ?: LoaLevel::High->value);
} catch (\ValueError $e) {
echo "Invalid LOA_REQUIRED URI; refusing to continue.\n";
exit(1);
}
$mapping = new LoaMapping();
$assertion = $mapping->loaForService($service);
// Privacy by default: signer identity fields are redacted in audit output.
$guard = new CertPiiGuard();
$audit = [
'loa' => $assertion->level->value,
'reasons' => $assertion->reasons,
'meets_required' => $assertion->level->meetsOrExceeds($required),
'signer' => $guard->guardSignerCommonName('CN=Jane Example, O=Example Corp, C=DE'),
];
echo json_encode($audit, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";

Expected output:

{
"loa": "http://eidas.europa.eu/LoA/high",
"reasons": [
"ca_qc_with_qscd",
"esig_or_eseal"
],
"meets_required": true,
"signer": "CN=[REDACTED], O=Example Corp, C=DE"
}
  • LoaLevel::from() throws \ValueError on unknown URIs. Use LoaLevel::tryFrom() where null handling is preferable.
  • Conflicting device evidence stays conservative. A service carrying both QCWithQSCD and QCNoQSCD maps to Substantial, not High.
  • A granted CA/QC service with no qualifiers maps to Substantial with reason ca_qc_no_qscd_or_unspecified — qualified by default, device unproven.
  • Qualifier URIs outside the tracked set do not affect classification. Unknown or future qualifiers never raise the level.
  • The mapping reads the current service status only. It does not evaluate statusStartingTime history; point-in-time windows belong to the validation layer.
  • Persist the enum’s backing URI, not the rank() integer. Ranks exist only for comparison.
  • CertPiiGuard collapses a bare name without = entirely to [REDACTED]; empty strings pass through all guards unchanged.
  • Legacy OpenSSL slash-separated DNs are detected and masked structurally. A / inside an RFC 4514 value is treated as content, not a separator.
  • Non-PII DN attributes (O, OU, C, ST, L) are preserved, so jurisdiction reasoning survives redaction.
  • Privacy by default. The guard constructor defaults to disclosePii: false. Construct new CertPiiGuard(disclosePii: true) only where you hold a documented lawful basis for processing the signer identity. This implements GDPR Article 5(1)(c) data minimisation at the serialization boundary.
  • Fail-closed redaction. When a DN component cannot be confidently tokenized, the whole component collapses to [REDACTED]. A privacy control never fails open.
  • Deterministic output. Guards use pure string processing — no clocks, no randomness — so masked output is byte-stable for identical input. Stable output keeps audit diffs meaningful.
  • Redaction is not encryption. [REDACTED] removes the value from the record. If you need the identity recoverable, store it separately under its own lawful basis and access control.
  • Garbage in, garbage out. An LoaAssertion is only as trustworthy as the trusted-list evidence behind it. Acquire and signature-check trusted lists through the Enterprise trusted-list subsystem before feeding entries to the mapping.

NextPDF Enterprise implements behavior informed by Regulation (EU) No 910/2014 Article 8 (assurance levels) and Article 3(12) (elements of a qualified electronic signature), and by the ETSI trusted-list qualifier vocabulary. An LoaAssertion is a software classification of published evidence.

Regulation (EU) 2024/1183 (eIDAS 2) continues to reference the Article 8 levels and requires European Digital Identity Wallets to be provided at assurance level high. This page cites that as regulatory context.

Whether a specific signature satisfies a structural eIDAS-oriented policy is a separate question, answered by the validation module; see Validation.

The eIDAS LoA classes perform no cryptographic operations — no hashing, no signature verification, no randomness. The Enterprise FIPS-mode policy gates cryptographic choices, so it has nothing to gate in this module. Enabling FIPS mode does not change LoA mapping or PII-guard behavior. Cryptographic verification of signatures and trusted lists is governed by the verification and security modules, where FIPS-mode policy applies.

  • LoaMapping::loaForService() is total and deterministic. Every TspService yields a LoaAssertion; the method never throws and consults no clock, network, or global state.
  • Classification is conservative. Missing, unknown, or conflicting evidence lowers the level; nothing raises it except explicit granted-CA/QC-with-QSCD evidence.
  • Reason codes are machine-readable and stable: service_not_granted, service_not_qualified_ca, ca_qc_with_qscd, esig_or_eseal, qc_default, ca_qc_no_qscd_or_unspecified.
  • Level order is fixed: Low < Substantial < High, exposed via rank() and meetsOrExceeds().
  • CertPiiGuard defaults to redaction and fails closed on tokenization doubt. With disclosePii: true, every guard returns its input verbatim.
  • Guard output is byte-stable for identical input.

NextPDF Core verifies PDF signatures cryptographically and fails closed on broken evidence. Core has no EU trusted-list model, no LoaLevel vocabulary, no LoA mapping, and no eIDAS-layer PII guard for audit serialization. On Core alone, you must derive assurance classifications yourself from trust data you maintain, and apply your own redaction before audit records leave the process.

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.