Configure trust anchor verification
This guide shows you how to configure the Truvity EUDIW Connector to verify credential issuers against the EU List of Trusted Lists (LoTL) hierarchy. In LoTL-enabled mode, the connector dynamically resolves whether each credential's issuer is recognized by the EU trust framework, determines the credential category, and reports enriched trust metadata on the callback payload. Static-only mode is available as a fallback for environments without LoTL infrastructure.
For conceptual background on how LoTL trust verification works, see LoTL trust verification.
- A running connector instance, or one you are preparing to deploy
- Access to the connector's configuration (environment variables or configuration file)
Overview
This guide covers:
- Configuring LoTL-driven trust resolution (production path)
- Setting up trust verification policy per credential category
- Interpreting trust verification results
- Configuring static-only mode (fallback)
- Testing and troubleshooting
Time to implement: 1-2 hours.
LoTL-enabled mode
Set the trust resolution mode to lotl and configure at least one trust list source.
Configuration reference
| Variable | Type | Default | Description |
|---|---|---|---|
X509_TRUST_RESOLUTION_MODE | string | static | Trust resolution mode. Set to lotl for LoTL-enabled mode, static for static-only mode. |
X509_TRUST_LIST_SOURCES | structured list | (none) | Trust list sources to fetch and verify. See format below. |
X509_TRUST_STORE_REFRESH_INTERVAL | duration | 6h | How often the connector refreshes the trust store from configured sources. |
X509_STALENESS_WINDOW | duration | 24h | Maximum age of cached trust data the connector honors after a fresh fetch fails before failing closed. |
X509_LOTL_FETCH_TIMEOUT | duration | 30s | HTTP timeout for trust list fetch requests. |
X509_MAX_RESPONSE_BYTES | int64 | 10485760 (10 MB) | Maximum response body size for trust list fetches. Responses exceeding this limit are treated as fetch failures. |
Trust list source configuration
The X509_TRUST_LIST_SOURCES variable configures which trust lists the connector fetches and verifies. Each source specifies a trust list endpoint, its category, and verification parameters.
For multi-source deployments, use a configuration file with the following structure:
# trust_list_sources structured configuration
# Each entry defines a trust list source the connector fetches and verifies.
trust_list_sources:
- category: LoTL
endpoint: "https://ec.europa.eu/tools/lotl/eu-lotl.xml"
signing_certificates:
- "MIICxzCCAa+gAwIBAgI..." # Base64-encoded DER of the LoTL signing certificate
refresh_interval: 6h
cache_ttl: 24h
fallback_mode: cached_trust # fail_closed | cached_trust | cached_trust_with_warning
max_response_bytes: 10485760
- category: PID_PROVIDER_LOTE
endpoint: "https://trust.pid.bund.de/lote/pid-providers.json"
signing_certificates:
- "MIICxzCCAa+gAwIBAgI..." # Base64-encoded DER of the German PID LoTE signing cert
refresh_interval: 6h
cache_ttl: 24h
fallback_mode: cached_trust
max_response_bytes: 10485760
- category: NATIONAL_TL
endpoint: "https://trust.tsl.bnetza.de/TSL-DE.xml"
signing_certificates:
- "MIICxzCCAa+gAwIBAgI..." # Base64-encoded DER of the German TSL signing cert
refresh_interval: 6h
cache_ttl: 24h
fallback_mode: cached_trust
max_response_bytes: 10485760
For single-source deployments, use the flat environment variable convenience fields instead of the structured list:
export X509_TRUST_LIST_SOURCE_ENDPOINT="https://ec.europa.eu/tools/lotl/eu-lotl.xml"
export X509_TRUST_LIST_SOURCE_CATEGORY=LoTL
export X509_TRUST_LIST_SOURCE_SIGNING_CERTIFICATE="MIICxzCCAa+gAwIBAgI..."
When X509_TRUST_LIST_SOURCE_ENDPOINT is set and no structured trust_list_sources are configured, the connector assembles a single source from these flat fields. The category defaults to LoTL if omitted.
Field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
category | string | Yes | Trust list category (see valid values below) |
endpoint | string (URL) | Yes | HTTPS URL of the trust list to fetch |
signing_certificates | array of strings | Yes | Base64-encoded DER certificates used to verify the list's signature |
refresh_interval | duration | No | How often to refresh this source (overrides global X509_TRUST_STORE_REFRESH_INTERVAL) |
cache_ttl | duration | No | Staleness window for this source (overrides global X509_STALENESS_WINDOW) |
fallback_mode | string | No | Behavior when fetch fails and cache exceeds staleness: fail_closed, cached_trust, cached_trust_with_warning. Default: fail_closed. |
max_response_bytes | int64 | No | Maximum response size for this source (overrides global X509_MAX_RESPONSE_BYTES) |
Valid category values:
| Category | Description | Verification path |
|---|---|---|
LoTL | The EU List of Trusted Lists | Root of the hierarchy—points to Member State lists |
PID_PROVIDER_LOTE | PID Provider LoTE | PID credential verification |
NATIONAL_TL | Member State national Trusted List | QEAA, PuB-EAA, and voluntary non-qualified EAA verification |
WALLET_PROVIDER_LOTE | Wallet Provider LoTE | WIA/WUA verification |
RULEBOOK_TL | Rulebook-governed list | Accepted for configuration but not consulted during trust resolution |
Example configuration
export X509_TRUST_RESOLUTION_MODE=lotl
export X509_TRUST_STORE_REFRESH_INTERVAL=6h
export X509_STALENESS_WINDOW=24h
export X509_LOTL_FETCH_TIMEOUT=30s
export X509_MAX_RESPONSE_BYTES=10485760
Restart the connector after setting these variables.
Trust verification policy
The trust verification policy controls how trust outcomes affect the callback per credential category. Configure a policy for each credential category independently.
Configuration
| Variable | Type | Default | Description |
|---|---|---|---|
X509_TRUST_POLICY_SET | string | default-evaluate | Named policy set baseline. The default set evaluates every category without enforcing. |
X509_TRUST_POLICY_BY_CATEGORY | map | (none) | Per-category policy overrides. Each key is a credential category (PID, QEAA, PUB_EAA, NON_QUALIFIED_EAA), each value is a policy (enforce, evaluate, disabled). |
Example—enforce trust for PID credentials:
export X509_TRUST_POLICY_BY_CATEGORY='{"PID":"enforce"}'
Setting a category to enforce requires X509_TRUST_RESOLUTION_MODE=lotl and a configured trust list source that can resolve that category. The connector rejects the configuration at startup if these requirements are not met.
Policy values
| Policy | Effect on callback | When to use |
|---|---|---|
evaluate (default) | Trust outcomes reported as fields on FULFILLED payloads. Flow completes regardless of trust verdict. | Production default—your backend applies appropriate legal weight. |
enforce | Trust failure for this category produces VERIFICATION_FAILED callback status. Credential is never delivered. | High-assurance deployments where untrusted credentials must be blocked before reaching your backend. |
disabled | Trust fields omitted entirely for this category. | Testing or when trust verification is not relevant for certain credential types. |
Policy × trust outcome → callback behavior
| Trust outcome | evaluate | enforce | disabled |
|---|---|---|---|
| Issuer trusted | FULFILLED with isTrusted: true + enriched fields | FULFILLED with isTrusted: true + enriched fields | FULFILLED without trust fields |
| Issuer not trusted | FULFILLED with isTrusted: false + trustFailureReason | VERIFICATION_FAILED | FULFILLED without trust fields |
| Trust data stale | FULFILLED with staleTrustData present | FULFILLED with staleTrustData present (stale ≠ untrusted) | FULFILLED without trust fields |
Interpret trust verification results
When the connector runs in LoTL-enabled mode with trust verification policy evaluate (the default), trust outcomes arrive as fields on FULFILLED callback payloads. This section explains what each trustFailureReason value means so you can make informed downstream decisions.
| Failure reason | Meaning | Conditions |
|---|---|---|
issuer_not_found | The issuer's certificate does not match any entry on any trust list in the LoTL hierarchy. | The issuer may not be registered, may use a certificate not yet published, or may operate outside the EU trust framework. |
issuer_withdrawn | The issuer was found on a trust list but the entry's status is "withdrawn." | The member state's supervisory body has revoked the issuer's authorization. |
issuer_suspended | The issuer was found on a trust list but the entry's status is "suspended." | The member state's supervisory body has temporarily suspended the issuer's authorization. |
trust_list_unavailable | The trust list that should contain the issuer's entry could not be fetched and no valid cached copy is available. | Network failure, endpoint down, or staleness window exceeded for all applicable trust list sources. |
This section does not prescribe specific customer actions—each failure reason represents a different risk profile, and the appropriate response depends on your business requirements and risk tolerance.
What to expect in the callback
When trust anchor verification is enabled in LoTL mode, the FULFILLED callback payload includes enriched trust fields:
{
"status": "FULFILLED",
"state": "abc123",
"credentials": {
"pid_identity": [
{
"issuer": "https://pid-provider.example.de",
"claims": {
"given_name": "Erika",
"family_name": "Mustermann",
"birthdate": "1964-08-12"
},
"signatureIsValid": true,
"kbSignatureIsValid": true,
"kbKeyId": "KrXxHnYsf-Inp0hW1M6r...",
"validFrom": "2026-01-15T10:00:00Z",
"validUntil": "2027-01-15T10:00:00Z",
"supportRevocation": true,
"isRevoked": false,
"supportTrustAnchor": true,
"isTrusted": true,
"isCertificateRevoked": false,
"credentialCategory": "PID",
"trustSource": "lotl",
"supervisoryMemberState": "DE",
"qualifiedStatus": false,
"trustDataTimestamp": "2026-05-15T08:30:00Z"
}
]
}
}
Field summary:
credentialCategory—The credential's legal-effect classification (PID, QEAA, PUB_EAA, NON_QUALIFIED_EAA)trustSource—Where trust was resolved (lotl,static,untrusted)supervisoryMemberState—ISO 3166-1 alpha-2 code of the supervisory Member State (present whentrustSource: "lotl")qualifiedStatus—Whether the matched trust list entry is a qualified trust service (present whentrustSource: "lotl")trustDataTimestamp—ISO 8601 timestamp of the trust data snapshot used for this verificationtrustFailureReason—Present whenisTrusted: false(see the preceding decision tree)staleTrustData—Present when trust data is stale (object withcachedFallbackandpastNextUpdatebooleans)
Static-only mode (fallback)
For environments without LoTL infrastructure, the connector resolves trust against manually configured trust anchor certificates.
Trust anchors are configured statically via environment variables. You must obtain trust anchor certificates from your federation operator and update the configuration manually when they change.
Configuration
| Variable | Required | Default | Description |
|---|---|---|---|
X509_VERIFY_TRUST_ANCHOR | Yes | false | Enable static trust anchor resolution and CRL checking. Set to true to activate. |
X509_TRUST_ANCHORS | Yes (when enabled) | "" | Comma-separated base64-encoded DER trust anchor certificates. |
X509_CRL_TIMEOUT_SECONDS | No | 10 | HTTP timeout in seconds for CRL fetch requests. |
Obtain trust anchor certificates
Contact your federation operator to obtain the trust anchor certificates for the credential issuers you want to verify. In the EUDI ecosystem, these are typically root CA certificates or federation trust anchor certificates published in a List of Trusted Entities (LoTE). Your federation operator should be able to provide them as PEM or DER files.
If your federation does not yet distribute trust anchor certificates, you cannot use this feature.
Encode the certificates
The connector's X509_TRUST_ANCHORS variable expects certificates in base64-encoded DER format. DER is the binary encoding of an X.509 certificate—as opposed to PEM, which is the same data wrapped in -----BEGIN CERTIFICATE----- / -----END CERTIFICATE----- headers and base64-encoded for text transport. You need to strip the PEM headers and re-encode the raw binary as a single base64 string with no line breaks.
If your certificate is already in DER format (a .der or .cer file), skip the first part of the command:
# From PEM: convert to DER, then base64-encode (no line breaks)
openssl x509 -in trust-anchor.pem -outform DER | base64 | tr -d '\n'
# From DER: base64-encode directly
base64 -i trust-anchor.der | tr -d '\n'
The output is a single line of base64 characters with no whitespace. That is the value you set in X509_TRUST_ANCHORS.
If you have multiple trust anchors, encode each certificate separately and join the values with commas:
<base64-cert-1>,<base64-cert-2>
Configure the connector
Example configuration:
export X509_VERIFY_TRUST_ANCHOR=true
export X509_TRUST_ANCHORS="<base64-encoded-DER-cert-1>,<base64-encoded-DER-cert-2>"
export X509_CRL_TIMEOUT_SECONDS=10
Restart the connector after setting these variables.
Testing
LoTL-enabled mode test checklist
-
X509_TRUST_RESOLUTION_MODE=lotlis set and the connector restarts without errors - A credential with an
x5cchain from a LoTL-registered issuer returnsisTrusted: truewithtrustSource: "lotl" - A credential from an unregistered issuer returns
isTrusted: falsewithtrustFailureReason: "issuer_not_found" -
credentialCategoryis populated with the correct category for the presented credential -
supervisoryMemberStatecontains the ISO 3166-1 alpha-2 code of the issuer's member state - Trust verification policy
enforcecorrectly producesVERIFICATION_FAILEDfor untrusted credentials
Static-only mode test checklist
You can only confirm static mode is working by presenting a real credential that includes an x5c certificate chain from an issuer whose trust anchor you have configured. The connector's test wallet does not produce credentials with x5c chains, so you cannot verify this feature in isolation using the standard development setup.
When a credential with a valid x5c chain is presented and trust anchor verification is enabled, verify:
supportTrustAnchor: true—the credential was signed with anx5ccertificate chainisTrusted: true—the chain terminated at a configured trust anchor and the last certificate's signature was verifiedisCertificateRevoked: false—no certificate in the chain was found on a CRL
If X509_VERIFY_TRUST_ANCHOR is false (the default), isTrusted and isCertificateRevoked are both false even when supportTrustAnchor is true. The fields are present but trust and revocation checking was skipped.
Troubleshooting
Trust resolution mode misconfiguration
X509_TRUST_RESOLUTION_MODE must be exactly lotl or static. Any other value prevents the connector from starting. Check for trailing whitespace or incorrect casing.
LoTL fetch failures on startup
If the connector cannot fetch the LoTL on startup, it delays readiness until the fetch succeeds or the configured timeout is exceeded. Check network connectivity from the connector to the configured trust list source endpoints.
Staleness window exceeded (fail-closed)
When cached trust data exceeds the X509_STALENESS_WINDOW and a fresh fetch fails, the connector fails closed—isTrusted is false with trustFailureReason: "trust_list_unavailable". Investigate why the trust list endpoint is unreachable and consider increasing the staleness window for unstable network environments.
issuer_not_found when issuer is expected to be on a trust list
Verify that the issuer's certificate is published on the correct trust list for the credential category. Check that the X509_TRUST_LIST_SOURCES configuration includes the appropriate source (for example, PID_PROVIDER_LOTE for PID credentials).
trust_list_unavailable when the list cannot be fetched
The connector could not fetch the trust list and the cached copy (if any) exceeded the staleness window. Check network connectivity, DNS resolution, and firewall rules from the connector to the trust list endpoint. Increase X509_LOTL_FETCH_TIMEOUT if the endpoint is slow.
Unexpected VERIFICATION_FAILED when trust policy is set to enforce
When trust verification policy is enforce for a credential category, any trust failure for that category produces VERIFICATION_FAILED—even trust_list_unavailable. If this is too aggressive, switch to evaluate mode and handle trust failures in your backend logic.
isTrusted: false—trust anchor not matching (static mode)
The chain's last certificate did not match any configured trust anchor. The connector first tries Authority Key Identifier (AKI) to Subject Key Identifier (SKI) matching, then falls back to Issuer Distinguished Name matching with signature verification. Verify that the correct trust anchor certificate is encoded in X509_TRUST_ANCHORS. Check that the certificate is in DER format before base64-encoding—encoding a PEM file directly (without the openssl x509 -outform DER conversion step) produces an invalid value.
CRL fetch timeout (static mode)
The connector could not fetch the CRL within the configured timeout. When a CRL fetch fails, the connector logs a warning and tries the next CRL Distribution Point listed in the certificate. If no valid CRL can be obtained from any distribution point, the certificate is treated as not revoked (fail-open for CRL specifically). Trust anchor resolution itself is fail-closed: if the chain does not end at a configured trust anchor, isTrusted is false. Increase X509_CRL_TIMEOUT_SECONDS if CRL endpoints are slow, or check network connectivity from the connector to the CRL distribution point URLs.
Self-signed certificates in the chain (static mode)
Self-signed certificates must not appear in the x5c chain. The trust anchor (root CA) is not included in the chain—it is configured separately via X509_TRUST_ANCHORS. If you see trust failures, verify the issuer is not including the root CA in the x5c header.
Next steps
- Get ready for production—production readiness checklist
- Handle verification errors—handle
VERIFICATION_FAILEDwhen chain validation fails
Further reading
- LoTL trust verification—conceptual explanation of how LoTL verification works
- Callback events—full credential object field reference
- Revocation mechanisms—how CRL checking works conceptually
- Certificates in EUDI—X.509 certificate types and trust establishment