From 5732946388d024c5fd930bfa2e60aa02c3888e55 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Mon, 29 Sep 2025 09:23:56 +0200 Subject: [PATCH] Add ECDSA as a valid key type that should return EC public key Closes #42588 Signed-off-by: rmartinc --- .../org/keycloak/crypto/JavaAlgorithm.java | 2 + .../AbstractClientAuthSignedJWTTest.java | 119 +++++++++++------- .../oauth/ClientAuthEdDSASignedJWTTest.java | 21 ++++ .../oauth/ClientAuthSignedJWTTest.java | 40 ++++-- .../client-auth-test/certificate.pem | 21 ---- .../resources/client-auth-test/publickey.pem | 9 -- 6 files changed, 130 insertions(+), 82 deletions(-) delete mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem delete mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index 6d5be702c1c..ed7a9ca0088 100644 --- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -33,6 +33,7 @@ public class JavaAlgorithm { public static final String Ed25519 = "Ed25519"; public static final String Ed448 = "Ed448"; public static final String AES = "AES"; + public static final String ECDSA = "ECDSA"; public static final String SHA256 = "SHA-256"; public static final String SHA384 = "SHA-384"; @@ -135,6 +136,7 @@ public class JavaAlgorithm { case KeyType.RSA: return KeyType.RSA; case KeyType.EC: + case ECDSA: return KeyType.EC; case Algorithm.EdDSA: case Algorithm.Ed448: diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java index 6135154112d..1685a8a21d2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -29,6 +30,7 @@ import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyStore; @@ -221,6 +223,7 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe .id(KeycloakModelUtils.generateId()) .clientId("client3") .directAccessGrants() + .redirectUris(OAuthClient.APP_ROOT + "/auth") .authenticatorType(JWTClientAuthenticator.PROVIDER_ID) .build(); @@ -230,22 +233,7 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe } public void testCodeToTokenRequestSuccess(String algorithm) throws Exception { - oauth.clientId("client2"); - oauth.doLogin("test-user@localhost", "password"); - EventRepresentation loginEvent = events.expectLogin() - .client("client2") - .assertEvent(); - - String code = oauth.parseLoginResponse().getCode(); - AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT(algorithm)); - - assertEquals(200, response.getStatusCode()); - oauth.verifyToken(response.getAccessToken()); - oauth.parseRefreshToken(response.getRefreshToken()); - events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()) - .client("client2") - .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) - .assertEvent(); + testCodeToTokenRequestSuccess("client2", getClient2KeyPair(), algorithm, null); } public void testCodeToTokenRequestSuccessForceAlgInClient(String algorithm) throws Exception { @@ -294,43 +282,62 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe } } + public void testUploadCertificatePEM(KeyPair keyPair, String algorithm, String curve) throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.BCFKS); + KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.BCFKS, "clientkey", "pwd2", "keypass", keyPair); + try { + Path tempFile = Files.createTempFile("cert_", ".pem"); + try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) { + writer.write(ksInfo.getCertificateInfo().getCertificate()); + } + testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM, + tempFile.toFile().getAbsolutePath(), "undefined", "undefined"); + Files.delete(tempFile); + + testCodeToTokenRequestSuccess("client3", keyPair, algorithm, curve); + } finally { + ksInfo.getKeystoreFile().delete(); + } + } + + protected void testUploadPublicKeyPem(KeyPair keyPair, String algorithm, String curve) throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.BCFKS); + KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.BCFKS, "clientkey", "pwd2", "keypass", keyPair); + try { + Path tempFile = Files.createTempFile("pubkey_", ".pem"); + try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) { + writer.write(ksInfo.getCertificateInfo().getPublicKey()); + } + testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM, + tempFile.toFile().getAbsolutePath(), "undefined", "undefined"); + Files.delete(tempFile); + + testCodeToTokenRequestSuccess("client3", keyPair, algorithm, curve); + } finally { + ksInfo.getKeystoreFile().delete(); + } + } + protected void testCodeToTokenRequestSuccess(String algorithm, boolean useJwksUri) throws Exception { testCodeToTokenRequestSuccess(algorithm, null, useJwksUri); } + private KeyPair setupKeyPair(ClientRepresentation clientRepresentation, ClientResource clientResource, + String algorithm, String curve, boolean useJwksUri) throws Exception { + if (useJwksUri) { + return setupJwksUrl(algorithm, curve, true, false, null, clientRepresentation, clientResource); + } else { + return setupJwks(algorithm, curve, clientRepresentation, clientResource); + } + } + protected void testCodeToTokenRequestSuccess(String algorithm, String curve, boolean useJwksUri) throws Exception { ClientRepresentation clientRepresentation = app2; ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId()); clientRepresentation = clientResource.toRepresentation(); try { - // setup Jwks - KeyPair keyPair; - if (useJwksUri) { - keyPair = setupJwksUrl(algorithm, curve, true, false, null, clientRepresentation, clientResource); - } else { - keyPair = setupJwks(algorithm, curve, clientRepresentation, clientResource); - } - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - - // test - oauth.clientId("client2"); - oauth.doLogin("test-user@localhost", "password"); - EventRepresentation loginEvent = events.expectLogin() - .client("client2") - .assertEvent(); - - String code = oauth.parseLoginResponse().getCode(); - AccessTokenResponse response = doAccessTokenRequest(code, - createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm, curve)); - - assertEquals(200, response.getStatusCode()); - oauth.verifyToken(response.getAccessToken()); - oauth.parseRefreshToken(response.getRefreshToken()); - events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()) - .client("client2") - .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) - .assertEvent(); + KeyPair keyPair = setupKeyPair(clientRepresentation, clientResource, algorithm, curve, useJwksUri); + testCodeToTokenRequestSuccess("client2", keyPair, algorithm, curve); } finally { // Revert jwks settings if (useJwksUri) { @@ -341,6 +348,30 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe } } + protected void testCodeToTokenRequestSuccess(String clientId, KeyPair keyPair, String algorithm, String curve) throws Exception { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + // test + oauth.realm("test").clientId(clientId); + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin() + .client(clientId) + .assertEvent(); + + String code = oauth.parseLoginResponse().getCode(); + AccessTokenResponse response = doAccessTokenRequest(code, + createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, algorithm, curve)); + + assertEquals(200, response.getStatusCode()); + oauth.verifyToken(response.getAccessToken()); + oauth.parseRefreshToken(response.getRefreshToken()); + events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()) + .client(clientId) + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + } + protected void testDirectGrantRequestSuccess(String algorithm) throws Exception { ClientRepresentation clientRepresentation = app2; ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java index a5537482103..4b296329c48 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.oauth; import org.junit.Test; import org.keycloak.crypto.Algorithm; +import org.keycloak.testsuite.util.KeyUtils; /** * @author Takashi Norimatsu @@ -35,6 +36,26 @@ public class ClientAuthEdDSASignedJWTTest extends AbstractClientAuthSignedJWTTes testCodeToTokenRequestSuccess(Algorithm.EdDSA, Algorithm.Ed25519, false); } + @Test + public void testUploadCertificatePemEd25519() throws Exception { + testUploadCertificatePEM(KeyUtils.generateEdDSAKey(Algorithm.Ed25519), Algorithm.EdDSA, Algorithm.Ed25519); + } + + @Test + public void testUploadCertificatePemEd448() throws Exception { + testUploadCertificatePEM(KeyUtils.generateEdDSAKey(Algorithm.Ed448), Algorithm.EdDSA, Algorithm.Ed448); + } + + @Test + public void testUploadPublicKeyPemEd25519() throws Exception { + testUploadPublicKeyPem(KeyUtils.generateEdDSAKey(Algorithm.Ed25519), Algorithm.EdDSA, Algorithm.Ed25519); + } + + @Test + public void testUploadPublicKeyPemEd448() throws Exception { + testUploadPublicKeyPem(KeyUtils.generateEdDSAKey(Algorithm.Ed448), Algorithm.EdDSA, Algorithm.Ed448); + } + @Override protected String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm, String curve) { if (!Algorithm.EdDSA.equals(jwaAlgorithm)) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 77f6f80b3e1..8fcacdbe7f2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -46,6 +46,7 @@ import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.KeystoreUtils; +import org.keycloak.testsuite.util.KeyUtils; import org.keycloak.testsuite.util.SignatureSignerUtil; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; @@ -361,30 +362,53 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest { public void testUploadKeystoreJKS() throws Exception { KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.JKS); testUploadKeystore("JKS", generatedKeystoreClient1.getKeystoreFile().getAbsolutePath(), "clientkey", "storepass"); + testCodeToTokenRequestSuccess("client3", keyPairClient1, Algorithm.RS256, null); } @Test public void testUploadKeystorePKCS12() throws Exception { KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.PKCS12); - KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.PKCS12, "clientkey", "pwd2", "keypass"); - testUploadKeystore(KeystoreFormat.PKCS12.toString(), ksInfo.getKeystoreFile().getAbsolutePath(), "clientkey", "pwd2"); + KeyPair keyPair = org.keycloak.common.util.KeyUtils.generateRsaKeyPair(2048); + KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.PKCS12, "clientkey", "pwd2", "keypass", keyPair); + try { + testUploadKeystore(KeystoreFormat.PKCS12.toString(), ksInfo.getKeystoreFile().getAbsolutePath(), "clientkey", "pwd2"); + testCodeToTokenRequestSuccess("client3", keyPair, Algorithm.RS256, null); + } finally { + ksInfo.getKeystoreFile().delete(); + } } @Test public void testUploadKeystoreBCFKS() throws Exception { KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.BCFKS); - KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.BCFKS, "clientkey", "pwd2", "keypass"); - testUploadKeystore(KeystoreFormat.BCFKS.toString(), ksInfo.getKeystoreFile().getAbsolutePath(), "clientkey", "pwd2"); + KeyPair keyPair = org.keycloak.common.util.KeyUtils.generateRsaKeyPair(2048); + KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.BCFKS, "clientkey", "pwd2", "keypass", keyPair); + try { + testUploadKeystore(KeystoreFormat.BCFKS.toString(), ksInfo.getKeystoreFile().getAbsolutePath(), "clientkey", "pwd2"); + testCodeToTokenRequestSuccess("client3", keyPair, Algorithm.RS256, null); + } finally { + ksInfo.getKeystoreFile().delete(); + } } @Test - public void testUploadCertificatePEM() throws Exception { - testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM, "client-auth-test/certificate.pem", "undefined", "undefined"); + public void testUploadCertificatePemRsa() throws Exception { + testUploadCertificatePEM(org.keycloak.common.util.KeyUtils.generateRsaKeyPair(2048), Algorithm.RS256, null); } @Test - public void testUploadPublicKeyPEM() throws Exception { - testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM, "client-auth-test/publickey.pem", "undefined", "undefined"); + public void testUploadCertificatePemEcdsa() throws Exception { + testUploadCertificatePEM(KeyUtils.generateECKey(Algorithm.ES256), Algorithm.ES256, null); + } + + @Test + public void testUploadPublicKeyPemRsa() throws Exception { + testUploadPublicKeyPem(org.keycloak.common.util.KeyUtils.generateRsaKeyPair(2048), Algorithm.RS256, null); + } + + @Test + public void testUploadPublicKeyPemEcdsa() throws Exception { + testUploadPublicKeyPem(KeyUtils.generateECKey(Algorithm.ES256), Algorithm.ES256, null); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem deleted file mode 100644 index a526c93da5f..00000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/certificate.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDXTCCAkWgAwIBAgIJAIzE3vQp7EQWMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwHhcNMTYwMjI5MDgzMDU0WhcNNDMwNzE2MDgzMDU0WjBF -MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 -ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAp1+GzdEkt2FZbISXYO12503FL6Oh8s4+tJ2fE66N8IezhugP8xiySDfW -TEMaO5Z2TaTnQQoF9SSZ9Edq1GPxpBX0cdkCOBopEGdlb3hUYDeMaDMs18KGemUc -Fj+CWB5VVcbmWMJ36WCz7FC+Oe38tmujR1AJpJL3pwqazyWIZzPqX8rW+rrNPGKP -C96oBPZMb4RJWivLBJi/o5MGSpo1sJNtxyF4zUUI00LX0wZAV1HH1XErd1Vz41on -nmB+tj9nevVRR4rDV280IELp9Ud0PIb3w843uJtwfSAwVG0pT6hv1VBDrBxTS08N -dPU8CtkQAXzCCr8nqfAbUFOhcWRQgQIDAQABo1AwTjAdBgNVHQ4EFgQUFE+uUZAI -n57ArEylqhCmHkAenTEwHwYDVR0jBBgwFoAUFE+uUZAIn57ArEylqhCmHkAenTEw -DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApkgD3OCtw3+sk7GR1YaJ -xNd8HT+fxXmnLqGnCQWX8lRIg5vj1PDMRev6vlIK3JfQV3zajcpKFfpy96klWsJy -ZLYBVW2QOtMzDdQ9I8dS4Pn/SJ/Vo/M/ucfY4ttcuUL3oQCrI/c/u9tcamGMfbwd -658MlXrUvt4B6qXY5AbgUvYR25P86uw7hSFMq5tQftNQsLbOh2FEeIiKhpgI7w8S -SPajaWjUXsfHc5H7f9MciE2NS1Vd3AViGrVWP1rgQ1Iv0UyQVQrnjmIs12ENJmTd -5lDqra5FJhaO7+RUG6er8n8HwXzhHkPmezGqtxWKikjitqvDY9prB3omJSa4Led+ -AQ== ------END CERTIFICATE----- diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem deleted file mode 100644 index 3169a04fc47..00000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApnZ/E2BUULHjsRiSnEgZ -4vGe15BRqZPdkHR+NcvVYpThc7JqY6nZrdrwO9sOjlMC5e2Q18Fypi4KbJpGSe9r -0DPgcbPsHSoe2xFO3M8XBE0DyoRblaQFhe6p/sj3ak32k2zn+fMZUmlx/MTNQh1I -Cki7So0NDCBXt8XGZNnEyvKeXOUZP5qicP9KxVAQiWJvlkaTjc8rrRTmf+HWw/Qf -gQC0tzBRpa7T+RpW9O+rnWfOaNfTkTb9itIc+ZOa2Z4iidZ7+ifMOp9cNT641Wb6 -iYqJ2ufqY+msxI54tYM1tPgGS7r4SnCwmnqTaO383wXUl8TQ7qStmAWIepV3nNyu -AQIDAQAB ------END PUBLIC KEY----- \ No newline at end of file