certificateChain, String type) {
+ KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.getSpecName()).toUpperCase());
+
+ KeyWrapper key = new KeyWrapper();
+
+ key.setProviderId(model.getId());
+ key.setProviderPriority(model.get("priority", 0L));
+
+ key.setKid(model.get(Attributes.KID_KEY) != null ? model.get(Attributes.KID_KEY) : KeyUtils.createKeyId(keyPair.getPublic()));
+ key.setUse(keyUse);
+ key.setType(type);
+ key.setAlgorithm(algorithm);
+ key.setStatus(status);
+ key.setPrivateKey(keyPair.getPrivate());
+ key.setPublicKey(keyPair.getPublic());
+ key.setCertificate(certificate);
+
+ if (!certificateChain.isEmpty()) {
+ if (certificate != null && !certificate.equals(certificateChain.get(0))) {
+ // just in case the chain does not contain the end-user certificate
+ certificateChain.add(0, certificate);
+ }
+ key.setCertificateChain(certificateChain);
+ }
+
+ return key;
+ }
+
/**
* Validates the giving certificate chain represented by {@code certificates}. If the list of certificates is empty
* or does not have at least 2 certificates (end-user certificate plus intermediary/root CAs) this method does nothing.
@@ -140,4 +222,10 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider {
validator.validate(certPath, params);
}
-}
+
+
+ @Override
+ public Stream getKeysStream() {
+ return Stream.of(key);
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
index e23cca0ff3..ea2f9b1651 100644
--- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
@@ -23,12 +23,15 @@ import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
+import org.keycloak.crypto.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
+import java.util.stream.Stream;
import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
@@ -36,7 +39,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
/**
* @author Stian Thorgersen
*/
-public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactory {
+public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory {
private static final Logger logger = Logger.getLogger(JavaKeystoreKeyProviderFactory.class);
public static final String ID = "java-keystore";
@@ -62,6 +65,7 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
private List configProperties;
+
@Override
public void init(Config.Scope config) {
String[] supportedKeystoreTypes = CryptoIntegration.getProvider().getSupportedKeyStoreTypes()
@@ -71,7 +75,11 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
"Keystore type. This parameter is not mandatory. If omitted, the type will be detected from keystore file or default keystore type will be used", LIST_TYPE,
supportedKeystoreTypes.length > 0 ? supportedKeystoreTypes[0] : null, supportedKeystoreTypes);
- configProperties = AbstractRsaKeyProviderFactory.configurationBuilder()
+ configProperties = ProviderConfigurationBuilder.create()
+ .property(Attributes.PRIORITY_PROPERTY)
+ .property(Attributes.ENABLED_PROPERTY)
+ .property(Attributes.ACTIVE_PROPERTY)
+ .property(mergedAlgorithmProperties())
.property(KEYSTORE_PROPERTY)
.property(KEYSTORE_PASSWORD_PROPERTY)
.property(keystoreTypeProperty)
@@ -88,9 +96,11 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
- super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
+ .checkLong(Attributes.PRIORITY_PROPERTY, false)
+ .checkBoolean(Attributes.ENABLED_PROPERTY, false)
+ .checkBoolean(Attributes.ACTIVE_PROPERTY, false)
.checkSingle(KEYSTORE_PROPERTY, true)
.checkSingle(KEYSTORE_PASSWORD_PROPERTY, true)
.checkSingle(keystoreTypeProperty, false)
@@ -105,6 +115,14 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
}
}
+ // merge the algorithms supported for RSA and EC keys and provide them as one configuration property
+ private static ProviderConfigProperty mergedAlgorithmProperties() {
+ List ecAlgorithms = List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
+ List algorithms = Stream.concat(Attributes.RS_ALGORITHM_PROPERTY.getOptions().stream(), ecAlgorithms.stream()).toList();
+ return new ProviderConfigProperty(Attributes.ALGORITHM_KEY, "Algorithm", "Intended algorithm for the key", LIST_TYPE, algorithms.toArray());
+
+ }
+
@Override
public String getHelpText() {
return HELP_TEXT;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java
index 3afeeac700..b88b3461d3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java
@@ -1,22 +1,27 @@
package org.keycloak.testsuite.util;
+import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
+import org.keycloak.keys.AbstractEcdsaKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
-import jakarta.ws.rs.core.Response;
+import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
+import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@@ -31,6 +36,20 @@ import static org.junit.Assert.fail;
* @author mhajas
*/
public class KeyUtils {
+ public static KeyPair generateECKey(String algorithm) {
+
+ try {
+ KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA");
+ String domainParamNistRep = AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm);
+ String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep);
+ ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve);
+ kpg.initialize(parameterSpec);
+ return kpg.generateKeyPair();
+ } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public static PublicKey publicKeyFromString(String key) {
try {
@@ -106,6 +125,7 @@ public class KeyUtils {
public static AutoCloseable generateNewRealmKey(RealmResource realm, KeyUse keyUse, String algorithm) {
return generateNewRealmKey(realm, keyUse, algorithm, "100");
}
+
/**
* @return key sizes, which are expected to be supported by Keycloak server for {@link org.keycloak.keys.GeneratedRsaKeyProviderFactory} and {@link org.keycloak.keys.GeneratedRsaEncKeyProviderFactory}.
*/
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java
index 4615118050..240b780861 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java
@@ -19,15 +19,6 @@
package org.keycloak.testsuite.util;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.security.KeyPair;
-import java.security.KeyStore;
-import java.security.cert.Certificate;
-import java.security.cert.X509Certificate;
-import java.util.Arrays;
-import java.util.stream.Stream;
-
import org.junit.Assume;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.crypto.CryptoIntegration;
@@ -37,6 +28,15 @@ import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.PemUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
import static org.junit.Assert.fail;
/**
@@ -63,16 +63,15 @@ public class KeystoreUtils {
.anyMatch(type -> type.equals(keystoreType.toString())));
}
- public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception {
+ public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword, KeyPair keyPair) throws Exception {
String fileName = "keystore." + keystoreType.getPrimaryExtension();
- KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject);
KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(keystoreType);
keyStore.load(null, null);
- Certificate[] chain = {certificate};
+ Certificate[] chain = {certificate};
keyStore.setKeyEntry(subject, keyPair.getPrivate(), keyPassword.trim().toCharArray(), chain);
File file = folder.newFile(fileName);
@@ -85,6 +84,10 @@ public class KeystoreUtils {
return new KeystoreInfo(certRep, file);
}
+ public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception {
+ return generateKeystore(folder, keystoreType, subject, keystorePassword, keyPassword, KeyUtils.generateRsaKeyPair(2048));
+ }
+
public static class KeystoreInfo {
private final CertificateRepresentation certificateInfo;
private final File keystoreFile;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
index e5f123b48a..4ed7b31d36 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java
@@ -17,12 +17,15 @@
package org.keycloak.testsuite.keys;
+import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
@@ -36,13 +39,14 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.KeystoreUtils;
-import jakarta.ws.rs.core.Response;
+import java.security.PublicKey;
import java.util.List;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.common.util.KeystoreUtil.KeystoreFormat.PKCS12;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@@ -64,6 +68,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Page
protected LoginPage loginPage;
private KeystoreUtils.KeystoreInfo generatedKeystore;
+ private String keyAlgorithm;
@Override
public void addTestRealms(List testRealms) {
@@ -72,34 +77,49 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
}
@Test
- public void createJks() throws Exception {
- createSuccess(KeystoreUtil.KeystoreFormat.JKS);
+ public void createJksRSA() throws Exception {
+ createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA);
}
@Test
- public void createPkcs12() throws Exception {
- createSuccess(PKCS12);
+ public void createPkcs12RSA() throws Exception {
+ createSuccess(PKCS12, AlgorithmType.RSA);
}
@Test
- public void createBcfks() throws Exception {
- createSuccess(KeystoreUtil.KeystoreFormat.BCFKS);
+ public void createBcfksRSA() throws Exception {
+ createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA);
}
- private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
+ @Test
+ public void createJksECDSA() throws Exception {
+ createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA);
+ }
+
+ @Test
+ public void createPkcs12ECDSA() throws Exception {
+ createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA);
+ }
+
+ @Test
+ public void createBcfksECDSA() throws Exception {
+ createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA);
+ }
+
+ private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception {
KeystoreUtils.assumeKeystoreTypeSupported(keystoreType);
- generateKeystore(keystoreType);
+ generateKeystore(keystoreType, algorithmType);
long priority = System.currentTimeMillis();
- ComponentRepresentation rep = createRep("valid", priority);
+ ComponentRepresentation rep = createRep("valid", priority, keyAlgorithm);
Response response = adminClient.realm("test").components().add(rep);
String id = ApiUtil.getCreatedId(response);
getCleanup().addComponentId(id);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
- assertEquals(5, createdRep.getConfig().size());
+ assertEquals(6, createdRep.getConfig().size());
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keystorePassword"));
assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keyPassword"));
@@ -109,16 +129,29 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
- assertEquals(AlgorithmType.RSA.name(), key.getType());
+ switch (algorithmType) {
+ case RSA: {
+ assertEquals(algorithmType.name(), key.getType());
+ PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "RSA");
+ PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "RSA");
+ assertEquals(exp, got);
+ break;
+ }
+ case ECDSA:
+ assertEquals("EC", key.getType());
+ PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "EC");
+ PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "EC");
+ assertEquals(exp, got);
+ }
+
assertEquals(priority, key.getProviderPriority());
- assertEquals(generatedKeystore.getCertificateInfo().getPublicKey(), key.getPublicKey());
assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate());
}
@Test
public void invalidKeystore() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
- ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
+ ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keystore", "/nosuchfile");
Response response = adminClient.realm("test").components().add(rep);
@@ -128,7 +161,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Test
public void invalidKeystorePassword() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
- ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
+ ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keystore", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@@ -138,7 +171,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Test
public void invalidKeyAlias() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
- ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
+ ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keyAlias", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@@ -158,7 +191,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
log.infof("Fallback to keystore type '%s' for the invalidKeyPassword() test", keystoreType);
}
generateKeystore(keystoreType);
- ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
+ ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keyPassword", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@@ -176,7 +209,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
response.close();
}
- protected ComponentRepresentation createRep(String name, long priority) {
+ protected ComponentRepresentation createRep(String name, long priority, String algorithm) {
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName(name);
rep.setParentId(adminClient.realm("test").toRepresentation().getId());
@@ -188,11 +221,25 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle("keystorePassword", "password");
rep.getConfig().putSingle("keyAlias", "selfsigned");
rep.getConfig().putSingle("keyPassword", "password");
+ rep.getConfig().putSingle("algorithm", algorithm);
return rep;
}
private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
- this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password");
+ generateKeystore(keystoreType, AlgorithmType.RSA);
+ }
+
+ private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception {
+ switch (algorithmType) {
+ case RSA: {
+ this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password");
+ this.keyAlgorithm = Algorithm.RS256;
+ return;
+ }
+ case ECDSA:
+ this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password", KeyUtils.generateECKey(Algorithm.ES256));
+ this.keyAlgorithm = Algorithm.ES256;
+ }
}
}