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 827a4cbf36
commit 2056aed50d
5 changed files with 129 additions and 18 deletions

View File

@ -72,8 +72,8 @@ final class CipherSuiteConverter {
"^(?:(" + // BEGIN handshake algorithm "^(?:(" + // BEGIN handshake algorithm
"(?:(?:EXP-)?" + "(?:(?:EXP-)?" +
"(?:" + "(?:" +
"(?:DHE|EDH|ECDH|ECDHE)-(?:DSS|RSA|ECDSA)|" + "(?:DHE|EDH|ECDH|ECDHE|SRP)-(?:DSS|RSA|ECDSA)|" +
"(?:ADH|AECDH|KRB5|PSK)" + "(?:ADH|AECDH|KRB5|PSK|SRP)" +
')' + ')' +
")|" + ")|" +
"EXP" + "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. * 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.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.util.internal.logging.InternalLoggerFactory;
import org.apache.tomcat.jni.Library; import org.apache.tomcat.jni.Library;
import org.apache.tomcat.jni.Pool;
import org.apache.tomcat.jni.SSL; 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 * 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 InternalLogger logger = InternalLoggerFactory.getInstance(OpenSsl.class);
private static final Throwable UNAVAILABILITY_CAUSE; private static final Throwable UNAVAILABILITY_CAUSE;
private static final List<String> AVAILABLE_CIPHER_SUITES;
static { static {
Throwable cause = null; Throwable cause = null;
try { try {
@ -44,6 +52,40 @@ public final class OpenSsl {
OpenSslEngine.class.getSimpleName() + " will be unavailable.", t); OpenSslEngine.class.getSimpleName() + " will be unavailable.", t);
} }
UNAVAILABILITY_CAUSE = cause; 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; 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) { static boolean isError(long errorCode) {
return errorCode != SSL.SSL_ERROR_NONE; 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. */ /* List the ciphers that are permitted to negotiate. */
try { try {
// Convert the cipher list into a colon-separated string. SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(this.ciphers));
StringBuilder cipherBuf = new StringBuilder();
for (String c: this.ciphers) {
cipherBuf.append(c);
cipherBuf.append(':');
}
cipherBuf.setLength(cipherBuf.length() - 1);
SSLContext.setCipherSuite(ctx, cipherBuf.toString());
} catch (SSLException e) { } catch (SSLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {

View File

@ -32,8 +32,8 @@ import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionBindingEvent; import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener; import javax.net.ssl.SSLSessionBindingListener;
import javax.net.ssl.SSLSessionContext; import javax.net.ssl.SSLSessionContext;
import javax.security.cert.X509Certificate;
import javax.security.cert.CertificateException; import javax.security.cert.CertificateException;
import javax.security.cert.X509Certificate;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException; import java.nio.ReadOnlyBufferException;
import java.security.Principal; import java.security.Principal;
@ -47,7 +47,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.regex.Pattern;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
import static javax.net.ssl.SSLEngineResult.Status.*; 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 AtomicIntegerFieldUpdater<OpenSslEngine> DESTROYED_UPDATER;
private static final AtomicReferenceFieldUpdater<OpenSslEngine, SSLSession> SESSION_UPDATER; private static final AtomicReferenceFieldUpdater<OpenSslEngine, SSLSession> SESSION_UPDATER;
private static final Pattern CIPHER_REPLACE_PATTERN = Pattern.compile("-");
// OpenSSL state // OpenSSL state
private long ssl; private long ssl;
private long networkBIO; private long networkBIO;
@ -652,17 +650,39 @@ public final class OpenSslEngine extends SSLEngine {
@Override @Override
public String[] getSupportedCipherSuites() { public String[] getSupportedCipherSuites() {
return EmptyArrays.EMPTY_STRINGS; return OpenSsl.availableCipherSuites();
} }
@Override @Override
public String[] getEnabledCipherSuites() { public String[] getEnabledCipherSuites() {
String[] enabled = SSL.getCiphers(ssl);
if (enabled == null) {
return EmptyArrays.EMPTY_STRINGS; 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 @Override
public void setEnabledCipherSuites(String[] strings) { public void setEnabledCipherSuites(String[] cipherSuites) {
throw new UnsupportedOperationException(); 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 @Override
@ -1225,6 +1245,7 @@ public final class OpenSslEngine extends SSLEngine {
} }
@Override @Override
@SuppressWarnings("FinalizeDeclaration")
protected void finalize() throws Throwable { protected void finalize() throws Throwable {
super.finalize(); super.finalize();
// Call shutdown as the user may have created the OpenSslEngine and not used it at all. // 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_RSA_WITH_CAMELLIA128_SHA", "DHE-RSA-CAMELLIA128-SHA");
testO2JMapping("DHE_DSS_WITH_CAMELLIA128_SHA", "DHE-DSS-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("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) { private static void testO2JMapping(String javaCipherSuite, String openSslCipherSuite) {