From aea7833b4c9f54e3c9840c278e005782341f1047 Mon Sep 17 00:00:00 2001 From: josh-tracy Date: Tue, 6 Oct 2026 13:24:43 +0000 Subject: [PATCH 1/2] Import all PEM certificates with BCFIPS Closes #53612 Signed-off-by: josh-tracy --- .../fips/test/FIPS1402KeystoreTypesTest.java | 59 +++++++++++++++++++ .../truststore/TruststoreBuilder.java | 3 +- 2 files changed, 61 insertions(+), 1 deletion(-) 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 index ba31798d2d5d..c28aa25cafdf 100644 --- 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 @@ -1,5 +1,10 @@ package org.keycloak.crypto.fips.test; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.security.KeyStore; import java.util.Set; import java.util.stream.Collectors; @@ -7,15 +12,20 @@ import org.keycloak.common.util.Environment; import org.keycloak.common.util.KeystoreUtil; import org.keycloak.rule.CryptoInitRule; +import org.keycloak.truststore.TruststoreBuilder; import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.hamcrest.Matchers; import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; /** * @author Marek Posolda @@ -25,6 +35,9 @@ public class FIPS1402KeystoreTypesTest { @ClassRule public static CryptoInitRule cryptoInitRule = new CryptoInitRule(); + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Before public void before() { // Run this test just if java is in FIPS mode @@ -67,4 +80,50 @@ public void testTruststoreFormatsInApprovedMode() { KeystoreUtil.TruststoreFormat.BCFKS)); assertThat(CryptoIntegration.getProvider().getPreferredGeneratedTrustStoreType(), Matchers.equalTo(KeystoreUtil.TruststoreFormat.BCFKS)); } + + @Test + public void testMultiCertificatePemTruststoreInApprovedMode() throws Exception { + Assume.assumeTrue(CryptoServicesRegistrar.isInApprovedOnlyMode()); + + File bundle = temporaryFolder.newFile("bundle.pem"); + try (OutputStream output = Files.newOutputStream(bundle.toPath())) { + for (String resource : new String[] { "/certs/UPN-cert.pem", "/certs/ANS-cert.pem" }) { + try (InputStream input = getClass().getResourceAsStream(resource)) { + assertNotNull(resource, input); + input.transferTo(output); + output.write('\n'); + } + } + } + + String truststoreKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_KEY; + String typeKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_TYPE_KEY; + String passwordKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_PASSWORD_KEY; + String originalTruststore = System.getProperty(truststoreKey); + String originalType = System.getProperty(typeKey); + String originalPassword = System.getProperty(passwordKey); + try { + TruststoreBuilder.setSystemTruststore(new String[] { bundle.getAbsolutePath() }, false, + temporaryFolder.getRoot().getAbsolutePath()); + + assertEquals(KeystoreUtil.TruststoreFormat.BCFKS.name(), System.getProperty(typeKey)); + KeyStore truststore = KeyStore.getInstance("BCFKS", "BCFIPS"); + try (InputStream input = Files.newInputStream(new File(System.getProperty(truststoreKey)).toPath())) { + truststore.load(input, TruststoreBuilder.DUMMY_PASSWORD.toCharArray()); + } + assertEquals(2, truststore.size()); + } finally { + restoreProperty(truststoreKey, originalTruststore); + restoreProperty(typeKey, originalType); + restoreProperty(passwordKey, originalPassword); + } + } + + private static void restoreProperty(String key, String value) { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } } diff --git a/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java b/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java index 031843062f08..ab21298ea03f 100644 --- a/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java +++ b/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java @@ -17,6 +17,7 @@ package org.keycloak.truststore; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -267,7 +268,7 @@ static KeyStore loadStore(String path, String type, String password) { } static boolean mergePemFile(KeyStore truststore, String file, boolean isPem) { - try (FileInputStream pemInputStream = new FileInputStream(file)) { + try (BufferedInputStream pemInputStream = new BufferedInputStream(new FileInputStream(file))) { CertificateFactory certFactory = CertificateFactory.getInstance("X509"); boolean loadedAny = false; while (pemInputStream.available() > 0) { From e903054e7ca16f670ce91f38a77306609d4ebfa2 Mon Sep 17 00:00:00 2001 From: josh-tracy Date: Thu, 8 Oct 2026 14:55:09 +0000 Subject: [PATCH 2/2] Import all certificates from PEM bundles Signed-off-by: josh-tracy --- .../truststore/TruststoreBuilder.java | 40 +++++++------------ .../truststore/TruststoreBuilderTest.java | 15 +++++++ 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java b/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java index ab21298ea03f..f588ea5988cf 100644 --- a/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java +++ b/services/src/main/java/org/keycloak/truststore/TruststoreBuilder.java @@ -17,7 +17,6 @@ package org.keycloak.truststore; -import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -28,6 +27,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Stream; @@ -268,34 +268,24 @@ static KeyStore loadStore(String path, String type, String password) { } static boolean mergePemFile(KeyStore truststore, String file, boolean isPem) { - try (BufferedInputStream pemInputStream = new BufferedInputStream(new FileInputStream(file))) { + try (FileInputStream pemInputStream = new FileInputStream(file)) { CertificateFactory certFactory = CertificateFactory.getInstance("X509"); - boolean loadedAny = false; - while (pemInputStream.available() > 0) { - X509Certificate cert; - try { - cert = (X509Certificate) certFactory.generateCertificate(pemInputStream); - loadedAny = true; - } catch (CertificateException e) { - if (pemInputStream.available() > 0 || !loadedAny) { - // any remaining input means there is an actual problem with the key contents or - // file format - if (isPem || loadedAny) { - throw e; - } - LOGGER.debugf(e, - "The file %s may not be in PEM format, it will not be used to create the merged truststore", - new File(file).getAbsolutePath()); - continue; - } - LOGGER.debugf(e, - "The trailing entry for %s generated a certificate exception, assuming instead that the file ends with comments", - new File(file).getAbsolutePath()); - continue; + Collection certs; + try { + certs = certFactory.generateCertificates(pemInputStream); + } catch (CertificateException e) { + if (isPem || KeystoreUtil.getTruststoreFormat(file).orElse(null) == TruststoreFormat.PEM) { + throw e; } + LOGGER.debugf(e, + "The file %s may not be in PEM format, it will not be used to create the merged truststore", + new File(file).getAbsolutePath()); + return false; + } + for (Certificate cert : certs) { setCertificateEntry(truststore, cert); } - return loadedAny; + return !certs.isEmpty(); } catch (Exception e) { throw new RuntimeException( "Failed to initialize truststore, could not merge: " + new File(file).getAbsolutePath(), e); diff --git a/services/src/test/java/org/keycloak/truststore/TruststoreBuilderTest.java b/services/src/test/java/org/keycloak/truststore/TruststoreBuilderTest.java index 0df31696dfba..27887a0d8dad 100644 --- a/services/src/test/java/org/keycloak/truststore/TruststoreBuilderTest.java +++ b/services/src/test/java/org/keycloak/truststore/TruststoreBuilderTest.java @@ -19,6 +19,8 @@ import java.io.File; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.security.KeyStore; import java.util.ArrayList; import java.util.Collections; @@ -172,6 +174,19 @@ public void testFailsWithInvalidFile() throws Exception { .createMergedTruststore(new String[] { new File(url.getPath()).getAbsolutePath() }, false)); } + @Test + public void testFailsWithMalformedPemInDirectory() throws Exception { + URL url = TruststoreBuilderTest.class.getResource("/truststores/keycloak.pem"); + File directory = temporaryFolder.newFolder(); + File bundle = new File(directory, "bundle.pem"); + Files.copy(new File(url.toURI()).toPath(), bundle.toPath()); + Files.writeString(bundle.toPath(), "\n-----BEGIN CERTIFICATE-----\nnot-base64\n-----END CERTIFICATE-----\n", + StandardOpenOption.APPEND); + + assertThrows(RuntimeException.class, () -> TruststoreBuilder + .createMergedTruststore(new String[] { directory.getAbsolutePath() }, false)); + } + @Test public void testRejectsPemAsGeneratedTruststoreFormat() { assertThrows(IllegalArgumentException.class, () -> TruststoreBuilder.createTrustStore(TruststoreFormat.PEM));