FileTruststoreProviderFactory.java
/*
* Copyright 2016 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.Config;
import org.keycloak.common.enums.HostnameVerificationPolicy;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.security.auth.x500.X500Principal;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class FileTruststoreProviderFactory implements TruststoreProviderFactory {
static final String HOSTNAME_VERIFICATION_POLICY = "hostname-verification-policy";
private static final Logger log = Logger.getLogger(FileTruststoreProviderFactory.class);
private TruststoreProvider provider;
@Override
public TruststoreProvider create(KeycloakSession session) {
return provider;
}
// For testing purposes
public void setProvider(TruststoreProvider provider) {
this.provider = provider;
}
@Override
public void init(Config.Scope config) {
String storepath = config.get("file");
String pass = config.get("password");
String policy = config.get(HOSTNAME_VERIFICATION_POLICY);
String configuredType = config.get("type");
if (storepath != null || pass != null || configuredType != null) {
log.warn("Using deprecated 'spi-truststore-file-*' options. Consider using 'truststore-paths' option.");
}
HostnameVerificationPolicy verificationPolicy = null;
KeyStore truststore = null;
boolean system = false;
if (storepath == null) {
storepath = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_KEY);
if (storepath == null) {
File defaultTrustStore = TruststoreBuilder.getJRETruststore();
if (!defaultTrustStore.exists()) {
throw new RuntimeException("Attribute 'file' missing in 'truststore':'file' configuration, and could not find the system truststore");
}
storepath = defaultTrustStore.getAbsolutePath();
system = true;
}
// should there be an exception if pass / type are configured for the spi-truststore
pass = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_PASSWORD_KEY, system ? "changeit" : null);
configuredType = System.getProperty(TruststoreBuilder.SYSTEM_TRUSTSTORE_TYPE_KEY);
}
String type = KeystoreUtil.getKeystoreType(configuredType, storepath, KeyStore.getDefaultType());
try {
truststore = KeystoreUtil.loadKeyStore(storepath, pass, type);
} catch (Exception e) {
// in fips mode the default truststore type can be pkcs12, but the cacerts file will still be jks
if (system && !"jks".equalsIgnoreCase(type)) {
try {
truststore = KeystoreUtil.loadKeyStore(storepath, pass, "jks");
} catch (Exception e1) {
}
}
if (truststore == null) {
throw new RuntimeException("Failed to initialize TruststoreProviderFactory: " + new File(storepath).getAbsolutePath() + ", truststore type: " + type, e);
}
}
if (policy == null) {
verificationPolicy = HostnameVerificationPolicy.DEFAULT;
} else {
try {
verificationPolicy = HostnameVerificationPolicy.valueOf(policy);
} catch (Exception e) {
throw new RuntimeException("Invalid value for 'hostname-verification-policy': " + policy
+ " (must be one of: " + Stream.of(HostnameVerificationPolicy.values())
.map(HostnameVerificationPolicy::name).collect(Collectors.joining(", "))
+ ")");
}
}
TruststoreCertificatesLoader certsLoader = new TruststoreCertificatesLoader(truststore);
provider = new FileTruststoreProvider(truststore, verificationPolicy, Collections.unmodifiableMap(certsLoader.trustedRootCerts)
, Collections.unmodifiableMap(certsLoader.intermediateCerts));
TruststoreProviderSingleton.set(provider);
log.debugf("File truststore provider initialized: %s, Truststore type: %s", new File(storepath).getAbsolutePath(), type);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "file";
}
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name("file")
.type("string")
.helpText("DEPRECATED: The file path of the trust store from where the certificates are going to be read from to validate TLS connections.")
.add()
.property()
.name("password")
.type("string")
.helpText("DEPRECATED: The trust store password.")
.add()
.property()
.name(HOSTNAME_VERIFICATION_POLICY)
.type("string")
.helpText("DEPRECATED: The hostname verification policy.")
.options(Arrays.stream(HostnameVerificationPolicy.values()).map(HostnameVerificationPolicy::name).toArray(String[]::new))
.defaultValue(HostnameVerificationPolicy.DEFAULT.name())
.add()
.property()
.name("type")
.type("string")
.helpText("DEPRECATED: Type of the truststore. If not provided, the type would be detected based on the truststore file extension or platform default type.")
.add()
.build();
}
private static class TruststoreCertificatesLoader {
private Map<X500Principal, List<X509Certificate>> trustedRootCerts = new HashMap<>();
private Map<X500Principal, List<X509Certificate>> intermediateCerts = new HashMap<>();
public TruststoreCertificatesLoader(KeyStore truststore) {
readTruststore(truststore);
}
/**
* Get all certificates from Keycloak Truststore, and classify them in two lists : root CAs and intermediates CAs
*/
private void readTruststore(KeyStore truststore) {
//Reading truststore aliases & certificates
Enumeration<String> enumeration;
try {
enumeration = truststore.aliases();
log.trace("Checking " + truststore.size() + " entries from the truststore.");
while(enumeration.hasMoreElements()) {
String alias = enumeration.nextElement();
readTruststoreEntry(truststore, alias);
}
} catch (KeyStoreException e) {
log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
}
}
private void readTruststoreEntry(KeyStore truststore, String alias) {
try {
Certificate certificate = truststore.getCertificate(alias);
if (certificate instanceof X509Certificate) {
X509Certificate cax509cert = (X509Certificate) certificate;
if (isSelfSigned(cax509cert)) {
X500Principal principal = cax509cert.getSubjectX500Principal();
List<X509Certificate> certs = trustedRootCerts.get(principal);
if (certs == null) {
certs = new ArrayList<>();
trustedRootCerts.put(principal, certs);
}
certs.add(cax509cert);
log.debug("Trusted root CA found in truststore : alias : " + alias + " | Subject DN : " + principal);
} else {
X500Principal principal = cax509cert.getSubjectX500Principal();
List<X509Certificate> certs = intermediateCerts.get(principal);
if (certs == null) {
certs = new ArrayList<>();
intermediateCerts.put(principal, certs);
}
certs.add(cax509cert);
log.debug("Intermediate CA found in truststore : alias : " + alias + " | Subject DN : " + principal);
}
} else
log.info("Skipping certificate with alias [" + alias + "] from truststore, because it's not an X509Certificate");
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException e) {
log.warnf("Error while reading Keycloak truststore entry [%s]. Exception message: %s", alias, e.getMessage(), e);
}
}
/**
* Checks whether given X.509 certificate is self-signed.
*/
private boolean isSelfSigned(X509Certificate cert)
throws CertificateException, NoSuchAlgorithmException,
NoSuchProviderException {
try {
// Try to verify certificate signature with its own public key
PublicKey key = cert.getPublicKey();
cert.verify(key);
log.trace("certificate " + cert.getSubjectDN() + " detected as root CA");
return true;
} catch (SignatureException sigEx) {
// Invalid signature --> not self-signed
log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA");
} catch (InvalidKeyException keyEx) {
// Invalid key --> not self-signed
log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA");
}
return false;
}
}
}