Author: remm Date: Thu Jun 18 17:13:40 2015 New Revision: 1686279 URL: http://svn.apache.org/r1686279 Log: Add SSL engine backed by OpenSSL, based on code from Nume de Montmollin and derived from work done by Netty and Twitter.
Added: tomcat/trunk/java/org/apache/tomcat/util/net/openssl/ tomcat/trunk/java/org/apache/tomcat/util/net/openssl/CipherSuiteConverter.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLImplementation.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLKeyManager.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLProtocols.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLServerSessionContext.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionContext.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionStats.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLX509Certificate.java Added: tomcat/trunk/java/org/apache/tomcat/util/net/openssl/CipherSuiteConverter.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/net/openssl/CipherSuiteConverter.java?rev=1686279&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/net/openssl/CipherSuiteConverter.java (added) +++ tomcat/trunk/java/org/apache/tomcat/util/net/openssl/CipherSuiteConverter.java Thu Jun 18 17:13:40 2015 @@ -0,0 +1,421 @@ +/* + * 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.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Converts a Java cipher suite string to an OpenSSL cipher suite string and vice versa. + * + * @see <a href="http://en.wikipedia.org/wiki/Cipher_suite">Wikipedia page about cipher suite</a> + */ +public final class CipherSuiteConverter { + + private static final Log logger = LogFactory.getLog(CipherSuiteConverter.class); + private static final StringManager sm = StringManager.getManager(CipherSuiteConverter.class); + + /** + * A_B_WITH_C_D, where: + * + * A - TLS or SSL (protocol) + * B - handshake algorithm (key exchange and authentication algorithms to be precise) + * C - bulk cipher + * D - HMAC algorithm + * + * This regular expression assumees that: + * + * 1) A is always TLS or SSL, and + * 2) D is always a single word. + */ + private static final Pattern JAVA_CIPHERSUITE_PATTERN = + Pattern.compile("^(?:TLS|SSL)_((?:(?!_WITH_).)+)_WITH_(.*)_(.*)$"); + + /** + * A-B-C, where: + * + * A - handshake algorithm (key exchange and authentication algorithms to be precise) + * B - bulk cipher + * C - HMAC algorithm + * + * This regular expression assumes that: + * + * 1) A has some deterministic pattern as shown below, and + * 2) C is always a single word + */ + private static final Pattern OPENSSL_CIPHERSUITE_PATTERN = + // Be very careful not to break the indentation while editing. + Pattern.compile( + "^(?:(" + // BEGIN handshake algorithm + "(?:(?:EXP-)?" + + "(?:" + + "(?:DHE|EDH|ECDH|ECDHE|SRP)-(?:DSS|RSA|ECDSA)|" + + "(?:ADH|AECDH|KRB5|PSK|SRP)" + + ')' + + ")|" + + "EXP" + + ")-)?" + // END handshake algorithm + "(.*)-(.*)$"); + + private static final Pattern JAVA_AES_CBC_PATTERN = Pattern.compile("^(AES)_([0-9]+)_CBC$"); + private static final Pattern JAVA_AES_PATTERN = Pattern.compile("^(AES)_([0-9]+)_(.*)$"); + private static final Pattern OPENSSL_AES_CBC_PATTERN = Pattern.compile("^(AES)([0-9]+)$"); + private static final Pattern OPENSSL_AES_PATTERN = Pattern.compile("^(AES)([0-9]+)-(.*)$"); + + /** + * Java-to-OpenSSL cipher suite conversion map + * Note that the Java cipher suite has the protocol prefix (TLS_, SSL_) + */ + private static final ConcurrentMap<String, String> j2o = new ConcurrentHashMap<>(); + + /** + * OpenSSL-to-Java cipher suite conversion map. + * Note that one OpenSSL cipher suite can be converted to more than one Java cipher suites because + * a Java cipher suite has the protocol name prefix (TLS_, SSL_) + */ + private static final ConcurrentMap<String, Map<String, String>> o2j = new ConcurrentHashMap<>(); + + /** + * Clears the cache for testing purpose. + */ + static void clearCache() { + j2o.clear(); + o2j.clear(); + } + + /** + * Tests if the specified key-value pair has been cached in Java-to-OpenSSL cache. + */ + static boolean isJ2OCached(String key, String value) { + return value.equals(j2o.get(key)); + } + + /** + * Tests if the specified key-value pair has been cached in OpenSSL-to-Java cache. + */ + static boolean isO2JCached(String key, String protocol, String value) { + Map<String, String> p2j = o2j.get(key); + if (p2j == null) { + return false; + } else { + return value.equals(p2j.get(protocol)); + } + } + + /** + * Converts the specified Java cipher suites to the colon-separated OpenSSL cipher suite specification. + */ + public static String toOpenSsl(Iterable<String> javaCipherSuites) { + final StringBuilder buf = new StringBuilder(); + for (String c: javaCipherSuites) { + if (c == null) { + break; + } + + String converted = toOpenSsl(c); + if (converted != null) { + c = converted; + } + + buf.append(c); + buf.append(':'); + } + + if (buf.length() > 0) { + buf.setLength(buf.length() - 1); + return buf.toString(); + } else { + return ""; + } + } + + /** + * Converts the specified Java cipher suite to its corresponding OpenSSL cipher suite name. + * + * @return {@code null} if the conversion has failed + */ + public static String toOpenSsl(String javaCipherSuite) { + String converted = j2o.get(javaCipherSuite); + if (converted != null) { + return converted; + } else { + return cacheFromJava(javaCipherSuite); + } + } + + private static String cacheFromJava(String javaCipherSuite) { + String openSslCipherSuite = toOpenSslUncached(javaCipherSuite); + if (openSslCipherSuite == null) { + return null; + } + + // Cache the mapping. + j2o.putIfAbsent(javaCipherSuite, openSslCipherSuite); + + // Cache the reverse mapping after stripping the protocol prefix (TLS_ or SSL_) + final String javaCipherSuiteSuffix = javaCipherSuite.substring(4); + Map<String, String> p2j = new HashMap<String, String>(4); + p2j.put("", javaCipherSuiteSuffix); + p2j.put("SSL", "SSL_" + javaCipherSuiteSuffix); + p2j.put("TLS", "TLS_" + javaCipherSuiteSuffix); + o2j.put(openSslCipherSuite, p2j); + + if (logger.isDebugEnabled()) { + logger.debug(sm.getString("converter.mapping", javaCipherSuite, openSslCipherSuite)); + } + + return openSslCipherSuite; + } + + static String toOpenSslUncached(String javaCipherSuite) { + Matcher m = JAVA_CIPHERSUITE_PATTERN.matcher(javaCipherSuite); + if (!m.matches()) { + return null; + } + + String handshakeAlgo = toOpenSslHandshakeAlgo(m.group(1)); + String bulkCipher = toOpenSslBulkCipher(m.group(2)); + String hmacAlgo = toOpenSslHmacAlgo(m.group(3)); + if (handshakeAlgo.length() == 0) { + return bulkCipher + '-' + hmacAlgo; + } else { + return handshakeAlgo + '-' + bulkCipher + '-' + hmacAlgo; + } + } + + private static String toOpenSslHandshakeAlgo(String handshakeAlgo) { + final boolean export = handshakeAlgo.endsWith("_EXPORT"); + if (export) { + handshakeAlgo = handshakeAlgo.substring(0, handshakeAlgo.length() - 7); + } + + if ("RSA".equals(handshakeAlgo)) { + handshakeAlgo = ""; + } else if (handshakeAlgo.endsWith("_anon")) { + handshakeAlgo = 'A' + handshakeAlgo.substring(0, handshakeAlgo.length() - 5); + } + + if (export) { + if (handshakeAlgo.length() == 0) { + handshakeAlgo = "EXP"; + } else { + handshakeAlgo = "EXP-" + handshakeAlgo; + } + } + + return handshakeAlgo.replace('_', '-'); + } + + private static String toOpenSslBulkCipher(String bulkCipher) { + if (bulkCipher.startsWith("AES_")) { + Matcher m = JAVA_AES_CBC_PATTERN.matcher(bulkCipher); + if (m.matches()) { + return m.replaceFirst("$1$2"); + } + + m = JAVA_AES_PATTERN.matcher(bulkCipher); + if (m.matches()) { + return m.replaceFirst("$1$2-$3"); + } + } + + if ("3DES_EDE_CBC".equals(bulkCipher)) { + return "DES-CBC3"; + } + + if ("RC4_128".equals(bulkCipher) || "RC4_40".equals(bulkCipher)) { + return "RC4"; + } + + if ("DES40_CBC".equals(bulkCipher) || "DES_CBC_40".equals(bulkCipher)) { + return "DES-CBC"; + } + + if ("RC2_CBC_40".equals(bulkCipher)) { + return "RC2-CBC"; + } + + return bulkCipher.replace('_', '-'); + } + + private static String toOpenSslHmacAlgo(String hmacAlgo) { + // Java and OpenSSL use the same algorithm names for: + // + // * SHA + // * SHA256 + // * MD5 + // + return hmacAlgo; + } + + /** + * Convert from OpenSSL cipher suite name convention to java cipher suite name convention. + * @param openSslCipherSuite An OpenSSL cipher suite name. + * @param protocol The cryptographic protocol (i.e. SSL, TLS, ...). + * @return The translated cipher suite name according to java conventions. This will not be {@code null}. + */ + public static String toJava(String openSslCipherSuite, String protocol) { + Map<String, String> p2j = o2j.get(openSslCipherSuite); + if (p2j == null) { + p2j = cacheFromOpenSsl(openSslCipherSuite); + } + + String javaCipherSuite = p2j.get(protocol); + if (javaCipherSuite == null) { + javaCipherSuite = protocol + '_' + p2j.get(""); + } + + return javaCipherSuite; + } + + private static Map<String, String> cacheFromOpenSsl(String openSslCipherSuite) { + String javaCipherSuiteSuffix = toJavaUncached(openSslCipherSuite); + if (javaCipherSuiteSuffix == null) { + return null; + } + + final String javaCipherSuiteSsl = "SSL_" + javaCipherSuiteSuffix; + final String javaCipherSuiteTls = "TLS_" + javaCipherSuiteSuffix; + + // Cache the mapping. + final Map<String, String> p2j = new HashMap<String, String>(4); + p2j.put("", javaCipherSuiteSuffix); + p2j.put("SSL", javaCipherSuiteSsl); + p2j.put("TLS", javaCipherSuiteTls); + o2j.putIfAbsent(openSslCipherSuite, p2j); + + // Cache the reverse mapping after adding the protocol prefix (TLS_ or SSL_) + j2o.putIfAbsent(javaCipherSuiteTls, openSslCipherSuite); + j2o.putIfAbsent(javaCipherSuiteSsl, openSslCipherSuite); + + if (logger.isDebugEnabled()) { + logger.debug(sm.getString("converter.mapping", javaCipherSuiteTls, openSslCipherSuite)); + logger.debug(sm.getString("converter.mapping", javaCipherSuiteSsl, openSslCipherSuite)); + } + + return p2j; + } + + static String toJavaUncached(String openSslCipherSuite) { + Matcher m = OPENSSL_CIPHERSUITE_PATTERN.matcher(openSslCipherSuite); + if (!m.matches()) { + return null; + } + + String handshakeAlgo = m.group(1); + final boolean export; + if (handshakeAlgo == null) { + handshakeAlgo = ""; + export = false; + } else if (handshakeAlgo.startsWith("EXP-")) { + handshakeAlgo = handshakeAlgo.substring(4); + export = true; + } else if ("EXP".equals(handshakeAlgo)) { + handshakeAlgo = ""; + export = true; + } else { + export = false; + } + + handshakeAlgo = toJavaHandshakeAlgo(handshakeAlgo, export); + String bulkCipher = toJavaBulkCipher(m.group(2), export); + String hmacAlgo = toJavaHmacAlgo(m.group(3)); + + return handshakeAlgo + "_WITH_" + bulkCipher + '_' + hmacAlgo; + } + + private static String toJavaHandshakeAlgo(String handshakeAlgo, boolean export) { + if (handshakeAlgo.length() == 0) { + handshakeAlgo = "RSA"; + } else if ("ADH".equals(handshakeAlgo)) { + handshakeAlgo = "DH_anon"; + } else if ("AECDH".equals(handshakeAlgo)) { + handshakeAlgo = "ECDH_anon"; + } + + handshakeAlgo = handshakeAlgo.replace('-', '_'); + if (export) { + return handshakeAlgo + "_EXPORT"; + } else { + return handshakeAlgo; + } + } + + private static String toJavaBulkCipher(String bulkCipher, boolean export) { + if (bulkCipher.startsWith("AES")) { + Matcher m = OPENSSL_AES_CBC_PATTERN.matcher(bulkCipher); + if (m.matches()) { + return m.replaceFirst("$1_$2_CBC"); + } + + m = OPENSSL_AES_PATTERN.matcher(bulkCipher); + if (m.matches()) { + return m.replaceFirst("$1_$2_$3"); + } + } + + if ("DES-CBC3".equals(bulkCipher)) { + return "3DES_EDE_CBC"; + } + + if ("RC4".equals(bulkCipher)) { + if (export) { + return "RC4_40"; + } else { + return "RC4_128"; + } + } + + if ("DES-CBC".equals(bulkCipher)) { + if (export) { + return "DES_CBC_40"; + } else { + return "DES_CBC"; + } + } + + if ("RC2-CBC".equals(bulkCipher)) { + if (export) { + return "RC2_CBC_40"; + } else { + return "RC2_CBC"; + } + } + + return bulkCipher.replace('-', '_'); + } + + private static String toJavaHmacAlgo(String hmacAlgo) { + // Java and OpenSSL use the same algorithm names for: + // + // * SHA + // * SHA256 + // * MD5 + // + return hmacAlgo; + } + + private CipherSuiteConverter() { } +} Added: tomcat/trunk/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties?rev=1686279&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties (added) +++ tomcat/trunk/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties Thu Jun 18 17:13:40 2015 @@ -0,0 +1,50 @@ +# 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. + +openssl.X509FactoryError=Error getting X509 factory instance +openssl.errorSSLCtxInit=Error initializing SSL context +openssl.doubleInit=SSL context already initialized, ignoring +openssl.certificateVerificationFailed=Certificate verification failed +openssl.keyManagerMissing=No key manager found +openssl.trustManagerMissing=No trust manager found + +engine.engineClosed=Engine is closed +engine.renegociationUnsupported=Renegociation is not supported +engine.oversizedPacket=Encrypted packet is oversized +engine.ciphersFailure=Failed getting cipher list +engine.noSSLContext=No SSL context +engine.writeToSSLFailed=Failed writing to SSL, returned: {0} +engine.invalidBufferArray=offset: {0}, length: {1} (expected: offset <= offset + length <= srcs.length ({2})) +engine.nullBufferInArray=Null buffer in array +engine.nullBuffer=Null buffer +engine.readFromSSLFailed=Read from SSL failed, {0} SSL_read failed: primingReadResult: {1} OpenSSL error: {2} +engine.inboundClose=Inbound closed before receiving peer's close_notify +engine.nullCipherSuite=Null cipher suite +engine.unsupportedCipher=Unsupported cipher suite: {0} ({1}) +engine.emptyCipherSuite=Empty cipher suite +engine.failedCipherSuite=Failed to enable cipher suite {0} +engine.unsupportedProtocol=Protocol {0} is not supported +engine.unverifiedPeer=Peer unverified +engine.noSession=SSL session ID not available +engine.nullName=Null value name +engine.nullValue=Null value +engine.handshakeFailure=Failed handshake: {0} + +converter.mapping=Cipher suite mapping: {} => {} {0} {1} + +keyManager.nullCertificateChain=Null certificate chain +keyManager.nullPrivateKey=Null private key + +sessionContext.nullTicketKeys=Null keys Added: tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java?rev=1686279&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java (added) +++ tomcat/trunk/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java Thu Jun 18 17:13:40 2015 @@ -0,0 +1,466 @@ +/* + * 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.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +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.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.SSLContext; +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.jsse.openssl.OpenSSLCipherConfigurationParser; +import org.apache.tomcat.util.res.StringManager; + +public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { + + 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 OpenSSLServerSessionContext sessionContext; + + private List<String> ciphers = new ArrayList<>(); + + public List<String> getCiphers() { + return ciphers; + } + + private String enabledProtocol; + + public String getEnabledProtocol() { + return enabledProtocol; + } + + public void setEnabledProtocol(String protocol) { + enabledProtocol = (protocol == null) ? defaultProtocol : protocol; + } + + private final long aprPool; + protected final long ctx; + + @SuppressWarnings("unused") + private volatile int aprPoolDestroyed; + private static final AtomicIntegerFieldUpdater<OpenSSLContext> DESTROY_UPDATER + = AtomicIntegerFieldUpdater.newUpdater(OpenSSLContext.class, "aprPoolDestroyed"); + static final CertificateFactory X509_CERT_FACTORY; + 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(SSLHostConfig sslHostConfig) throws SSLException { + this.sslHostConfig = sslHostConfig; + aprPool = Pool.create(0); + boolean success = false; + try { + if (SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateFile()) == null) { + // This is required + throw new Exception(netSm.getString("endpoint.apr.noSslCertFile")); + } + + // SSL protocol + int value = SSL.SSL_PROTOCOL_NONE; + if (sslHostConfig.getProtocols().size() == 0) { + value = SSL.SSL_PROTOCOL_ALL; + } else { + for (String protocol : sslHostConfig.getProtocols()) { + 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_ALL.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_ALL; + } else { + // Protocol not recognized, fail to start as it is safer than + // continuing with the default which might enable more than the + // is required + 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); + } + success = true; + } catch(Exception e) { + throw new SSLException(sm.getString("openssl.errorSSLCtxInit"), e); + } finally { + if (!success) { + destroyPools(); + } + } + } + + private void destroyPools() { + // Guard against multiple destroyPools() calls triggered by construction exception and finalize() later + if (aprPool != 0 && DESTROY_UPDATER.compareAndSet(this, 0, 1)) { + Pool.destroy(aprPool); + } + } + + /** + * Setup the SSL_CTX + * + * @param kms Must contain a KeyManager of the type + * {@code OpenSSLKeyManager} + * @param tms + * @param sr Is not used for this implementation. + * @throws SSLException + */ + @Override + public synchronized void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) { + if (initialized) { + log.warn(sm.getString("openssl.doubleInit")); + return; + } + try { + boolean legacyRenegSupported = false; + try { + legacyRenegSupported = SSL.hasOp(SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); + if (legacyRenegSupported) + if (sslHostConfig.getInsecureRenegotiation()) { + SSLContext.setOptions(ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); + } else { + SSLContext.clearOptions(ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); + } + } catch (UnsatisfiedLinkError e) { + // Ignore + } + if (!legacyRenegSupported) { + // OpenSSL does not support unsafe legacy renegotiation. + log.warn(netSm.getString("endpoint.warn.noInsecureReneg", + SSL.versionString())); + } + // Use server's preference order for ciphers (rather than + // client's) + boolean orderCiphersSupported = false; + try { + orderCiphersSupported = SSL.hasOp(SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + if (orderCiphersSupported) { + if (sslHostConfig.getHonorCipherOrder()) { + SSLContext.setOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } else { + SSLContext.clearOptions(ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } + } + } catch (UnsatisfiedLinkError e) { + // Ignore + } + if (!orderCiphersSupported) { + // OpenSSL does not support ciphers ordering. + log.warn(netSm.getString("endpoint.warn.noHonorCipherOrder", + SSL.versionString())); + } + + // Disable compression if requested + boolean disableCompressionSupported = false; + try { + disableCompressionSupported = SSL.hasOp(SSL.SSL_OP_NO_COMPRESSION); + if (disableCompressionSupported) { + if (sslHostConfig.getDisableCompression()) { + SSLContext.setOptions(ctx, SSL.SSL_OP_NO_COMPRESSION); + } else { + SSLContext.clearOptions(ctx, SSL.SSL_OP_NO_COMPRESSION); + } + } + } catch (UnsatisfiedLinkError e) { + // Ignore + } + if (!disableCompressionSupported) { + // OpenSSL does not support ciphers ordering. + log.warn(netSm.getString("endpoint.warn.noDisableCompression", + SSL.versionString())); + } + + // Disable TLS Session Tickets (RFC4507) to protect perfect forward secrecy + boolean disableSessionTicketsSupported = false; + try { + disableSessionTicketsSupported = SSL.hasOp(SSL.SSL_OP_NO_TICKET); + if (disableSessionTicketsSupported) { + if (sslHostConfig.getDisableSessionTickets()) { + SSLContext.setOptions(ctx, SSL.SSL_OP_NO_TICKET); + } else { + SSLContext.clearOptions(ctx, SSL.SSL_OP_NO_TICKET); + } + } + } catch (UnsatisfiedLinkError e) { + // Ignore + } + if (!disableSessionTicketsSupported) { + // OpenSSL is too old to support TLS Session Tickets. + log.warn(netSm.getString("endpoint.warn.noDisableSessionTickets", + SSL.versionString())); + } + + // Set session cache size, if specified + if (sslHostConfig.getSessionCacheSize() > 0) { + SSLContext.setSessionCacheSize(ctx, sslHostConfig.getSessionCacheSize()); + } else { + // Get the default session cache size using SSLContext.setSessionCacheSize() + long sessionCacheSize = SSLContext.setSessionCacheSize(ctx, 20480); + // Revert the session cache size to the default value. + SSLContext.setSessionCacheSize(ctx, sessionCacheSize); + } + + // Set session timeout, if specified + if (sslHostConfig.getSessionTimeout() > 0) { + SSLContext.setSessionCacheTimeout(ctx, sslHostConfig.getSessionTimeout()); + } else { + // Get the default session timeout using SSLContext.setSessionCacheTimeout() + long sessionTimeout = SSLContext.setSessionCacheTimeout(ctx, 300); + // Revert the session timeout to the default value. + SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); + } + + // List the ciphers that the client is permitted to negotiate + String ciphers = sslHostConfig.getCiphers(); + if (!("ALL".equals(ciphers)) && ciphers.indexOf(":") == -1) { + StringTokenizer tok = new StringTokenizer(ciphers, ","); + this.ciphers = new ArrayList<>(); + while (tok.hasMoreTokens()) { + String token = tok.nextToken().trim(); + if (!"".equals(token)) { + this.ciphers.add(token); + } + } + ciphers = CipherSuiteConverter.toOpenSsl(ciphers); + } else { + this.ciphers = OpenSSLCipherConfigurationParser.parseExpression(ciphers); + } + SSLContext.setCipherSuite(ctx, ciphers); + // Load Server key and certificate + SSLContext.setCertificate(ctx, + SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateFile()), + SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateKeyFile()), + sslHostConfig.getCertificateKeyPassword(), SSL.SSL_AIDX_RSA); + // Set certificate chain file + SSLContext.setCertificateChainFile(ctx, + SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateChainFile()), + false); + // Support Client Certificates + SSLContext.setCACertificate(ctx, + SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile()), + SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())); + // Set revocation + SSLContext.setCARevocation(ctx, + SSLHostConfig.adjustRelativePath( + sslHostConfig.getCertificateRevocationListFile()), + SSLHostConfig.adjustRelativePath( + sslHostConfig.getCertificateRevocationListPath())); + // 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) { + final X509TrustManager manager = chooseTrustManager(tms); + SSLContext.setCertVerifyCallback(ctx, new CertificateVerifier() { + @Override + public boolean verify(long ssl, byte[][] chain, String auth) { + X509Certificate[] peerCerts = certificates(chain); + try { + manager.checkClientTrusted(peerCerts, auth); + return true; + } catch (Exception e) { + log.debug(sm.getString("openssl.certificateVerificationFailed"), e); + } + return false; + } + }); + } + String[] protos = new OpenSSLProtocols(enabledProtocol).getProtocols(); + SSLContext.setNpnProtos(ctx, protos, SSL.SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL); + + sessionContext = new OpenSSLServerSessionContext(ctx); + initialized = true; + } catch (Exception e) { + log.warn(sm.getString("openssl.errorSSLCtxInit"), e); + destroyPools(); + } + } + + static OpenSSLKeyManager chooseKeyManager(KeyManager[] managers) throws Exception { + for (KeyManager manager : managers) { + if (manager instanceof OpenSSLKeyManager) { + return (OpenSSLKeyManager) manager; + } + } + throw new IllegalStateException(sm.getString("openssl.keyManagerMissing")); + } + + 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; + } + + @Override + public SSLSessionContext getServerSessionContext() { + return sessionContext; + } + + @Override + public SSLEngine createSSLEngine() { + return new OpenSSLEngine(ctx, defaultProtocol, false, sessionContext); + } + + @Override + public SSLServerSocketFactory getServerSocketFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public SSLParameters getSupportedSSLParameters() { + throw new UnsupportedOperationException(); + } + + /** + * Generates a key specification for an (encrypted) private key. + * + * @param password characters, if {@code null} or empty an unencrypted key + * is assumed + * @param key bytes of the DER encoded private key + * + * @return a key specification + * + * @throws IOException if parsing {@code key} fails + * @throws NoSuchAlgorithmException if the algorithm used to encrypt + * {@code key} is unknown + * @throws NoSuchPaddingException if the padding scheme specified in the + * decryption algorithm is unknown + * @throws InvalidKeySpecException if the decryption key based on + * {@code password} cannot be generated + * @throws InvalidKeyException if the decryption key based on + * {@code password} cannot be used to decrypt {@code key} + * @throws InvalidAlgorithmParameterException if decryption algorithm + * parameters are somehow faulty + */ + protected static PKCS8EncodedKeySpec generateKeySpec(char[] password, byte[] key) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidKeyException, InvalidAlgorithmParameterException { + + if (password == null || password.length == 0) { + return new PKCS8EncodedKeySpec(key); + } + + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password); + SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec); + + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); + + return encryptedPrivateKeyInfo.getKeySpec(cipher); + } + + @Override + @SuppressWarnings("FinalizeDeclaration") + protected final void finalize() throws Throwable { + super.finalize(); + synchronized (OpenSSLContext.class) { + if (ctx != 0) { + SSLContext.free(ctx); + } + } + destroyPools(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org