Repository navigation
[OID4VC] Support ETSI Lists of Trusted Entities - #54522
dominikschlosser wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Refresh throttling can leave expired trust material usable, while public-key-based deduplication can discard valid renewed CA certificates and reject valid chains.
2 open issues
What changed in this PR
Adds ETSI List of Trusted Entities support to Keycloak’s trust-material identity providers, enabling EUDI Wallet trust for issuance, attestation-based client authentication, and presentation flows.
Changes:
- Adds signed trust-list loading with list-type, service-scope, and freshness checks.
- Supports subordinate CA trust anchors and client-attestation
x5cvalidation. - Adds Admin Console configuration, documentation, and integration tests.
| File | Description |
|---|---|
| tests/base/src/test/java/org/keycloak/tests/oid4vc/issuance/signing/OID4VCKeyAttestationTest.java | Updates expectations for subordinate CA anchors. |
| tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCMockClientAttester.java | Supports certificate chains in mock attestations. |
| tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCAttestationBasedClientAuthenticationTest.java | Adds trust-list-backed authentication coverage. |
| tests/base/src/test/java/org/keycloak/tests/broker/trust/TrustListTestSupport.java | Generates certificates and serves signed test lists. |
| tests/base/src/test/java/org/keycloak/tests/broker/trust/EtsiTrustListIdentityProviderTest.java | Tests trust selection, validation, and refresh behavior. |
| services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory | Registers the ETSI provider. |
| services/src/main/java/org/keycloak/broker/trust/etsi/ListOfTrustedEntities.java | Models trust-list JSON fields. |
| services/src/main/java/org/keycloak/broker/trust/etsi/EtsiTrustListPublicKeyLoader.java | Downloads, verifies, and extracts list certificates. |
| services/src/main/java/org/keycloak/broker/trust/etsi/EtsiTrustListIdentityProviderFactory.java | Defines provider settings and feature availability. |
| services/src/main/java/org/keycloak/broker/trust/etsi/EtsiTrustListIdentityProviderConfig.java | Parses and validates configuration. |
| services/src/main/java/org/keycloak/broker/trust/etsi/EtsiTrustListIdentityProvider.java | Exposes listed CA certificates as trust anchors. |
| services/src/main/java/org/keycloak/broker/trust/DefaultTrustIdentityProviderConfig.java | Updates validation wording for CA anchors. |
| services/src/main/java/org/keycloak/broker/oid4vp/OID4VPIdentityProvider.java | Removes the trust-list resolver TODO comment. |
| services/src/main/java/org/keycloak/authentication/authenticators/client/AttestationBasedClientAuthenticator.java | Adds attestation certificate-chain validation. |
| server-spi-private/src/main/java/org/keycloak/broker/provider/X509TrustMaterial.java | Permits subordinate CA trust anchors. |
| js/apps/admin-ui/src/identity-providers/routes/IdentityProviderEtsiTrustList.tsx | Defines the provider creation route. |
| js/apps/admin-ui/src/identity-providers/routes.ts | Registers the new route. |
| js/apps/admin-ui/src/identity-providers/add/EtsiTrustListSettings.tsx | Renders trust-list configuration fields. |
| js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx | Integrates provider-specific settings. |
| js/apps/admin-ui/src/identity-providers/add/AddEtsiTrustList.tsx | Adds the provider creation form. |
| js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties | Updates CA certificate help text. |
| docs/documentation/server_admin/topics/oid4vci/proofs.adoc | Documents ETSI trust-list setup and trust behavior. |
Review effort: Balanced
ae35af9 to
86e8bf8
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Critical security gaps allow expired trust-list CAs to remain trusted during cache reload throttling and permit missing x5c chains to bypass mandatory X.509 validation through kid fallback.
1 open issue
2 resolved since last review
Review effort: Balanced
86e8bf8 to
e5af3de
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Domain-owner security review is needed to validate ETSI/JAdES conformance, real signed LoTE interoperability, and broader X.509 anchor trust semantics beyond the synthetic-list tests.
No open issues 🎉
1 resolved since last review
Review effort: Balanced
e5af3de to
9415b4e
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The service-scope test uses an end-entity certificate independently rejected by isCaCertificate, masking missing service-type filtering; using Pki.ROOT would make it detect that regression.
1 open issue
Review effort: Balanced
9415b4e to
4233913
Compare
4233913 to
8e3be3c
Compare
8e3be3c to
e4f0861
Compare
e4f0861 to
1348c18
Compare
Signed-off-by: Dominik Schlosser <dominik.schlosser@gmail.com>
1348c18 to
cc6361d
Compare
| if (getServiceTypes().isEmpty()) { | ||
| throw new IllegalArgumentException("At least one trust list service type is required"); | ||
| } | ||
| try { |
There was a problem hiding this comment.
The isEmpty check is inside the try, so its own exception gets caught by the catch below. An empty field ends up reporting "must be a valid PEM bundle" instead of "at least one trust list signing certificate is required". Move the check above the try. Default Trust has the same problem, so this is copied, but no need to copy it.
| try { | ||
| return Integer.parseInt(refreshInterval.trim()); | ||
| } catch (NumberFormatException e) { | ||
| return -1; |
There was a problem hiding this comment.
Returning -1 for a value that does not parse means abc is reported as "must be a positive number of seconds", which sends the admin in the wrong direction. A separate message for unparseable input would be clearer.
| /** | ||
| * Exposes the CA certificates of an ETSI TS 119 602 List of Trusted Entities as X.509 trust anchors. | ||
| * HAIP requires the signing certificate in x5c and keeps the trust anchor out of it, so an artifact | ||
| * needs a chain to a listed CA. No key can be selected by kid and listed end entity certificates are |
There was a problem hiding this comment.
Clause 6.6.3 allows any X.509 certificate in ServiceDigitalIdentity, not only CA certificates. We drop end entity certificates, so a scheme that publishes a leaf gives us nothing to trust. The docs do say this, just want to be sure it is a deliberate scope decision and not something we plan to support later.
| } | ||
|
|
||
| @Override | ||
| public void close() { |
There was a problem hiding this comment.
reloadKeys() is not overridden here, so GET /identity-providers/{alias}/reload-keys returns false and there is no way to force a refresh. Default Trust, OIDC and SAML all override it. Worth adding so an admin does not have to wait out the refresh interval after a certificate is withdrawn.
| import org.jboss.logging.Logger; | ||
|
|
||
| /** | ||
| * Loads the certificates of the configured service types from a signed ETSI TS 119 602 V1.1.1 trust |
There was a problem hiding this comment.
Clause E.4 requires a compact JAdES Baseline B signature. What happens here is a plain JWS verify against the configured certificates. It is safe, just more lenient than the spec. A sentence in the docs would help so nobody expects full JAdES validation.
| return new PublicKeysWrapper(toKeys(list), Math.min(refreshAt, nextUpdate)); | ||
| } | ||
|
|
||
| private void verifySignature(JWSInput jws) throws JWSInputException { |
There was a problem hiding this comment.
Clause 6.8.0 of TS 119 602 says the C and O fields of the signing certificate subject shall match SchemeTerritory and SchemeOperatorName. We never read those two fields. The German sandbox list would not pass that check either (cert has C=DE, O=Trust Lists, list says EU and SPRIND GmbH), so I assume leaving it out is deliberate. Could you note that somewhere so it does not look like an oversight?
| Trust list signing certificates:: A PEM bundle of the certificates allowed to sign the list. A list is used only when one of these certificates verifies its signature. To rotate the signing key, configure the old and the new certificate. | ||
| LoTE type:: The required type of the list, for example `+http://uri.etsi.org/19602/LoTEType/EUWalletProvidersList+`. {project_name} rejects a list whose `LoTEType` value does not match. | ||
| Service types:: The service type identifiers whose certificates are trusted, for example `+http://uri.etsi.org/19602/SvcType/WalletSolution/Issuance+`. | ||
| Service statuses:: Services with one of these statuses are trusted. Services without a status are always trusted. |
There was a problem hiding this comment.
One detail is missing: with Service statuses left empty, services that do carry a status are ignored (see servicesWithAStatusAreIgnoredWithoutConfiguredStatuses). Worth spelling that out here.
| .type("trustlist+jwt") | ||
| .jsonContent(Map.of("LoTE", Map.of( | ||
| "ListAndSchemeInformation", scheme, | ||
| "TrustedEntitiesList", List.of(Map.of("TrustedEntityServices", services))))) |
There was a problem hiding this comment.
The test list is thinner than a real one. A published list also carries TrustedEntityInformation, ListIssueDateTime, SchemeTerritory and friends. The mapping handles them fine today, but one test with the full shape would stop someone breaking that later.



Closes #48106
Adds an
etsi-trust-listidentity provider that uses a signed ETSI TS 119 602 List of Trusted Entities (LoTE) as trust material for OID4VCI key attestations, attestation-based client authentication and OID4VP. This is necessary to use Keycloak as issuer or verifier in the EUDI ecosystem.x5cis required, as in HAIP, and keys cannot be selected bykid. Listed end entity certificates are ignored.NextUpdate, fails the trust decision until a valid list is available. Lists withoutNextUpdateare not trusted at all.X509TrustMaterialnow accepts subordinate CAs as trust anchors. Real lists contain them, for example the German sandbox PID list.Lists of Trusted Lists (#54562) and Certificate Revocation (using CRL, #54561) are follow ups.
XML Trusted Lists are not supported for now, but may be needed in the future as well (QEAA providers might use them).
Live demo to test it: https://demo.eudi-test.dev/etsi-tl/
Keycloak admin UI with the changes in this PR: https://keycloak.eudi-test.dev/etsi-tl/admin/wallet-demo/console/ (Login: viewer/viewer)
Wallet/Key Attestation LoTE: https://eudi-test.dev/api/trustlists/wallet-provider
Credential LoTE (for OID4VP): https://keycloak.eudi-test.dev/trust-lists/issuers.jwt
German Sandbox LoTEs: https://bmi.usercontent.opencode.de/eudi-wallet/test-trust-lists/
Note: Client attestations with an ETSI trust list need
x5csupport inAttestationBasedClientAuthenticator, which #52883 adds. Key attestations and OID4VP work without it.