Trusted lists¶
A wallet connection can take its issuer trust anchors from published, signed trusted lists that HelixIAM fetches, verifies and refreshes itself, instead of (or as well as) PEM certificates you paste in. A source is set up once per realm; the wallet connection and each of its other credentials name the sources they trust. (Beta; signer rotation and LoTE pointers are Preview, not yet tested with a real national wallet.)
Status
Trusted lists are Beta: verified against the published EUDI test list of trusted lists and the European Commission's acceptance List of Trusted Entities. The automation added in 1.1.0-rc.1 — signer rotation (the LOTL "pivot"), LoTE pointers and nested lists — is Preview: implemented from the ETSI specifications and tested with generated lists only, because no published pivot LOTL or LoTE pointer was available to test against. Nothing here has been tested with a real national wallet yet.
Console: Federation & eIDs → Trusted lists. Admin API: /admin/realms/{realm}/trusted-lists. Reads need view-identity-providers; creating, updating, deleting, refreshing and resetting need manage-identity-providers.
Formats and what is published¶
Helix reads both formats the EU publishes for wallet trust:
- ETSI TS 119 612 XML: a trusted list, or a list of trusted lists (LOTL) whose pointers are followed;
- ETSI TS 119 602 List of Trusted Entities (LoTE), JSON in a JWS, whose
PointersToOtherLoTEcan be followed too.
What is published, as checked by the project on 15 October 2026:
| Source | Format | Status |
|---|---|---|
EUDI Wallet reference implementation, https://trustedlist.serviceproviders.eudiw.dev/LOTL/01.xml |
TS 119 612 XML, a list of trusted lists | Test only. PID providers, wallet providers, access-certificate CAs, (Q)EAA providers |
European Commission, https://acceptance.trust.tech.ec.europa.eu/lists/eudiw/pid-providers.json |
TS 119 602 LoTE (JWS) | Acceptance environment. No production list of PID providers is published yet |
EU list of trusted lists, https://ec.europa.eu/tools/lotl/eu-lotl.xml |
TS 119 612 XML | Production, but it does not list PID providers |
Nothing is specific to one publisher: the URL, the pinned signer certificates, the service types and the accepted statuses are configuration, so a list published later in either format works without code changes.
Add a source¶
A source has:
| Field | Meaning |
|---|---|
name |
what wallet connections refer to |
url |
the https URL of the list. No redirect is followed: configure the final URL |
format |
ETSI_119612_XML or ETSI_119602_JSON |
signerCertificates |
the pinned signer certificates (PEM) |
serviceTypes |
the service types to extract, for example http://uri.etsi.org/Svc/Svctype/Provider/PID |
acceptedStatuses |
the statuses that count; default granted and recognisedatnationallevel (withdrawn and deprecatedatnationallevel can never be chosen) |
territories |
for a list of lists, optionally the territories whose lists are read (NL, DE, …) |
graceHours |
how long the last good anchors are kept past NextUpdate; default 24, at most 720 |
signerRotation |
TS 119 612 lists of lists only, default off: accept a new signer the verified list announces (signer rotation). The console's LOTL presets turn it on |
pointerDepth |
0–3: how many levels of pointers to other lists are followed (pointers). Unset: 1 for XML, 0 for LoTE |
enabled |
the switch |
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/trusted-lists" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)
jq -n --rawfile signer lotl-signer.pem '{
name: "eudi-pid-providers",
url: "https://trustedlist.serviceproviders.eudiw.dev/LOTL/01.xml",
format: "ETSI_119612_XML",
signerCertificates: $signer,
serviceTypes: ["http://uri.etsi.org/Svc/Svctype/Provider/PID"],
signerRotation: true
}' > trusted-list.json
curl -s -b cookies.txt \
-H "Content-Type: application/json" \
-H "X-XSRF-TOKEN: $CSRF" \
-d @trusted-list.json \
"$HELIX_URL/admin/realms/$REALM/trusted-lists"
The answer is 201 with the source, including its id, configVersion and status. Load it at once with Refresh now in the console, or:
curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" \
"$HELIX_URL/admin/realms/$REALM/trusted-lists/$LIST_ID/refresh"
The answer is the source as it is after the attempt; its status says how it went. 409 means another refresh holds the source.
Then name the source on the wallet connection, optionally narrowed by service type:
"config": {
"accessKeyRef": "software:master/primary",
"trustedLists.sources": "[\"eudi-pid-providers\"]",
"trustedLists.serviceTypes": "[\"http://uri.etsi.org/Svc/Svctype/Provider/PID\"]"
}
The anchors are read at every verification, so a refresh, a withdrawal or an expiry takes effect at once. Pasted trustAnchors are then optional; when both are set, they are combined.
Editing. Every update (PUT /admin/realms/{realm}/trusted-lists/{id}) must send the configVersion of the source it edited. When another admin saved in between, the update is a 409 (the console offers to reload) instead of overwriting their change. Renaming or deleting a source that a wallet connection names, for the PID or for any other credential, is refused with 409: remove it from the connections first. Changing what a source loads (URL, format, signers, types, statuses, territories, signer rotation, pointer depth) drops its stored anchors at once.
Pin the signer through a channel you trust
The signer certificates must come from the publisher through a channel you trust, never from the list itself. For the EU list of trusted lists that is the Official Journal publication of the LOTL signing certificates. The console's presets fill in the URL, format and service types, never a signer.
What is verified¶
A list counts only when:
- it was fetched over
httpsthrough the outbound guard (the connection goes to the checked address, TLS is verified against the host name, no redirect is followed; at most 16 MB; connect 5 s, 30 s in total); - its signature verifies with one of the pinned signer certificates, or with signers that a verified list announced (signer rotation) — never with the certificate the list carries in its own
KeyInfoorx5c. The signer must have an RSA (2048 bits or more) or EC (P-256 or stronger) key and must have been valid at the list's issue time; - it was parsed without a DOCTYPE (so no entities), without XInclude or external DTD or schema access, at most 64 levels deep;
- its
NextUpdatehas not passed (a list without one is refused), counting it at most until its issue time plus six months; - its sequence number is not lower than that of the last list accepted (rollback protection).
XML signatures must be one enveloped signature over the whole list, RSA PKCS#1 v1.5, RSASSA-PSS or ECDSA with SHA-256/384/512 digests; JSON lists RS/PS/ES 256/384/512, never alg=none.
From a verified list Helix keeps every service of the configured types whose current status is accepted. Only CA certificates that pass the usual anchor rule (EC P-256/384/521 or RSA 2048 bits or more, keyCertSign) become anchors. A listed end-entity certificate is shown in the preview as "not a CA" and not used.
Expired signers¶
Lists are signed when issued and stay valid until NextUpdate, so a publisher's signer certificate can expire first. How long after its expiry a signer may still verify a list issued while it was valid is set per deployment:
| Property | Default | Applies to |
|---|---|---|
helix.wallet.trusted-lists.signer-expiry-grace-days |
0 |
an admin-pinned signer of a top-level list |
helix.wallet.trusted-lists.member-signer-expiry-grace-days |
180 |
a member list's signer named by the verified list of lists |
With the default 0, a list whose pinned signer has expired is refused, and the console says so. As of 2026-10-15 the EUDI reference implementation's test LOTL (signer expired 2026-07-04) loads only with helix.wallet.trusted-lists.signer-expiry-grace-days set to about 120 or more, and the Commission's acceptance LoTE (signer expired 2026-09-15) with about 30 or more. Set it deliberately for such test lists; there is no lenient default, not even in dev:
Pointers to other lists¶
A list of trusted lists, or a LoTE with PointersToOtherLoTE, points at other lists. Up to the source's pointerDepth (0–3; unset: 1 for XML, 0 for LoTE), each pointer to a list in the chosen territories is a member, judged on its own. A member must:
- be
https(a plain-http pointer is skipped and reported); - verify with the signer certificates the parent list names for it;
- carry the territory and list type the pointer names, and be fresh;
- not have a lower sequence number than the highest one accepted for its territory and list type (the high-water mark, kept per source even when the member's URL changes).
A member that fails keeps its own last good anchors until its own NextUpdate plus grace, then gives none; the other members are not affected, and the source shows DEGRADED. Members the list no longer points at are dropped. Every list is read at most once in a load (a pointer back to a list already read is a cycle and stops), at most 64 lists in all. A member's format follows the pointer's MIME type (JSON: LoTE, XML: TS 119 612). The services the top list lists itself also count, so a LoTE that lists entities and points at others gives both.
A whole load (the list, its pivots and every list it points at) has a wall-clock budget, helix.wallet.trusted-lists.max-refresh-seconds (300). Past it the refresh fails and the last good state is kept.
Signer rotation (the LOTL pivot)¶
Without signer rotation, when a publisher rotates its signing key the list stops verifying: the source goes FAILING, then STALE, then EXPIRED, unless you pin the new certificate before the grace period ends.
With signerRotation on (TS 119 612 lists of lists only), one rule applies: a list is verified only with certificates that come from an admin pin or from a list that was itself verified.
- Self-pointer. The LOTL's pointer to itself names the certificates authorised to sign it. Once a verified LOTL names them, they replace the pins for the next refresh. During a rollover the publisher lists both the old and the new key, so both verify; a key it drops is no longer trusted.
- Pivots. A LOTL that does not verify with the trusted set may still verify through its pivot LOTLs: the
httpsURIs in itsSchemeInformationURI(at most 8 fetched). The links are taken in ascending sequence number, each verified with the keys trusted at that step, each after the last accepted list and before the new LOTL. Once a list has been accepted, a link's signer must also still be valid now, so an expired key cannot sign a backdated pivot. The LOTL must finally verify with the last set; otherwise the refresh fails withSIGNATURE. - Rollback protection is unchanged: the LOTL's sequence number never goes down, and pivots give no anchors.
Every change of the trusted set is audited as TRUSTED_LIST_SIGNER_ROTATED with the old and new SHA-256 fingerprints; a member whose parent names other signers than at the last load is audited the same way. The console shows the certificate that verified the last list and the announced signers.
Not checked
The Official Journal reference among the SchemeInformationURI values is not compared with a configured value.
Refresh, backoff and failing closed¶
Each replica looks for due sources every minute; the one that takes the source's lease refreshes it, the others wait and then reload the stored result. A healthy source is refreshed at the earlier of its NextUpdate and the refresh interval (6 hours) after the last refresh.
A failed refresh is retried with exponential backoff: 15 minutes after the first failure, doubling with each failure in a row (30, 60, …) up to 6 hours, but never later than the moment the last good anchors stop being used. A success resets the count. The console shows the failures in a row and the next attempt.
| Status | Anchors used | When |
|---|---|---|
OK |
yes | the last refresh verified a list whose NextUpdate has not passed |
FAILING |
yes (last good) | the last refresh failed, but the last good list is before its NextUpdate |
STALE |
yes (last good) | past the last good list's NextUpdate, within the grace period |
EXPIRED |
no | past NextUpdate plus grace: fail closed until a new list verifies |
DEGRADED |
yes (the good members) | a list of lists that is itself fine, with some members failing, stale, expired or never verified |
NEVER / ERROR |
no | never loaded / never verified |
INTEGRITY |
no | the stored settings or trust state failed their integrity check (below) |
DISABLED |
no | switched off |
Failing closed, with a clear code. When a source gives no anchors (EXPIRED, NEVER, ERROR, INTEGRITY, DISABLED) and an issuer's chain reaches no other anchor of the connection, the presentation is refused with the audit reason TRUST_LIST_UNAVAILABLE, naming the source, instead of ISSUER_UNTRUSTED: the trust source, not the issuer, is the problem. The user sees "technical". An expired or not-yet-valid issuer certificate stays ISSUER_UNTRUSTED.
| Property | Default | Meaning |
|---|---|---|
helix.wallet.trusted-lists.refresh-interval-minutes |
360 |
longest time between refreshes of a healthy source |
helix.wallet.trusted-lists.retry-interval-minutes |
15 |
wait after a failed refresh (doubles with each failure in a row) |
helix.wallet.trusted-lists.max-retry-interval-minutes |
360 |
longest wait after failed refreshes |
helix.wallet.trusted-lists.check-interval-seconds |
60 |
how often each replica looks for due sources (0: only Refresh now) |
helix.wallet.trusted-lists.poll-interval-seconds |
10 |
how often each replica reloads sources that changed elsewhere |
helix.wallet.trusted-lists.max-refresh-seconds |
300 |
wall-clock budget of one refresh, every fetch included (at least 30) |
helix.wallet.trusted-lists.max-bytes |
16777216 (16 MB) |
largest list fetched |
helix.wallet.trusted-lists.max-validity-months |
6 |
a list counts at most this long after its issue time |
Tampering and Reset and reload¶
Every stored value that decides trust is covered by an HMAC-SHA256 keyed from DB_ENCRYPTION, in two records: the source's settings (URL, format, pins, filters, grace, the switch, and the number of successful loads) and its trust state (anchors, sequence number, issue time, NextUpdate, announced signers and the members' high-water marks). Outside the dev profile a source cannot be saved without DB_ENCRYPTION.
What is detected. Against someone who can write the database but does not hold DB_ENCRYPTION: changing any single trust-deciding field (a pinned signer, a setting, an anchor, a sequence floor, the announced signers, a high-water mark), wiping the trust state of a loaded source, and setting the load counter back. The source then shows INTEGRITY, gives no anchors, and TRUSTED_LIST_INTEGRITY_FAILED is audited.
What is not detected. Replacing a source's rows with a complete, earlier copy of themselves, such as a database backup taken under the same configVersion. Every MAC in such a copy is valid, so the source goes back to that older list (a rollback). There is no anchor outside the database to compare against. A wallet connection's pasted trustAnchors are plain configuration and not covered either.
Recovering. Nothing of a row that failed its check is trusted or signed again: a refresh does not rebuild it, and saving the settings does not clear it.
- When the settings fail (modified outside Helix, stored before Flyway V84, or
DB_ENCRYPTIONchanged): check them and save them again first. -
Then Reset and reload: the button on the failed list in the console (after a confirmation that explains the rollback floors are reset), or the admin API with the source's
configVersion. It discards the stored anchors and floors and starts a clean first load from the pinned signers:curl -s -b cookies.txt -X POST \ -H "Content-Type: application/json" -H "X-XSRF-TOKEN: $CSRF" \ -d "{\"configVersion\": $CONFIG_VERSION}" \ "$HELIX_URL/admin/realms/$REALM/trusted-lists/$LIST_ID/reset"It is refused with
409while the settings themselves fail their check, when the source changed meanwhile, or while a refresh holds it. It is audited asTRUSTED_LIST_RESET_REQUEST(who) andTRUSTED_LIST_RESET(the previous sequence number and status).
The only other action that drops the rollback floors is a change of the list itself: another URL or format, or removing a pinned signer. Pins compare as certificate sets, so reformatting the PEM is no change, and adding a pin keeps the floors. Removing a pin needs resetFloors: true in the update (the console asks first), because the removed key may be the one that set the floors; without it the update is a 400.
After changing DB_ENCRYPTION, check and save each source again, then Reset and reload it.
Audit¶
Refreshes are audited as TRUSTED_LIST_REFRESHED, TRUSTED_LIST_REFRESH_FAILED, TRUSTED_LIST_REFRESH_DISCARDED, TRUSTED_LIST_MEMBER_REFRESH_FAILED, TRUSTED_LIST_STALE, TRUSTED_LIST_EXPIRED, TRUSTED_LIST_MEMBER_STALE, TRUSTED_LIST_MEMBER_EXPIRED, TRUSTED_LIST_SIGNER_EXPIRING / TRUSTED_LIST_SIGNER_EXPIRED (a pinned signer expiring within 30 days), TRUSTED_LIST_SIGNER_ROTATED, TRUSTED_LIST_RESET and TRUSTED_LIST_INTEGRITY_FAILED, with actor system. Admin writes are TRUSTED_LIST_CREATE|UPDATE|DELETE|REFRESH_REQUEST|RESET_REQUEST, with the name, URL and SHA-256 fingerprints of the pinned signers.
Endpoints¶
Method & path (under /admin/realms/{realm}) |
Purpose |
|---|---|
GET /trusted-lists · POST /trusted-lists |
List / add sources |
GET · PUT · DELETE /trusted-lists/{id} |
Read / update (needs configVersion) / delete a source |
GET /trusted-lists/{id}/anchors |
The verified services of the source's last good list |
POST /trusted-lists/{id}/refresh |
Refresh a source now |
POST /trusted-lists/{id}/reset |
Reset and reload (needs configVersion) |
Limitations¶
- Trusted list sources are not exported or imported with a realm: a realm import whose wallet connection names a source fails validation until the source exists in the target realm. There is no Terraform resource for sources.
- Signer rotation and LoTE pointers are tested with generated lists only.
- No production EU list of PID providers is published yet.
See also¶
- EU Digital Identity Wallet (OpenID4VP) — the wallet connection that names these sources
- Credentials beyond the PID — per-credential trusted lists, for example (Q)EAA providers
- Upgrading from rc.13 to 1.1.0-rc.1 — Flyway V90 for this feature