| /* |
| * 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.openssl; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.security.PrivateKey; |
| import java.security.SecureRandom; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.net.ssl.KeyManager; |
| import javax.net.ssl.SSLEngine; |
| import javax.net.ssl.SSLException; |
| import javax.net.ssl.SSLParameters; |
| import javax.net.ssl.SSLServerSocketFactory; |
| import javax.net.ssl.SSLSessionContext; |
| import javax.net.ssl.TrustManager; |
| import javax.net.ssl.X509KeyManager; |
| import javax.net.ssl.X509TrustManager; |
| |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.jni.CertificateVerifier; |
| import org.apache.tomcat.jni.Pool; |
| import org.apache.tomcat.jni.SSL; |
| import org.apache.tomcat.jni.SSLConf; |
| import org.apache.tomcat.jni.SSLContext; |
| import org.apache.tomcat.util.codec.binary.Base64; |
| import org.apache.tomcat.util.net.AbstractEndpoint; |
| import org.apache.tomcat.util.net.Constants; |
| import org.apache.tomcat.util.net.SSLHostConfig; |
| import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; |
| import org.apache.tomcat.util.net.SSLHostConfigCertificate; |
| import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { |
| |
| private static final Base64 BASE64_ENCODER = new Base64(64, new byte[] {'\n'}); |
| |
| private static final Log log = LogFactory.getLog(OpenSSLContext.class); |
| |
| // Note: this uses the main "net" package strings as many are common with APR |
| private static final StringManager netSm = StringManager.getManager(AbstractEndpoint.class); |
| private static final StringManager sm = StringManager.getManager(OpenSSLContext.class); |
| |
| private static final String defaultProtocol = "TLS"; |
| |
| private final SSLHostConfig sslHostConfig; |
| private final SSLHostConfigCertificate certificate; |
| private OpenSSLSessionContext sessionContext; |
| private X509TrustManager x509TrustManager; |
| |
| private final List<String> negotiableProtocols; |
| |
| private String enabledProtocol; |
| |
| public String getEnabledProtocol() { |
| return enabledProtocol; |
| } |
| |
| public void setEnabledProtocol(String protocol) { |
| enabledProtocol = (protocol == null) ? defaultProtocol : protocol; |
| } |
| |
| private final long aprPool; |
| private final AtomicInteger aprPoolDestroyed = new AtomicInteger(0); |
| |
| // OpenSSLConfCmd context |
| protected final long cctx; |
| // SSL context |
| protected final long ctx; |
| |
| static final CertificateFactory X509_CERT_FACTORY; |
| |
| private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----\n"; |
| |
| private static final Object END_KEY = "\n-----END PRIVATE KEY-----"; |
| private boolean initialized = false; |
| |
| static { |
| try { |
| X509_CERT_FACTORY = CertificateFactory.getInstance("X.509"); |
| } catch (CertificateException e) { |
| throw new IllegalStateException(sm.getString("openssl.X509FactoryError"), e); |
| } |
| } |
| |
| public OpenSSLContext(SSLHostConfigCertificate certificate, List<String> negotiableProtocols) |
| throws SSLException { |
| this.sslHostConfig = certificate.getSSLHostConfig(); |
| this.certificate = certificate; |
| aprPool = Pool.create(0); |
| boolean success = false; |
| try { |
| // Create OpenSSLConfCmd context if used |
| OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); |
| if (openSslConf != null) { |
| try { |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("openssl.makeConf")); |
| cctx = SSLConf.make(aprPool, |
| SSL.SSL_CONF_FLAG_FILE | |
| SSL.SSL_CONF_FLAG_SERVER | |
| SSL.SSL_CONF_FLAG_CERTIFICATE | |
| SSL.SSL_CONF_FLAG_SHOW_ERRORS); |
| } catch (Exception e) { |
| throw new SSLException(sm.getString("openssl.errMakeConf"), e); |
| } |
| } else { |
| cctx = 0; |
| } |
| sslHostConfig.setOpenSslConfContext(Long.valueOf(cctx)); |
| |
| // SSL protocol |
| int value = SSL.SSL_PROTOCOL_NONE; |
| for (String protocol : sslHostConfig.getEnabledProtocols()) { |
| if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(protocol)) { |
| // NO-OP. OpenSSL always supports SSLv2Hello |
| } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_SSLV2; |
| } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_SSLV3; |
| } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_TLSV1; |
| } else if (Constants.SSL_PROTO_TLSv1_1.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_TLSV1_1; |
| } else if (Constants.SSL_PROTO_TLSv1_2.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_TLSV1_2; |
| } else if (Constants.SSL_PROTO_TLSv1_3.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_TLSV1_3; |
| } else if (Constants.SSL_PROTO_ALL.equalsIgnoreCase(protocol)) { |
| value |= SSL.SSL_PROTOCOL_ALL; |
| } else { |
| // Should not happen since filtering to build |
| // enabled protocols removes invalid values. |
| throw new Exception(netSm.getString( |
| "endpoint.apr.invalidSslProtocol", protocol)); |
| } |
| } |
| |
| // Create SSL Context |
| try { |
| ctx = SSLContext.make(aprPool, value, SSL.SSL_MODE_SERVER); |
| } catch (Exception e) { |
| // If the sslEngine is disabled on the AprLifecycleListener |
| // there will be an Exception here but there is no way to check |
| // the AprLifecycleListener settings from here |
| throw new Exception( |
| netSm.getString("endpoint.apr.failSslContextMake"), e); |
| } |
| |
| this.negotiableProtocols = negotiableProtocols; |
| |
| success = true; |
| } catch(Exception e) { |
| throw new SSLException(sm.getString("openssl.errorSSLCtxInit"), e); |
| } finally { |
| if (!success) { |
| destroy(); |
| } |
| } |
| } |
| |
| @Override |
| public synchronized void destroy() { |
| // Guard against multiple destroyPools() calls triggered by construction exception and finalize() later |
| if (aprPoolDestroyed.compareAndSet(0, 1)) { |
| if (ctx != 0) { |
| SSLContext.free(ctx); |
| } |
| if (cctx != 0) { |
| SSLConf.free(cctx); |
| } |
| if (aprPool != 0) { |
| Pool.destroy(aprPool); |
| } |
| } |
| } |
| |
| /** |
| * Setup the SSL_CTX. |
| * |
| * @param kms Must contain a KeyManager of the type |
| * {@code OpenSSLKeyManager} |
| * @param tms Must contain a TrustManager of the type |
| * {@code X509TrustManager} |
| * @param sr Is not used for this implementation. |
| */ |
| @Override |
| public synchronized void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) { |
| if (initialized) { |
| log.warn(sm.getString("openssl.doubleInit")); |
| return; |
| } |
| try { |
| if (sslHostConfig.getInsecureRenegotiation()) { |
| SSLContext.setOptions(ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); |
| } else { |
| SSLContext.clearOptions(ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); |
| } |
| |
| // Use server's preference order for ciphers (rather than |
| // client's) |
| String honorCipherOrderStr = sslHostConfig.getHonorCipherOrder(); |
| if (honorCipherOrderStr != null) { |
| if (Boolean.parseBoolean(honorCipherOrderStr)) { |
| SSLContext.setOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); |
| } else { |
| SSLContext.clearOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); |
| } |
| } |
| |
| // Disable compression if requested |
| if (sslHostConfig.getDisableCompression()) { |
| SSLContext.setOptions(ctx, SSL.SSL_OP_NO_COMPRESSION); |
| } else { |
| SSLContext.clearOptions(ctx, SSL.SSL_OP_NO_COMPRESSION); |
| } |
| |
| // Disable TLS Session Tickets (RFC4507) to protect perfect forward secrecy |
| if (sslHostConfig.getDisableSessionTickets()) { |
| SSLContext.setOptions(ctx, SSL.SSL_OP_NO_TICKET); |
| } else { |
| SSLContext.clearOptions(ctx, SSL.SSL_OP_NO_TICKET); |
| } |
| |
| // List the ciphers that the client is permitted to negotiate |
| SSLContext.setCipherSuite(ctx, sslHostConfig.getCiphers()); |
| |
| if (certificate.getCertificateFile() == null) { |
| certificate.setCertificateKeyManager(OpenSSLUtil.chooseKeyManager(kms)); |
| } |
| |
| addCertificate(certificate); |
| |
| // Client certificate verification |
| int value = 0; |
| switch (sslHostConfig.getCertificateVerification()) { |
| case NONE: |
| value = SSL.SSL_CVERIFY_NONE; |
| break; |
| case OPTIONAL: |
| value = SSL.SSL_CVERIFY_OPTIONAL; |
| break; |
| case OPTIONAL_NO_CA: |
| value = SSL.SSL_CVERIFY_OPTIONAL_NO_CA; |
| break; |
| case REQUIRED: |
| value = SSL.SSL_CVERIFY_REQUIRE; |
| break; |
| } |
| SSLContext.setVerify(ctx, value, sslHostConfig.getCertificateVerificationDepth()); |
| |
| if (tms != null) { |
| // Client certificate verification based on custom trust managers |
| x509TrustManager = chooseTrustManager(tms); |
| SSLContext.setCertVerifyCallback(ctx, new CertificateVerifier() { |
| @Override |
| public boolean verify(long ssl, byte[][] chain, String auth) { |
| X509Certificate[] peerCerts = certificates(chain); |
| try { |
| x509TrustManager.checkClientTrusted(peerCerts, auth); |
| return true; |
| } catch (Exception e) { |
| log.debug(sm.getString("openssl.certificateVerificationFailed"), e); |
| } |
| return false; |
| } |
| }); |
| // Pass along the DER encoded certificates of the accepted client |
| // certificate issuers, so that their subjects can be presented |
| // by the server during the handshake to allow the client choosing |
| // an acceptable certificate |
| for (X509Certificate caCert : x509TrustManager.getAcceptedIssuers()) { |
| SSLContext.addClientCACertificateRaw(ctx, caCert.getEncoded()); |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); |
| } |
| } else { |
| // Client certificate verification based on trusted CA files and dirs |
| SSLContext.setCACertificate(ctx, |
| SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile()), |
| SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())); |
| } |
| |
| if (negotiableProtocols != null && negotiableProtocols.size() > 0) { |
| List<String> protocols = new ArrayList<>(negotiableProtocols); |
| protocols.add("http/1.1"); |
| String[] protocolsArray = protocols.toArray(new String[0]); |
| SSLContext.setAlpnProtos(ctx, protocolsArray, SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE); |
| SSLContext.setNpnProtos(ctx, protocolsArray, SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE); |
| } |
| |
| // Apply OpenSSLConfCmd if used |
| OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); |
| if (openSslConf != null && cctx != 0) { |
| // Check OpenSSLConfCmd if used |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("openssl.checkConf")); |
| try { |
| if (!openSslConf.check(cctx)) { |
| log.error(sm.getString("openssl.errCheckConf")); |
| throw new Exception(sm.getString("openssl.errCheckConf")); |
| } |
| } catch (Exception e) { |
| throw new Exception(sm.getString("openssl.errCheckConf"), e); |
| } |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("openssl.applyConf")); |
| try { |
| if (!openSslConf.apply(cctx, ctx)) { |
| log.error(sm.getString("openssl.errApplyConf")); |
| throw new SSLException(sm.getString("openssl.errApplyConf")); |
| } |
| } catch (Exception e) { |
| throw new SSLException(sm.getString("openssl.errApplyConf"), e); |
| } |
| // Reconfigure the enabled protocols |
| int opts = SSLContext.getOptions(ctx); |
| List<String> enabled = new ArrayList<>(); |
| // Seems like there is no way to explicitly disable SSLv2Hello |
| // in OpenSSL so it is always enabled |
| enabled.add(Constants.SSL_PROTO_SSLv2Hello); |
| if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { |
| enabled.add(Constants.SSL_PROTO_TLSv1); |
| } |
| if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { |
| enabled.add(Constants.SSL_PROTO_TLSv1_1); |
| } |
| if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { |
| enabled.add(Constants.SSL_PROTO_TLSv1_2); |
| } |
| if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { |
| enabled.add(Constants.SSL_PROTO_SSLv2); |
| } |
| if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { |
| enabled.add(Constants.SSL_PROTO_SSLv3); |
| } |
| sslHostConfig.setEnabledProtocols( |
| enabled.toArray(new String[0])); |
| // Reconfigure the enabled ciphers |
| sslHostConfig.setEnabledCiphers(SSLContext.getCiphers(ctx)); |
| } |
| |
| sessionContext = new OpenSSLSessionContext(this); |
| // If client authentication is being used, OpenSSL requires that |
| // this is set so always set it in case an app is configured to |
| // require it |
| sessionContext.setSessionIdContext(SSLContext.DEFAULT_SESSION_ID_CONTEXT); |
| sslHostConfig.setOpenSslContext(Long.valueOf(ctx)); |
| initialized = true; |
| } catch (Exception e) { |
| log.warn(sm.getString("openssl.errorSSLCtxInit"), e); |
| destroy(); |
| } |
| } |
| |
| |
| public void addCertificate(SSLHostConfigCertificate certificate) throws Exception { |
| // Load Server key and certificate |
| if (certificate.getCertificateFile() != null) { |
| // Set certificate |
| SSLContext.setCertificate(ctx, |
| SSLHostConfig.adjustRelativePath(certificate.getCertificateFile()), |
| SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyFile()), |
| certificate.getCertificateKeyPassword(), getCertificateIndex(certificate)); |
| // Set certificate chain file |
| SSLContext.setCertificateChainFile(ctx, |
| SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false); |
| // Set revocation |
| SSLContext.setCARevocation(ctx, |
| SSLHostConfig.adjustRelativePath( |
| sslHostConfig.getCertificateRevocationListFile()), |
| SSLHostConfig.adjustRelativePath( |
| sslHostConfig.getCertificateRevocationListPath())); |
| } else { |
| String alias = certificate.getCertificateKeyAlias(); |
| X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); |
| if (alias == null) { |
| alias = "tomcat"; |
| } |
| X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); |
| if (chain == null) { |
| alias = findAlias(x509KeyManager, certificate); |
| chain = x509KeyManager.getCertificateChain(alias); |
| } |
| PrivateKey key = x509KeyManager.getPrivateKey(alias); |
| StringBuilder sb = new StringBuilder(BEGIN_KEY); |
| String encoded = BASE64_ENCODER.encodeToString(key.getEncoded()); |
| if (encoded.endsWith("\n")) { |
| encoded = encoded.substring(0, encoded.length() - 1); |
| } |
| sb.append(encoded); |
| sb.append(END_KEY); |
| SSLContext.setCertificateRaw(ctx, chain[0].getEncoded(), |
| sb.toString().getBytes(StandardCharsets.US_ASCII), |
| getCertificateIndex(certificate)); |
| for (int i = 1; i < chain.length; i++) { |
| SSLContext.addChainCertificateRaw(ctx, chain[i].getEncoded()); |
| } |
| } |
| } |
| |
| |
| private static int getCertificateIndex(SSLHostConfigCertificate certificate) { |
| int result; |
| // If the type is undefined there will only be one certificate (enforced |
| // in SSLHostConfig) so use the RSA slot. |
| if (certificate.getType() == Type.RSA || certificate.getType() == Type.UNDEFINED) { |
| result = SSL.SSL_AIDX_RSA; |
| } else if (certificate.getType() == Type.EC) { |
| result = SSL.SSL_AIDX_ECC; |
| } else if (certificate.getType() == Type.DSA) { |
| result = SSL.SSL_AIDX_DSA; |
| } else { |
| result = SSL.SSL_AIDX_MAX; |
| } |
| return result; |
| } |
| |
| |
| /* |
| * Find a valid alias when none was specified in the config. |
| */ |
| private static String findAlias(X509KeyManager keyManager, |
| SSLHostConfigCertificate certificate) { |
| |
| Type type = certificate.getType(); |
| String result = null; |
| |
| List<Type> candidateTypes = new ArrayList<>(); |
| if (Type.UNDEFINED.equals(type)) { |
| // Try all types to find an suitable alias |
| candidateTypes.addAll(Arrays.asList(Type.values())); |
| candidateTypes.remove(Type.UNDEFINED); |
| } else { |
| // Look for the specific type to find a suitable alias |
| candidateTypes.add(type); |
| } |
| |
| Iterator<Type> iter = candidateTypes.iterator(); |
| while (result == null && iter.hasNext()) { |
| result = keyManager.chooseServerAlias(iter.next().toString(), null, null); |
| } |
| |
| return result; |
| } |
| |
| private static X509TrustManager chooseTrustManager(TrustManager[] managers) { |
| for (TrustManager m : managers) { |
| if (m instanceof X509TrustManager) { |
| return (X509TrustManager) m; |
| } |
| } |
| throw new IllegalStateException(sm.getString("openssl.trustManagerMissing")); |
| } |
| |
| private static X509Certificate[] certificates(byte[][] chain) { |
| X509Certificate[] peerCerts = new X509Certificate[chain.length]; |
| for (int i = 0; i < peerCerts.length; i++) { |
| peerCerts[i] = new OpenSSLX509Certificate(chain[i]); |
| } |
| return peerCerts; |
| } |
| |
| |
| long getSSLContextID() { |
| return ctx; |
| } |
| |
| |
| @Override |
| public SSLSessionContext getServerSessionContext() { |
| return sessionContext; |
| } |
| |
| @Override |
| public SSLEngine createSSLEngine() { |
| return new OpenSSLEngine(ctx, defaultProtocol, false, sessionContext, |
| (negotiableProtocols != null && negotiableProtocols.size() > 0), initialized, |
| sslHostConfig.getCertificateVerificationDepth(), |
| sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA); |
| } |
| |
| @Override |
| public SSLServerSocketFactory getServerSocketFactory() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public SSLParameters getSupportedSSLParameters() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public X509Certificate[] getCertificateChain(String alias) { |
| X509Certificate[] chain = null; |
| X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); |
| if (x509KeyManager != null) { |
| if (alias == null) { |
| alias = "tomcat"; |
| } |
| chain = x509KeyManager.getCertificateChain(alias); |
| if (chain == null) { |
| alias = findAlias(x509KeyManager, certificate); |
| chain = x509KeyManager.getCertificateChain(alias); |
| } |
| } |
| |
| return chain; |
| } |
| |
| @Override |
| public X509Certificate[] getAcceptedIssuers() { |
| X509Certificate[] acceptedCerts = null; |
| if (x509TrustManager != null) { |
| acceptedCerts = x509TrustManager.getAcceptedIssuers(); |
| } |
| return acceptedCerts; |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| /* |
| * When an SSLHostConfig is replaced at runtime, it is not possible to |
| * call destroy() on the associated OpenSSLContext since it is likely |
| * that there will be in-progress connections using the OpenSSLContext. |
| * A reference chain has been deliberately established (see |
| * OpenSSLSessionContext) to ensure that the OpenSSLContext remains |
| * ineligible for GC while those connections are alive. Once those |
| * connections complete, the OpenSSLContext will become eligible for GC |
| * and this method will ensure that the associated native resources are |
| * cleaned up. |
| */ |
| try { |
| destroy(); |
| } finally { |
| super.finalize(); |
| } |
| } |
| } |