提交 e4266aa0 authored 作者: Noel Grandin's avatar Noel Grandin

Merge branch 'fix-anon-ssl' of https://github.com/tomasp8/h2database into tomasp8-fix-anon-ssl

...@@ -21,6 +21,8 @@ Change Log ...@@ -21,6 +21,8 @@ Change Log
<h2>Next Version (unreleased)</h2> <h2>Next Version (unreleased)</h2>
<ul> <ul>
<li>Issue #235: Anonymous SSL connections fail in many situations
</li>
<li>Fix race condition in FILE_LOCK=SOCKET, which could result in the watchdog thread not running <li>Fix race condition in FILE_LOCK=SOCKET, which could result in the watchdog thread not running
</li> </li>
<li>Experimental support for datatype TIMESTAMP WITH TIMEZONE <li>Experimental support for datatype TIMESTAMP WITH TIMEZONE
......
...@@ -17,13 +17,18 @@ import java.net.Socket; ...@@ -17,13 +17,18 @@ import java.net.Socket;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import javax.net.ServerSocketFactory; import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLServerSocketFactory;
...@@ -47,6 +52,18 @@ public class CipherFactory { ...@@ -47,6 +52,18 @@ public class CipherFactory {
*/ */
public static final String KEYSTORE_PASSWORD = public static final String KEYSTORE_PASSWORD =
"h2pass"; "h2pass";
/**
* The security property which can prevent anonymous TLS connections.
* Introduced into Java 6,7,8 in updates from July 2015.
*/
public static final String LEGACY_ALGORITHMS_SECURITY_KEY =
"jdk.tls.legacyAlgorithms";
/**
* The value of {@value #LEGACY_ALGORITHMS_SECURITY_KEY} security
* property at the time of class initialization.
* Null if it is not set.
*/
public static final String DEFAULT_LEGACY_ALGORITHMS = getLegacyAlgoritmsSilently();
private static final String KEYSTORE = private static final String KEYSTORE =
"~/.h2.keystore"; "~/.h2.keystore";
...@@ -55,6 +72,7 @@ public class CipherFactory { ...@@ -55,6 +72,7 @@ public class CipherFactory {
private static final String KEYSTORE_PASSWORD_KEY = private static final String KEYSTORE_PASSWORD_KEY =
"javax.net.ssl.keyStorePassword"; "javax.net.ssl.keyStorePassword";
private CipherFactory() { private CipherFactory() {
// utility class // utility class
} }
...@@ -105,8 +123,13 @@ public class CipherFactory { ...@@ -105,8 +123,13 @@ public class CipherFactory {
} }
/** /**
* Create a secure server socket. If a bind address is specified, the socket * Create a secure server socket. If a bind address is specified, the
* is only bound to this address. * socket is only bound to this address.
* If h2.enableAnonymousTLS is true, an attempt is made to modify
* the security property jdk.tls.legacyAlgorithms (in newer JVMs) to allow
* anonymous TLS. This system change is effectively permanent for the
* lifetime of the JVM.
* @see #removeAnonFromLegacyAlgorithms()
* *
* @param port the port to listen on * @param port the port to listen on
* @param bindAddress the address to bind to, or null to bind to all * @param bindAddress the address to bind to, or null to bind to all
...@@ -116,6 +139,9 @@ public class CipherFactory { ...@@ -116,6 +139,9 @@ public class CipherFactory {
public static ServerSocket createServerSocket(int port, public static ServerSocket createServerSocket(int port,
InetAddress bindAddress) throws IOException { InetAddress bindAddress) throws IOException {
ServerSocket socket = null; ServerSocket socket = null;
if (SysProperties.ENABLE_ANONYMOUS_TLS) {
removeAnonFromLegacyAlgorithms();
}
setKeystore(); setKeystore();
ServerSocketFactory f = SSLServerSocketFactory.getDefault(); ServerSocketFactory f = SSLServerSocketFactory.getDefault();
SSLServerSocket secureSocket; SSLServerSocket secureSocket;
...@@ -137,6 +163,95 @@ public class CipherFactory { ...@@ -137,6 +163,95 @@ public class CipherFactory {
return socket; return socket;
} }
/**
* Removes DH_anon and ECDH_anon from a comma separated list of ciphers.
* Only the first occurrence is removed.
* If there is nothing to remove, returns the reference to the argument.
* @param commaSepList a list of names separated by commas (and spaces)
* @return a new string without DH_anon and ECDH_anon items,
* or the original if none were found
*/
public static String removeDhAnonFromCommaSepList(String commaSepList) {
if (commaSepList == null) {
return commaSepList;
}
List<String> algos = new LinkedList<String>(Arrays.asList(commaSepList.split("\\s*,\\s*")));
boolean dhAnonRemoved = algos.remove("DH_anon");
boolean ecdhAnonRemoved = algos.remove("ECDH_anon");
if (dhAnonRemoved || ecdhAnonRemoved) {
String algosStr = Arrays.toString(algos.toArray(new String[algos.size()]));
return (algos.size() > 0) ? algosStr.substring(1, algosStr.length() - 1): "";
} else {
return commaSepList;
}
}
/**
* Attempts to weaken the security properties to allow anonymous TLS.
* New JREs would not choose an anonymous cipher suite in a TLS handshake
* if server-side security property
* {@value #LEGACY_ALGORITHMS_SECURITY_KEY}
* were not modified from the default value.
* <p>
* NOTE: In current (as of 2016) default implementations of JSSE which use
* this security property, the value is permanently cached inside the
* ServerHandshake class upon its first use.
* Therefore the modification accomplished by this method has to be done
* before the first use of a server SSL socket.
* Later changes to this property will not have any effect on server socket
* behavior.
*/
public static synchronized void removeAnonFromLegacyAlgorithms() {
String legacyAlgosOrig = getLegacyAlgoritmsSilently();
if (legacyAlgosOrig == null) {
return;
}
String legacyAlgosNew = removeDhAnonFromCommaSepList(legacyAlgosOrig);
if (!legacyAlgosOrig.equals(legacyAlgosNew)) {
setLegacyAlgorithmsSilently(legacyAlgosNew);
}
}
/**
* Attempts to resets the security property to the default value.
* The default value of {@value #LEGACY_ALGORITHMS_SECURITY_KEY} was
* obtained at time of class initialization.
* <p>
* NOTE: Resetting the property might not have any effect on server
* socket behavior.
* @see #removeAnonFromLegacyAlgorithms()
*/
public static synchronized void resetDefaultLegacyAlgorithms() {
setLegacyAlgorithmsSilently(DEFAULT_LEGACY_ALGORITHMS);
}
/**
* Returns the security property {@value #LEGACY_ALGORITHMS_SECURITY_KEY}.
* Ignores security exceptions.
* @return the value of the security property, or null if not set
* or not accessible
*/
public static String getLegacyAlgoritmsSilently() {
String defaultLegacyAlgorithms = null;
try {
defaultLegacyAlgorithms = Security.getProperty(LEGACY_ALGORITHMS_SECURITY_KEY);
} catch (SecurityException e) {
// ignore
}
return defaultLegacyAlgorithms;
}
private static void setLegacyAlgorithmsSilently(String legacyAlgos) {
if (legacyAlgos == null) {
return;
}
try {
Security.setProperty(LEGACY_ALGORITHMS_SECURITY_KEY, legacyAlgos);
} catch (SecurityException e) {
// ignore
}
}
private static byte[] getKeyStoreBytes(KeyStore store, String password) private static byte[] getKeyStoreBytes(KeyStore store, String password)
throws IOException { throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream();
...@@ -270,16 +385,16 @@ public class CipherFactory { ...@@ -270,16 +385,16 @@ public class CipherFactory {
} }
private static String[] enableAnonymous(String[] enabled, String[] supported) { private static String[] enableAnonymous(String[] enabled, String[] supported) {
HashSet<String> set = new HashSet<String>(); LinkedHashSet<String> set = new LinkedHashSet<String>();
Collections.addAll(set, enabled);
for (String x : supported) { for (String x : supported) {
if (!x.startsWith("SSL") && if (!x.startsWith("SSL") &&
x.indexOf("_anon_") >= 0 && x.indexOf("_anon_") >= 0 &&
x.indexOf("_AES_") >= 0 && (x.indexOf("_AES_") >= 0 || x.indexOf("_3DES_") >= 0) &&
x.indexOf("_SHA") >= 0) { x.indexOf("_SHA") >= 0) {
set.add(x); set.add(x);
} }
} }
Collections.addAll(set, enabled);
return set.toArray(new String[0]); return set.toArray(new String[0]);
} }
......
...@@ -146,7 +146,12 @@ public class NetUtils { ...@@ -146,7 +146,12 @@ public class NetUtils {
/** /**
* Create a server socket. The system property h2.bindAddress is used if * Create a server socket. The system property h2.bindAddress is used if
* set. * set. If SSL is used and h2.enableAnonymousTLS is true, an attempt is
* made to modify the security property jdk.tls.legacyAlgorithms
* (in newer JVMs) to allow anonymous TLS.
* <p>
* This system change is effectively permanent for the lifetime of the JVM.
* @see CipherFactory#removeAnonFromLegacyAlgorithms()
* *
* @param port the port to listen on * @param port the port to listen on
* @param ssl if SSL should be used * @param ssl if SSL should be used
......
...@@ -11,19 +11,30 @@ import java.net.Socket; ...@@ -11,19 +11,30 @@ import java.net.Socket;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.h2.engine.SysProperties;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import org.h2.util.NetUtils; import org.h2.util.NetUtils;
import org.h2.util.Task; import org.h2.util.Task;
/** /**
* Test the network utilities. * Test the network utilities from {@link NetUtils}.
* *
* @author Sergi Vladykin * @author Sergi Vladykin
* @author Tomas Pospichal
*/ */
public class TestNetUtils extends TestBase { public class TestNetUtils extends TestBase {
private static final int WORKER_COUNT = 10; private static final int WORKER_COUNT = 10;
private static final int PORT = 9111; private static final int PORT = 9111;
private static final int WAIT_MILLIS = 100;
private static final int WAIT_LONGER_MILLIS = 2 * WAIT_MILLIS;
private static final String TASK_PREFIX = "ServerSocketThread-";
/** /**
* Run just this test. * Run just this test.
...@@ -36,10 +47,130 @@ public class TestNetUtils extends TestBase { ...@@ -36,10 +47,130 @@ public class TestNetUtils extends TestBase {
@Override @Override
public void test() throws Exception { public void test() throws Exception {
testAnonymousTlsSession();
testTlsSessionWithServerSideAnonymousDisabled();
testFrequentConnections(true, 100); testFrequentConnections(true, 100);
testFrequentConnections(false, 1000); testFrequentConnections(false, 1000);
} }
/**
* With default settings, H2 client SSL socket should be able to connect
* to an H2 server SSL socket using an anonymous cipher suite
* (no SSL certificate is needed).
* @throws Exception
*/
private void testAnonymousTlsSession() throws Exception {
assertTrue("Failed assumption: the default value of ENABLE_ANONYMOUS_TLS" +
" property should be true", SysProperties.ENABLE_ANONYMOUS_TLS);
boolean ssl = true;
Task task = null;
ServerSocket serverSocket = null;
Socket socket = null;
try {
serverSocket = NetUtils.createServerSocket(PORT, ssl);
serverSocket.setSoTimeout(WAIT_LONGER_MILLIS);
task = createServerSocketTask(serverSocket);
task.execute(TASK_PREFIX + "AnonEnabled");
Thread.sleep(WAIT_MILLIS);
socket = NetUtils.createLoopbackSocket(PORT, ssl);
assertTrue("loopback anon socket should be connected", socket.isConnected());
SSLSession session = ((SSLSocket) socket).getSession();
assertTrue("TLS session should be valid when anonymous TLS is enabled",
session.isValid());
// in case of handshake failure:
// the cipher suite is the pre-handshake SSL_NULL_WITH_NULL_NULL
assertContains(session.getCipherSuite(), "_anon_");
} finally {
closeSilently(socket);
closeSilently(serverSocket);
if (task != null) {
// SSL server socket should succeed using an anonymous cipher
// suite, and not throw javax.net.ssl.SSLHandshakeException
assertNull(task.getException());
task.join();
}
}
}
/**
* TLS connections (without trusted certificates) should fail if the server
* does not allow anonymous TLS.
* The global property ENABLE_ANONYMOUS_TLS cannot be modified for the test;
* instead, the server socket is altered.
* @throws Exception
*/
private void testTlsSessionWithServerSideAnonymousDisabled() throws Exception {
boolean ssl = true;
Task task = null;
ServerSocket serverSocket = null;
Socket socket = null;
try {
serverSocket = NetUtils.createServerSocket(PORT, ssl);
serverSocket.setSoTimeout(WAIT_LONGER_MILLIS);
// emulate the situation ENABLE_ANONYMOUS_TLS=false on server side
String[] defaultCipherSuites = SSLContext.getDefault().getServerSocketFactory()
.getDefaultCipherSuites();
((SSLServerSocket) serverSocket).setEnabledCipherSuites(defaultCipherSuites);
task = createServerSocketTask(serverSocket);
task.execute(TASK_PREFIX + "AnonDisabled");
Thread.sleep(WAIT_MILLIS);
socket = NetUtils.createLoopbackSocket(PORT, ssl);
assertTrue("loopback socket should be connected", socket.isConnected());
// Java 6 API does not have getHandshakeSession() which could
// reveal the actual cipher selected in the attempted handshake
SSLSession session = ((SSLSocket) socket).getSession();
assertFalse("TLS session should be invalid when the server" +
"disables anonymous TLS", session.isValid());
// the SSL handshake should fail, because non-anon ciphers require
// a trusted certificate
assertEquals("SSL_NULL_WITH_NULL_NULL", session.getCipherSuite());
} finally {
closeSilently(socket);
closeSilently(serverSocket);
if (task != null) {
assertTrue(task.getException() != null);
assertEquals(javax.net.ssl.SSLHandshakeException.class.getName(),
task.getException().getClass().getName());
assertContains(task.getException().getMessage(), "certificate_unknown");
task.join();
}
}
}
private Task createServerSocketTask(final ServerSocket serverSocket) {
Task task = new Task() {
@Override
public void call() throws Exception {
Socket ss = null;
try {
ss = serverSocket.accept();
ss.getOutputStream().write(123);
} finally {
closeSilently(ss);
}
}
};
return task;
}
private void closeSilently(Socket socket) {
try {
socket.close();
} catch (Exception e) {
// ignore
}
}
private void closeSilently(ServerSocket socket) {
try {
socket.close();
} catch (Exception e) {
// ignore
}
}
private void testFrequentConnections(boolean ssl, int count) throws Exception { private void testFrequentConnections(boolean ssl, int count) throws Exception {
final ServerSocket serverSocket = NetUtils.createServerSocket(PORT, ssl); final ServerSocket serverSocket = NetUtils.createServerSocket(PORT, ssl);
final AtomicInteger counter = new AtomicInteger(count); final AtomicInteger counter = new AtomicInteger(count);
...@@ -96,7 +227,7 @@ public class TestNetUtils extends TestBase { ...@@ -96,7 +227,7 @@ public class TestNetUtils extends TestBase {
private final AtomicInteger counter; private final AtomicInteger counter;
private Exception exception; private Exception exception;
public ConnectWorker(boolean ssl, AtomicInteger counter) { ConnectWorker(boolean ssl, AtomicInteger counter) {
this.ssl = ssl; this.ssl = ssl;
this.counter = counter; this.counter = counter;
} }
......
...@@ -9,6 +9,7 @@ import java.sql.Connection; ...@@ -9,6 +9,7 @@ import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import org.h2.security.BlockCipher; import org.h2.security.BlockCipher;
import org.h2.security.CipherFactory; import org.h2.security.CipherFactory;
import org.h2.security.SHA256; import org.h2.security.SHA256;
...@@ -35,6 +36,8 @@ public class TestSecurity extends TestBase { ...@@ -35,6 +36,8 @@ public class TestSecurity extends TestBase {
testSHA(); testSHA();
testAES(); testAES();
testBlockCiphers(); testBlockCiphers();
testRemoveAnonFromLegacyAlgos();
//testResetLegacyAlgos();
} }
private static void testConnectWithHash() throws SQLException { private static void testConnectWithHash() throws SQLException {
...@@ -251,4 +254,43 @@ public class TestSecurity extends TestBase { ...@@ -251,4 +254,43 @@ public class TestSecurity extends TestBase {
return len * r < len * 120; return len * r < len * 120;
} }
private void testRemoveAnonFromLegacyAlgos() {
String legacyAlgos = "K_NULL, C_NULL, M_NULL, DHE_DSS_EXPORT" +
", DHE_RSA_EXPORT, DH_anon_EXPORT, DH_DSS_EXPORT, DH_RSA_EXPORT, RSA_EXPORT" +
", DH_anon, ECDH_anon, RC4_128, RC4_40, DES_CBC, DES40_CBC";
String expectedLegacyAlgosWithoutDhAnon = "K_NULL, C_NULL, M_NULL, DHE_DSS_EXPORT" +
", DHE_RSA_EXPORT, DH_anon_EXPORT, DH_DSS_EXPORT, DH_RSA_EXPORT, RSA_EXPORT" +
", RC4_128, RC4_40, DES_CBC, DES40_CBC";
assertEquals(expectedLegacyAlgosWithoutDhAnon,
CipherFactory.removeDhAnonFromCommaSepList(legacyAlgos));
legacyAlgos = "ECDH_anon, DH_anon_EXPORT, DH_anon";
expectedLegacyAlgosWithoutDhAnon = "DH_anon_EXPORT";
assertEquals(expectedLegacyAlgosWithoutDhAnon,
CipherFactory.removeDhAnonFromCommaSepList(legacyAlgos));
legacyAlgos = null;
assertNull(CipherFactory.removeDhAnonFromCommaSepList(legacyAlgos));
}
/**
* This test is meaningful when run in isolation. However, tests of server
* sockets or ssl connections may modify the global state given by the
* jdk.tls.legacyAlgorithms security property (for a good reason).
* It is best to avoid running it in test suites, as it could itself lead
* to a modification of the global state with hard-to-track consequences.
*/
@SuppressWarnings("unused")
private void testResetLegacyAlgos() {
String legacyAlgorithmsBefore = CipherFactory.getLegacyAlgoritmsSilently();
assertEquals("Failed assumption: jdk.tls.legacyAlgorithms" +
" has been modified from its initial setting",
CipherFactory.DEFAULT_LEGACY_ALGORITHMS, legacyAlgorithmsBefore);
CipherFactory.removeAnonFromLegacyAlgorithms();
CipherFactory.resetDefaultLegacyAlgorithms();
String legacyAlgorithmsAfter = CipherFactory.getLegacyAlgoritmsSilently();
assertEquals(CipherFactory.DEFAULT_LEGACY_ALGORITHMS, legacyAlgorithmsAfter);
}
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论