Skip to content

[OID4VCI] Harden key attestation x5c certificate validation - #51042

Merged
mposolda merged 5 commits into
keycloak:mainfrom
adorsys:issue-50518
Jul 31, 2026
Merged

[OID4VCI] Harden key attestation x5c certificate validation#51042
mposolda merged 5 commits into
keycloak:mainfrom
adorsys:issue-50518

Conversation

@forkimenjeckayang

@forkimenjeckayang forkimenjeckayang commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This change hardens validation of X.509 certificate chains supplied through the
x5c header of OID4VCI key attestations.

Previously, attestation chains were validated against the JVM system truststore,
which allowed certificates issued by general Web PKI certificate authorities.
Revocation checking was disabled and no Extended Key Usage policy was enforced.

The implementation now:

  • validates key-attestation chains only against dedicated trust anchors exposed
    by the trust-material identity providers configured for the OID4VCI client;
  • never uses the JVM system truststore as an attestation trust source;
  • requires configured trust anchors to be self-signed CA root certificates with
    certificate-signing usage;
  • requires the attestation signing certificate to be an end-entity certificate;
  • enforces administrator-configured attestation EKU OIDs;
  • explicitly disables provider-default PKIX revocation because this validation
    path does not yet configure CRL or OCSP sources, and documents that revocation
    must be enforced externally until dedicated support is implemented;
  • keeps each configured trust provider as an independent trust domain;
  • preserves x5c as a key-conveyance mechanism for ordinary JWT proofs, where
    attestation trust validation does not apply;
  • adds Admin Console configuration and documentation for X.509 attestation
    trust.

closes #50518

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Copilot AI review requested due to automatic review settings July 20, 2026 14:23
@forkimenjeckayang
forkimenjeckayang requested review from a team as code owners July 20, 2026 14:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens OID4VCI key-attestation certificate validation by introducing dedicated X.509 trust material and policies.

Changes:

  • Adds isolated PKIX validation with EKU, end-entity, and revocation enforcement.
  • Adds trust-provider and Admin Console X.509 configuration.
  • Expands documentation and integration/UI tests.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/base/.../OID4VCKeyAttestationTest.java Tests attestation certificate policies.
services/.../X5cKeyUtils.java Decodes chains and converts leaf keys.
services/.../TrustedAttestationKeyResolver.java Resolves configured X.509 trust.
services/.../StaticAttestationKeyResolver.java Supports static X.509 trust material.
services/.../JwtProofValidator.java Preserves proof-key x5c conveyance.
services/.../AttestationX509CertificateValidator.java Implements PKIX policy validation.
services/.../AttestationValidatorUtil.java Removes system-truststore attestation validation.
services/.../AttestationKeyResolver.java Adds certificate-chain resolution API.
services/.../DefaultTrustIdentityProviderConfig.java Defines and validates X.509 settings.
services/.../DefaultTrustIdentityProvider.java Exposes X.509 trust material.
services/.../TrustMaterialResolver.java Resolves X.509 material by provider.
server-spi-private/.../X509TrustMaterial.java Models trust anchors and policy.
server-spi-private/.../TrustMaterialIdentityProvider.java Extends the trust-provider SPI.
js/.../default-trust.spec.ts Tests new Admin Console controls.
js/.../DefaultTrustSettings.tsx Adds X.509 configuration fields.
js/.../messages_en.properties Adds X.509 setting labels and help.
docs/.../proofs.adoc Documents attestation trust configuration.

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.forms.BrowserFlowTest#testUserWithOneAdditionalFactorOtpSuccess

Keycloak CI - Base IT (5)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.forms.MultipleTabsLoginTest#multipleTabsParallelLoginTestWithAuthSessionExpiredAndRequiredAction

Keycloak CI - Forms IT (chrome)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.oauth.ClientAuthPostMethodTest#testPostAuthenticationNotAllowedWhenBasicRequested

Keycloak CI - Base IT (6)

java.lang.NullPointerException: Cannot read field "features" because the return value of "org.keycloak.common.Profile.getInstance()" is null
	at org.keycloak.common.Profile.isFeatureEnabled(Profile.java:534)
	at org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper.<init>(OIDCClientSecretConfigWrapper.java:41)
	at org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper.fromClientRepresentation(OIDCClientSecretConfigWrapper.java:49)
	at org.keycloak.testsuite.oauth.ClientAuthPostMethodTest.testPostAuthenticationNotAllowedWhenBasicRequested(ClientAuthPostMethodTest.java:141)
...

Report flaky test

org.keycloak.testsuite.oauth.ClientAuthPostMethodTest#testBasicAuthenticationNotAllowedWhenPostRequested

Keycloak CI - Base IT (6)

java.lang.NullPointerException: Cannot read field "features" because the return value of "org.keycloak.common.Profile.getInstance()" is null
	at org.keycloak.common.Profile.isFeatureEnabled(Profile.java:534)
	at org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper.<init>(OIDCClientSecretConfigWrapper.java:41)
	at org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper.fromClientRepresentation(OIDCClientSecretConfigWrapper.java:49)
	at org.keycloak.testsuite.oauth.ClientAuthPostMethodTest.testBasicAuthenticationNotAllowedWhenPostRequested(ClientAuthPostMethodTest.java:112)
...

Report flaky test

org.keycloak.testsuite.forms.MultipleTabsLoginTest#testEmptyBaseUrl

Keycloak CI - Forms IT (firefox)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.forms.MultipleTabsLoginTest#multipleTabsParallelLoginTest

Keycloak CI - Forms IT (firefox)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Copilot AI review requested due to automatic review settings July 21, 2026 07:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Copilot AI review requested due to automatic review settings July 21, 2026 08:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.forms.BrowserFlowTest#testUserWithOneAdditionalFactorOtpSuccess

Keycloak CI - Base IT (5)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.forms.MultipleTabsLoginTest#multipleTabsParallelLoginTestWithAuthSessionExpiredInTheMiddle

Keycloak CI - Forms IT (chrome)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

org.keycloak.testsuite.forms.MultipleTabsLoginTest#multipleTabsParallelLoginTestWithAuthSessionExpiredAndRefreshInTab1

Keycloak CI - Forms IT (chrome)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@mposolda mposolda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@forkimenjeckayang Nice, Thanks for the PR!

I've added some comments inline. Also, one additional question: It looks that there are not much automated tests (or maybe none), which tests JwtProofValidator testing the full x5c validations (In other words: It looks that all the automated tests calling AttestationX509CertificateValidator.validate are calling it from AttestationProofValidator, but no tests for calling it from JwtProofValidator). Am I correct or did I overlooked?

IMO it is fine to go with this PR as is and then possibly create follow-up issue for add some more testing to JwtProofValidator. But just checking :-)

. Create or configure a trust-material identity provider, such as **Default Trust**, that exposes the trusted attester public keys or X.509 trust anchors.
** For `kid`-based attestations, configure a JWKS URL or validating public key.
** For `x5c`-based attestations, enable **Use X.509 attestation trust**, provide a PEM bundle containing only self-signed device-vendor attestation CA roots, and list the Extended Key Usage OIDs allowed for attestation end-entity certificates.
** Certificate revocation checking is enabled by default. Disable it only when the attestation ecosystem uses a separate mandatory revocation mechanism that is enforced elsewhere.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add more details about this option? For example:

  • How exactly is revocation verified?
  • Does the leaf certificate (or ca certificate) needs to have some elements in itself, which allow to validate this? Does it use revocation list, OCSP or something similar?

I am asking as it is not clear to me how this works and it looks that automated test OID4VCKeyAttestationTest does not have any successful test with certificate revocation checking enabled. If it is not clear, would it be possible to remove this option Certificate revocation checking for now and instead add some WARNING to this documentation that certificate revocation checking is not yet supported?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced the current behavior through the configured PKIX provider. In the standard crypto provider, enabling this flag invokes Bouncy Castle’s default CRL checker, but we do not provide a CRL store or configure CRL Distribution Point retrieval, and no OCSP checker is wired into this path. Therefore, there is no supported successful revocation configuration through Keycloak, which also explains why the test only covers failure when revocation information is unavailable.
I will remove the option for now, explicitly disable provider-default PKIX revocation, and document the limitation with a warning. Proper CRL/OCSP support should be handled separately with explicit configuration, Keycloak-managed network access, and successful/revoked/unavailable test coverage.

. Create or configure a trust-material identity provider, such as **Default Trust**, that exposes the trusted attester public keys. The identity provider must be configured with either a JWKS URL or a validating public key.
. Create or configure a trust-material identity provider, such as **Default Trust**, that exposes the trusted attester public keys or X.509 trust anchors.
** For `kid`-based attestations, configure a JWKS URL or validating public key.
** For `x5c`-based attestations, enable **Use X.509 attestation trust**, provide a PEM bundle containing only self-signed device-vendor attestation CA roots, and list the Extended Key Usage OIDs allowed for attestation end-entity certificates.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, for x5c-based attestations, it is required that extended key usage needs to be configured. Is it please possible to mention this more clearly in the documentation, that it is required in the configuration and also required to be present in the certificate? Also possibly with some example value, which is typically used for this (not sure if 1.3.6.1.4.1.55555.1.1 is typical OID used for this purpose or if different OIDs are usually used for this?)

@mposolda mposolda Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@forkimenjeckayang One additional thing: The OIDCIdentityProvider also implements TrustMaterialIdentityProvider . However it doesn't override resolveX509Trust method. So if I understand correctly, for OIDCIdentityProvider, it is not yet supported to handle x5c-based attestations? Might be likely good to document this limitation of OIDC Identity provider with some warning here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I clarified that the EKU configuration is mandatory and that the attestation leaf certificate must contain at least one EKU OID exactly matching a configured value. OID4VCI does not define a universal key-attestation EKU, so the documentation directs administrators to use the device-vendor or trust-framework OID and provides a documentation-only private-PKI example.
I also added a warning that the built-in OIDC identity provider exposes only JWK trust material. It cannot validate x5c chains because it does not implement X.509 trust resolution; x5c configurations must currently use Default Trust or a custom X.509-capable trust-material provider.

----

For a JWT proof with a `key_attestation` JOSE header, or for an `attestation` proof, {project_name} resolves the attester signing key from the configured identity providers. Key resolution can use the attestation JWT header and payload, including the `kid`, `alg`, and `iss` values. If no configured identity provider exposes a matching trusted key, proof validation fails.
For a JWT proof with a `key_attestation` JOSE header, or for an `attestation` proof, {project_name} resolves the attester signing key from the configured identity providers. Key resolution can use the attestation JWT header and payload, including the `kid`, `alg`, and `iss` values. If the attestation contains `x5c`, {project_name} builds the certificate path independently against each configured attestation trust provider and enforces the provider's EKU and revocation policy. The signing certificate must be an end-entity certificate, and configured trust anchors must be self-signed CA roots. A root included in the presented chain does not become trusted merely by being present; it must match a configured trust anchor. The JVM system truststore is never used as attestation trust. If no configured identity provider trusts the key or certificate chain, proof validation fails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to replace EKU with something like Extended key usage (EKU) . As it may not be clear what the shortcut means...

It could be nice if this documentation paragraph is a bit expanded (or converted into dedicated section) - maybe with some example picture/diagram of 2 certificates like ca and leaf and example showing that ca should be configured on IDP side and both leaf and ca included in the x5c of proof etc. As this topic is not trivial and might be nice to make sure that documentation is not confusing for people, who are not deeply familiar with the topic...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded this into a dedicated X.509 certificate-chain validation section with a diagram showing the relationship between the certificates presented in x5c and the root/EKU policy configured in the identity provider.
I clarified one detail: the leaf must be x5c[0], and any required intermediate certificates follow it. The configured root may also be included as the final entry, but it is not required because it is already available as the trust anchor. For a leaf issued directly by the configured root, x5c can therefore contain only the leaf. Including a root in x5c never makes it trusted by itself.

String leafPrivateKeyPem = X5C_TEST_CERTIFICATE_CHAIN.leafPrivateKeyPem();
runOnServer.run(session -> {
setupSessionContext(session);
runAttestationWithX5cCertificateChain(session, cNonce, caCertificatePem, leafCertificatePem,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: It seems confusing that using revocationEnabled=null as parameter of this method runAttestationWithX5cCertificateChain means testing with system truststore. Is it perhaps possible to rather introduce new flag to the method runAttestationWithX5cCertificateChain instead of mis-using revocationEnabled flag?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been addressed as part of removing the unsupported certificate-revocation option. runAttestationWithX5cCertificateChain now has a dedicated configureTrustProvider boolean. The system-truststore regression passes false, while tests using the configured X.509 attestation trust provider pass true. There is no longer a nullable revocationEnabled parameter or overloaded meaning.

@mposolda mposolda self-assigned this Jul 31, 2026
Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 10:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

@forkimenjeckayang

Copy link
Copy Markdown
Contributor Author

@forkimenjeckayang Nice, Thanks for the PR!

I've added some comments inline. Also, one additional question: It looks that there are not much automated tests (or maybe none), which tests JwtProofValidator testing the full x5c validations (In other words: It looks that all the automated tests calling AttestationX509CertificateValidator.validate are calling it from AttestationProofValidator, but no tests for calling it from JwtProofValidator). Am I correct or did I overlooked?

IMO it is fine to go with this PR as is and then possibly create follow-up issue for add some more testing to JwtProofValidator. But just checking :-)

You are correct. The existing JwtProofValidator tests covered nested key_attestation using kid/JWK trust, while the full PKIX and Extended Key Usage validation cases were exercised only through AttestationProofValidator.
I have parameterized the trusted-chain, system-truststore rejection, and disallowed-EKU regression tests over both attestatio and jwt proof types. The JWT cases now exercise JwtProofValidator through TrustedAttestationKeyResolver and AttestationX509CertificateValidator, so both proof entry points have equivalent coverage.

@mposolda mposolda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@forkimenjeckayang Thanks for the updates! I've added one comment inline, however I don't think that it is a blocker for this PR. Hopefully can be addressed as a follow-up...


The first `x5c` entry must be the certificate containing the public key that signed the attestation JWT. Each following entry must be an issuer of the previous certificate. The entries are base64-encoded DER certificates, not PEM values or base64url-encoded values.

Include any intermediate CA certificates required to connect the leaf to the configured root. The root certificate can be the final `x5c` entry, but it is not required because the same root is already configured as a trust anchor. For example, if the configured root directly issued the leaf, `x5c` can contain only the leaf certificate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When checking this, I am thinking about this corner-case scenario:

  • default-trust identity provider has configured certificate with subject=my-ca, issuer=my-ca, which is self-signed by ca-keypair1
  • The proof has x5c element with something like this (single leaf certificate, root certificate is omitted):
    • cert[0]: subject=my-leaf, issuer=my-ca, which is signed by ca-keypair2

In other words, the issuer from x5c is same value my-ca as the trusted CA certificate from identity provider configuration. However the x5c is signed by incorrect keypair ca-keypair2 (not by the ca-keypair1 from IDP config).

Just thinking about possible attack, when attacker knows the name of configured CA, however attacker creates his own CA certificate of same name, but signed by his own keypair (ca-keypair2), which he would use to sign his own root certificate and leaf certificate attached.

Do you think that such scenario would be rejected? Might be good to have automated test for this though?

@mposolda mposolda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@forkimenjeckayang Approving, Thanks! IMO the comment #51042 (comment) can be handled as a follow-up issue (either fixing this or just adding an automated test).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OID4VCI: Key attestation x5c chain validated against system cacerts with no EKU or revocation

3 participants