diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65b5d2af51..62f3c4372a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,6 +274,77 @@ jobs: path: reports-${{ matrix.server }}-base-tests-${{ matrix.tests }}.zip if-no-files-found: ignore + test-fips: + name: Base testsuite (fips) + needs: build + runs-on: ubuntu-latest + strategy: + matrix: + server: ['bcfips-nonapproved-pkcs12'] + tests: ['group1'] + fail-fast: false + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: cache-2-${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: cache-1-${{ runner.os }}-m2 + + - name: Download built keycloak + id: download-keycloak + uses: actions/download-artifact@v3 + with: + path: ~/.m2/repository/org/keycloak/ + name: keycloak-artifacts.zip + + # - name: List M2 repo + # run: | + # find ~ -name *dist*.zip + # ls -lR ~/.m2/repository + + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: ${{ env.DEFAULT_JDK_VERSION }} + - name: Update maven settings + run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/ + - name: Prepare quarkus distribution with BCFIPS + run: ./mvnw clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server/quarkus -Pauth-server-quarkus,auth-server-fips140-2 + - name: Run base tests + 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" + + ./mvnw clean install -nsu -B ${PARAMS["${{ matrix.server }}"]} ${TESTGROUP["${{ matrix.tests }}"]} -f testsuite/integration-arquillian/tests/base/pom.xml | misc/log/trimmer.sh + + TEST_RESULT=${PIPESTATUS[0]} + find . -path '*/target/surefire-reports/*.xml' | zip -q reports-${{ matrix.server }}-base-tests-${{ matrix.tests }}.zip -@ + exit $TEST_RESULT + + - name: Analyze Test and/or Coverage Results + uses: runforesight/foresight-test-kit-action@v1 + if: always() && github.repository == 'keycloak/keycloak' + with: + api_key: ${{ secrets.FORESIGHT_API_KEY }} + test_format: JUNIT + test_framework: JUNIT + test_path: 'testsuite/integration-arquillian/tests/base/target/surefire-reports/*.xml' + + - name: Base test reports + uses: actions/upload-artifact@v3 + if: failure() + with: + name: reports-${{ matrix.server }}-base-tests-${{ matrix.tests }} + retention-days: 14 + path: reports-${{ matrix.server }}-base-tests-${{ matrix.tests }}.zip + if-no-files-found: ignore + test-posgres: name: Base testsuite (postgres) needs: build diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java b/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java index e02b6dd7ed..436f833d2f 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java @@ -1,10 +1,12 @@ package org.keycloak.common.crypto; +import java.security.KeyStore; import java.security.Provider; import java.security.Security; import java.util.List; import java.util.ServiceLoader; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.jboss.logging.Logger; @@ -33,6 +35,7 @@ public class CryptoIntegration { if (logger.isTraceEnabled()) { logger.tracef(dumpJavaSecurityProviders()); + logger.tracef(dumpSecurityProperties()); } } @@ -68,7 +71,19 @@ public class CryptoIntegration { return builder.append("]").toString(); } + public static String dumpSecurityProperties() { + StringBuilder builder = new StringBuilder("Security properties: [ \n") + .append(" Java security properties file: " + System.getProperty("java.security.properties") + "\n") + .append(" Default keystore type: " + KeyStore.getDefaultType() + "\n") + .append(" keystore.type.compat: " + Security.getProperty("keystore.type.compat") + "\n"); + Stream.of("javax.net.ssl.trustStoreType", "javax.net.ssl.trustStore", "javax.net.ssl.trustStoreProvider", + "javax.net.ssl.keyStoreType", "javax.net.ssl.keyStore", "javax.net.ssl.keyStoreProvider") + .forEach(propertyName -> builder.append(" " + propertyName + ": " + System.getProperty(propertyName) + "\n")); + return builder.append("]").toString(); + } + public static void setProvider(CryptoProvider provider) { + logger.debugf("Using the crypto provider: %s", provider.getClass().getName()); cryptoProvider = provider; } } diff --git a/common/src/main/java/org/keycloak/common/crypto/FipsMode.java b/common/src/main/java/org/keycloak/common/crypto/FipsMode.java new file mode 100644 index 0000000000..1f6c876791 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/crypto/FipsMode.java @@ -0,0 +1,21 @@ +package org.keycloak.common.crypto; + +public enum FipsMode { + enabled("org.keycloak.crypto.fips.FIPS1402Provider"), + strict("org.keycloak.crypto.fips.Fips1402StrictCryptoProvider"), + disabled("org.keycloak.crypto.def.DefaultCryptoProvider"); + + private String providerClassName; + + FipsMode(String providerClassName) { + this.providerClassName = providerClassName; + } + + public boolean isFipsEnabled() { + return this.equals(enabled) || this.equals(strict); + } + + public String getProviderClassName() { + return providerClassName; + } +} diff --git a/common/src/main/java/org/keycloak/common/util/BouncyIntegration.java b/common/src/main/java/org/keycloak/common/util/BouncyIntegration.java index 8c25439aef..3e120d2b43 100755 --- a/common/src/main/java/org/keycloak/common/util/BouncyIntegration.java +++ b/common/src/main/java/org/keycloak/common/util/BouncyIntegration.java @@ -17,7 +17,6 @@ package org.keycloak.common.util; -import org.jboss.logging.Logger; import org.keycloak.common.crypto.CryptoIntegration; import java.security.Provider; @@ -29,8 +28,6 @@ import java.security.Security; */ public class BouncyIntegration { - private static final Logger log = Logger.getLogger(BouncyIntegration.class); - public static final String PROVIDER = loadProvider(); private static String loadProvider() { @@ -39,12 +36,6 @@ public class BouncyIntegration { return Security.getProviders()[0].getName(); // throw new RuntimeException("Failed to load required security provider: BouncyCastleProvider or BouncyCastleFipsProvider"); } - if (Security.getProvider(provider.getName()) == null) { - Security.addProvider(provider); - log.debugv("Loaded {0} security provider", provider.getClass().getName()); - } else { - log.debugv("Security provider {0} already loaded", provider.getClass().getName()); - } return provider.getName(); } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java index 5b2c0434ca..339cfdec31 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java @@ -27,6 +27,7 @@ import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.jce.spec.ECNamedCurveSpec; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jboss.logging.Logger; import org.keycloak.common.crypto.CryptoProvider; import org.keycloak.common.crypto.CryptoConstants; import org.keycloak.common.crypto.ECDSACryptoProvider; @@ -42,6 +43,8 @@ import org.keycloak.crypto.JavaAlgorithm; */ public class DefaultCryptoProvider implements CryptoProvider { + private static final Logger log = Logger.getLogger(DefaultCryptoProvider.class); + private final Provider bcProvider; private Map providers = new ConcurrentHashMap<>(); @@ -55,6 +58,13 @@ public class DefaultCryptoProvider implements CryptoProvider { providers.put(CryptoConstants.RSA1_5, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/PKCS1Padding")); providers.put(CryptoConstants.RSA_OAEP, new DefaultRsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); providers.put(CryptoConstants.RSA_OAEP_256, new DefaultRsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + + if (existingBc == null) { + Security.addProvider(this.bcProvider); + log.debugv("Loaded {0} security provider", this.bcProvider.getClass().getName()); + } else { + log.debugv("Security provider {0} already loaded", this.bcProvider.getClass().getName()); + } } diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java index 477780d5a9..40c5e91134 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java @@ -21,6 +21,7 @@ import java.security.cert.CertStore; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; +import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -33,7 +34,9 @@ import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.fips.FipsRSA; import org.bouncycastle.crypto.fips.FipsSHS; import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.bouncycastle.math.ec.ECCurve; +import org.jboss.logging.Logger; import org.keycloak.common.crypto.CryptoProvider; import org.keycloak.common.crypto.ECDSACryptoProvider; import org.keycloak.common.crypto.CryptoConstants; @@ -52,6 +55,8 @@ import org.keycloak.crypto.JavaAlgorithm; */ public class FIPS1402Provider implements CryptoProvider { + private static final Logger log = Logger.getLogger(FIPS1402Provider.class); + private final BouncyCastleFipsProvider bcFipsProvider; private final Map providers = new ConcurrentHashMap<>(); @@ -66,6 +71,14 @@ public class FIPS1402Provider implements CryptoProvider { providers.put(CryptoConstants.RSA_OAEP_256, new FIPSRsaKeyEncryptionJWEAlgorithmProvider(FipsRSA.WRAP_OAEP.withDigest(FipsSHS.Algorithm.SHA256))); Security.insertProviderAt(new KeycloakFipsSecurityProvider(bcFipsProvider), 1); + if (existingBcFipsProvider == null) { + Security.insertProviderAt(this.bcFipsProvider, 2); + Provider bcJsseProvider = new BouncyCastleJsseProvider("fips:BCFIPS"); + Security.insertProviderAt(bcJsseProvider, 3); + log.debugf("Inserted security providers: %s", Arrays.asList(this.bcFipsProvider.getName(),bcJsseProvider.getName())); + } else { + log.debugf("Security provider %s already loaded", existingBcFipsProvider.getName()); + } } diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402SslTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402SslTest.java new file mode 100644 index 0000000000..085f542dca --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/FIPS1402SslTest.java @@ -0,0 +1,142 @@ +package org.keycloak.crypto.fips.test; + +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; + +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.jboss.logging.Logger; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.Environment; +import org.keycloak.rule.CryptoInitRule; + +import static org.hamcrest.Matchers.greaterThan; + +/** + * @author Marek Posolda + */ +public class FIPS1402SslTest { + + protected static final Logger logger = Logger.getLogger(FIPS1402SslTest.class); + + @ClassRule + public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + + + @BeforeClass + public static void dumpSecurityProviders() { + logger.info(CryptoIntegration.dumpJavaSecurityProviders()); + logger.info(CryptoIntegration.dumpSecurityProperties()); + } + + @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 testPkcs12KeyStoreWithPKIXKeyMgrFactory() throws Exception { + // PKCS12 keystore works just in non-approved mode + Assume.assumeFalse(CryptoServicesRegistrar.isInApprovedOnlyMode()); + String type = "PKCS12"; + String password = "passwordpassword"; + + KeyStore keystore = loadKeystore(type, password); + String keyMgrDefaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory keyMgrFact = getKeyMgrFactory(password, keystore, keyMgrDefaultAlgorithm); + testSSLContext(keyMgrFact); + } + + // This works with BCFIPS, but requires addition of security provider "com.sun.net.ssl.internal.ssl.Provider BCFIPS" to Java Security providers + @Test + @Ignore("Skip for now and keep it just for the reference. We can check if we want to test this path with SunX509 algorithm withadditional security provider") + public void testPkcs12KeyStoreWithSunX509KeyMgrFactory() throws Exception { + // PKCS12 keystore works just in non-approved mode + Assume.assumeFalse(CryptoServicesRegistrar.isInApprovedOnlyMode()); + String type = "PKCS12"; + String password = "passwordpassword"; + + KeyStore keystore = loadKeystore(type, password); + String keyMgrDefaultAlgorithm = "SunX509"; + KeyManagerFactory keyMgrFact = getKeyMgrFactory(password, keystore, keyMgrDefaultAlgorithm); + testSSLContext(keyMgrFact); + } + + @Test + public void testBcfksKeyStoreWithPKIXKeyMgrFactory() throws Exception { + String type = "BCFKS"; + String password = "passwordpassword"; + + KeyStore keystore = loadKeystore(type, password); + String keyMgrDefaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory keyMgrFact = getKeyMgrFactory(password, keystore, keyMgrDefaultAlgorithm); + testSSLContext(keyMgrFact); + } + + // This works with BCFIPS, but requires addition of security provider "com.sun.net.ssl.internal.ssl.Provider BCFIPS" to Java Security providers + @Test + @Ignore("Skip for now and keep it just for the reference. We can check if we want to test this path with SunX509 algorithm withadditional security provider") + public void testBcfksKeyStoreWithSunX509KeyMgrFactory() throws Exception { + String type = "BCFKS"; + String password = "passwordpassword"; + + KeyStore keystore = loadKeystore(type, password); + String keyMgrDefaultAlgorithm = "SunX509"; + KeyManagerFactory keyMgrFact = getKeyMgrFactory(password, keystore, keyMgrDefaultAlgorithm); + testSSLContext(keyMgrFact); + } + + private KeyStore loadKeystore(String type, String password) throws Exception { + KeyStore keystore = KeyStore.getInstance(type); + InputStream in = FIPS1402SslTest.class.getClassLoader().getResourceAsStream("bcfips-keystore." + type.toLowerCase()); + keystore.load(in, password != null ? password.toCharArray() : null); + logger.infof("Keystore loaded successfully. Type: %s, provider: %s", keystore.getProvider().getName()); + return keystore; + } + + private KeyManagerFactory getKeyMgrFactory(String password, KeyStore keystore, String keyMgrAlgorithm) throws Exception { + KeyManagerFactory keyMgrFact = KeyManagerFactory.getInstance(keyMgrAlgorithm); + char[] keyPassword = password.toCharArray(); + keyMgrFact.init(keystore, keyPassword); + logger.infof("KeyManagerFactory loaded for algorithm: %s", keyMgrAlgorithm); + return keyMgrFact; + } + + + private void testSSLContext(KeyManagerFactory keyMgrFact) throws Exception { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyMgrFact.getKeyManagers(), null, null); + SSLEngine engine = context.createSSLEngine(); + + List enabledCipherSuites = Arrays.asList(engine.getEnabledCipherSuites()); + List supportedProtocols = Arrays.asList(context.getDefaultSSLParameters().getProtocols()); + List supportedCiphers = Arrays.asList(engine.getSupportedCipherSuites()); + + logger.infof("Enabled ciphersuites: %s", enabledCipherSuites.size()); + logger.infof("Supported protocols: %s", supportedProtocols); + logger.infof("Supported ciphers size: %d", supportedCiphers.size()); + Assert.assertThat(enabledCipherSuites.size(), greaterThan(0)); + Assert.assertThat(supportedProtocols.size(), greaterThan(0)); + Assert.assertThat(supportedCiphers.size(), greaterThan(0)); + + SSLSessionContext sslServerCtx = context.getServerSessionContext(); + Assert.assertNotNull(sslServerCtx); + } +} diff --git a/crypto/fips1402/src/test/resources/bcfips-keystore.bcfks b/crypto/fips1402/src/test/resources/bcfips-keystore.bcfks new file mode 100644 index 0000000000..26635fa443 Binary files /dev/null and b/crypto/fips1402/src/test/resources/bcfips-keystore.bcfks differ diff --git a/crypto/fips1402/src/test/resources/bcfips-keystore.pkcs12 b/crypto/fips1402/src/test/resources/bcfips-keystore.pkcs12 new file mode 100644 index 0000000000..537395180d Binary files /dev/null and b/crypto/fips1402/src/test/resources/bcfips-keystore.pkcs12 differ diff --git a/crypto/fips1402/src/test/resources/kc.java.security b/crypto/fips1402/src/test/resources/kc.java.security index 2e5f292caf..9267176d29 100644 --- a/crypto/fips1402/src/test/resources/kc.java.security +++ b/crypto/fips1402/src/test/resources/kc.java.security @@ -1,29 +1,51 @@ # 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 distribution. +# 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) -# NOTE: List is empty for now, so we test just with BCFIPS provider, which is registered programatically +# Uses only BouncyCastle FIPS providers to make sure to use only FIPS compliant cryptography. # -security.provider.1= +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. -# NOTE: List is empty for now, so we test just with BCFIPS provider, which is registered programatically +# 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= -#fips.provider.1=SunPKCS11 ${java.home}/conf/security/nss.fips.cfg -#fips.provider.2=SunEC -#fips.provider.3=com.sun.net.ssl.internal.ssl.Provider SunPKCS11-NSS-FIPS +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 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 +# 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=PKCS11 -fips.keystore.type=PKCS11 \ No newline at end of file +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 diff --git a/docs/fips.md b/docs/fips.md index ac3f4f2b0b..7ea3b8948e 100644 --- a/docs/fips.md +++ b/docs/fips.md @@ -1,57 +1,140 @@ FIPS 140-2 Integration ====================== -Build with FIPS ---------------- +Run the server with FIPS +------------------------ -With OpenJDK 11 on the classpath, run this from the project root directory: +To run Keycloak quarkus distribution, on the FIPS enabled host and FIPS enabled OpenJDK, you need to: +- Make sure that Keycloak will use the BouncyCastle FIPS dependencies instead of the normal BouncyCastle dependencies +- Make sure to start the server with the FIPS mode. + +1) Copy BCFIPS dependencies to your Keycloak distribution. +You can either download them from BouncyCastle page and add it manually to the directory `KEYCLOAK_HOME/providers`(make sure to +use proper versions compatible with BouncyCastle Keycloak dependencies). + +Or you can use for example commands like this to copy the appropriate BCFIPS jars to the Keycloak distribution. Again, replace +the BCFIPS versions with the appropriate versions from pom.xml. Assumption is that you have already these BCFIPS in +your local maven repository, which can be achieved for example by building `crypto/fips1402` module (See the section for +running the unit tests below): ``` -mvn clean install -DskipTests=true -Dfips140-2 -Pquarkus -``` -The property `fips140-2` is used to trigger maven profile to build keycloak+quarkus distribution with `bouncycastle-fips` dependencies instead of plain `bouncycastle` -and also with `keycloak-crypto-fips1402` module containing some security code dependent on bouncycastle-fips APIs. - -Note, that if you ommit the `fips140-2` property from the command above, then the quarkus distribution will be built -with the plain non-fips bouncycastle dependencies and with `keycloak-crypto-default` module. - -Then unzip and check only bouncycastle-fips libraries are inside "lib" directory: -``` -tar xf $KEYCLOAK_SOURCES/quarkus/dist/target/keycloak-999-SNAPSHOT.tar.gz -ls keycloak-999-SNAPSHOT/lib/lib/main/org.bouncycastle.bc* -``` -Output should be something like: -``` -keycloak-999-SNAPSHOT/lib/lib/main/org.bouncycastle.bc-fips-1.0.2.jar keycloak-999-SNAPSHOT/lib/lib/main/org.bouncycastle.bctls-fips-1.0.11.jar -keycloak-999-SNAPSHOT/lib/lib/main/org.bouncycastle.bcpkix-fips-1.0.3.jar +cd $KEYCLOAK_HOME/bin +export MAVEN_REPO_HOME=$HOME/.m2/repository +cp $MAVEN_REPO_HOME/org/bouncycastle/bc-fips/1.0.2.3/bc-fips-1.0.2.3.jar ../providers/ +cp $MAVEN_REPO_HOME/org/bouncycastle/bctls-fips/1.0.12.2/bctls-fips-1.0.12.2.jar ../providers/ +cp $MAVEN_REPO_HOME/org/bouncycastle/bcpkix-fips/1.0.5/bcpkix-fips-1.0.5.jar ../providers/ ``` -Similarly the JAR keycloak-fips-integration should be available: +2) Now create either pkcs12 or bcfks keystore. The pkcs12 works just in BCFIPS non-approved mode. + +Please choose either `bcfips` or `pkcs12` and use the appropriate value of `KEYSTORE_FILE` variable according to your choice: + +Also make sure to set `KEYCLOAK_SOURCES` to the location with your Keycloak codebase. + +Note that for keystore generation, it is needed to use the BouncyCastle FIPS libraries and use custom security file, which +will remove default SUN and SunPKCS11 providers as it doesn't work to create keystore with them on FIPS enabled OpenJDK11 due +the limitation described here https://access.redhat.com/solutions/6954451 and in the related bugzilla https://bugzilla.redhat.com/show_bug.cgi?id=2048582. ``` -ls keycloak-999-SNAPSHOT/lib/lib/main/org.keycloak.keycloak-fips-integration-999-SNAPSHOT.jar +export KEYSTORE_FILE=keycloak-server.pkcs12 +#export KEYSTORE_FILE=keycloak-server.bcfks +export KEYCLOAK_SOURCES=$HOME/IdeaProjects/keycloak + +export KEYSTORE_FORMAT=$(echo $KEYSTORE_FILE | cut -d. -f2) + +# Removing old keystore file to start from fresh +rm keycloak-server.pkcs12 +rm keycloak-server.bcfks + +keytool -keystore $KEYSTORE_FILE \ + -storetype $KEYSTORE_FORMAT \ + -providername BCFIPS \ + -providerclass org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \ + -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \ + -providerpath $MAVEN_REPO_HOME/org/bouncycastle/bc-fips/1.0.2.3/bc-fips-1.0.2.3.jar \ + -alias localhost \ + -genkeypair -sigalg SHA512withRSA -keyalg RSA -storepass passwordpassword \ + -dname CN=localhost -keypass passwordpassword \ + -J-Djava.security.properties=$KEYCLOAK_SOURCES/crypto/fips1402/src/test/resources/kc.java.security ``` -Now run the server on the FIPS enabled machine with FIPS-enabled OpenJDK (Tested on RHEL 8.6): +3) Run "build" to re-augment with `enabled` fips mode and start the server. + +For the `fips-mode`, he alternative is to use `--fips-mode=strict` in which case BouncyCastle FIPS will use "approved mode", +which means even stricter security algorithms. As mentioned above, strict mode won't work with `pkcs12` keystore: + ``` -cd keycloak-999-SNAPSHOT/bin -./kc.sh start-dev +./kc.sh build --fips-mode=enabled +./kc.sh start --optimized --hostname=localhost \ + --https-key-store-file=$PWD/$KEYSTORE_FILE \ + --https-key-store-type=$KEYSTORE_FORMAT \ + --https-key-store-password=passwordpassword \ + --log-level=INFO,org.keycloak.common.crypto:TRACE,org.keycloak.crypto:TRACE ``` -NOTE: Right now, server should start, and you should be able to use `http://localhost:8080` and login to admin console etc. -Keycloak will now use bouncycastle-fips libraries and the `CryptoIntegration` will use `FIPS1402Provider`. +4) The approach above will run the Keycloak JVM with all the default java security providers and will add also +BouncyCastle FIPS security providers on top of that in runtime. This works fine, however it may not be guaranteed that +all the crypto algorithms are used in the FIPS compliant way as the default providers like "Sun" potentially allow non-FIPS +usage in the Java. Some more details here: 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 -Run the tests in the FIPS environment -------------------------------------- +To ensure that Java strictly allows to use only FIPS-compliant crypto, it can be good to rely solely just on the BCFIPS. +This is possible by using custom java security file, which adds just the BouncyCastle FIPS security providers. This requires +BouncyCastle FIPS dependencies to be available in the bootstrap classpath instead of adding them in runtime. + +So for this approach, it is needed to move the BCFIPS jars from the `providers` directory to bootstrap classpath. +``` +mkdir ../lib/bootstrap +mv ../providers/bc*.jar ../lib/bootstrap/ +``` +Then run `build` and `start` commands as above, but with additional property for the alternative security file like +``` +-Djava.security.properties=$KEYCLOAK_SOURCES/crypto/fips1402/src/test/resources/kc.java.security +``` +At the server startup, you should see the message like this in the log and you can check if correct providers are present and not any others: +``` +2022-10-10 08:23:07,097 TRACE [org.keycloak.common.crypto.CryptoIntegration] (main) Java security providers: [ + KC(BCFIPS version 1.000203) version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider, + BCFIPS version 1.000203 - class org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider, + BCJSSE version 1.001202 - class org.bouncycastle.jsse.provider.BouncyCastleJsseProvider, +] +``` + +NOTE: If you want to use BouncyCastle approved mode, then it is recommended to change/add these properties into the `kc.java.security` +file: +``` +keystore.type=BCFKS +fips.keystore.type=BCFKS +org.bouncycastle.fips.approved_only=true +``` +and then check that startup log contains `KC` provider contains KC provider with the note about `Approved Mode` like this: +``` +KC(BCFIPS version 1.000203 Approved Mode) version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider, +``` +Note that in approved mode, there are few limitations at the moment like for example: +- User passwords must be at least 14 characters long +- Keystore/truststore must be of type bcfks due the both of `jks` and `pkcs12` don't work +- Some warnings in the server.log at startup + +Run the unit tests in the FIPS environment +------------------------------------------ This instruction is about running automated tests on the FIPS enabled RHEL 8.6 system with the FIPS enabled OpenJDK 11. So far only the unit tests inside the `crypto` module are supported. More effort is needed to have whole testsuite passing. First it is needed to build the project (See above). Then run the tests in the `crypto` module. ``` -mvn clean install -f crypto +mvn clean install -f common -DskipTests=true +mvn clean install -f core -DskipTests=true +mvn clean install -f server-spi -DskipTests=true +mvn clean install -f server-spi-private -DskipTests=true +mvn clean install -f crypto/fips1402 ``` The tests should work also with the BouncyCastle approved mode, which is more strict in the used crypto algorithms ``` -mvn clean install -f crypto -Dorg.bouncycastle.fips.approved_only=true +mvn clean install -f crypto/fips1402 -Dorg.bouncycastle.fips.approved_only=true ``` + +Run the integration tests in the FIPS environment +------------------------------------------------- +See the FIPS section in the [MySQL docker image](../testsuite/integration-arquillian/HOW-TO-RUN.md) + diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/SecurityOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/SecurityOptions.java index 63cc4fe1cb..4b31ddcec9 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/SecurityOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/SecurityOptions.java @@ -1,27 +1,9 @@ package org.keycloak.config; +import org.keycloak.common.crypto.FipsMode; + public class SecurityOptions { - public enum FipsMode { - enabled("org.keycloak.crypto.fips.FIPS1402Provider"), - strict("org.keycloak.crypto.fips.Fips1402StrictCryptoProvider"), - disabled("org.keycloak.crypto.def.DefaultCryptoProvider"); - - private String providerClassName; - - FipsMode(String providerClassName) { - this.providerClassName = providerClassName; - } - - public boolean isFipsEnabled() { - return this.equals(enabled) || this.equals(strict); - } - - public String getProviderClassName() { - return providerClassName; - } - } - public static final Option FIPS_MODE = new OptionBuilder<>("fips-mode", FipsMode.class) .category(OptionCategory.SECURITY) .buildTime(true) diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 8636f0a971..7c0f4e9c03 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -89,6 +89,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; import org.jboss.resteasy.spi.ResteasyDeployment; import org.keycloak.Config; +import org.keycloak.common.crypto.FipsMode; import org.keycloak.config.SecurityOptions; import org.keycloak.config.StorageOptions; import org.keycloak.connections.jpa.JpaConnectionProvider; @@ -305,10 +306,9 @@ class KeycloakProcessor { * * @param recorder */ - @Consume(BootstrapConfigSetupCompleteBuildItem.class) @Record(ExecutionTime.STATIC_INIT) @BuildStep - KeycloakSessionFactoryPreInitBuildItem configureProviders(KeycloakRecorder recorder, List descriptors) { + KeycloakSessionFactoryPreInitBuildItem configureKeycloakSessionFactory(KeycloakRecorder recorder, List descriptors) { Profile.setInstance(new QuarkusProfile()); Map, Map>>> factories = new HashMap<>(); Map, String> defaultProviders = new HashMap<>(); @@ -572,13 +572,13 @@ class KeycloakProcessor { })); } - @Consume(KeycloakSessionFactoryPreInitBuildItem.class) + @Consume(BootstrapConfigSetupCompleteBuildItem.class) @BuildStep @Record(ExecutionTime.STATIC_INIT) void setCryptoProvider(KeycloakRecorder recorder) { - SecurityOptions.FipsMode fipsMode = Configuration.getOptionalValue( + FipsMode fipsMode = Configuration.getOptionalValue( MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + SecurityOptions.FIPS_MODE.getKey()).map( - SecurityOptions.FipsMode::valueOf).orElse(SecurityOptions.FipsMode.disabled); + FipsMode::valueOf).orElse(FipsMode.disabled); recorder.setCryptoProvider(fipsMode); } diff --git a/quarkus/dist/src/main/content/bin/kc.bat b/quarkus/dist/src/main/content/bin/kc.bat index 9eb3a18c41..4fe25e31db 100644 --- a/quarkus/dist/src/main/content/bin/kc.bat +++ b/quarkus/dist/src/main/content/bin/kc.bat @@ -124,7 +124,7 @@ if "x%JAVA_HOME%" == "x" ( ) ) -set "CLASSPATH_OPTS=%DIRNAME%..\lib\quarkus-run.jar" +set "CLASSPATH_OPTS=%DIRNAME%..\lib\quarkus-run.jar:%DIRNAME%..\lib\bootstrap\*" set "JAVA_RUN_OPTS=%JAVA_OPTS% -Dkc.home.dir="%DIRNAME%.." -Djboss.server.config.dir="%DIRNAME%..\conf" -Dkeycloak.theme.dir="%DIRNAME%..\themes" %SERVER_OPTS% -cp "%CLASSPATH_OPTS%" io.quarkus.bootstrap.runner.QuarkusEntryPoint %CONFIG_ARGS%" diff --git a/quarkus/dist/src/main/content/bin/kc.sh b/quarkus/dist/src/main/content/bin/kc.sh index 2a4d785d9b..d7be862cde 100644 --- a/quarkus/dist/src/main/content/bin/kc.sh +++ b/quarkus/dist/src/main/content/bin/kc.sh @@ -36,7 +36,7 @@ SERVER_OPTS="-Dkc.home.dir='$(abs_path '..')'" SERVER_OPTS="$SERVER_OPTS -Djboss.server.config.dir='$(abs_path '../conf')'" SERVER_OPTS="$SERVER_OPTS -Djava.util.logging.manager=org.jboss.logmanager.LogManager" SERVER_OPTS="$SERVER_OPTS -Dquarkus-log-max-startup-records=10000" -CLASSPATH_OPTS="'$(abs_path "../lib/quarkus-run.jar")'" +CLASSPATH_OPTS="'$(abs_path "../lib/quarkus-run.jar"):$(abs_path "../lib/bootstrap/*")'" DEBUG_MODE="${DEBUG:-false}" DEBUG_PORT="${DEBUG_PORT:-8787}" diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index adcaef1cd4..bf1a3579ee 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -40,7 +40,7 @@ import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.crypto.CryptoProvider; -import org.keycloak.config.SecurityOptions; +import org.keycloak.common.crypto.FipsMode; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; @@ -166,7 +166,7 @@ public class KeycloakRecorder { }; } - public void setCryptoProvider(SecurityOptions.FipsMode fipsMode) { + public void setCryptoProvider(FipsMode fipsMode) { String cryptoProvider = fipsMode.getProviderClassName(); try { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClassLoaderPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClassLoaderPropertyMappers.java index 8026565ce6..51368edfc1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClassLoaderPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClassLoaderPropertyMappers.java @@ -3,6 +3,8 @@ package org.keycloak.quarkus.runtime.configuration.mappers; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import java.util.Optional; + +import org.keycloak.common.crypto.FipsMode; import org.keycloak.config.ClassLoaderOptions; import org.keycloak.config.SecurityOptions; import org.keycloak.quarkus.runtime.Environment; @@ -30,7 +32,7 @@ final class ClassLoaderPropertyMappers { ConfigValue fipsEnabled = Configuration.getConfigValue( MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + SecurityOptions.FIPS_MODE.getKey()); - if (fipsEnabled != null && SecurityOptions.FipsMode.valueOf(fipsEnabled.getValue()).isFipsEnabled()) { + if (fipsEnabled != null && FipsMode.valueOf(fipsEnabled.getValue()).isFipsEnabled()) { return Optional.of( "org.bouncycastle:bcprov-jdk15on,org.bouncycastle:bcpkix-jdk15on,org.keycloak:keycloak-crypto-default"); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/SecurityPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/SecurityPropertyMappers.java index 6c7db320a7..e15272d5a7 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/SecurityPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/SecurityPropertyMappers.java @@ -4,6 +4,8 @@ import static java.util.Optional.of; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import java.util.Optional; + +import org.keycloak.common.crypto.FipsMode; import org.keycloak.config.SecurityOptions; import io.smallrye.config.ConfigSourceInterceptorContext; @@ -23,16 +25,16 @@ final class SecurityPropertyMappers { private static Optional resolveFipsMode(Optional value, ConfigSourceInterceptorContext context) { if (value.isEmpty()) { - return of(SecurityOptions.FipsMode.disabled.toString()); + return of(FipsMode.disabled.toString()); } - return of(SecurityOptions.FipsMode.valueOf(value.get()).toString()); + return of(FipsMode.valueOf(value.get()).toString()); } private static Optional resolveSecurityProvider(Optional value, ConfigSourceInterceptorContext configSourceInterceptorContext) { - SecurityOptions.FipsMode fipsMode = value.map(SecurityOptions.FipsMode::valueOf) - .orElse(SecurityOptions.FipsMode.disabled); + FipsMode fipsMode = value.map(FipsMode::valueOf) + .orElse(FipsMode.disabled); if (fipsMode.isFipsEnabled()) { return of("BCFIPS"); diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java index 6473c22e51..6b848971ca 100644 --- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java +++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java @@ -38,8 +38,6 @@ import javax.mail.internet.MimeMultipart; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import java.io.UnsupportedEncodingException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Map; import java.util.Properties; @@ -49,9 +47,8 @@ import java.util.Properties; */ public class DefaultEmailSenderProvider implements EmailSenderProvider { - private static final String SUPPORTED_SSL_PROTOCOLS = getSupportedSslProtocols(); - private static final Logger logger = Logger.getLogger(DefaultEmailSenderProvider.class); + private static final String SUPPORTED_SSL_PROTOCOLS = getSupportedSslProtocols(); private final KeycloakSession session; diff --git a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java index 578e612424..5b5f09b790 100755 --- a/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java +++ b/services/src/main/java/org/keycloak/truststore/FileTruststoreProviderFactory.java @@ -19,6 +19,7 @@ package org.keycloak.truststore; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.common.util.KeystoreUtil; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; @@ -44,6 +45,8 @@ 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; /** @@ -66,6 +69,7 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory String storepath = config.get("file"); String pass = config.get("password"); String policy = config.get("hostname-verification-policy"); + String configuredType = config.get("type"); // if "truststore" . "file" is not configured then it is disabled if (storepath == null && pass == null && policy == null) { @@ -82,10 +86,11 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory throw new RuntimeException("Attribute 'password' missing in 'truststore':'file' configuration"); } + String type = getTruststoreType(storepath, configuredType); try { - truststore = loadStore(storepath, pass == null ? null :pass.toCharArray()); + truststore = loadStore(storepath, type, pass == null ? null :pass.toCharArray()); } catch (Exception e) { - throw new RuntimeException("Failed to initialize TruststoreProviderFactory: " + new File(storepath).getAbsolutePath(), e); + throw new RuntimeException("Failed to initialize TruststoreProviderFactory: " + new File(storepath).getAbsolutePath() + ", truststore type: " + type, e); } if (policy == null) { verificationPolicy = HostnameVerificationPolicy.WILDCARD; @@ -101,11 +106,11 @@ public class FileTruststoreProviderFactory implements TruststoreProviderFactory provider = new FileTruststoreProvider(truststore, verificationPolicy, Collections.unmodifiableMap(certsLoader.trustedRootCerts) , Collections.unmodifiableMap(certsLoader.intermediateCerts)); TruststoreProviderSingleton.set(provider); - log.debug("File truststore provider initialized: " + new File(storepath).getAbsolutePath()); + log.debugf("File truststore provider initialized: %s, Truststore type: %s", new File(storepath).getAbsolutePath(), type); } - private KeyStore loadStore(String path, char[] password) throws Exception { - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + private KeyStore loadStore(String path, String type, char[] password) throws Exception { + KeyStore ks = KeyStore.getInstance(type); InputStream is = new FileInputStream(path); try { ks.load(is, password); @@ -154,6 +159,25 @@ 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/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index 99b47ad179..c0b89516ec 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -923,3 +923,25 @@ DefaultHostnameTest.java:226) when running these tests on your local machine. This happens when something on your machine or network is blocking DNS queries to [nip.io](https://nip.io) One possible workaround is to add a commonly used public dns server (e.g. 8.8.8.8 for google dns server) to your local networks dns configuration and run the tests. + +## FIPS 140-2 testing + +On the FIPS enabled platform with FIPS enabled OpenJDK 11, you can run this to test against Keycloak server on Quarkus +with FIPS 140.2 integration enabled +``` +mvn -B -f testsuite/integration-arquillian/pom.xml \ + clean install \ + -Pauth-server-quarkus,auth-server-fips140-2 \ + -Dcom.redhat.fips=false +``` +NOTE 1: The property `com.redhat.fips` is needed so that testsuite itself is executed in the JVM with FIPS disabled. However +most important part is that Keycloak itself is running on the JVM with FIPS enabled. You can check log from server startup and +there should be messages similar to those: +``` +2022-10-11 19:34:29,521 DEBUG [org.keycloak.common.crypto.CryptoIntegration] (main) Using the crypto provider: org.keycloak.crypto.fips.FIPS1402Provider +2022-10-11 19:34:31,072 TRACE [org.keycloak.common.crypto.CryptoIntegration] (main) Java security providers: [ + KC(BCFIPS version 1.000203) version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider, + BCFIPS version 1.000203 - class org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider, + BCJSSE version 1.001202 - class org.bouncycastle.jsse.provider.BouncyCastleJsseProvider, +] +``` diff --git a/testsuite/integration-arquillian/servers/auth-server/common/fips/kc.java.security b/testsuite/integration-arquillian/servers/auth-server/common/fips/kc.java.security new file mode 100644 index 0000000000..9267176d29 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/common/fips/kc.java.security @@ -0,0 +1,51 @@ +# 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 diff --git a/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.keystore.pkcs12 b/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.keystore.pkcs12 new file mode 100644 index 0000000000..450664a866 Binary files /dev/null and b/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.keystore.pkcs12 differ diff --git a/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.truststore.pkcs12 b/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.truststore.pkcs12 new file mode 100644 index 0000000000..918f154b68 Binary files /dev/null and b/testsuite/integration-arquillian/servers/auth-server/common/fips/keycloak-fips.truststore.pkcs12 differ diff --git a/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml b/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml index d08838bef1..b761948438 100644 --- a/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml +++ b/testsuite/integration-arquillian/servers/auth-server/quarkus/pom.xml @@ -21,6 +21,7 @@ local ${project.parent.basedir}/../../tests/base/src/test/resources + disabled @@ -300,5 +301,68 @@ + + auth-server-fips140-2 + + enabled + + + + + org.keycloak + keycloak-crypto-fips1402 + + + + + + + maven-resources-plugin + + + copy-fips-pki-conf + process-resources + + copy-resources + + + ${auth.server.home}/conf + + + ${common.resources}/fips + + ** + + + + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-bcfips-deps + generate-resources + + copy-dependencies + + + ${auth.server.home}/lib/bootstrap + bc-fips,bctls-fips,bcpkix-fips + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java index 19b2c97c62..b93eafbeab 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.jboss.arquillian.container.spi.ConfigurationException; import org.jboss.arquillian.container.spi.client.container.ContainerConfiguration; import org.jboss.logging.Logger; +import org.keycloak.common.crypto.FipsMode; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -23,6 +24,20 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { private int bindHttpPort = 8080; private int bindHttpsPortOffset = 0; private int bindHttpsPort = Integer.getInteger("auth.server.https.port", 8543); + + private String keystoreFile = System.getProperty("auth.server.keystore"); + + private String keystorePassword = System.getProperty("auth.server.keystore.password"); + + private String keystoreType = System.getProperty("auth.server.keystore.type"); + + + private String truststoreFile = System.getProperty("auth.server.truststore"); + + private String truststorePassword = System.getProperty("auth.server.truststore.password"); + + private String truststoreType = System.getProperty("auth.server.truststore.type"); + private int debugPort = -1; private Path providersPath = Paths.get(System.getProperty("auth.server.home")); private int startupTimeoutInSeconds = 300; @@ -34,6 +49,8 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { private boolean reaugmentBeforeStart; private String importFile = System.getProperty("migration.import.file.name"); + private FipsMode fipsMode = FipsMode.valueOf(System.getProperty("auth.server.fips.mode")); + @Override public void validate() throws ConfigurationException { int basePort = getBindHttpPort(); @@ -88,6 +105,54 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { this.bindHttpPort = bindHttpPort; } + public String getKeystoreFile() { + return keystoreFile; + } + + public void setKeystoreFile(String keystoreFile) { + this.keystoreFile = keystoreFile; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public void setKeystorePassword(String keystorePassword) { + this.keystorePassword = keystorePassword; + } + + public String getKeystoreType() { + return keystoreType; + } + + public void setKeystoreType(String keystoreType) { + this.keystoreType = keystoreType; + } + + public String getTruststoreFile() { + return truststoreFile; + } + + public void setTruststoreFile(String truststoreFile) { + this.truststoreFile = truststoreFile; + } + + public String getTruststorePassword() { + return truststorePassword; + } + + public void setTruststorePassword(String truststorePassword) { + this.truststorePassword = truststorePassword; + } + + public String getTruststoreType() { + return truststoreType; + } + + public void setTruststoreType(String truststoreType) { + this.truststoreType = truststoreType; + } + public Path getProvidersPath() { return providersPath; } @@ -140,6 +205,14 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { return javaOpts; } + public void appendJavaOpts(String javaOpts) { + if (javaOpts == null) { + setJavaOpts(javaOpts); + } else { + setJavaOpts(this.javaOpts + " " + javaOpts); + } + } + public boolean isReaugmentBeforeStart() { return reaugmentBeforeStart; } @@ -163,4 +236,12 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { public void setImportFile(String importFile) { this.importFile = importFile; } + + public FipsMode getFipsMode() { + return fipsMode; + } + + public void setFipsMode(FipsMode fipsMode) { + this.fipsMode = fipsMode; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java index 1b259d5b70..ef3527c52e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java @@ -47,6 +47,7 @@ import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.descriptor.api.Descriptor; +import org.keycloak.common.crypto.FipsMode; import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.model.StoreProvider; @@ -227,8 +228,10 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta final Supplier shouldSetUpDb = () -> !restart.get() && !storeProvider.equals(StoreProvider.DEFAULT); final Supplier getClusterConfig = () -> System.getProperty("auth.server.quarkus.cluster.config", "local"); + log.debugf("FIPS Mode: %s", configuration.getFipsMode()); + // only run build during first execution of the server (if the DB is specified), restarts or when running cluster tests - if (restart.get() || shouldSetUpDb.get() || "ha".equals(getClusterConfig.get())) { + if (restart.get() || shouldSetUpDb.get() || "ha".equals(getClusterConfig.get()) || configuration.getFipsMode() != FipsMode.disabled) { commands.removeIf("--optimized"::equals); commands.add("--http-relative-path=/auth"); @@ -241,6 +244,10 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta commands.add("--cache-config-file=cluster-" + cacheMode + ".xml"); } } + + if (configuration.getFipsMode() != FipsMode.disabled) { + addFipsOptions(commands); + } } addStorageOptions(storeProvider, commands); @@ -257,6 +264,26 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta storeProvider.addStoreOptions(commands); } + private void addFipsOptions(List commands) { + commands.add("--fips-mode=" + configuration.getFipsMode().toString()); + + log.debugf("Keystore file: %s, keystore type: %s, truststore file: %s, truststore type: %s", + configuration.getKeystoreFile(), configuration.getKeystoreType(), + configuration.getTruststoreFile(), configuration.getTruststoreType()); + commands.add("--https-key-store-file=" + configuration.getKeystoreFile()); + commands.add("--https-key-store-type=" + configuration.getKeystoreType()); + commands.add("--https-key-store-password=" + configuration.getKeystorePassword()); + commands.add("--https-trust-store-file=" + configuration.getTruststoreFile()); + commands.add("--https-trust-store-type=" + configuration.getTruststoreType()); + commands.add("--https-trust-store-password=" + configuration.getTruststorePassword()); + commands.add("--spi-truststore-file-file=" + configuration.getTruststoreFile()); + commands.add("--spi-truststore-file-password=" + configuration.getTruststorePassword()); + commands.add("--spi-truststore-file-type=" + configuration.getTruststoreType()); + commands.add("--log-level=INFO,org.keycloak.common.crypto:TRACE,org.keycloak.crypto:TRACE,org.keycloak.truststore:TRACE"); + + configuration.appendJavaOpts("-Djava.security.properties=" + System.getProperty("auth.server.java.security.file")); + } + private void waitForReadiness() throws MalformedURLException, LifecycleException { SuiteContext suiteContext = this.suiteContext.get(); //TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 4d62d9f58a..6bbfc6897b 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -73,8 +73,10 @@ org.jboss.as.arquillian.container.managed.ManagedDeployableContainer ${auth.server.config.dir}/keycloak.truststore secret + jks ${auth.server.config.dir}/keycloak.jks secret + jks integration-arquillian-servers-auth-server-${auth.server} @@ -260,6 +262,7 @@ false default local + disabled @@ -522,8 +525,11 @@ ${auth.server.jboss.jvm.debug.args} ${auth.server.truststore} ${auth.server.truststore.password} + ${auth.server.truststore.type} ${auth.server.keystore} ${auth.server.keystore.password} + ${auth.server.keystore.type} + ${auth.server.java.security.file} ${auth.server.jvm.args.extra} ${auth.server.profile} @@ -676,6 +682,10 @@ ${keycloak.connectionsJpa.user} ${keycloak.connectionsJpa.password} + + ${auth.server.fips.mode} + ${auth.server.fips.keystore.type} +