Add ECDSA as a valid key type that should return EC public key

Closes #42588

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2025-09-29 09:23:56 +02:00 committed by Marek Posolda
parent 9f9f5ae97a
commit 5732946388
6 changed files with 130 additions and 82 deletions

View File

@ -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:

View File

@ -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());

View File

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
@ -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)) {

View File

@ -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

View File

@ -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-----

View File

@ -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-----