/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.tomcat.util.net; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.Key; import java.security.KeyStore; import java.security.cert.CRL; import java.security.cert.CRLException; import java.security.cert.CertPathParameters; import java.security.cert.CertStore; import java.security.cert.CertStoreParameters; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Set; import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.file.ConfigFileLoader; import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; import org.apache.tomcat.util.net.jsse.JSSEKeyManager; import org.apache.tomcat.util.net.jsse.PEMFile; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.security.KeyStoreUtil; /** * Common base class for {@link SSLUtil} implementations. */ public abstract class SSLUtilBase implements SSLUtil { private static final Log log = LogFactory.getLog(SSLUtilBase.class); private static final StringManager sm = StringManager.getManager(SSLUtilBase.class); protected final SSLHostConfig sslHostConfig; protected final SSLHostConfigCertificate certificate; private final String[] enabledProtocols; private final String[] enabledCiphers; protected SSLUtilBase(SSLHostConfigCertificate certificate) { this(certificate, true); } protected SSLUtilBase(SSLHostConfigCertificate certificate, boolean warnTls13) { this.certificate = certificate; this.sslHostConfig = certificate.getSSLHostConfig(); // Calculate the enabled protocols Set configuredProtocols = sslHostConfig.getProtocols(); Set implementedProtocols = getImplementedProtocols(); // If TLSv1.3 is not implemented and not explicitly requested we can // ignore it. It is included in the defaults so it may be configured. if (!implementedProtocols.contains(Constants.SSL_PROTO_TLSv1_3) && !sslHostConfig.isExplicitlyRequestedProtocol(Constants.SSL_PROTO_TLSv1_3)) { configuredProtocols.remove(Constants.SSL_PROTO_TLSv1_3); } // Newer JREs are dropping support for SSLv2Hello. If it is not // implemented and not explicitly requested we can ignore it. It is // included in the defaults so it may be configured. if (!implementedProtocols.contains(Constants.SSL_PROTO_SSLv2Hello) && !sslHostConfig.isExplicitlyRequestedProtocol(Constants.SSL_PROTO_SSLv2Hello)) { configuredProtocols.remove(Constants.SSL_PROTO_SSLv2Hello); } List enabledProtocols = getEnabled("protocols", getLog(), warnTls13, configuredProtocols, implementedProtocols); if (enabledProtocols.contains("SSLv3")) { log.warn(sm.getString("jsse.ssl3")); } this.enabledProtocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); if (enabledProtocols.contains(Constants.SSL_PROTO_TLSv1_3) && sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL && !isTls13RenegAuthAvailable() && warnTls13) { log.warn(sm.getString("jsse.tls13.auth")); } // Calculate the enabled ciphers List configuredCiphers = sslHostConfig.getJsseCipherNames(); Set implementedCiphers = getImplementedCiphers(); List enabledCiphers = getEnabled("ciphers", getLog(), false, configuredCiphers, implementedCiphers); this.enabledCiphers = enabledCiphers.toArray(new String[enabledCiphers.size()]); } static List getEnabled(String name, Log log, boolean warnOnSkip, Collection configured, Collection implemented) { List enabled = new ArrayList<>(); if (implemented.size() == 0) { // Unable to determine the list of available protocols. This will // have been logged previously. // Use the configuredProtocols and hope they work. If not, an error // will be generated when the list is used. Not ideal but no more // can be done at this point. enabled.addAll(configured); } else { enabled.addAll(configured); enabled.retainAll(implemented); if (enabled.isEmpty()) { // Don't use the defaults in this case. They may be less secure // than the configuration the user intended. // Force the failure of the connector throw new IllegalArgumentException( sm.getString("sslUtilBase.noneSupported", name, configured)); } if (log.isDebugEnabled()) { log.debug(sm.getString("sslUtilBase.active", name, enabled)); } if (log.isDebugEnabled() || warnOnSkip) { if (enabled.size() != configured.size()) { List skipped = new ArrayList<>(); skipped.addAll(configured); skipped.removeAll(enabled); String msg = sm.getString("sslUtilBase.skipped", name, skipped); if (warnOnSkip) { log.warn(msg); } else { log.debug(msg); } } } } return enabled; } /* * Gets the key- or truststore with the specified type, path, and password. */ static KeyStore getStore(String type, String provider, String path, String pass) throws IOException { KeyStore ks = null; InputStream istream = null; try { if (provider == null) { ks = KeyStore.getInstance(type); } else { ks = KeyStore.getInstance(type, provider); } if ("DKS".equalsIgnoreCase(type)) { URI uri = ConfigFileLoader.getURI(path); ks.load(JreCompat.getInstance().getDomainLoadStoreParameter(uri)); } else { // Some key store types (e.g. hardware) expect the InputStream // to be null if(!("PKCS11".equalsIgnoreCase(type) || "".equalsIgnoreCase(path)) || "NONE".equalsIgnoreCase(path)) { istream = ConfigFileLoader.getInputStream(path); } // The digester cannot differentiate between null and "". // Unfortunately, some key stores behave differently with null // and "". // JKS key stores treat null and "" interchangeably. // PKCS12 key stores (Java 7 onwards) don't return the cert if // null is used. // Key stores that do not use passwords expect null // Therefore: // - generally use null if pass is null or "" // - for JKS or PKCS12 only use null if pass is null // (because JKS will auto-switch to PKCS12) char[] storePass = null; if (pass != null && (!"".equals(pass) || "JKS".equalsIgnoreCase(type) || "PKCS12".equalsIgnoreCase(type))) { storePass = pass.toCharArray(); } KeyStoreUtil.load(ks, istream, storePass); } } catch (FileNotFoundException fnfe) { throw fnfe; } catch (IOException ioe) { // May be expected when working with a trust store // Re-throw. Caller will catch and log as required throw ioe; } catch(Exception ex) { String msg = sm.getString("jsse.keystore_load_failed", type, path, ex.getMessage()); log.error(msg, ex); throw new IOException(msg); } finally { if (istream != null) { try { istream.close(); } catch (IOException ioe) { // Do nothing } } } return ks; } @Override public final SSLContext createSSLContext(List negotiableProtocols) throws Exception { SSLContext sslContext = createSSLContextInternal(negotiableProtocols); sslContext.init(getKeyManagers(), getTrustManagers(), null); SSLSessionContext sessionContext = sslContext.getServerSessionContext(); if (sessionContext != null) { configureSessionContext(sessionContext); } return sslContext; } @Override public void configureSessionContext(SSLSessionContext sslSessionContext) { // <0 - don't set anything - use the implementation default if (sslHostConfig.getSessionCacheSize() >= 0) { sslSessionContext.setSessionCacheSize(sslHostConfig.getSessionCacheSize()); } // <0 - don't set anything - use the implementation default if (sslHostConfig.getSessionTimeout() >= 0) { sslSessionContext.setSessionTimeout(sslHostConfig.getSessionTimeout()); } } @Override public KeyManager[] getKeyManagers() throws Exception { String keyAlias = certificate.getCertificateKeyAlias(); String algorithm = sslHostConfig.getKeyManagerAlgorithm(); String keyPass = certificate.getCertificateKeyPassword(); // This has to be here as it can't be moved to SSLHostConfig since the // defaults vary between JSSE and OpenSSL. if (keyPass == null) { keyPass = certificate.getCertificateKeystorePassword(); } KeyStore ks = certificate.getCertificateKeystore(); KeyStore ksUsed = ks; /* * Use an in memory key store where possible. * For PEM format keys and certificates, it allows them to be imported * into the expected format. * For Java key stores with PKCS8 encoded keys (e.g. JKS files), it * enables Tomcat to handle the case where multiple keys exist in the * key store, each with a different password. The KeyManagerFactory * can't handle that so using an in memory key store with just the * required key works around that. * Other keys stores (hardware, MS, etc.) will be used as is. */ char[] keyPassArray = keyPass.toCharArray(); if (ks == null) { if (certificate.getCertificateFile() == null) { throw new IOException(sm.getString("jsse.noCertFile")); } PEMFile privateKeyFile = new PEMFile( certificate.getCertificateKeyFile() != null ? certificate.getCertificateKeyFile() : certificate.getCertificateFile(), keyPass); PEMFile certificateFile = new PEMFile(certificate.getCertificateFile()); Collection chain = new ArrayList<>(); chain.addAll(certificateFile.getCertificates()); if (certificate.getCertificateChainFile() != null) { PEMFile certificateChainFile = new PEMFile(certificate.getCertificateChainFile()); chain.addAll(certificateChainFile.getCertificates()); } if (keyAlias == null) { keyAlias = "tomcat"; } // Switch to in-memory key store ksUsed = KeyStore.getInstance("JKS"); ksUsed.load(null, null); ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), keyPass.toCharArray(), chain.toArray(new Certificate[chain.size()])); } else { if (keyAlias != null && !ks.isKeyEntry(keyAlias)) { throw new IOException(sm.getString("jsse.alias_no_key_entry", keyAlias)); } else if (keyAlias == null) { Enumeration aliases = ks.aliases(); if (!aliases.hasMoreElements()) { throw new IOException(sm.getString("jsse.noKeys")); } while (aliases.hasMoreElements() && keyAlias == null) { keyAlias = aliases.nextElement(); if (!ks.isKeyEntry(keyAlias)) { keyAlias = null; } } if (keyAlias == null) { throw new IOException(sm.getString("jsse.alias_no_key_entry", (Object) null)); } } Key k = ks.getKey(keyAlias, keyPassArray); if (k != null && !"DKS".equalsIgnoreCase(certificate.getCertificateKeystoreType()) && "PKCS#8".equalsIgnoreCase(k.getFormat())) { // Switch to in-memory key store String provider = certificate.getCertificateKeystoreProvider(); if (provider == null) { ksUsed = KeyStore.getInstance(certificate.getCertificateKeystoreType()); } else { ksUsed = KeyStore.getInstance(certificate.getCertificateKeystoreType(), provider); } ksUsed.load(null, null); ksUsed.setKeyEntry(keyAlias, k, keyPassArray, ks.getCertificateChain(keyAlias)); } // Non-PKCS#8 key stores will use the original key store } KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); kmf.init(ksUsed, keyPassArray); KeyManager[] kms = kmf.getKeyManagers(); // Only need to filter keys by alias if there are key managers to filter // and the original key store was used. The in memory key stores only // have a single key so don't need filtering if (kms != null && ksUsed == ks) { String alias = keyAlias; // JKS keystores always convert the alias name to lower case if ("JKS".equals(certificate.getCertificateKeystoreType())) { alias = alias.toLowerCase(Locale.ENGLISH); } for(int i = 0; i < kms.length; i++) { kms[i] = new JSSEKeyManager((X509KeyManager)kms[i], alias); } } return kms; } @Override public String[] getEnabledProtocols() { return enabledProtocols; } @Override public String[] getEnabledCiphers() { return enabledCiphers; } @Override public TrustManager[] getTrustManagers() throws Exception { String className = sslHostConfig.getTrustManagerClassName(); if(className != null && className.length() > 0) { ClassLoader classLoader = getClass().getClassLoader(); Class clazz = classLoader.loadClass(className); if(!(TrustManager.class.isAssignableFrom(clazz))){ throw new InstantiationException(sm.getString( "jsse.invalidTrustManagerClassName", className)); } Object trustManagerObject = clazz.getConstructor().newInstance(); TrustManager trustManager = (TrustManager) trustManagerObject; return new TrustManager[]{ trustManager }; } TrustManager[] tms = null; KeyStore trustStore = sslHostConfig.getTruststore(); if (trustStore != null) { checkTrustStoreEntries(trustStore); String algorithm = sslHostConfig.getTruststoreAlgorithm(); String crlf = sslHostConfig.getCertificateRevocationListFile(); boolean revocationEnabled = sslHostConfig.getRevocationEnabled(); if ("PKIX".equalsIgnoreCase(algorithm)) { TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); CertPathParameters params = getParameters(crlf, trustStore, revocationEnabled); ManagerFactoryParameters mfp = new CertPathTrustManagerParameters(params); tmf.init(mfp); tms = tmf.getTrustManagers(); } else { TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); tmf.init(trustStore); tms = tmf.getTrustManagers(); if (crlf != null && crlf.length() > 0) { throw new CRLException(sm.getString("jsseUtil.noCrlSupport", algorithm)); } // Only warn if the attribute has been explicitly configured if (sslHostConfig.isCertificateVerificationDepthConfigured()) { log.warn(sm.getString("jsseUtil.noVerificationDepth", algorithm)); } } } return tms; } private void checkTrustStoreEntries(KeyStore trustStore) throws Exception { Enumeration aliases = trustStore.aliases(); if (aliases != null) { Date now = new Date(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); if (trustStore.isCertificateEntry(alias)) { Certificate cert = trustStore.getCertificate(alias); if (cert instanceof X509Certificate) { try { ((X509Certificate) cert).checkValidity(now); } catch (CertificateExpiredException | CertificateNotYetValidException e) { String msg = sm.getString("jsseUtil.trustedCertNotValid", alias, ((X509Certificate) cert).getSubjectDN(), e.getMessage()); if (log.isDebugEnabled()) { log.debug(msg, e); } else { log.warn(msg); } } } else { if (log.isDebugEnabled()) { log.debug(sm.getString("jsseUtil.trustedCertNotChecked", alias)); } } } } } } /** * Return the initialization parameters for the TrustManager. * Currently, only the default PKIX is supported. * * @param crlf The path to the CRL file. * @param trustStore The configured TrustStore. * @param revocationEnabled Should the JSSE provider perform revocation * checks? Ignored if {@code crlf} is non-null. * Configuration of revocation checks are expected * to be via proprietary JSSE provider methods. * @return The parameters including the CRLs and TrustStore. * @throws Exception An error occurred */ protected CertPathParameters getParameters(String crlf, KeyStore trustStore, boolean revocationEnabled) throws Exception { PKIXBuilderParameters xparams = new PKIXBuilderParameters(trustStore, new X509CertSelector()); if (crlf != null && crlf.length() > 0) { Collection crls = getCRLs(crlf); CertStoreParameters csp = new CollectionCertStoreParameters(crls); CertStore store = CertStore.getInstance("Collection", csp); xparams.addCertStore(store); xparams.setRevocationEnabled(true); } else { xparams.setRevocationEnabled(revocationEnabled); } xparams.setMaxPathLength(sslHostConfig.getCertificateVerificationDepth()); return xparams; } /** * Load the collection of CRLs. * @param crlf The path to the CRL file. * @return the CRLs collection * @throws IOException Error reading CRL file * @throws CRLException CRL error * @throws CertificateException Error processing certificate */ protected Collection getCRLs(String crlf) throws IOException, CRLException, CertificateException { Collection crls = null; try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); try (InputStream is = ConfigFileLoader.getInputStream(crlf)) { crls = cf.generateCRLs(is); } } catch(IOException iex) { throw iex; } catch(CRLException crle) { throw crle; } catch(CertificateException ce) { throw ce; } return crls; } protected abstract Set getImplementedProtocols(); protected abstract Set getImplementedCiphers(); protected abstract Log getLog(); protected abstract boolean isTls13RenegAuthAvailable(); protected abstract SSLContext createSSLContextInternal(List negotiableProtocols) throws Exception; }