TruststoreBuilder.java
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.truststore;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
/**
* Builds a system-wide truststore from the given config options.
*/
public class TruststoreBuilder {
public static final String SYSTEM_TRUSTSTORE_KEY = "javax.net.ssl.trustStore";
public static final String SYSTEM_TRUSTSTORE_PASSWORD_KEY = "javax.net.ssl.trustStorePassword";
public static final String SYSTEM_TRUSTSTORE_TYPE_KEY = "javax.net.ssl.trustStoreType";
private static final String CERT_PROTECTION_ALGORITHM_KEY = "keystore.pkcs12.certProtectionAlgorithm";
public static final String DUMMY_PASSWORD = "keycloakchangeit"; // fips length compliant dummy password
static final String PKCS12 = "PKCS12";
private static final Logger LOGGER = Logger.getLogger(TruststoreBuilder.class);
public static void setSystemTruststore(String[] truststores, boolean trustStoreIncludeDefault, String dataDir) {
KeyStore truststore = createMergedTruststore(truststores, trustStoreIncludeDefault);
// save with a dummy password just in case some logic that uses the system properties needs to have one
File file = saveTruststore(truststore, dataDir, DUMMY_PASSWORD.toCharArray());
// finally update the system properties
System.setProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_KEY, file.getAbsolutePath());
System.setProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_TYPE_KEY, PKCS12);
System.setProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_PASSWORD_KEY, DUMMY_PASSWORD);
}
static File saveTruststore(KeyStore truststore, String dataDir, char[] password) {
File file = new File(dataDir, "keycloak-truststore.p12");
file.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(file)) {
// this should inhibit the use of encryption in storing the certs
// it's of course not concurrency safe, but it should only be run at startup
String oldValue = System.setProperty(CERT_PROTECTION_ALGORITHM_KEY, "NONE");
truststore.store(fos, password);
if (oldValue != null) {
System.setProperty(CERT_PROTECTION_ALGORITHM_KEY, oldValue);
} else {
System.getProperties().remove(CERT_PROTECTION_ALGORITHM_KEY);
}
} catch (Exception e) {
throw new RuntimeException("Failed to save truststore: " + file.getAbsolutePath(), e);
}
return file;
}
static KeyStore createMergedTruststore(String[] truststores, boolean trustStoreIncludeDefault) {
KeyStore truststore = createPkcs12KeyStore();
if (trustStoreIncludeDefault) {
includeDefaultTruststore(truststore);
}
List<String> discoveredFiles = new ArrayList<>();
mergeFiles(truststores, truststore, true, discoveredFiles);
if (!discoveredFiles.isEmpty()) {
LOGGER.infof("Found the following truststore files under directories specified in the truststore paths %s",
discoveredFiles);
}
return truststore;
}
private static void mergeFiles(String[] truststores, KeyStore truststore, boolean topLevel, List<String> discoveredFiles) {
for (String file : truststores) {
File f = new File(file);
if (f.isDirectory()) {
mergeFiles(Stream.of(f.listFiles()).map(File::getAbsolutePath).toArray(String[]::new), truststore, false, discoveredFiles);
} else {
var format = KeystoreUtil.getKeystoreFormat(file).orElse(null);
if (format == KeystoreFormat.PKCS12) {
mergeTrustStore(truststore, file, loadStore(file, PKCS12, null));
if (!topLevel) {
discoveredFiles.add(f.getAbsolutePath());
}
} else if (mergePemFile(truststore, file, topLevel) && !topLevel) {
discoveredFiles.add(f.getAbsolutePath());
}
}
}
}
static KeyStore createPkcs12KeyStore() {
try {
KeyStore truststore = KeyStore.getInstance(PKCS12);
truststore.load(null, null);
return truststore;
} catch (Exception e) {
throw new RuntimeException("Failed to initialize truststore: cannot create a PKCS12 keystore", e);
}
}
/**
* Include the default truststore, if it can be found.
* <p>
* The existing system properties will be preserved so that this logic can be rerun without consuming
* the newly created merged truststore.
*/
static void includeDefaultTruststore(KeyStore truststore) {
String originalTruststoreKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_KEY + ".orig";
String originalTruststoreTypeKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_TYPE_KEY + ".orig";
String originalTruststorePasswordKey = TruststoreBuilder.SYSTEM_TRUSTSTORE_PASSWORD_KEY + ".orig";
String trustStorePath = System.getProperty(originalTruststoreKey);
String type = PKCS12;
String password = null;
File defaultTrustStore = null;
if (trustStorePath == null) {
trustStorePath = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_KEY);
if (trustStorePath == null) {
defaultTrustStore = getJRETruststore();
} else {
type = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_TYPE_KEY, KeyStore.getDefaultType());
password = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_PASSWORD_KEY);
// save the original information
System.setProperty(originalTruststoreKey, trustStorePath);
System.setProperty(originalTruststoreTypeKey, type);
if (password == null) {
System.getProperties().remove(originalTruststorePasswordKey);
} else {
System.setProperty(originalTruststorePasswordKey, password);
}
defaultTrustStore = new File(trustStorePath);
}
} else {
type = System.getProperty(originalTruststoreTypeKey);
password = System.getProperty(originalTruststorePasswordKey);
defaultTrustStore = new File(trustStorePath);
}
if (defaultTrustStore.exists()) {
String path = defaultTrustStore.getAbsolutePath();
mergeTrustStore(truststore, path, loadStore(path, type, password));
} else {
LOGGER.warnf("Default truststore was to be included, but could not be found at: %s", defaultTrustStore);
}
}
static File getJRETruststore() {
// try jre locations - there doesn't seem to be a good default mechanism for this
String securityDirectory = System.getProperty("java.home") + File.separator + "lib" + File.separator
+ "security";
File jssecacertsFile = new File(securityDirectory, "jssecacerts");
if (jssecacertsFile.exists() && jssecacertsFile.isFile()) {
return jssecacertsFile;
}
return new File(securityDirectory, "cacerts");
}
static KeyStore loadStore(String path, String type, String password) {
try {
return KeystoreUtil.loadKeyStore(path, password, type);
} catch (Exception e) {
throw new RuntimeException(
"Failed to initialize truststore: " + new File(path).getAbsolutePath() + ", type: " + type, e);
}
}
private static boolean mergePemFile(KeyStore truststore, String file, boolean isPem) {
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;
}
setCertificateEntry(truststore, cert);
}
return loadedAny;
} catch (Exception e) {
throw new RuntimeException(
"Failed to initialize truststore, could not merge: " + new File(file).getAbsolutePath(), e);
}
}
private static void setCertificateEntry(KeyStore truststore, Certificate cert) throws KeyStoreException {
String alias = null;
if (cert instanceof X509Certificate) {
X509Certificate x509Cert = (X509Certificate)cert;
// use an alias that should be unique, yet deterministic
alias = x509Cert.getSubjectX500Principal().getName() + "_" + x509Cert.getSerialNumber().toString(16);
} else {
// isn't expected
alias = String.valueOf(Collections.list(truststore.aliases()).size());
}
truststore.setCertificateEntry(alias, cert);
}
private static void mergeTrustStore(KeyStore truststore, String file, KeyStore additionalStore) {
try {
for (String alias : Collections.list(additionalStore.aliases())) {
if (additionalStore.isCertificateEntry(alias)) {
setCertificateEntry(truststore, additionalStore.getCertificate(alias));
}
}
} catch (Exception e) {
throw new RuntimeException(
"Failed to initialize truststore, could not merge: " + new File(file).getAbsolutePath(), e);
}
}
}