| /* |
| * 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.DomainLoadStoreParameter; |
| 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.Collections; |
| 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.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<String> configuredProtocols = sslHostConfig.getProtocols(); |
| Set<String> 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<String> enabledProtocols = |
| getEnabled("protocols", getLog(), warnTls13, configuredProtocols, implementedProtocols); |
| if (enabledProtocols.contains("SSLv3")) { |
| log.warn(sm.getString("sslUtilBase.ssl3")); |
| } |
| this.enabledProtocols = enabledProtocols.toArray(new String[0]); |
| |
| if (enabledProtocols.contains(Constants.SSL_PROTO_TLSv1_3) && |
| sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL && |
| !isTls13RenegAuthAvailable() && warnTls13) { |
| log.warn(sm.getString("sslUtilBase.tls13.auth")); |
| } |
| |
| // Calculate the enabled ciphers |
| List<String> configuredCiphers = sslHostConfig.getJsseCipherNames(); |
| Set<String> implementedCiphers = getImplementedCiphers(); |
| List<String> enabledCiphers = |
| getEnabled("ciphers", getLog(), false, configuredCiphers, implementedCiphers); |
| this.enabledCiphers = enabledCiphers.toArray(new String[0]); |
| } |
| |
| |
| static <T> List<T> getEnabled(String name, Log log, boolean warnOnSkip, Collection<T> configured, |
| Collection<T> implemented) { |
| |
| List<T> 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<T> skipped = new ArrayList<>(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.getSource().getURI(path); |
| ks.load(new DomainLoadStoreParameter(uri, Collections.emptyMap())); |
| } else { |
| // Some key store types (e.g. hardware) expect the InputStream |
| // to be null |
| if(!("PKCS11".equalsIgnoreCase(type) || |
| path.isEmpty()) || |
| "NONE".equalsIgnoreCase(path)) { |
| istream = ConfigFileLoader.getSource().getResource(path).getInputStream(); |
| } |
| |
| // 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 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("sslUtilBase.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<String> 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(); |
| |
| KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); |
| if (kmf.getProvider().getInfo().indexOf("FIPS") != -1) { |
| // FIPS doesn't like ANY wrapping nor key manipulation. |
| if (keyAlias != null) { |
| log.warn(sm.getString("sslUtilBase.aliasIgnored", keyAlias)); |
| } |
| kmf.init(ksUsed, keyPassArray); |
| return kmf.getKeyManagers(); |
| } |
| |
| if (ks == null) { |
| if (certificate.getCertificateFile() == null) { |
| throw new IOException(sm.getString("sslUtilBase.noCertFile")); |
| } |
| |
| PEMFile privateKeyFile = new PEMFile( |
| certificate.getCertificateKeyFile() != null ? certificate.getCertificateKeyFile() : certificate.getCertificateFile(), |
| keyPass); |
| PEMFile certificateFile = new PEMFile(certificate.getCertificateFile()); |
| |
| Collection<Certificate> chain = new ArrayList<>(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[0])); |
| } else { |
| if (keyAlias != null && !ks.isKeyEntry(keyAlias)) { |
| throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias)); |
| } else if (keyAlias == null) { |
| Enumeration<String> aliases = ks.aliases(); |
| if (!aliases.hasMoreElements()) { |
| throw new IOException(sm.getString("sslUtilBase.noKeys")); |
| } |
| while (aliases.hasMoreElements() && keyAlias == null) { |
| keyAlias = aliases.nextElement(); |
| if (!ks.isKeyEntry(keyAlias)) { |
| keyAlias = null; |
| } |
| } |
| if (keyAlias == null) { |
| throw new IOException(sm.getString("sslUtilBase.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 |
| } |
| |
| |
| 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( |
| "sslUtilBase.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("sslUtilBase.noCrlSupport", algorithm)); |
| } |
| // Only warn if the attribute has been explicitly configured |
| if (sslHostConfig.isCertificateVerificationDepthConfigured()) { |
| log.warn(sm.getString("sslUtilBase.noVerificationDepth", algorithm)); |
| } |
| } |
| } |
| |
| return tms; |
| } |
| |
| |
| private void checkTrustStoreEntries(KeyStore trustStore) throws Exception { |
| Enumeration<String> 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("sslUtilBase.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("sslUtilBase.trustedCertNotChecked", alias)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Return the initialization parameters for the TrustManager. |
| * Currently, only the default <code>PKIX</code> 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<? extends CRL> 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<? extends CRL> getCRLs(String crlf) |
| throws IOException, CRLException, CertificateException { |
| |
| Collection<? extends CRL> crls = null; |
| try { |
| CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
| try (InputStream is = ConfigFileLoader.getSource().getResource(crlf).getInputStream()) { |
| crls = cf.generateCRLs(is); |
| } |
| } catch(IOException iex) { |
| throw iex; |
| } catch(CRLException crle) { |
| throw crle; |
| } catch(CertificateException ce) { |
| throw ce; |
| } |
| return crls; |
| } |
| |
| |
| protected abstract Set<String> getImplementedProtocols(); |
| protected abstract Set<String> getImplementedCiphers(); |
| protected abstract Log getLog(); |
| protected abstract boolean isTls13RenegAuthAvailable(); |
| protected abstract SSLContext createSSLContextInternal(List<String> negotiableProtocols) throws Exception; |
| } |