diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d617d1d5ba1..23d61cab345 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -319,7 +319,8 @@ jobs: run: | declare -A PARAMS TESTGROUP PARAMS["bcfips-nonapproved-pkcs12"]="-Pauth-server-quarkus,auth-server-fips140-2" - TESTGROUP["group1"]="-Dtest=org.keycloak.testsuite.forms.**" # Tests in the package "forms" + # Tests in the package "forms" and some keystore related tests + TESTGROUP["group1"]="-Dtest=org.keycloak.testsuite.forms.**,ClientAuthSignedJWTTest,CredentialsTest,JavaKeystoreKeyProviderTest,ServerInfoTest" ./mvnw clean install -nsu -B ${PARAMS["${{ matrix.server }}"]} ${TESTGROUP["${{ matrix.tests }}"]} -f testsuite/integration-arquillian/tests/base/pom.xml | misc/log/trimmer.sh diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java index 901b04d6872..db43c02159d 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java @@ -15,6 +15,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; import java.security.spec.ECParameterSpec; +import java.util.stream.Stream; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; @@ -89,6 +90,21 @@ public interface CryptoProvider { KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException, NoSuchProviderException; + /** + * @return Keystore types/algorithms supported by this CryptoProvider + */ + default Stream getSupportedKeyStoreTypes() { + return Stream.of(KeystoreFormat.values()) + .filter(format -> { + try { + getKeyStore(format); + return true; + } catch (KeyStoreException | NoSuchProviderException ex) { + return false; + } + }); + } + CertificateFactory getX509CertFactory() throws CertificateException, NoSuchProviderException; CertStore getCertStore(CollectionCertStoreParameters collectionCertStoreParameters) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException; diff --git a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java index b46b92f815a..a05ff43c1c6 100755 --- a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java +++ b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java @@ -27,6 +27,8 @@ import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Arrays; +import java.util.Optional; /** * @author Bill Burke @@ -35,12 +37,24 @@ import java.security.PublicKey; public class KeystoreUtil { public enum KeystoreFormat { - JKS, - PKCS12 + JKS("jks"), + PKCS12("p12"), + BCFKS("bcfks"); + + // Typical file extension for this keystore format + private final String fileExtension; + KeystoreFormat(String extension) { + this.fileExtension = extension; + } + + public String getFileExtension() { + return fileExtension; + } } public static KeyStore loadKeyStore(String filename, String password) throws Exception { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + String keystoreType = getKeystoreType(null, filename, KeyStore.getDefaultType()); + KeyStore trustStore = KeyStore.getInstance(keystoreType); InputStream trustStream = null; if (filename.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { String resourcePath = filename.replace(GenericConstants.PROTOCOL_CLASSPATH, ""); @@ -79,4 +93,31 @@ public class KeystoreUtil { throw new RuntimeException("Failed to load private key: " + e.getMessage(), e); } } + + + /** + * Try to return supported keystore type + * + * @param preferredType The preferred format - usually the one from the configuration. When present, it should be preferred over anything else + * @param path Path of the file. We can try to detect keystore type from that (EG. my-keystore.pkcs12 will return "pkcs12") in case that preferredType is not defined + * @param defaultType Default format as last fallback when none of the above can be used. Should be non-null + * @return format as specified above + */ + public static String getKeystoreType(String preferredType, String path, String defaultType) { + // Configured type has precedence + if (preferredType != null) return preferredType; + + // Fallback to path + int lastDotIndex = path.lastIndexOf('.'); + if (lastDotIndex > -1) { + String ext = path.substring(lastDotIndex + 1).toLowerCase(); + Optional detectedType = Arrays.stream(KeystoreUtil.KeystoreFormat.values()) + .filter(ksFormat -> ksFormat.getFileExtension().equals(ext)) + .findFirst(); + if (detectedType.isPresent()) return detectedType.get().toString(); + } + + // Fallback to default + return defaultType; + } } diff --git a/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java new file mode 100644 index 00000000000..97f77db9094 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/info/CryptoInfoRepresentation.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.representations.info; + +import java.util.List; +import java.util.stream.Collectors; + +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.crypto.CryptoProvider; +import org.keycloak.common.util.KeystoreUtil; + +/** + * @author Marek Posolda + */ +public class CryptoInfoRepresentation { + + private String cryptoProvider; + private List supportedKeystoreTypes; + + public static CryptoInfoRepresentation create() { + CryptoInfoRepresentation info = new CryptoInfoRepresentation(); + + CryptoProvider cryptoProvider = CryptoIntegration.getProvider(); + info.cryptoProvider = cryptoProvider.getClass().getSimpleName(); + info.supportedKeystoreTypes = CryptoIntegration.getProvider().getSupportedKeyStoreTypes() + .map(KeystoreUtil.KeystoreFormat::toString) + .collect(Collectors.toList()); + + return info; + } + + public String getCryptoProvider() { + return cryptoProvider; + } + + public void setCryptoProvider(String cryptoProvider) { + this.cryptoProvider = cryptoProvider; + } + + public List getSupportedKeystoreTypes() { + return supportedKeystoreTypes; + } + + public void setSupportedKeystoreTypes(List supportedKeystoreTypes) { + this.supportedKeystoreTypes = supportedKeystoreTypes; + } +} diff --git a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java index 8c98ce3468b..42c479a086c 100755 --- a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java @@ -34,6 +34,8 @@ public class ServerInfoRepresentation { private MemoryInfoRepresentation memoryInfo; private ProfileInfoRepresentation profileInfo; + private CryptoInfoRepresentation cryptoInfo; + private Map> themes; private List> socialProviders; @@ -75,6 +77,14 @@ public class ServerInfoRepresentation { this.profileInfo = profileInfo; } + public CryptoInfoRepresentation getCryptoInfo() { + return cryptoInfo; + } + + public void setCryptoInfo(CryptoInfoRepresentation cryptoInfo) { + this.cryptoInfo = cryptoInfo; + } + public Map> getThemes() { return themes; } diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultKeyStoreTypesTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultKeyStoreTypesTest.java new file mode 100644 index 00000000000..1e6df5487f3 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultKeyStoreTypesTest.java @@ -0,0 +1,41 @@ +package org.keycloak.crypto.def.test; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.rule.CryptoInitRule; + +/** + * @author Marek Posolda + */ +public class DefaultKeyStoreTypesTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Test + public void testKeystoreFormats() { + Set supportedKeystoreFormats = CryptoIntegration.getProvider().getSupportedKeyStoreTypes().collect(Collectors.toSet()); + Assert.assertThat(supportedKeystoreFormats, Matchers.containsInAnyOrder( + KeystoreUtil.KeystoreFormat.JKS, + KeystoreUtil.KeystoreFormat.PKCS12, + KeystoreUtil.KeystoreFormat.BCFKS)); + } + + @Test + public void testDefaultKeystoreType() { + Assert.assertEquals("PKCS12", KeystoreUtil.getKeystoreType("PKCS12", "some/foo.jks", "JKS")); + Assert.assertEquals("PKCS12", KeystoreUtil.getKeystoreType("PKCS12", "some/foo.pkcs12", "JKS")); + Assert.assertEquals("PKCS12", KeystoreUtil.getKeystoreType("PKCS12", "some/foo.bcfks", "JKS")); + Assert.assertEquals("JKS", KeystoreUtil.getKeystoreType(null, "some/foo.jks", "JKS")); + Assert.assertEquals("PKCS12", KeystoreUtil.getKeystoreType(null, "some/foo.p12", "JKS")); + Assert.assertEquals("BCFKS", KeystoreUtil.getKeystoreType(null, "some/foo.bcfks", "JKS")); + Assert.assertEquals("JKS", KeystoreUtil.getKeystoreType(null, "some/foo.bcfksl", "JKS")); + } +} diff --git a/crypto/elytron/pom.xml b/crypto/elytron/pom.xml index 564a5e00c8b..020514e89c5 100644 --- a/crypto/elytron/pom.xml +++ b/crypto/elytron/pom.xml @@ -66,6 +66,11 @@ junit test + + org.hamcrest + hamcrest + test + diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronKeyStoreTypesTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronKeyStoreTypesTest.java new file mode 100644 index 00000000000..d3be5db2a94 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronKeyStoreTypesTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.crypto.elytron.test; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.rule.CryptoInitRule; + +/** + * @author Marek Posolda + */ +public class ElytronKeyStoreTypesTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + // No BCFKS keystore type supported for elytron + @Test + public void testKeystoreFormats() { + Set supportedKeystoreFormats = CryptoIntegration.getProvider().getSupportedKeyStoreTypes().collect(Collectors.toSet()); + Assert.assertThat(supportedKeystoreFormats, Matchers.containsInAnyOrder( + KeystoreUtil.KeystoreFormat.JKS, + KeystoreUtil.KeystoreFormat.PKCS12 + )); + } +} diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402KeystoreTypesTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402KeystoreTypesTest.java new file mode 100644 index 00000000000..ca64ccbfa6c --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402KeystoreTypesTest.java @@ -0,0 +1,49 @@ +package org.keycloak.crypto.fips.test; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.Environment; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.rule.CryptoInitRule; + +/** + * @author Marek Posolda + */ +public class FIPS1402KeystoreTypesTest { + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + @Before + public void before() { + // Run this test just if java is in FIPS mode + Assume.assumeTrue("Java is not in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode()); + } + + @Test + public void testKeystoreFormatsInNonApprovedMode() { + Assume.assumeFalse(CryptoServicesRegistrar.isInApprovedOnlyMode()); + Set supportedKeystoreFormats = CryptoIntegration.getProvider().getSupportedKeyStoreTypes().collect(Collectors.toSet()); + Assert.assertThat(supportedKeystoreFormats, Matchers.containsInAnyOrder( + KeystoreUtil.KeystoreFormat.PKCS12, + KeystoreUtil.KeystoreFormat.BCFKS)); + } + + // BCFIPS approved mode supports only BCFKS. No JKS nor PKCS12 support for keystores + @Test + public void testKeystoreFormatsInApprovedMode() { + Assume.assumeTrue(CryptoServicesRegistrar.isInApprovedOnlyMode()); + Set supportedKeystoreFormats = CryptoIntegration.getProvider().getSupportedKeyStoreTypes().collect(Collectors.toSet()); + Assert.assertThat(supportedKeystoreFormats, Matchers.containsInAnyOrder( + KeystoreUtil.KeystoreFormat.BCFKS)); + } +} diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index 0e2fc05567e..2d3b9433a1f 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -19,6 +19,7 @@ package org.keycloak.keys; import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.KeystoreUtil; import org.keycloak.component.ComponentModel; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; @@ -61,8 +62,11 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider { @Override protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) { - try (FileInputStream is = new FileInputStream(model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_KEY))) { - KeyStore keyStore = KeyStore.getInstance("JKS"); + String keystorePath = model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_KEY); + try (FileInputStream is = new FileInputStream(keystorePath)) { + // Use "JKS" as default type for backwards compatibility + String keystoreType = KeystoreUtil.getKeystoreType(model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_TYPE_KEY), keystorePath, "JKS"); + KeyStore keyStore = KeyStore.getInstance(keystoreType); keyStore.load(is, model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_PASSWORD_KEY).toCharArray()); String keyAlias = model.get(JavaKeystoreKeyProviderFactory.KEY_ALIAS_KEY); diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java index ff39ab94484..e23cca0ff34 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java @@ -18,6 +18,9 @@ package org.keycloak.keys; import org.jboss.logging.Logger; +import org.keycloak.Config; +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.models.KeycloakSession; @@ -27,6 +30,7 @@ import org.keycloak.provider.ProviderConfigProperty; import java.util.List; +import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE; /** @@ -43,6 +47,11 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor public static String KEYSTORE_PASSWORD_KEY = "keystorePassword"; public static ProviderConfigProperty KEYSTORE_PASSWORD_PROPERTY = new ProviderConfigProperty(KEYSTORE_PASSWORD_KEY, "Keystore Password", "Password for the keys", STRING_TYPE, null, true); + public static String KEYSTORE_TYPE_KEY = "keystoreType"; + + // Initialization of this property is postponed to "init()" due the CryptoProvider must be set + private ProviderConfigProperty keystoreTypeProperty; + public static String KEY_ALIAS_KEY = "keyAlias"; public static ProviderConfigProperty KEY_ALIAS_PROPERTY = new ProviderConfigProperty(KEY_ALIAS_KEY, "Key Alias", "Alias for the private key", STRING_TYPE, null); @@ -51,13 +60,26 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor private static final String HELP_TEXT = "Loads keys from a Java keys file"; - private static final List CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder() - .property(KEYSTORE_PROPERTY) - .property(KEYSTORE_PASSWORD_PROPERTY) - .property(KEY_ALIAS_PROPERTY) - .property(KEY_PASSWORD_PROPERTY) - .property(Attributes.KEY_USE_PROPERTY) - .build(); + private List configProperties; + + @Override + public void init(Config.Scope config) { + String[] supportedKeystoreTypes = CryptoIntegration.getProvider().getSupportedKeyStoreTypes() + .map(KeystoreUtil.KeystoreFormat::toString) + .toArray(String[]::new); + this.keystoreTypeProperty = new ProviderConfigProperty(KEYSTORE_TYPE_KEY, "Keystore Type", + "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() + .property(KEYSTORE_PROPERTY) + .property(KEYSTORE_PASSWORD_PROPERTY) + .property(keystoreTypeProperty) + .property(KEY_ALIAS_PROPERTY) + .property(KEY_PASSWORD_PROPERTY) + .property(Attributes.KEY_USE_PROPERTY) + .build(); + } @Override public KeyProvider create(KeycloakSession session, ComponentModel model) { @@ -71,6 +93,7 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor ConfigurationValidationHelper.check(model) .checkSingle(KEYSTORE_PROPERTY, true) .checkSingle(KEYSTORE_PASSWORD_PROPERTY, true) + .checkSingle(keystoreTypeProperty, false) .checkSingle(KEY_ALIAS_PROPERTY, true) .checkSingle(KEY_PASSWORD_PROPERTY, true); @@ -89,7 +112,7 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor @Override public List getConfigProperties() { - return CONFIG_PROPERTIES; + return this.configProperties; } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index 02944c5c181..5a81563ec89 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -63,6 +63,8 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * @resource Client Attribute Certificate @@ -268,9 +270,7 @@ public class ClientAttributeCertificateResource { public byte[] getKeystore(final KeyStoreConfig config) { auth.clients().requireView(client); - if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) { - throw new NotAcceptableException("Only support jks or pkcs12 format."); - } + checkKeystoreFormat(config); CertificateRepresentation info = CertificateInfoHelper.getCertificateFromClient(client, attributePrefix); String privatePem = info.getPrivateKey(); @@ -307,9 +307,7 @@ public class ClientAttributeCertificateResource { public byte[] generateAndGetKeystore(final KeyStoreConfig config) { auth.clients().requireConfigure(client); - if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) { - throw new NotAcceptableException("Only support jks or pkcs12 format."); - } + checkKeystoreFormat(config); if (config.getKeyPassword() == null) { throw new ErrorResponseException("password-missing", "Need to specify a key password for jks generation and download", Response.Status.BAD_REQUEST); } @@ -369,5 +367,20 @@ public class ClientAttributeCertificateResource { } } + private void checkKeystoreFormat(KeyStoreConfig config) throws NotAcceptableException { + if (config.getFormat() != null) { + Set supportedKeystoreFormats = CryptoIntegration.getProvider().getSupportedKeyStoreTypes() + .collect(Collectors.toSet()); + try { + KeystoreFormat format = Enum.valueOf(KeystoreFormat.class, config.getFormat().toUpperCase()); + if (config.getFormat() != null && !supportedKeystoreFormats.contains(format)) { + throw new NotAcceptableException("Not supported keystore format. Supported keystore formats: " + supportedKeystoreFormats); + } + } catch (IllegalArgumentException iae) { + throw new NotAcceptableException("Not supported keystore format. Supported keystore formats: " + supportedKeystoreFormats); + } + } + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 87abc2933b1..ae9757fa514 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -44,6 +44,7 @@ import org.keycloak.representations.idm.PasswordPolicyTypeRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.representations.info.ClientInstallationRepresentation; +import org.keycloak.representations.info.CryptoInfoRepresentation; import org.keycloak.representations.info.MemoryInfoRepresentation; import org.keycloak.representations.info.ProfileInfoRepresentation; import org.keycloak.representations.info.ProviderRepresentation; @@ -93,6 +94,7 @@ public class ServerInfoAdminResource { info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp())); info.setMemoryInfo(MemoryInfoRepresentation.create()); info.setProfileInfo(ProfileInfoRepresentation.create()); + info.setCryptoInfo(CryptoInfoRepresentation.create()); setSocialProviders(info); setIdentityProviders(info); diff --git a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java index 5b5f09b790c..fc5749b12f2 100755 --- a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java +++ b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java @@ -45,7 +45,6 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import javax.security.auth.x500.X500Principal; @@ -86,7 +85,7 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory throw new RuntimeException("Attribute 'password' missing in 'truststore':'file' configuration"); } - String type = getTruststoreType(storepath, configuredType); + String type = KeystoreUtil.getKeystoreType(configuredType, storepath, KeyStore.getDefaultType()); try { truststore = loadStore(storepath, type, pass == null ? null :pass.toCharArray()); } catch (Exception e) { @@ -159,25 +158,6 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory .build(); } - private String getTruststoreType(String path, String configuredType) { - // Configured type has precedence - if (configuredType != null) return configuredType; - - // Fallback to detected tyoe from the file format (EG. my-keystore.pkcs12 will return "pkcs12") - int lastDotIndex = path.lastIndexOf('.'); - if (lastDotIndex > -1) { - String ext = path.substring(lastDotIndex).toUpperCase(); - Optional detectedType = Arrays.stream(KeystoreUtil.KeystoreFormat.values()) - .map(KeystoreUtil.KeystoreFormat::toString) - .filter(ksFormat -> ksFormat.equals(ext)) - .findFirst(); - if (detectedType.isPresent()) return detectedType.get(); - } - - // Fallback to default JVM - return KeyStore.getDefaultType(); - } - private static class TruststoreCertificatesLoader { private Map trustedRootCerts = new HashMap<>(); 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 new file mode 100644 index 00000000000..a4a09ab857f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java @@ -0,0 +1,105 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +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; +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.common.util.PemUtils; +import org.keycloak.representations.idm.CertificateRepresentation; + +import static org.junit.Assert.fail; + +/** + * @author Marek Posolda + */ +public class KeystoreUtils { + + public static String[] getSupportedKeystoreTypes() { + String supportedKeystoreTypes = System.getProperty("auth.server.supported.keystore.types"); + if (supportedKeystoreTypes == null || supportedKeystoreTypes.trim().isEmpty()) { + fail("Property 'auth.server.supported.keystore.types' not set"); + } + return supportedKeystoreTypes.split(","); + } + + public static KeystoreUtil.KeystoreFormat getPreferredKeystoreType() { + return Enum.valueOf(KeystoreUtil.KeystoreFormat.class, getSupportedKeystoreTypes()[0]); + } + + public static void assumeKeystoreTypeSupported(KeystoreUtil.KeystoreFormat keystoreType) { + String[] supportedKeystoreTypes = KeystoreUtils.getSupportedKeystoreTypes(); + Assume.assumeTrue("Keystore type '" + keystoreType + "' not supported. Supported keystore types: " + Arrays.asList(supportedKeystoreTypes), + Stream.of(supportedKeystoreTypes) + .anyMatch(type -> type.equals(keystoreType.toString()))); + } + + public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception { + String fileName = "keystore." + keystoreType.getFileExtension(); + + KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); + X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject); + + KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(keystoreType); + keyStore.load(null, null); + + Certificate[] chain = {certificate}; + keyStore.setKeyEntry(subject, keyPair.getPrivate(), keyPassword.trim().toCharArray(), chain); + + File file = folder.newFile(fileName); + keyStore.store(new FileOutputStream(file), keystorePassword.trim().toCharArray()); + + CertificateRepresentation certRep = new CertificateRepresentation(); + certRep.setPrivateKey(PemUtils.encodeKey(keyPair.getPrivate())); + certRep.setPublicKey(PemUtils.encodeKey(keyPair.getPublic())); + certRep.setCertificate(PemUtils.encodeCertificate(certificate)); + return new KeystoreInfo(certRep, file); + } + + public static class KeystoreInfo { + private final CertificateRepresentation certificateInfo; + private final File keystoreFile; + + private KeystoreInfo(CertificateRepresentation certificateInfo, File keystoreFile) { + this.certificateInfo = certificateInfo; + this.keystoreFile = keystoreFile; + } + + public CertificateRepresentation getCertificateInfo() { + return certificateInfo; + } + + public File getKeystoreFile() { + return keystoreFile; + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java index 1133f66154f..442f03c2c12 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java @@ -23,12 +23,14 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.info.ProviderRepresentation; import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; /** * @author Stian Thorgersen @@ -56,6 +58,12 @@ public class ServerInfoTest extends AbstractKeycloakTest { assertNotNull(info.getMemoryInfo()); assertNotNull(info.getSystemInfo()); + assertNotNull(info.getCryptoInfo()); + String expectedSupportedKeystoreTypes = System.getProperty("auth.server.supported.keystore.types"); + if (expectedSupportedKeystoreTypes == null) { + fail("Property 'auth.server.supported.keystore.types' not set"); + } + Assert.assertNames(info.getCryptoInfo().getSupportedKeystoreTypes(), expectedSupportedKeystoreTypes.split(",")); assertEquals(Version.VERSION, info.getSystemInfo().getVersion()); assertNotNull(info.getSystemInfo().getServerTime()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java index caedaeef984..1189d1da00c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java @@ -20,8 +20,11 @@ package org.keycloak.testsuite.admin.client; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.junit.Before; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.keycloak.admin.client.resource.ClientAttributeCertificateResource; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.PemUtils; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -31,13 +34,12 @@ import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.testsuite.util.AdminEventPaths; +import org.keycloak.testsuite.util.KeystoreUtils; import javax.ws.rs.core.MediaType; import java.io.ByteArrayInputStream; -import java.net.URL; +import java.io.FileInputStream; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; import java.security.Key; import java.security.KeyStore; import java.security.cert.Certificate; @@ -108,81 +110,90 @@ public class CredentialsTest extends AbstractClientTest { @Test public void testUploadKeyAndCertificate() throws Exception { - String privateKey = "MIIEowIBAAKCAQEAhSOOC/5Xez3o75lr3TTYun+2u0a4cF5p5Uv10UowrM7Yw+p1GYcHg+o2UN13bxHB/lefqJZ0WnJQo6cj/JcMuF1y4WlHSww0r8L0u36FKk8Uu7MOqC0+AOi2UzGIchYM5nuD3+A9g1ds2+O/ydKLKqiC6gJCKJp9b3Rs8eyJUt0/tkhTAJx+LWpCbsWHFEnU2Jbl29SS4KedYR/RdH5bNzl4L0SAHS1osWI+xIQiVYybnGVqFjJeQ9006pmOJGetNablji6TxlywP8ps9N//u3txBeKlVqzCCN1iLWQrb/NHA6GDVDBYVf+qa91358vFXRHpWpEOGftB6nZzHAzEuwIDAQABAoIBAHb5IsJM8lfLJxCVBPKTeuiNn/kSZVbkx7SDgJMZvQ1vefz40tOQ+oJDFW6FuWijcbubCa1ZZXg9lxnnDh11zYQi3bnYnkDOE3bMvG2fzdfU+y4QABUA+NtPGT6WkNuCIN0Fmv7AH7fys/B7QLNVVc807me2xPALvfOPEpvNR5mnjquCTOfDzbh5U6hGFcuLnZdQbCK2hG5R8DXE2pLvoa0i1cMMgVaWQ5mVSg0N3G0Q5ZF8YJEasAeJUCGlPFgJ4ySfGsKSUcMODQzHmqvLzArJmFgW6Uah0CgqedBTujmzJ6FwfbzGR0wpk08cf5BPzs9Flwka10ITA4h4QzlBnuECgYEA8TFZWq42biHaZmo0NVVEoltIDl1ci5m2xr7yU6TMfrsGKFFiszCPWuKcK5J8Svm0P9H9vlVpCHZ+JVfEGnve1/wVB/6E0lY6cz4uJTV4t4F1QJN5j9nyRrS0i9zDEIRgO4mvD9Zlm/OvHEdTmtVg97cbS4nWvRAPdB2DaZ0w0V8CgYEAjVAAb5Q6Jqb5XT5ZM1Cc6S3PzBAA7GGc3Rqyugxts5WEReRXdNITocj/71c0VZ+qC9+EvV8im/7QPl5NbRiI2p3oPqqV5Brk/MVfDLhu/mkawW0mlPtuBkZIRE0/eXTGN9Dq6yvxo9d6kwka7RW1CBZxi1/M78hKGCHXM7umviUCgYEA4cLvgJHRIQVPCM4gUEugEtieedOp7IHVM/NHoEOBpp4pBVQortGlXcz/oUlcTlGtBo/ok2AfEGzZZtrgFGoeDM1IYlM6wCc2TujFCM8kT6A9wFRKVPwMa2J6HPBnJe7CpPgbhReJxJA0OKQK/cL9IOGkCvDar914mZeGijU4nMECgYAqZL7Muo47fEpBE+xUvbFlLu4xDPgJ8jrKBjFqKUJb5tYY1aj7De7/0Toexm2X5l9wUm0TFtBeNjKpE0dtHDgqRccfzbNMDFl4D4o1WbtKraNuNd2mQku+rCUQAJCzUjoJEq73QGasvX8zTz75s1JtC7ailmn34YGA/d3+0iPy1QKBgHXneWpJVcQ9Lk34DnSLZLK+W1sTK8xLTJSyy3U0F84r+ir8bvsP9EQpZI0Nx3DqvF4/ZHmK2cfSxGSKm4VhZfG0LYCqtSmaHErZJaLJA8xJELkkEKj/ZUqkZ+4zhY7RMwyZtmXcxvaR/pzRZZwbTQ4ueZKKUIsK2AaHTsSCGDMq"; - String certificate = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="; - String certificate2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="; + TemporaryFolder folder = new TemporaryFolder(); + folder.create(); + try { + String certificate2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="; - ClientAttributeCertificateResource certRsc = accountClient.getCertficateResource("jwt.credential"); + ClientAttributeCertificateResource certRsc = accountClient.getCertficateResource("jwt.credential"); - // Upload privateKey and certificate as JKS store - MultipartFormDataOutput keyCertForm = new MultipartFormDataOutput(); - keyCertForm.addFormData("keystoreFormat", "JKS", MediaType.TEXT_PLAIN_TYPE); - keyCertForm.addFormData("keyAlias", "clientkey", MediaType.TEXT_PLAIN_TYPE); - keyCertForm.addFormData("keyPassword", "keypass", MediaType.TEXT_PLAIN_TYPE); - keyCertForm.addFormData("storePassword", "storepass", MediaType.TEXT_PLAIN_TYPE); + KeystoreUtil.KeystoreFormat preferredKeystoreType = KeystoreUtils.getPreferredKeystoreType(); - URL idpMeta = getClass().getClassLoader().getResource("client-auth-test/keystore-client1.jks"); - byte [] content = Files.readAllBytes(Paths.get(idpMeta.toURI())); - keyCertForm.addFormData("file", content, MediaType.APPLICATION_OCTET_STREAM_TYPE); - CertificateRepresentation cert = certRsc.uploadJks(keyCertForm); + // Generate keystore file and upload privateKey and certificate from it as JKS store (or eventually PKCS12 or BCFKS store according to which one is preferred type) + KeystoreUtils.KeystoreInfo generatedKeystore = KeystoreUtils.generateKeystore(folder, preferredKeystoreType, "clientkey", "storepass", "keypass"); + MultipartFormDataOutput keyCertForm = new MultipartFormDataOutput(); - // Returned cert is not the new state but rather what was extracted from inputs - assertNotNull("cert not null", cert); - assertEquals("cert properly extracted", certificate, cert.getCertificate()); - assertEquals("privateKey properly extracted", privateKey, cert.getPrivateKey()); + keyCertForm.addFormData("keystoreFormat", preferredKeystoreType.toString(), MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("keyAlias", "clientkey", MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("keyPassword", "keypass", MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("storePassword", "storepass", MediaType.TEXT_PLAIN_TYPE); - // Get the certificate - to make sure cert was properly updated - cert = certRsc.getKeyInfo(); - assertEquals("cert properly set", certificate, cert.getCertificate()); - assertEquals("privateKey properly set", privateKey, cert.getPrivateKey()); + FileInputStream fs = new FileInputStream(generatedKeystore.getKeystoreFile()); + byte [] content = fs.readAllBytes(); + fs.close(); + keyCertForm.addFormData("file", content, MediaType.APPLICATION_OCTET_STREAM_TYPE); + CertificateRepresentation cert = certRsc.uploadJks(keyCertForm); - // Upload a different certificate via /upload-certificate, privateKey should be nullified - MultipartFormDataOutput form = new MultipartFormDataOutput(); - form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); - form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); - cert = certRsc.uploadJksCertificate(form); - assertNotNull("cert not null", cert); - assertEquals("cert properly extracted", certificate2, cert.getCertificate()); - assertNull("privateKey not included", cert.getPrivateKey()); + // Returned cert is not the new state but rather what was extracted from inputs + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", generatedKeystore.getCertificateInfo().getCertificate(), cert.getCertificate()); + assertEquals("privateKey properly extracted", generatedKeystore.getCertificateInfo().getPrivateKey(), cert.getPrivateKey()); - // Get the certificate - to make sure cert was properly updated, and privateKey is null - cert = certRsc.getKeyInfo(); - assertEquals("cert properly set", certificate2, cert.getCertificate()); - assertNull("privateKey nullified", cert.getPrivateKey()); + // Get the certificate - to make sure cert was properly updated + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", generatedKeystore.getCertificateInfo().getCertificate(), cert.getCertificate()); + assertEquals("privateKey properly set", generatedKeystore.getCertificateInfo().getPrivateKey(), cert.getPrivateKey()); - // Re-upload the private key - certRsc.uploadJks(keyCertForm); + // Upload a different certificate via /upload-certificate, privateKey should be nullified + MultipartFormDataOutput form = new MultipartFormDataOutput(); + form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); + form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); + cert = certRsc.uploadJksCertificate(form); + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate2, cert.getCertificate()); + assertNull("privateKey not included", cert.getPrivateKey()); - // Upload certificate as PEM via /upload - nullifies the private key - form = new MultipartFormDataOutput(); - form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); - form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); - cert = certRsc.uploadJks(form); - assertNotNull("cert not null", cert); - assertEquals("cert properly extracted", certificate2, cert.getCertificate()); - assertNull("privateKey not included", cert.getPrivateKey()); + // Get the certificate - to make sure cert was properly updated, and privateKey is null + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate2, cert.getCertificate()); + assertNull("privateKey nullified", cert.getPrivateKey()); - // Get the certificate again - to make sure cert is set, and privateKey is null - cert = certRsc.getKeyInfo(); - assertEquals("cert properly set", certificate2, cert.getCertificate()); - assertNull("privateKey nullified", cert.getPrivateKey()); + // Re-upload the private key + certRsc.uploadJks(keyCertForm); - // Upload certificate with header - should be stored without header - form = new MultipartFormDataOutput(); - form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); + // Upload certificate as PEM via /upload - nullifies the private key + form = new MultipartFormDataOutput(); + form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); + form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); + cert = certRsc.uploadJks(form); + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate2, cert.getCertificate()); + assertNull("privateKey not included", cert.getPrivateKey()); - String certificate2WithHeaders = PemUtils.BEGIN_CERT + "\n" + certificate2 + "\n" + PemUtils.END_CERT; + // Get the certificate again - to make sure cert is set, and privateKey is null + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate2, cert.getCertificate()); + assertNull("privateKey nullified", cert.getPrivateKey()); - form.addFormData("file", certificate2WithHeaders.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); - cert = certRsc.uploadJks(form); - assertNotNull("cert not null", cert); - assertEquals("cert properly extracted", certificate2, cert.getCertificate()); - assertNull("privateKey not included", cert.getPrivateKey()); + // Upload certificate with header - should be stored without header + form = new MultipartFormDataOutput(); + form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); - // Get the certificate again - to make sure cert is set, and privateKey is null - cert = certRsc.getKeyInfo(); - assertEquals("cert properly set", certificate2, cert.getCertificate()); - assertNull("privateKey nullified", cert.getPrivateKey()); + String certificate2WithHeaders = PemUtils.BEGIN_CERT + "\n" + certificate2 + "\n" + PemUtils.END_CERT; + + form.addFormData("file", certificate2WithHeaders.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); + cert = certRsc.uploadJks(form); + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate2, cert.getCertificate()); + assertNull("privateKey not included", cert.getPrivateKey()); + + // Get the certificate again - to make sure cert is set, and privateKey is null + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate2, cert.getCertificate()); + assertNull("privateKey nullified", cert.getPrivateKey()); + } finally { + folder.delete(); + } } @Test @@ -192,15 +203,17 @@ public class CredentialsTest extends AbstractClientTest { // generate a key pair first CertificateRepresentation certrep = certRsc.generate(); + KeystoreUtil.KeystoreFormat preferredKeystoreType = KeystoreUtils.getPreferredKeystoreType(); + // download the key and certificate KeyStoreConfig config = new KeyStoreConfig(); - config.setFormat("JKS"); + config.setFormat(preferredKeystoreType.toString()); config.setKeyAlias("alias"); config.setKeyPassword("keyPass"); config.setStorePassword("storePass"); byte[] result = certRsc.getKeystore(config); - KeyStore keyStore = KeyStore.getInstance("JKS"); + KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(preferredKeystoreType); keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray()); Key key = keyStore.getKey("alias", "keyPass".toCharArray()); Certificate cert = keyStore.getCertificate("alias"); @@ -220,13 +233,15 @@ public class CredentialsTest extends AbstractClientTest { // generate a key pair first CertificateRepresentation firstcert = certRsc.generate(); + KeystoreUtil.KeystoreFormat preferredKeystoreType = KeystoreUtils.getPreferredKeystoreType(); + KeyStoreConfig config = new KeyStoreConfig(); - config.setFormat("JKS"); + config.setFormat(preferredKeystoreType.toString()); config.setKeyAlias("alias"); config.setKeyPassword("keyPass"); config.setStorePassword("storePass"); byte[] result = certRsc.generateAndGetKeystore(config); - KeyStore keyStore = KeyStore.getInstance("JKS"); + KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(preferredKeystoreType); keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray()); Key key = keyStore.getKey("alias", "keyPass".toCharArray()); Certificate cert = keyStore.getCertificate("alias"); 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 e912e87cf1a..86739a62b0a 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,11 +17,11 @@ package org.keycloak.testsuite.keys; -import org.apache.commons.io.IOUtils; 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.jose.jws.AlgorithmType; import org.keycloak.keys.JavaKeystoreKeyProviderFactory; @@ -31,18 +31,20 @@ import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; 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.KeystoreUtils; import javax.ws.rs.core.Response; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; import java.util.List; -import static org.junit.Assert.*; +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.keycloak.common.util.KeystoreUtil.KeystoreFormat.PKCS12; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; /** @@ -50,10 +52,6 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; */ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { - private static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPAJ/X39oNRkoS+baWVhAghfO86ZPfkSHm4evmMDhbA0KqW1/hg55qUJoT91ytGozIsIxoCLKzQvZTluRpt0AMp7cmfaGWBQ8cBtb8/BL+5FkUucigmOcTrfPq9/xR9g4AMSXRItjLRsJPy2Bnjau64DVQ3N5NVbWAMw7/1XjuobEyPnw0RLqEr/TxWMteuaiV1n8amIAiT91xZ8UFyPv3urCkAz+r+iyVvdJcZwn2tUL6KLM7qX/HSX8SUtPrIMB8EdW1yNt5McO8Ro5GxwiyXimDKbY9ur2WP8/wrdk/0TkoUYeI1UsnFyoJcqqg2+1T+dNAMtJhF7uDhURVQ33QIDAQAB"; - private static final String CERTIFICATE = "MIIDeTCCAmGgAwIBAgIEbhSauDANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMCAXDTE2MTAxMzE4MjUxNFoYDzIyOTAwNzI4MTgyNTE0WjBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPAJ/X39oNRkoS+baWVhAghfO86ZPfkSHm4evmMDhbA0KqW1/hg55qUJoT91ytGozIsIxoCLKzQvZTluRpt0AMp7cmfaGWBQ8cBtb8/BL+5FkUucigmOcTrfPq9/xR9g4AMSXRItjLRsJPy2Bnjau64DVQ3N5NVbWAMw7/1XjuobEyPnw0RLqEr/TxWMteuaiV1n8amIAiT91xZ8UFyPv3urCkAz+r+iyVvdJcZwn2tUL6KLM7qX/HSX8SUtPrIMB8EdW1yNt5McO8Ro5GxwiyXimDKbY9ur2WP8/wrdk/0TkoUYeI1UsnFyoJcqqg2+1T+dNAMtJhF7uDhURVQ33QIDAQABoyEwHzAdBgNVHQ4EFgQUgz0ABmkImZUEO2/w0shoH4rp6pwwDQYJKoZIhvcNAQELBQADggEBAK+syjqfFXmv7942+ZfmJfb4i/JilhwSyA2G1VvGR39dLW1nPmKMMUY6kKgJ2NZgaCGvJ4jxDhfNJ1jPG7rcO/eQuF3cx9r+nHiTcJ5PNLqG2q4dNNFshJ8aGuIaTQEB7S1OlGsEj0rd0YlJ+LTrFfEHsnsJvpvDRLdVMklib5fPk4W8ziuQ3rr6T/a+be3zfAqmFZx8j6E46jz9QO841uwqdzcR9kfSHS/76TNGZv8OB6jheyHrUdBygR85iizHgMqats/0zWmKEAvSp/DhAfyIFp8zZHvPjmpBl+mfmAqnrYY0oJRb5rRXmL8DKq5plc7jgO1H6aHh5mV6slXQDEw="; - - @Rule public TemporaryFolder folder = new TemporaryFolder(); @@ -65,7 +63,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Page protected LoginPage loginPage; - private File file; + private KeystoreUtils.KeystoreInfo generatedKeystore; @Override public void addTestRealms(List testRealms) { @@ -73,24 +71,32 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { testRealms.add(realm); } - @Override - public void beforeAbstractKeycloakTest() throws Exception { - super.beforeAbstractKeycloakTest(); - - file = folder.newFile("keystore.jsk"); - - InputStream resourceAsStream = JavaKeystoreKeyProviderTest.class.getResourceAsStream("keystore.jks"); - IOUtils.copy(resourceAsStream, new FileOutputStream(file)); + @Test + public void createJks() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.JKS); } @Test - public void create() throws Exception { + public void createPkcs12() throws Exception { + createSuccess(PKCS12); + } + + @Test + public void createBcfks() throws Exception { + createSuccess(KeystoreUtil.KeystoreFormat.BCFKS); + } + + private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType) throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(keystoreType); + generateKeystore(keystoreType); + long priority = System.currentTimeMillis(); ComponentRepresentation rep = createRep("valid", priority); 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()); @@ -105,12 +111,13 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { assertEquals(id, key.getProviderId()); assertEquals(AlgorithmType.RSA.name(), key.getType()); assertEquals(priority, key.getProviderPriority()); - assertEquals(PUBLIC_KEY, key.getPublicKey()); - assertEquals(CERTIFICATE, key.getCertificate()); + 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()); rep.getConfig().putSingle("keystore", "/nosuchfile"); @@ -120,6 +127,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeystorePassword() throws Exception { + generateKeystore(KeystoreUtils.getPreferredKeystoreType()); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis()); rep.getConfig().putSingle("keystore", "invalid"); @@ -129,6 +137,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeyAlias() throws Exception { + generateKeystore(KeystoreUtils.getPreferredKeystoreType()); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis()); rep.getConfig().putSingle("keyAlias", "invalid"); @@ -138,10 +147,22 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { @Test public void invalidKeyPassword() throws Exception { + KeystoreUtil.KeystoreFormat keystoreType = KeystoreUtils.getPreferredKeystoreType(); + if (keystoreType == PKCS12) { + // only the keyStore password is significant with PKCS12. Hence we need to test with different keystore type + String[] supportedKsTypes = KeystoreUtils.getSupportedKeystoreTypes(); + if (supportedKsTypes.length <= 1) { + Assert.fail("Only PKCS12 type is supported, but invalidKeyPassword() scenario cannot be tested with it"); + } + keystoreType = Enum.valueOf(KeystoreUtil.KeystoreFormat.class, supportedKsTypes[1]); + log.infof("Fallback to keystore type '%s' for the invalidKeyPassword() test", keystoreType); + } + generateKeystore(keystoreType); ComponentRepresentation rep = createRep("valid", System.currentTimeMillis()); rep.getConfig().putSingle("keyPassword", "invalid"); Response response = adminClient.realm("test").components().add(rep); + Assert.assertEquals(400, response.getStatus()); assertErrror(response, "Failed to load keys. Keystore on server can not be recovered."); } @@ -163,12 +184,16 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest { rep.setProviderType(KeyProvider.class.getName()); rep.setConfig(new MultivaluedHashMap<>()); rep.getConfig().putSingle("priority", Long.toString(priority)); - rep.getConfig().putSingle("keystore", file.getAbsolutePath()); + rep.getConfig().putSingle("keystore", generatedKeystore.getKeystoreFile().getAbsolutePath()); rep.getConfig().putSingle("keystorePassword", "password"); rep.getConfig().putSingle("keyAlias", "selfsigned"); rep.getConfig().putSingle("keyPassword", "password"); return rep; } + private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception { + this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password"); + } + } 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 db4990f4ad9..067ec4beceb 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 @@ -32,8 +32,11 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.adapters.AdapterUtils; @@ -85,13 +88,12 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.KeystoreUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.util.JsonSerialization; -import com.sun.jna.StringArray; - import javax.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.File; @@ -130,6 +132,21 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); + + @ClassRule + public static TemporaryFolder folder = new TemporaryFolder(); + + private static KeystoreUtils.KeystoreInfo generatedKeystoreClient1; + private static KeyPair keyPairClient1; + + @BeforeClass + public static void generateClient1KeyPair() throws Exception { + generatedKeystoreClient1 = KeystoreUtils.generateKeystore(folder, KeystoreFormat.JKS, "clientkey", "storepass", "keypass"); + PublicKey publicKey = PemUtils.decodePublicKey(generatedKeystoreClient1.getCertificateInfo().getPublicKey()); + PrivateKey privateKey = PemUtils.decodePrivateKey(generatedKeystoreClient1.getCertificateInfo().getPrivateKey()); + keyPairClient1 = new KeyPair(publicKey, privateKey); + } + private static String client1SAUserId; private static RealmRepresentation testRealm; @@ -151,7 +168,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { app1 = ClientBuilder.create() .id(KeycloakModelUtils.generateId()) .clientId("client1") - .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ==") + .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, generatedKeystoreClient1.getCertificateInfo().getCertificate()) .attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true") .authenticatorType(JWTClientAuthenticator.PROVIDER_ID) .serviceAccountsEnabled(true) @@ -171,9 +188,6 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { realmBuilder.client(app2); - // This one is for keystore-client2.p12 , which doesn't work on Sun JDK -// app2.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLGHHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjMzMVoXDTI1MDgxNzE3MjUxMVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIsatXj38fFD9fHslNrsWrubobudXYwwdZpGYqkHIhuDeSojGvhBSLmKIFmtbHMVcLEbS0dIEsSbNVrwjdFfuRuvd9Vu6Ng0JUC8fRhSeQniC3jcBuP8P4WlXK4+ir3Wlya+T6Hum9b68BiH0KyNZtFGJ6zLHuCcq9Bl0JifvibnUkDeTZPwgJNA9+GxS/x8fAkApcAbJrgBZvr57PwhbgHoZdB8aAY5f5ogbGzKDtSUMvFh+Jah39gWtn7p3VOuuMXA8SugogoH8C5m2itrPBL1UPhAcKUeWiqx4SmZe/lZo7x2WbSecNiFaiqBhIW+QbqCYW6I4u0YvuLuEe3+TC8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZzW5DZviCxUQdV5Ab07PZkUfvImHZ73oWWHZqzUQtZtbVdzfp3cnbb2wyXtlOvingO3hgpoTxV8vbKgLbIQfvkGGHBG1F5e0QVdtikfdcwWb7cy4/9F80OD7cgG0ZAzFbQ8ZY7iS3PToBp3+4tbIK2NK0ntt/MYgJnPbHeG4V4qfgUbFm1YgEK7WpbSVU8jGuJ5DWE+mlYgECZKZ5TSlaVGs2XOm6WXrJScucNekwcBWWiHyRsFHZEDzWmzt8TLTLnnb0vVjhx3qCYxah3RbyyMZm6WLZlLAaGEcwNDO8jaA3hAjrxoOA1xEaolQfGVsb/ElelHcR1Zfe0u4Ekd4tw=="); - defaultUser = UserBuilder.create() .id(KeycloakModelUtils.generateId()) //.serviceAccountId(app1.getClientId()) @@ -590,14 +604,22 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { @Test public void testClientWithGeneratedKeysJKS() throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.JKS); testClientWithGeneratedKeys("JKS"); } @Test public void testClientWithGeneratedKeysPKCS12() throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.PKCS12); testClientWithGeneratedKeys("PKCS12"); } + @Test + public void testClientWithGeneratedKeysBCFKS() throws Exception { + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.BCFKS); + testClientWithGeneratedKeys(KeystoreFormat.BCFKS.toString()); + } + private void testClientWithGeneratedKeys(String format) throws Exception { ClientRepresentation client = app3; UserRepresentation user = defaultUser; @@ -664,12 +686,22 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { @Test public void testUploadKeystoreJKS() throws Exception { - testUploadKeystore("JKS", "client-auth-test/keystore-client1.jks", "clientkey", "storepass"); + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.JKS); + testUploadKeystore("JKS", generatedKeystoreClient1.getKeystoreFile().getAbsolutePath(), "clientkey", "storepass"); } @Test public void testUploadKeystorePKCS12() throws Exception { - testUploadKeystore("PKCS12", "client-auth-test/keystore-client2.p12", "clientkey", "pwd2"); + KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.PKCS12); + KeystoreUtils.KeystoreInfo ksInfo = KeystoreUtils.generateKeystore(folder, KeystoreFormat.PKCS12, "clientkey", "pwd2", "keypass"); + testUploadKeystore(KeystoreFormat.PKCS12.toString(), ksInfo.getKeystoreFile().getAbsolutePath(), "clientkey", "pwd2"); + } + + @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"); } @Test @@ -696,10 +728,10 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { // Load the keystore file URL fileUrl = (getClass().getClassLoader().getResource(filePath)); - if (fileUrl == null) { - throw new IOException("File not found: " + filePath); + File keystoreFile = fileUrl != null ? new File(fileUrl.getFile()) : new File(filePath); + if (!keystoreFile.exists()) { + throw new IOException("File not found: " + keystoreFile.getAbsolutePath()); } - File keystoreFile = new File(fileUrl.getFile()); // Get admin access token, no matter it's master realm's admin OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest( @@ -792,7 +824,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { @Test public void testAssertionMissingIssuer() throws Exception { - String invalidJwt = getClientSignedJWT(getClient1KeyPair(), null); + String invalidJwt = getClientSignedJWT(keyPairClient1, null); List parameters = new LinkedList(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); @@ -807,7 +839,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { @Test public void testAssertionUnknownClient() throws Exception { - String invalidJwt = getClientSignedJWT(getClient1KeyPair(), "unknown-client"); + String invalidJwt = getClientSignedJWT(keyPairClient1, "unknown-client"); List parameters = new LinkedList(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); @@ -1072,7 +1104,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { private OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception { CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider(); - jwtProvider.setupKeyPair(getClient1KeyPair()); + jwtProvider.setupKeyPair(keyPairClient1); jwtProvider.setTokenTimeout(10); for (String claim : claims) { @@ -1304,19 +1336,14 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { return getClientSignedJWT(getClient2KeyPair(), "client2", algorithm); } - private String getClient1SignedJWT() { - return getClientSignedJWT(getClient1KeyPair(), "client1", Algorithm.RS256); + private String getClient1SignedJWT() throws Exception { + return getClientSignedJWT(keyPairClient1, "client1", Algorithm.RS256); } private String getClient2SignedJWT() { return getClientSignedJWT(getClient2KeyPair(), "client2", Algorithm.RS256); } - private KeyPair getClient1KeyPair() { - return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client1.jks", - "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS); - } - private KeyPair getClient2KeyPair() { return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client2.p12 b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client2.p12 deleted file mode 100644 index 7f3ca526a8d..00000000000 Binary files a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client2.p12 and /dev/null differ diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 6bbfc6897ba..97b46cbbdac 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -263,6 +263,7 @@ default local disabled + JKS,PKCS12,BCFKS @@ -685,6 +686,7 @@ ${auth.server.fips.mode} ${auth.server.fips.keystore.type} + ${auth.server.supported.keystore.types}