Implement OpenSslEngine.getSupportedCipherSuites() and get/setEnabledCipherSuites()

Motivation:

To make OpenSslEngine a full drop-in replacement, we need to implement
getSupportedCipherSuites() and get/setEnabledCipherSuites().

Modifications:

- Retrieve the list of the available cipher suites when initializing
  OpenSsl.
- Improve CipherSuiteConverter to understand SRP
- Add more test data to CipherSuiteConverterTest
- Add bulk-conversion method to CipherSuiteConverter

Result:

OpenSslEngine should now be a drop-in replacement for JDK SSLEngineImpl
for most cases.
This commit is contained in:
Trustin Lee 2014-12-30 18:59:30 +09:00
parent a093f00b67
commit 7d50f7864c
5 changed files with 129 additions and 18 deletions

View File

@ -72,8 +72,8 @@ final class CipherSuiteConverter {
"^(?:(" + // BEGIN handshake algorithm
"(?:(?:EXP-)?" +
"(?:" +
"(?:DHE|EDH|ECDH|ECDHE)-(?:DSS|RSA|ECDSA)|" +
"(?:ADH|AECDH|KRB5|PSK)" +
"(?:DHE|EDH|ECDH|ECDHE|SRP)-(?:DSS|RSA|ECDSA)|" +
"(?:ADH|AECDH|KRB5|PSK|SRP)" +
')' +
")|" +
"EXP" +
@ -243,6 +243,33 @@ final class CipherSuiteConverter {
}
}
/**
* Converts the specified Java cipher suites to the colon-separated OpenSSL cipher suite specification.
*/
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.
*

View File

@ -20,7 +20,13 @@ import io.netty.util.internal.NativeLibraryLoader;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import org.apache.tomcat.jni.Library;
import org.apache.tomcat.jni.Pool;
import org.apache.tomcat.jni.SSL;
import org.apache.tomcat.jni.SSLContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Tells if <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
@ -31,6 +37,8 @@ public final class OpenSsl {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSsl.class);
private static final Throwable UNAVAILABILITY_CAUSE;
private static final List<String> AVAILABLE_CIPHER_SUITES;
static {
Throwable cause = null;
try {
@ -44,6 +52,40 @@ public final class OpenSsl {
OpenSslEngine.class.getSimpleName() + " will be unavailable.", t);
}
UNAVAILABILITY_CAUSE = cause;
if (cause == null) {
final List<String> availableCipherSuites = new ArrayList<String>(128);
final long aprPool = Pool.create(0);
try {
final long sslCtx = SSLContext.make(aprPool, SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER);
try {
SSLContext.setOptions(sslCtx, SSL.SSL_OP_ALL);
SSLContext.setCipherSuite(sslCtx, "ALL");
final long ssl = SSL.newSSL(sslCtx, true);
try {
for (String c: SSL.getCiphers(ssl)) {
// Filter out bad input.
if (c == null || c.length() == 0 || availableCipherSuites.contains(c)) {
continue;
}
availableCipherSuites.add(c);
}
} finally {
SSL.freeSSL(ssl);
}
} finally {
SSLContext.free(sslCtx);
}
} catch (Exception e) {
logger.warn("Failed to get the list of available OpenSSL cipher suites.", e);
} finally {
Pool.destroy(aprPool);
}
AVAILABLE_CIPHER_SUITES = Collections.unmodifiableList(availableCipherSuites);
} else {
AVAILABLE_CIPHER_SUITES = Collections.emptyList();
}
}
/**
@ -78,6 +120,14 @@ public final class OpenSsl {
return UNAVAILABILITY_CAUSE;
}
/**
* Returns all the available OpenSSL cipher suites.
* Please note that the returned array may include the cipher suites that are insecure or non-functional.
*/
public static String[] availableCipherSuites() {
return AVAILABLE_CIPHER_SUITES.toArray(new String[AVAILABLE_CIPHER_SUITES.size()]);
}
static boolean isError(long errorCode) {
return errorCode != SSL.SSL_ERROR_NONE;
}

View File

@ -139,15 +139,7 @@ public abstract class OpenSslContext extends SslContext {
/* List the ciphers that are permitted to negotiate. */
try {
// Convert the cipher list into a colon-separated string.
StringBuilder cipherBuf = new StringBuilder();
for (String c: this.ciphers) {
cipherBuf.append(c);
cipherBuf.append(':');
}
cipherBuf.setLength(cipherBuf.length() - 1);
SSLContext.setCipherSuite(ctx, cipherBuf.toString());
SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(this.ciphers));
} catch (SSLException e) {
throw e;
} catch (Exception e) {

View File

@ -32,8 +32,8 @@ import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener;
import javax.net.ssl.SSLSessionContext;
import javax.security.cert.X509Certificate;
import javax.security.cert.CertificateException;
import javax.security.cert.X509Certificate;
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.security.Principal;
@ -47,7 +47,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.regex.Pattern;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
import static javax.net.ssl.SSLEngineResult.Status.*;
@ -119,7 +118,6 @@ public final class OpenSslEngine extends SSLEngine {
private static final AtomicIntegerFieldUpdater<OpenSslEngine> DESTROYED_UPDATER;
private static final AtomicReferenceFieldUpdater<OpenSslEngine, SSLSession> SESSION_UPDATER;
private static final Pattern CIPHER_REPLACE_PATTERN = Pattern.compile("-");
// OpenSSL state
private long ssl;
private long networkBIO;
@ -652,17 +650,39 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public String[] getSupportedCipherSuites() {
return EmptyArrays.EMPTY_STRINGS;
return OpenSsl.availableCipherSuites();
}
@Override
public String[] getEnabledCipherSuites() {
return EmptyArrays.EMPTY_STRINGS;
String[] enabled = SSL.getCiphers(ssl);
if (enabled == null) {
return EmptyArrays.EMPTY_STRINGS;
} else {
for (int i = 0; i < enabled.length; i++) {
String c = enabled[i];
// TODO: Determine the protocol using SSL_CIPHER_get_version()
String mapped = CipherSuiteConverter.toJava(c, "TLS");
if (mapped != null) {
enabled[i] = mapped;
}
}
return enabled;
}
}
@Override
public void setEnabledCipherSuites(String[] strings) {
throw new UnsupportedOperationException();
public void setEnabledCipherSuites(String[] cipherSuites) {
if (cipherSuites == null) {
throw new NullPointerException("cipherSuites");
}
final String converted = CipherSuiteConverter.toOpenSsl(Arrays.asList(cipherSuites));
try {
SSL.setCipherSuites(ssl, converted);
} catch (Exception e) {
throw new IllegalStateException("failed to enable cipher suites: " + converted, e);
}
}
@Override
@ -1225,6 +1245,7 @@ public final class OpenSslEngine extends SSLEngine {
}
@Override
@SuppressWarnings("FinalizeDeclaration")
protected void finalize() throws Throwable {
super.finalize();
// Call shutdown as the user may have created the OpenSslEngine and not used it at all.

View File

@ -237,6 +237,27 @@ public class CipherSuiteConverterTest {
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");
testO2JMapping("SRP_DSS_WITH_AES_256_CBC_SHA", "SRP-DSS-AES-256-CBC-SHA");
testO2JMapping("SRP_RSA_WITH_AES_256_CBC_SHA", "SRP-RSA-AES-256-CBC-SHA");
testO2JMapping("SRP_WITH_AES_256_CBC_SHA", "SRP-AES-256-CBC-SHA");
testO2JMapping("DH_anon_WITH_AES_256_GCM_SHA384", "ADH-AES256-GCM-SHA384");
testO2JMapping("DH_anon_WITH_AES_256_CBC_SHA256", "ADH-AES256-SHA256");
testO2JMapping("DH_anon_WITH_CAMELLIA256_SHA", "ADH-CAMELLIA256-SHA");
testO2JMapping("SRP_DSS_WITH_AES_128_CBC_SHA", "SRP-DSS-AES-128-CBC-SHA");
testO2JMapping("SRP_RSA_WITH_AES_128_CBC_SHA", "SRP-RSA-AES-128-CBC-SHA");
testO2JMapping("SRP_WITH_AES_128_CBC_SHA", "SRP-AES-128-CBC-SHA");
testO2JMapping("DH_anon_WITH_SEED_SHA", "ADH-SEED-SHA");
testO2JMapping("DH_anon_WITH_CAMELLIA128_SHA", "ADH-CAMELLIA128-SHA");
testO2JMapping("RSA_WITH_RC2_CBC_MD5", "RC2-CBC-MD5");
testO2JMapping("SRP_DSS_WITH_3DES_EDE_CBC_SHA", "SRP-DSS-3DES-EDE-CBC-SHA");
testO2JMapping("SRP_RSA_WITH_3DES_EDE_CBC_SHA", "SRP-RSA-3DES-EDE-CBC-SHA");
testO2JMapping("SRP_WITH_3DES_EDE_CBC_SHA", "SRP-3DES-EDE-CBC-SHA");
testO2JMapping("RSA_WITH_3DES_EDE_CBC_MD5", "DES-CBC3-MD5");
testO2JMapping("EDH_RSA_WITH_DES_CBC_SHA", "EDH-RSA-DES-CBC-SHA");
testO2JMapping("EDH_DSS_WITH_DES_CBC_SHA", "EDH-DSS-DES-CBC-SHA");
testO2JMapping("RSA_WITH_DES_CBC_MD5", "DES-CBC-MD5");
testO2JMapping("EDH_RSA_EXPORT_WITH_DES_CBC_40_SHA", "EXP-EDH-RSA-DES-CBC-SHA");
testO2JMapping("EDH_DSS_EXPORT_WITH_DES_CBC_40_SHA", "EXP-EDH-DSS-DES-CBC-SHA");
}
private static void testO2JMapping(String javaCipherSuite, String openSslCipherSuite) {