Enterprise edition
eIDAS assurance levels
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Install
Section titled “Install”composer require nextpdf/enterpriseThe 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.
Conceptual overview
Section titled “Conceptual overview”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 evidence | Level | Reason codes |
|---|---|---|
| Service status is not granted | Low | service_not_granted |
Service type is not CA/QC | Low | service_not_qualified_ca |
Granted CA/QC with QCWithQSCD and without QCNoQSCD | High | ca_qc_with_qscd plus esig_or_eseal or qc_default |
Granted CA/QC otherwise | Substantial | ca_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.
Why it works this way
Section titled “Why it works this way”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.
API surface
Section titled “API surface”All symbols below are public API in nextpdf/enterprise 3.1.0.
LoaLevel
Section titled “LoaLevel”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.
LoaMapping
Section titled “LoaMapping”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.
LoaAssertion
Section titled “LoaAssertion”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.
CertPiiGuard
Section titled “CertPiiGuard”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.
Code sample — Quick start
Section titled “Code sample — Quick start”Parse a LoA URI and compare it against a required floor.
<?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: yesMeets high: noCode sample — Production
Section titled “Code sample — Production”Classify a trusted-list entry, gate on a required level, and emit a redacted audit record.
<?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"}Edge cases & gotchas
Section titled “Edge cases & gotchas”LoaLevel::from()throws\ValueErroron unknown URIs. UseLoaLevel::tryFrom()wherenullhandling is preferable.- Conflicting device evidence stays conservative. A service carrying both
QCWithQSCDandQCNoQSCDmaps toSubstantial, notHigh. - A granted
CA/QCservice with no qualifiers maps toSubstantialwith reasonca_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
statusStartingTimehistory; point-in-time windows belong to the validation layer. - Persist the enum’s backing URI, not the
rank()integer. Ranks exist only for comparison. CertPiiGuardcollapses 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.
Security notes
Section titled “Security notes”- Privacy by default. The guard constructor defaults to
disclosePii: false. Constructnew 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
LoaAssertionis 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.
Conformance
Section titled “Conformance”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.
FIPS-mode behavior
Section titled “FIPS-mode behavior”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.
Behavior contract
Section titled “Behavior contract”LoaMapping::loaForService()is total and deterministic. EveryTspServiceyields aLoaAssertion; 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 viarank()andmeetsOrExceeds(). CertPiiGuarddefaults to redaction and fails closed on tokenization doubt. WithdisclosePii: true, every guard returns its input verbatim.- Guard output is byte-stable for identical input.
Core fallback
Section titled “Core fallback”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.
Publication boundary
Section titled “Publication boundary”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.
See also
Section titled “See also”- Validation — structural compliance policies, including
eidasQualified()semantics and pass/fail criteria - Signature verification — the AdES/PAdES cryptographic verify-side whose reports the PII guard protects
- Security — Deep Reference — the Security module’s deep reference
- Qualified signatures, explained — Insider essay on qualification and assurance
- How a signature proves who signed — Insider essay on verify-side trust