From a093f00b67222c4e82d9d271545ad89d6204ddfe Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Mon, 29 Dec 2014 19:13:54 +0900 Subject: [PATCH] Cipher suite conversion between Java and OpenSSL Related: #3285 Motivation: When a user attempts to switch from JdkSslContext to OpenSslContext, he or she will see the initialization failure if he or she specified custom cipher suites. Modifications: - Provide a utility class that converts between Java cipher suite string and OpenSSL cipher suite string - Attempt to convert the cipher suite so that a user can use the cipher suite string format of Java regardless of the chosen SslContext impl Result: - It is possible to convert all known cipher suite strings. - It is possible to switch from JdkSslContext and OpenSslContext and vice versa without any configuration changes --- .../handler/ssl/CipherSuiteConverter.java | 503 ++++++++++++++++++ .../io/netty/handler/ssl/OpenSslContext.java | 6 + .../io/netty/handler/ssl/OpenSslEngine.java | 30 +- .../handler/ssl/CipherSuiteConverterTest.java | 306 +++++++++++ 4 files changed, 844 insertions(+), 1 deletion(-) create mode 100644 handler/src/main/java/io/netty/handler/ssl/CipherSuiteConverter.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/CipherSuiteConverterTest.java diff --git a/handler/src/main/java/io/netty/handler/ssl/CipherSuiteConverter.java b/handler/src/main/java/io/netty/handler/ssl/CipherSuiteConverter.java new file mode 100644 index 0000000000..458529dc8f --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/CipherSuiteConverter.java @@ -0,0 +1,503 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLContext; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Converts a Java cipher suite string to an OpenSSL cipher suite string and vice versa. + * + * @see Wikipedia page about cipher suite + */ +final class CipherSuiteConverter { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(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)-(?:DSS|RSA|ECDSA)|" + + "(?:ADH|AECDH|KRB5|PSK)" + + ')' + + ")|" + + "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 j2o = PlatformDependent.newConcurrentHashMap(); + + /** + * 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> o2j = PlatformDependent.newConcurrentHashMap(); + + static { + String[] cipherSuites = EmptyArrays.EMPTY_STRINGS; + try { + cipherSuites = SSLContext.getDefault().getSupportedSSLParameters().getCipherSuites(); + } catch (NoSuchAlgorithmException e) { + logger.warn("Failed to get the default SSLContext:", e); + } + + // Populate the initial mapping from the currently supported cipher suites. + for (String c: cipherSuites) { + cacheFromJava(c); + } + + // Also popluate those unavailable from Java but maybe available in OpenSSL. + cacheFromJava("SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("SSL_DHE_DSS_WITH_DES_CBC_SHA"); + cacheFromJava("SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("SSL_DHE_RSA_WITH_DES_CBC_SHA"); + cacheFromJava("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("SSL_DH_anon_EXPORT_WITH_RC4_40_MD5"); + cacheFromJava("SSL_DH_anon_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("SSL_DH_anon_WITH_DES_CBC_SHA"); + cacheFromJava("SSL_DH_anon_WITH_RC4_128_MD5"); + cacheFromJava("SSL_RSA_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"); + cacheFromJava("SSL_RSA_EXPORT_WITH_RC4_40_MD5"); + cacheFromJava("SSL_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("SSL_RSA_WITH_DES_CBC_SHA"); + cacheFromJava("SSL_RSA_WITH_NULL_MD5"); + cacheFromJava("SSL_RSA_WITH_NULL_SHA"); + cacheFromJava("SSL_RSA_WITH_RC4_128_MD5"); + cacheFromJava("SSL_RSA_WITH_RC4_128_SHA"); + cacheFromJava("TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_DHE_DSS_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_DHE_DSS_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_DHE_DSS_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_DHE_DSS_WITH_DES_CBC_SHA"); + cacheFromJava("TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_DHE_RSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_DHE_RSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_DHE_RSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_DHE_RSA_WITH_DES_CBC_SHA"); + cacheFromJava("TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("TLS_DH_anon_EXPORT_WITH_RC4_40_MD5"); + cacheFromJava("TLS_DH_anon_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_DH_anon_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_DH_anon_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_DH_anon_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_DH_anon_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_DH_anon_WITH_DES_CBC_SHA"); + cacheFromJava("TLS_DH_anon_WITH_RC4_128_MD5"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_NULL_SHA"); + cacheFromJava("TLS_ECDHE_ECDSA_WITH_RC4_128_SHA"); + cacheFromJava("TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_ECDHE_RSA_WITH_NULL_SHA"); + cacheFromJava("TLS_ECDHE_RSA_WITH_RC4_128_SHA"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_NULL_SHA"); + cacheFromJava("TLS_ECDH_ECDSA_WITH_RC4_128_SHA"); + cacheFromJava("TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_ECDH_RSA_WITH_NULL_SHA"); + cacheFromJava("TLS_ECDH_RSA_WITH_RC4_128_SHA"); + cacheFromJava("TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_ECDH_anon_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_ECDH_anon_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_ECDH_anon_WITH_NULL_SHA"); + cacheFromJava("TLS_ECDH_anon_WITH_RC4_128_SHA"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_RC4_40_MD5"); + cacheFromJava("TLS_KRB5_EXPORT_WITH_RC4_40_SHA"); + cacheFromJava("TLS_KRB5_WITH_3DES_EDE_CBC_MD5"); + cacheFromJava("TLS_KRB5_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_KRB5_WITH_DES_CBC_MD5"); + cacheFromJava("TLS_KRB5_WITH_DES_CBC_SHA"); + cacheFromJava("TLS_KRB5_WITH_RC4_128_MD5"); + cacheFromJava("TLS_KRB5_WITH_RC4_128_SHA"); + cacheFromJava("TLS_RSA_EXPORT_WITH_DES40_CBC_SHA"); + cacheFromJava("TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5"); + cacheFromJava("TLS_RSA_EXPORT_WITH_RC4_40_MD5"); + cacheFromJava("TLS_RSA_WITH_3DES_EDE_CBC_SHA"); + cacheFromJava("TLS_RSA_WITH_AES_128_CBC_SHA"); + cacheFromJava("TLS_RSA_WITH_AES_128_CBC_SHA256"); + cacheFromJava("TLS_RSA_WITH_AES_128_GCM_SHA256"); + cacheFromJava("TLS_RSA_WITH_AES_256_CBC_SHA"); + cacheFromJava("TLS_RSA_WITH_DES_CBC_SHA"); + cacheFromJava("TLS_RSA_WITH_NULL_MD5"); + cacheFromJava("TLS_RSA_WITH_NULL_SHA"); + cacheFromJava("TLS_RSA_WITH_NULL_SHA256"); + cacheFromJava("TLS_RSA_WITH_RC4_128_MD5"); + cacheFromJava("TLS_RSA_WITH_RC4_128_SHA"); + } + + /** + * 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 p2j = o2j.get(key); + if (p2j == null) { + return false; + } else { + return value.equals(p2j.get(protocol)); + } + } + + /** + * Converts the specified Java cipher suite to its corresponding OpenSSL cipher suite name. + * + * @return {@code null} if the conversion has failed + */ + 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 p2j = new HashMap(4); + p2j.put("", javaCipherSuiteSuffix); + p2j.put("SSL", "SSL_" + javaCipherSuiteSuffix); + p2j.put("TLS", "TLS_" + javaCipherSuiteSuffix); + o2j.put(openSslCipherSuite, p2j); + + logger.debug("Cipher suite 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; + } + + static String toJava(String openSslCipherSuite, String protocol) { + Map 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 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 p2j = new HashMap(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); + + logger.debug("Cipher suite mapping: {} => {}", javaCipherSuiteTls, openSslCipherSuite); + logger.debug("Cipher suite 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() { } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java index becfd6632e..213f58bdfc 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java @@ -105,6 +105,12 @@ public abstract class OpenSslContext extends SslContext { if (c == null) { break; } + + String converted = CipherSuiteConverter.toOpenSsl(c); + if (converted != null) { + c = converted; + } + this.ciphers.add(c); } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java index 7381f5d992..4ecc44467f 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslEngine.java @@ -1093,9 +1093,16 @@ public final class OpenSslEngine extends SSLEngine { handshakeFinished = true; String c = SSL.getCipherForSSL(ssl); if (c != null) { + String protocol = toProtocolFamily(SSL.getVersion(ssl)); + String converted = CipherSuiteConverter.toJava(c, protocol); + if (converted != null) { + c = converted; + } else { + c = protocol + '_' + c.replace('-', '_'); + } // OpenSSL returns the ciphers seperated by '-' but the JDK SSLEngine does by '_', so replace '-' // with '_' to match the behaviour. - cipher = CIPHER_REPLACE_PATTERN.matcher(c).replaceAll("_"); + cipher = c; } String applicationProtocol = SSL.getNextProtoNegotiated(ssl); if (applicationProtocol == null) { @@ -1128,6 +1135,27 @@ public final class OpenSslEngine extends SSLEngine { return NOT_HANDSHAKING; } + /** + * Converts the protocol version string returned by {@link SSL#getVersion(long)} to protocol family string. + */ + private static String toProtocolFamily(String protocolVersion) { + final char c; + if (protocolVersion == null || protocolVersion.length() == 0) { + c = 0; + } else { + c = protocolVersion.charAt(0); + } + + switch (c) { + case 'T': + return "TLS"; + case 'S': + return "SSL"; + default: + return "UNKNOWN"; + } + } + @Override public void setUseClientMode(boolean clientMode) { if (clientMode != this.clientMode) { diff --git a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteConverterTest.java b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteConverterTest.java new file mode 100644 index 0000000000..66a995163a --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteConverterTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ssl; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class CipherSuiteConverterTest { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(CipherSuiteConverterTest.class); + + @Test + public void testJ2OMappings() throws Exception { + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "ECDHE-ECDSA-AES128-SHA256"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "ECDHE-RSA-AES128-SHA256"); + testJ2OMapping("TLS_RSA_WITH_AES_128_CBC_SHA256", "AES128-SHA256"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", "ECDH-ECDSA-AES128-SHA256"); + testJ2OMapping("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", "ECDH-RSA-AES128-SHA256"); + testJ2OMapping("TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", "DHE-RSA-AES128-SHA256"); + testJ2OMapping("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", "DHE-DSS-AES128-SHA256"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "ECDHE-ECDSA-AES128-SHA"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "ECDHE-RSA-AES128-SHA"); + testJ2OMapping("TLS_RSA_WITH_AES_128_CBC_SHA", "AES128-SHA"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", "ECDH-ECDSA-AES128-SHA"); + testJ2OMapping("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", "ECDH-RSA-AES128-SHA"); + testJ2OMapping("TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "DHE-RSA-AES128-SHA"); + testJ2OMapping("TLS_DHE_DSS_WITH_AES_128_CBC_SHA", "DHE-DSS-AES128-SHA"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "ECDHE-RSA-AES128-GCM-SHA256"); + testJ2OMapping("TLS_RSA_WITH_AES_128_GCM_SHA256", "AES128-GCM-SHA256"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", "ECDH-ECDSA-AES128-GCM-SHA256"); + testJ2OMapping("TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", "ECDH-RSA-AES128-GCM-SHA256"); + testJ2OMapping("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "DHE-RSA-AES128-GCM-SHA256"); + testJ2OMapping("TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", "DHE-DSS-AES128-GCM-SHA256"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", "ECDHE-ECDSA-DES-CBC3-SHA"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", "ECDHE-RSA-DES-CBC3-SHA"); + testJ2OMapping("SSL_RSA_WITH_3DES_EDE_CBC_SHA", "DES-CBC3-SHA"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", "ECDH-ECDSA-DES-CBC3-SHA"); + testJ2OMapping("TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", "ECDH-RSA-DES-CBC3-SHA"); + testJ2OMapping("SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", "DHE-RSA-DES-CBC3-SHA"); + testJ2OMapping("SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", "DHE-DSS-DES-CBC3-SHA"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "ECDHE-ECDSA-RC4-SHA"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_RC4_128_SHA", "ECDHE-RSA-RC4-SHA"); + testJ2OMapping("SSL_RSA_WITH_RC4_128_SHA", "RC4-SHA"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_RC4_128_SHA", "ECDH-ECDSA-RC4-SHA"); + testJ2OMapping("TLS_ECDH_RSA_WITH_RC4_128_SHA", "ECDH-RSA-RC4-SHA"); + testJ2OMapping("SSL_RSA_WITH_RC4_128_MD5", "RC4-MD5"); + testJ2OMapping("TLS_DH_anon_WITH_AES_128_GCM_SHA256", "ADH-AES128-GCM-SHA256"); + testJ2OMapping("TLS_DH_anon_WITH_AES_128_CBC_SHA256", "ADH-AES128-SHA256"); + testJ2OMapping("TLS_ECDH_anon_WITH_AES_128_CBC_SHA", "AECDH-AES128-SHA"); + testJ2OMapping("TLS_DH_anon_WITH_AES_128_CBC_SHA", "ADH-AES128-SHA"); + testJ2OMapping("TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", "AECDH-DES-CBC3-SHA"); + testJ2OMapping("SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", "ADH-DES-CBC3-SHA"); + testJ2OMapping("TLS_ECDH_anon_WITH_RC4_128_SHA", "AECDH-RC4-SHA"); + testJ2OMapping("SSL_DH_anon_WITH_RC4_128_MD5", "ADH-RC4-MD5"); + testJ2OMapping("SSL_RSA_WITH_DES_CBC_SHA", "DES-CBC-SHA"); + testJ2OMapping("SSL_DHE_RSA_WITH_DES_CBC_SHA", "DHE-RSA-DES-CBC-SHA"); + testJ2OMapping("SSL_DHE_DSS_WITH_DES_CBC_SHA", "DHE-DSS-DES-CBC-SHA"); + testJ2OMapping("SSL_DH_anon_WITH_DES_CBC_SHA", "ADH-DES-CBC-SHA"); + testJ2OMapping("SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", "EXP-DES-CBC-SHA"); + testJ2OMapping("SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "EXP-DHE-RSA-DES-CBC-SHA"); + testJ2OMapping("SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "EXP-DHE-DSS-DES-CBC-SHA"); + testJ2OMapping("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", "EXP-ADH-DES-CBC-SHA"); + testJ2OMapping("SSL_RSA_EXPORT_WITH_RC4_40_MD5", "EXP-RC4-MD5"); + testJ2OMapping("SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", "EXP-ADH-RC4-MD5"); + testJ2OMapping("TLS_RSA_WITH_NULL_SHA256", "NULL-SHA256"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_NULL_SHA", "ECDHE-ECDSA-NULL-SHA"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_NULL_SHA", "ECDHE-RSA-NULL-SHA"); + testJ2OMapping("SSL_RSA_WITH_NULL_SHA", "NULL-SHA"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_NULL_SHA", "ECDH-ECDSA-NULL-SHA"); + testJ2OMapping("TLS_ECDH_RSA_WITH_NULL_SHA", "ECDH-RSA-NULL-SHA"); + testJ2OMapping("TLS_ECDH_anon_WITH_NULL_SHA", "AECDH-NULL-SHA"); + testJ2OMapping("SSL_RSA_WITH_NULL_MD5", "NULL-MD5"); + testJ2OMapping("TLS_KRB5_WITH_3DES_EDE_CBC_SHA", "KRB5-DES-CBC3-SHA"); + testJ2OMapping("TLS_KRB5_WITH_3DES_EDE_CBC_MD5", "KRB5-DES-CBC3-MD5"); + testJ2OMapping("TLS_KRB5_WITH_RC4_128_SHA", "KRB5-RC4-SHA"); + testJ2OMapping("TLS_KRB5_WITH_RC4_128_MD5", "KRB5-RC4-MD5"); + testJ2OMapping("TLS_KRB5_WITH_DES_CBC_SHA", "KRB5-DES-CBC-SHA"); + testJ2OMapping("TLS_KRB5_WITH_DES_CBC_MD5", "KRB5-DES-CBC-MD5"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", "EXP-KRB5-DES-CBC-SHA"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", "EXP-KRB5-DES-CBC-MD5"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_RC4_40_SHA", "EXP-KRB5-RC4-SHA"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_RC4_40_MD5", "EXP-KRB5-RC4-MD5"); + testJ2OMapping("SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5", "EXP-RC2-CBC-MD5"); + testJ2OMapping("TLS_DHE_DSS_WITH_AES_256_CBC_SHA", "DHE-DSS-AES256-SHA"); + testJ2OMapping("TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "DHE-RSA-AES256-SHA"); + testJ2OMapping("TLS_DH_anon_WITH_AES_256_CBC_SHA", "ADH-AES256-SHA"); + testJ2OMapping("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "ECDHE-ECDSA-AES256-SHA"); + testJ2OMapping("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "ECDHE-RSA-AES256-SHA"); + testJ2OMapping("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", "ECDH-ECDSA-AES256-SHA"); + testJ2OMapping("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", "ECDH-RSA-AES256-SHA"); + testJ2OMapping("TLS_ECDH_anon_WITH_AES_256_CBC_SHA", "AECDH-AES256-SHA"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", "EXP-KRB5-RC2-CBC-MD5"); + testJ2OMapping("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", "EXP-KRB5-RC2-CBC-SHA"); + testJ2OMapping("TLS_RSA_WITH_AES_256_CBC_SHA", "AES256-SHA"); + } + + private static void testJ2OMapping(String javaCipherSuite, String openSslCipherSuite) { + final String actual = CipherSuiteConverter.toOpenSslUncached(javaCipherSuite); + logger.info("{} => {}", javaCipherSuite, actual); + assertThat(actual, is(openSslCipherSuite)); + } + + @Test + public void testO2JMappings() throws Exception { + testO2JMapping("ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "ECDHE-ECDSA-AES128-SHA256"); + testO2JMapping("ECDHE_RSA_WITH_AES_128_CBC_SHA256", "ECDHE-RSA-AES128-SHA256"); + testO2JMapping("RSA_WITH_AES_128_CBC_SHA256", "AES128-SHA256"); + testO2JMapping("ECDH_ECDSA_WITH_AES_128_CBC_SHA256", "ECDH-ECDSA-AES128-SHA256"); + testO2JMapping("ECDH_RSA_WITH_AES_128_CBC_SHA256", "ECDH-RSA-AES128-SHA256"); + testO2JMapping("DHE_RSA_WITH_AES_128_CBC_SHA256", "DHE-RSA-AES128-SHA256"); + testO2JMapping("DHE_DSS_WITH_AES_128_CBC_SHA256", "DHE-DSS-AES128-SHA256"); + testO2JMapping("ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "ECDHE-ECDSA-AES128-SHA"); + testO2JMapping("ECDHE_RSA_WITH_AES_128_CBC_SHA", "ECDHE-RSA-AES128-SHA"); + testO2JMapping("RSA_WITH_AES_128_CBC_SHA", "AES128-SHA"); + testO2JMapping("ECDH_ECDSA_WITH_AES_128_CBC_SHA", "ECDH-ECDSA-AES128-SHA"); + testO2JMapping("ECDH_RSA_WITH_AES_128_CBC_SHA", "ECDH-RSA-AES128-SHA"); + testO2JMapping("DHE_RSA_WITH_AES_128_CBC_SHA", "DHE-RSA-AES128-SHA"); + testO2JMapping("DHE_DSS_WITH_AES_128_CBC_SHA", "DHE-DSS-AES128-SHA"); + testO2JMapping("ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256"); + testO2JMapping("ECDHE_RSA_WITH_AES_128_GCM_SHA256", "ECDHE-RSA-AES128-GCM-SHA256"); + testO2JMapping("RSA_WITH_AES_128_GCM_SHA256", "AES128-GCM-SHA256"); + testO2JMapping("ECDH_ECDSA_WITH_AES_128_GCM_SHA256", "ECDH-ECDSA-AES128-GCM-SHA256"); + testO2JMapping("ECDH_RSA_WITH_AES_128_GCM_SHA256", "ECDH-RSA-AES128-GCM-SHA256"); + testO2JMapping("DHE_RSA_WITH_AES_128_GCM_SHA256", "DHE-RSA-AES128-GCM-SHA256"); + testO2JMapping("DHE_DSS_WITH_AES_128_GCM_SHA256", "DHE-DSS-AES128-GCM-SHA256"); + testO2JMapping("ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", "ECDHE-ECDSA-DES-CBC3-SHA"); + testO2JMapping("ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", "ECDHE-RSA-DES-CBC3-SHA"); + testO2JMapping("RSA_WITH_3DES_EDE_CBC_SHA", "DES-CBC3-SHA"); + testO2JMapping("ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", "ECDH-ECDSA-DES-CBC3-SHA"); + testO2JMapping("ECDH_RSA_WITH_3DES_EDE_CBC_SHA", "ECDH-RSA-DES-CBC3-SHA"); + testO2JMapping("DHE_RSA_WITH_3DES_EDE_CBC_SHA", "DHE-RSA-DES-CBC3-SHA"); + testO2JMapping("DHE_DSS_WITH_3DES_EDE_CBC_SHA", "DHE-DSS-DES-CBC3-SHA"); + testO2JMapping("ECDHE_ECDSA_WITH_RC4_128_SHA", "ECDHE-ECDSA-RC4-SHA"); + testO2JMapping("ECDHE_RSA_WITH_RC4_128_SHA", "ECDHE-RSA-RC4-SHA"); + testO2JMapping("RSA_WITH_RC4_128_SHA", "RC4-SHA"); + testO2JMapping("ECDH_ECDSA_WITH_RC4_128_SHA", "ECDH-ECDSA-RC4-SHA"); + testO2JMapping("ECDH_RSA_WITH_RC4_128_SHA", "ECDH-RSA-RC4-SHA"); + testO2JMapping("RSA_WITH_RC4_128_MD5", "RC4-MD5"); + testO2JMapping("DH_anon_WITH_AES_128_GCM_SHA256", "ADH-AES128-GCM-SHA256"); + testO2JMapping("DH_anon_WITH_AES_128_CBC_SHA256", "ADH-AES128-SHA256"); + testO2JMapping("ECDH_anon_WITH_AES_128_CBC_SHA", "AECDH-AES128-SHA"); + testO2JMapping("DH_anon_WITH_AES_128_CBC_SHA", "ADH-AES128-SHA"); + testO2JMapping("ECDH_anon_WITH_3DES_EDE_CBC_SHA", "AECDH-DES-CBC3-SHA"); + testO2JMapping("DH_anon_WITH_3DES_EDE_CBC_SHA", "ADH-DES-CBC3-SHA"); + testO2JMapping("ECDH_anon_WITH_RC4_128_SHA", "AECDH-RC4-SHA"); + testO2JMapping("DH_anon_WITH_RC4_128_MD5", "ADH-RC4-MD5"); + testO2JMapping("RSA_WITH_DES_CBC_SHA", "DES-CBC-SHA"); + testO2JMapping("DHE_RSA_WITH_DES_CBC_SHA", "DHE-RSA-DES-CBC-SHA"); + testO2JMapping("DHE_DSS_WITH_DES_CBC_SHA", "DHE-DSS-DES-CBC-SHA"); + testO2JMapping("DH_anon_WITH_DES_CBC_SHA", "ADH-DES-CBC-SHA"); + testO2JMapping("RSA_EXPORT_WITH_DES_CBC_40_SHA", "EXP-DES-CBC-SHA"); + testO2JMapping("DHE_RSA_EXPORT_WITH_DES_CBC_40_SHA", "EXP-DHE-RSA-DES-CBC-SHA"); + testO2JMapping("DHE_DSS_EXPORT_WITH_DES_CBC_40_SHA", "EXP-DHE-DSS-DES-CBC-SHA"); + testO2JMapping("DH_anon_EXPORT_WITH_DES_CBC_40_SHA", "EXP-ADH-DES-CBC-SHA"); + testO2JMapping("RSA_EXPORT_WITH_RC4_40_MD5", "EXP-RC4-MD5"); + testO2JMapping("DH_anon_EXPORT_WITH_RC4_40_MD5", "EXP-ADH-RC4-MD5"); + testO2JMapping("RSA_WITH_NULL_SHA256", "NULL-SHA256"); + testO2JMapping("ECDHE_ECDSA_WITH_NULL_SHA", "ECDHE-ECDSA-NULL-SHA"); + testO2JMapping("ECDHE_RSA_WITH_NULL_SHA", "ECDHE-RSA-NULL-SHA"); + testO2JMapping("RSA_WITH_NULL_SHA", "NULL-SHA"); + testO2JMapping("ECDH_ECDSA_WITH_NULL_SHA", "ECDH-ECDSA-NULL-SHA"); + testO2JMapping("ECDH_RSA_WITH_NULL_SHA", "ECDH-RSA-NULL-SHA"); + testO2JMapping("ECDH_anon_WITH_NULL_SHA", "AECDH-NULL-SHA"); + testO2JMapping("RSA_WITH_NULL_MD5", "NULL-MD5"); + testO2JMapping("KRB5_WITH_3DES_EDE_CBC_SHA", "KRB5-DES-CBC3-SHA"); + testO2JMapping("KRB5_WITH_3DES_EDE_CBC_MD5", "KRB5-DES-CBC3-MD5"); + testO2JMapping("KRB5_WITH_RC4_128_SHA", "KRB5-RC4-SHA"); + testO2JMapping("KRB5_WITH_RC4_128_MD5", "KRB5-RC4-MD5"); + testO2JMapping("KRB5_WITH_DES_CBC_SHA", "KRB5-DES-CBC-SHA"); + testO2JMapping("KRB5_WITH_DES_CBC_MD5", "KRB5-DES-CBC-MD5"); + testO2JMapping("KRB5_EXPORT_WITH_DES_CBC_40_SHA", "EXP-KRB5-DES-CBC-SHA"); + testO2JMapping("KRB5_EXPORT_WITH_DES_CBC_40_MD5", "EXP-KRB5-DES-CBC-MD5"); + testO2JMapping("KRB5_EXPORT_WITH_RC4_40_SHA", "EXP-KRB5-RC4-SHA"); + testO2JMapping("KRB5_EXPORT_WITH_RC4_40_MD5", "EXP-KRB5-RC4-MD5"); + testO2JMapping("RSA_EXPORT_WITH_RC2_CBC_40_MD5", "EXP-RC2-CBC-MD5"); + testO2JMapping("DHE_DSS_WITH_AES_256_CBC_SHA", "DHE-DSS-AES256-SHA"); + testO2JMapping("DHE_RSA_WITH_AES_256_CBC_SHA", "DHE-RSA-AES256-SHA"); + testO2JMapping("DH_anon_WITH_AES_256_CBC_SHA", "ADH-AES256-SHA"); + testO2JMapping("ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "ECDHE-ECDSA-AES256-SHA"); + testO2JMapping("ECDHE_RSA_WITH_AES_256_CBC_SHA", "ECDHE-RSA-AES256-SHA"); + testO2JMapping("ECDH_ECDSA_WITH_AES_256_CBC_SHA", "ECDH-ECDSA-AES256-SHA"); + testO2JMapping("ECDH_RSA_WITH_AES_256_CBC_SHA", "ECDH-RSA-AES256-SHA"); + testO2JMapping("ECDH_anon_WITH_AES_256_CBC_SHA", "AECDH-AES256-SHA"); + testO2JMapping("KRB5_EXPORT_WITH_RC2_CBC_40_MD5", "EXP-KRB5-RC2-CBC-MD5"); + testO2JMapping("KRB5_EXPORT_WITH_RC2_CBC_40_SHA", "EXP-KRB5-RC2-CBC-SHA"); + testO2JMapping("RSA_WITH_AES_256_CBC_SHA", "AES256-SHA"); + + // Test the known mappings that actually do not exist in Java + testO2JMapping("EDH_DSS_WITH_3DES_EDE_CBC_SHA", "EDH-DSS-DES-CBC3-SHA"); + testO2JMapping("RSA_WITH_SEED_SHA", "SEED-SHA"); + testO2JMapping("RSA_WITH_CAMELLIA128_SHA", "CAMELLIA128-SHA"); + testO2JMapping("RSA_WITH_IDEA_CBC_SHA", "IDEA-CBC-SHA"); + testO2JMapping("PSK_WITH_AES_128_CBC_SHA", "PSK-AES128-CBC-SHA"); + testO2JMapping("PSK_WITH_3DES_EDE_CBC_SHA", "PSK-3DES-EDE-CBC-SHA"); + testO2JMapping("KRB5_WITH_IDEA_CBC_SHA", "KRB5-IDEA-CBC-SHA"); + testO2JMapping("KRB5_WITH_IDEA_CBC_MD5", "KRB5-IDEA-CBC-MD5"); + testO2JMapping("PSK_WITH_RC4_128_SHA", "PSK-RC4-SHA"); + testO2JMapping("ECDHE_RSA_WITH_AES_256_GCM_SHA384", "ECDHE-RSA-AES256-GCM-SHA384"); + testO2JMapping("ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"); + testO2JMapping("ECDHE_RSA_WITH_AES_256_CBC_SHA384", "ECDHE-RSA-AES256-SHA384"); + testO2JMapping("ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "ECDHE-ECDSA-AES256-SHA384"); + testO2JMapping("DHE_DSS_WITH_AES_256_GCM_SHA384", "DHE-DSS-AES256-GCM-SHA384"); + testO2JMapping("DHE_RSA_WITH_AES_256_GCM_SHA384", "DHE-RSA-AES256-GCM-SHA384"); + testO2JMapping("DHE_RSA_WITH_AES_256_CBC_SHA256", "DHE-RSA-AES256-SHA256"); + testO2JMapping("DHE_DSS_WITH_AES_256_CBC_SHA256", "DHE-DSS-AES256-SHA256"); + testO2JMapping("DHE_RSA_WITH_CAMELLIA256_SHA", "DHE-RSA-CAMELLIA256-SHA"); + testO2JMapping("DHE_DSS_WITH_CAMELLIA256_SHA", "DHE-DSS-CAMELLIA256-SHA"); + testO2JMapping("ECDH_RSA_WITH_AES_256_GCM_SHA384", "ECDH-RSA-AES256-GCM-SHA384"); + testO2JMapping("ECDH_ECDSA_WITH_AES_256_GCM_SHA384", "ECDH-ECDSA-AES256-GCM-SHA384"); + testO2JMapping("ECDH_RSA_WITH_AES_256_CBC_SHA384", "ECDH-RSA-AES256-SHA384"); + testO2JMapping("ECDH_ECDSA_WITH_AES_256_CBC_SHA384", "ECDH-ECDSA-AES256-SHA384"); + testO2JMapping("RSA_WITH_AES_256_GCM_SHA384", "AES256-GCM-SHA384"); + testO2JMapping("RSA_WITH_AES_256_CBC_SHA256", "AES256-SHA256"); + testO2JMapping("RSA_WITH_CAMELLIA256_SHA", "CAMELLIA256-SHA"); + testO2JMapping("PSK_WITH_AES_256_CBC_SHA", "PSK-AES256-CBC-SHA"); + testO2JMapping("DHE_RSA_WITH_SEED_SHA", "DHE-RSA-SEED-SHA"); + testO2JMapping("DHE_DSS_WITH_SEED_SHA", "DHE-DSS-SEED-SHA"); + testO2JMapping("DHE_RSA_WITH_CAMELLIA128_SHA", "DHE-RSA-CAMELLIA128-SHA"); + testO2JMapping("DHE_DSS_WITH_CAMELLIA128_SHA", "DHE-DSS-CAMELLIA128-SHA"); + testO2JMapping("EDH_RSA_WITH_3DES_EDE_CBC_SHA", "EDH-RSA-DES-CBC3-SHA"); + } + + private static void testO2JMapping(String javaCipherSuite, String openSslCipherSuite) { + final String actual = CipherSuiteConverter.toJavaUncached(openSslCipherSuite); + logger.info("{} => {}", openSslCipherSuite, actual); + assertThat(actual, is(javaCipherSuite)); + } + + @Test + public void testCachedJ2OMappings() { + testCachedJ2OMapping("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "ECDHE-ECDSA-AES128-SHA256"); + } + + private static void testCachedJ2OMapping(String javaCipherSuite, String openSslCipherSuite) { + CipherSuiteConverter.clearCache(); + + final String actual1 = CipherSuiteConverter.toOpenSsl(javaCipherSuite); + assertThat(actual1, is(openSslCipherSuite)); + + // Ensure that the cache entries have been created. + assertThat(CipherSuiteConverter.isJ2OCached(javaCipherSuite, actual1), is(true)); + assertThat(CipherSuiteConverter.isO2JCached(actual1, "", javaCipherSuite.substring(4)), is(true)); + assertThat(CipherSuiteConverter.isO2JCached(actual1, "SSL", "SSL_" + javaCipherSuite.substring(4)), is(true)); + assertThat(CipherSuiteConverter.isO2JCached(actual1, "TLS", "TLS_" + javaCipherSuite.substring(4)), is(true)); + + final String actual2 = CipherSuiteConverter.toOpenSsl(javaCipherSuite); + assertThat(actual2, is(openSslCipherSuite)); + + // Test if the returned cipher strings are identical, + // so that the TLS sessions with the same cipher suite do not create many strings. + assertThat(actual1, is(sameInstance(actual2))); + } + + @Test + public void testCachedO2JMappings() { + testCachedO2JMapping("ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "ECDHE-ECDSA-AES128-SHA256"); + } + + private static void testCachedO2JMapping(String javaCipherSuite, String openSslCipherSuite) { + CipherSuiteConverter.clearCache(); + + final String tlsExpected = "TLS_" + javaCipherSuite; + final String sslExpected = "SSL_" + javaCipherSuite; + + final String tlsActual1 = CipherSuiteConverter.toJava(openSslCipherSuite, "TLS"); + final String sslActual1 = CipherSuiteConverter.toJava(openSslCipherSuite, "SSL"); + assertThat(tlsActual1, is(tlsExpected)); + assertThat(sslActual1, is(sslExpected)); + + // Ensure that the cache entries have been created. + assertThat(CipherSuiteConverter.isO2JCached(openSslCipherSuite, "", javaCipherSuite), is(true)); + assertThat(CipherSuiteConverter.isO2JCached(openSslCipherSuite, "SSL", sslExpected), is(true)); + assertThat(CipherSuiteConverter.isO2JCached(openSslCipherSuite, "TLS", tlsExpected), is(true)); + assertThat(CipherSuiteConverter.isJ2OCached(tlsExpected, openSslCipherSuite), is(true)); + assertThat(CipherSuiteConverter.isJ2OCached(sslExpected, openSslCipherSuite), is(true)); + + final String tlsActual2 = CipherSuiteConverter.toJava(openSslCipherSuite, "TLS"); + final String sslActual2 = CipherSuiteConverter.toJava(openSslCipherSuite, "SSL"); + assertThat(tlsActual2, is(tlsExpected)); + assertThat(sslActual2, is(sslExpected)); + + // Test if the returned cipher strings are identical, + // so that the TLS sessions with the same cipher suite do not create many strings. + assertThat(tlsActual1, is(sameInstance(tlsActual2))); + assertThat(sslActual1, is(sameInstance(sslActual2))); + } +}