Make Keycloak FIPS working with OpenJDK 17 on FIPS enabled RHEL

Closes #15721
This commit is contained in:
mposolda 2022-12-08 18:54:14 +01:00 committed by Marek Posolda
parent 44715fe397
commit 36bd76957d
13 changed files with 87 additions and 99 deletions

View file

@ -89,19 +89,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.security.properties>${basedir}/target/test-classes/kc.java.security</java.security.properties>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> </project>

View file

@ -9,6 +9,7 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.Provider; import java.security.Provider;
import java.security.SecureRandom;
import java.security.spec.ECField; import java.security.spec.ECField;
import java.security.spec.ECFieldF2m; import java.security.spec.ECFieldF2m;
import java.security.spec.ECFieldFp; import java.security.spec.ECFieldFp;
@ -83,7 +84,7 @@ public class FIPS1402Provider implements CryptoProvider {
Security.insertProviderAt(new KeycloakFipsSecurityProvider(bcFipsProvider), 1); Security.insertProviderAt(new KeycloakFipsSecurityProvider(bcFipsProvider), 1);
if (existingBcFipsProvider == null) { if (existingBcFipsProvider == null) {
Security.insertProviderAt(this.bcFipsProvider, 2); checkSecureRandom(() -> Security.insertProviderAt(this.bcFipsProvider, 2));
Provider bcJsseProvider = new BouncyCastleJsseProvider("fips:BCFIPS"); Provider bcJsseProvider = new BouncyCastleJsseProvider("fips:BCFIPS");
Security.insertProviderAt(bcJsseProvider, 3); Security.insertProviderAt(bcJsseProvider, 3);
log.debugf("Inserted security providers: %s", Arrays.asList(this.bcFipsProvider.getName(),bcJsseProvider.getName())); log.debugf("Inserted security providers: %s", Arrays.asList(this.bcFipsProvider.getName(),bcJsseProvider.getName()));
@ -191,12 +192,8 @@ public class FIPS1402Provider implements CryptoProvider {
@Override @Override
public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException, NoSuchProviderException { public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException, NoSuchProviderException {
if (format == KeystoreFormat.JKS) {
return KeyStore.getInstance(format.toString());
} else {
return KeyStore.getInstance(format.toString(), BouncyIntegration.PROVIDER); return KeyStore.getInstance(format.toString(), BouncyIntegration.PROVIDER);
} }
}
@Override @Override
public CertificateFactory getX509CertFactory() throws CertificateException, NoSuchProviderException { public CertificateFactory getX509CertFactory() throws CertificateException, NoSuchProviderException {
@ -262,4 +259,34 @@ public class FIPS1402Provider implements CryptoProvider {
}; };
} }
// BCFIPS require "SecureRandom.getInstanceStrong" to be available. But it may not be available on RHEL 8 on OpenJDK 17 due the https://bugzilla.redhat.com/show_bug.cgi?id=2155060
private void checkSecureRandom(Runnable insertBcFipsProvider) {
try {
SecureRandom sr = SecureRandom.getInstanceStrong();
log.debugf("Strong secure random available. Algorithm: %s, Provider: %s", sr.getAlgorithm(), sr.getProvider());
insertBcFipsProvider.run();
} catch (NoSuchAlgorithmException nsae) {
// Fallback to regular SecureRandom
SecureRandom secRandom = new SecureRandom();
String origStrongAlgs = Security.getProperty("securerandom.strongAlgorithms");
String usedAlg = secRandom.getAlgorithm() + ":" + secRandom.getProvider().getName();
log.debugf("Strong secure random not available. Tried algorithms: %s. Using algorithm as a fallback for strong secure random: %s", origStrongAlgs, usedAlg);
String strongAlgs = origStrongAlgs == null ? usedAlg : usedAlg + "," + origStrongAlgs;
Security.setProperty("securerandom.strongAlgorithms", strongAlgs);
try {
// Need to insert BCFIPS provider to security providers with "strong algorithm" available
insertBcFipsProvider.run();
SecureRandom.getInstance("DEFAULT", "BCFIPS");
log.debugf("Initialized BCFIPS secured random");
} catch (NoSuchAlgorithmException | NoSuchProviderException nsaee) {
throw new IllegalStateException("Not possible to initiate BCFIPS secure random", nsaee);
} finally {
Security.setProperty("securerandom.strongAlgorithms", origStrongAlgs != null ? origStrongAlgs : "");
}
}
}
} }

View file

@ -1,51 +0,0 @@
# Configuration file just with the security properties, which are supposed to be overriden. The properties, which are not mentioned in this file,
# are inherited from the default java.security file bundled within the Java distribution.
#
# NOTE: Each property is specified 2 times. This is so the same file can be used on both FIPS based RHEL host (which uses "fips" prefixed properties by default)
# and the non-fips based (EG. when running the tests on GH actions)
#
# List of providers and their preference orders (see above). Used on the host without FIPS (EG. when running the tests on GH actions)
# Uses only BouncyCastle FIPS providers to make sure to use only FIPS compliant cryptography.
#
security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
security.provider.3=
#
# Security providers used when global crypto-policies are set to FIPS (Usually it is used when FIPS enabled on system/JVM level)
#
fips.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
fips.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
fips.provider.3=
# Commented this provider for now (and also other providers) as it uses lots of non-FIPS services.
# See https://access.redhat.com/documentation/en-us/openjdk/11/html-single/configuring_openjdk_11_on_rhel_with_fips/index#ref_openjdk-default-fips-configuration_openjdk
# fips.provider.2=SUN
#
# Default keystore type.
#
keystore.type=PKCS12
fips.keystore.type=PKCS12
# This is needed especially if we cannot add security provider "com.sun.net.ssl.internal.ssl.Provider BCFIPS" as a security provider.
# OpenJDK has "SunX509" as default algorithm, but that one is not supported by BCJSSE. So adding the Sun provider delegating to BCFIPS is needed (as above)
# or changing default algorithm as described here
ssl.KeyManagerFactory.algorithm=PKIX
fips.ssl.KeyManagerFactory.algorithm=PKIX
ssl.TrustManagerFactory.algorithm=PKIX
fips.ssl.TrustManagerFactory.algorithm=PKIX
#
# Controls compatibility mode for JKS and PKCS12 keystore types.
#
# When set to 'true', both JKS and PKCS12 keystore types support loading
# keystore files in either JKS or PKCS12 format. When set to 'false' the
# JKS keystore type supports loading only JKS keystore files and the PKCS12
# keystore type supports loading only PKCS12 keystore files.
#
# This is set to false as BCFIPS providers don't support JKS
keystore.type.compat=false
fips.keystore.type.compat=false

View file

@ -66,8 +66,7 @@ For the `fips-mode`, he alternative is to use `--fips-mode=strict` in which case
which means even stricter security algorithms. As mentioned above, strict mode won't work with `pkcs12` keystore: which means even stricter security algorithms. As mentioned above, strict mode won't work with `pkcs12` keystore:
``` ```
./kc.sh build --fips-mode=enabled ./kc.sh start --fips-mode=enabled --hostname=localhost \
./kc.sh start --optimized --hostname=localhost \
--https-key-store-file=$PWD/$KEYSTORE_FILE \ --https-key-store-file=$PWD/$KEYSTORE_FILE \
--https-key-store-type=$KEYSTORE_FORMAT \ --https-key-store-type=$KEYSTORE_FORMAT \
--https-key-store-password=passwordpassword \ --https-key-store-password=passwordpassword \

View file

@ -191,7 +191,8 @@ public class AuthUtil {
public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) { public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS); KeystoreUtil.KeystoreFormat keystoreType = Enum.valueOf(KeystoreUtil.KeystoreFormat.class, KeystoreUtil.getKeystoreType(null, keystore, KeystoreUtil.KeystoreFormat.JKS.toString()));
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, keystoreType);
JsonWebToken reqToken = new JsonWebToken(); JsonWebToken reqToken = new JsonWebToken();
reqToken.id(UUID.randomUUID().toString()); reqToken.id(UUID.randomUUID().toString());

View file

@ -31,7 +31,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.UUID; import java.util.UUID;
import static java.lang.System.currentTimeMillis; import static java.lang.System.currentTimeMillis;
@ -193,7 +192,8 @@ public class AuthUtil {
public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) { public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS); KeystoreUtil.KeystoreFormat keystoreType = Enum.valueOf(KeystoreUtil.KeystoreFormat.class, KeystoreUtil.getKeystoreType(null, keystore, KeystoreUtil.KeystoreFormat.JKS.toString()));
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, keystoreType);
JsonWebToken reqToken = new JsonWebToken(); JsonWebToken reqToken = new JsonWebToken();
reqToken.id(UUID.randomUUID().toString()); reqToken.id(UUID.randomUUID().toString());

View file

@ -34,7 +34,7 @@ final class ClassLoaderPropertyMappers {
if (fipsEnabled != null && FipsMode.valueOf(fipsEnabled.getValue()).isFipsEnabled()) { if (fipsEnabled != null && FipsMode.valueOf(fipsEnabled.getValue()).isFipsEnabled()) {
return Optional.of( return Optional.of(
"org.bouncycastle:bcprov-jdk15on,org.bouncycastle:bcpkix-jdk15on,org.keycloak:keycloak-crypto-default"); "org.bouncycastle:bcprov-jdk15on,org.bouncycastle:bcpkix-jdk15on,org.bouncycastle:bcutil-jdk15on,org.keycloak:keycloak-crypto-default");
} }
return Optional.of( return Optional.of(

View file

@ -8,18 +8,19 @@
# List of providers and their preference orders (see above). Used on the host without FIPS (EG. when running the tests on GH actions) # List of providers and their preference orders (see above). Used on the host without FIPS (EG. when running the tests on GH actions)
# Uses only BouncyCastle FIPS providers to make sure to use only FIPS compliant cryptography. # Uses only BouncyCastle FIPS providers to make sure to use only FIPS compliant cryptography.
# #
security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider #security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS #security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
security.provider.3= #security.provider.3=
# #
# Security providers used when global crypto-policies are set to FIPS (Usually it is used when FIPS enabled on system/JVM level) # Security providers used when global crypto-policies are set to FIPS (Usually it is used when FIPS enabled on system/JVM level)
# #
fips.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider #fips.provider.1=SunPKCS11 ${java.home}/conf/security/nss.fips.cfg
fips.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS #fips.provider.2=SUN
fips.provider.3= #fips.provider.3=SunEC
#fips.provider.3=SunJGSS #fips.provider.4=com.sun.net.ssl.internal.ssl.Provider SunPKCS11-NSS-FIPS
#fips.provider.4=XMLDSig #fips.provider.5=SunJGSS
#fips.provider.6=XMLDSig
#fips.provider.5= #fips.provider.5=
# Commented this provider for now (and also other providers) as it uses lots of non-FIPS services. # Commented this provider for now (and also other providers) as it uses lots of non-FIPS services.

View file

@ -352,7 +352,7 @@
<goal>copy-dependencies</goal> <goal>copy-dependencies</goal>
</goals> </goals>
<configuration> <configuration>
<outputDirectory>${auth.server.home}/lib/bootstrap</outputDirectory> <outputDirectory>${auth.server.home}/providers</outputDirectory>
<includeArtifactIds>bc-fips,bctls-fips,bcpkix-fips</includeArtifactIds> <includeArtifactIds>bc-fips,bctls-fips,bcpkix-fips</includeArtifactIds>
</configuration> </configuration>
</execution> </execution>

View file

@ -5,8 +5,10 @@ import org.junit.Test;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.FileConfigHandler; import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData; import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.cli.KcAdmExec; import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.KeystoreUtils;
import org.keycloak.testsuite.util.TempFileResource; import org.keycloak.testsuite.util.TempFileResource;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -501,13 +503,24 @@ public class KcAdmTest extends AbstractAdmCliTest {
} }
@Test @Test
public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException { public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient_JKSKeystore() throws IOException {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreUtil.KeystoreFormat.JKS);
testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(KeystoreUtil.KeystoreFormat.JKS.getFileExtension());
}
@Test
public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient_PKCS12Keystore() throws IOException {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreUtil.KeystoreFormat.PKCS12);
testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(KeystoreUtil.KeystoreFormat.PKCS12.getFileExtension());
}
private void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(String keystoreFileExtension) throws IOException {
/* /*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file. * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using username, password, and client JWT signature. * Login is performed by each operation again, and again using username, password, and client JWT signature.
*/ */
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks"); File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore." + keystoreFileExtension);
Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile()); Assert.assertTrue("admin-cli-keystore." + keystoreFileExtension + " must exist, but it does not exists", keystore.isFile());
// try client without direct grants enabled // try client without direct grants enabled
KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" + KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
@ -536,7 +549,7 @@ public class KcAdmTest extends AbstractAdmCliTest {
assertExitCodeAndStreamSizes(exe, 1, 0, 2); assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0)); Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(exe.stderrLines().size() - 1)); Assert.assertTrue("error message", exe.stderrLines().get(exe.stderrLines().size() - 1).startsWith("Failed to load private key:"));
// try whole CRUD // try whole CRUD
@ -563,8 +576,8 @@ public class KcAdmTest extends AbstractAdmCliTest {
* Test create, get, update, and delete using on-the-fly authentication - without using any config file. * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using only client JWT signature - service account is used. * Login is performed by each operation again, and again using only client JWT signature - service account is used.
*/ */
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks"); File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.p12");
Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile()); Assert.assertTrue("admin-cli-keystore.p12 exists", keystore.isFile());
testCRUDWithOnTheFlyAuth(serverUrl, testCRUDWithOnTheFlyAuth(serverUrl,
"--client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias admin-cli", "", "--client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias admin-cli", "",

View file

@ -6,8 +6,10 @@ import org.junit.Test;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.FileConfigHandler; import org.keycloak.client.registration.cli.config.FileConfigHandler;
import org.keycloak.client.registration.cli.config.RealmConfigData; import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.cli.KcRegExec; import org.keycloak.testsuite.cli.KcRegExec;
import org.keycloak.testsuite.util.KeystoreUtils;
import org.keycloak.testsuite.util.TempFileResource; import org.keycloak.testsuite.util.TempFileResource;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -502,13 +504,24 @@ public class KcRegTest extends AbstractRegCliTest {
} }
@Test @Test
public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException { public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient_JKSKeystore() throws IOException {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreUtil.KeystoreFormat.JKS);
testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(KeystoreUtil.KeystoreFormat.JKS.getFileExtension());
}
@Test
public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient_PKCS12Keystore() throws IOException {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreUtil.KeystoreFormat.PKCS12);
testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(KeystoreUtil.KeystoreFormat.PKCS12.getFileExtension());
}
private void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient(String keystoreFileExtension) throws IOException {
/* /*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file. * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using username, password, and client JWT signature. * Login is performed by each operation again, and again using username, password, and client JWT signature.
*/ */
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore.jks"); File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore." + keystoreFileExtension);
Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile()); Assert.assertTrue("reg-cli-keystore." + keystoreFileExtension + " exists", keystore.isFile());
// try client without direct grants enabled // try client without direct grants enabled
KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" + KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
@ -537,7 +550,7 @@ public class KcRegTest extends AbstractRegCliTest {
assertExitCodeAndStreamSizes(exe, 1, 0, 2); assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0)); Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(exe.stderrLines().size() - 1)); Assert.assertTrue("error message", exe.stderrLines().get(exe.stderrLines().size() - 1).startsWith("Failed to load private key: "));
// try whole CRUD // try whole CRUD
@ -564,8 +577,8 @@ public class KcRegTest extends AbstractRegCliTest {
* Test create, get, update, and delete using on-the-fly authentication - without using any config file. * Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using only client JWT signature - service account is used. * Login is performed by each operation again, and again using only client JWT signature - service account is used.
*/ */
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore.jks"); File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore.p12");
Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile()); Assert.assertTrue("reg-cli-keystore.p12 exists", keystore.isFile());
testCRUDWithOnTheFlyAuth(serverUrl, testCRUDWithOnTheFlyAuth(serverUrl,
"--client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias reg-cli", "", "--client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias reg-cli", "",