diff --git a/src/com/mysql/jdbc/ConnectionProperties.java b/src/com/mysql/jdbc/ConnectionProperties.java index 90cc558..79ae538 100644 --- a/src/com/mysql/jdbc/ConnectionProperties.java +++ b/src/com/mysql/jdbc/ConnectionProperties.java @@ -1426,6 +1426,10 @@ public interface ConnectionProperties { public void setEnabledSSLCipherSuites(String cipherSuites); + public String getEnabledTLSProtocols(); + + public void setEnabledTLSProtocols(String protocols); + public boolean getEnableEscapeProcessing(); public void setEnableEscapeProcessing(boolean flag); diff --git a/src/com/mysql/jdbc/ConnectionPropertiesImpl.java b/src/com/mysql/jdbc/ConnectionPropertiesImpl.java index d3836d1..fa2b667 100644 --- a/src/com/mysql/jdbc/ConnectionPropertiesImpl.java +++ b/src/com/mysql/jdbc/ConnectionPropertiesImpl.java @@ -1334,6 +1334,9 @@ public class ConnectionPropertiesImpl implements Serializable, ConnectionPropert private StringConnectionProperty enabledSSLCipherSuites = new StringConnectionProperty("enabledSSLCipherSuites", null, Messages.getString("ConnectionProperties.enabledSSLCipherSuites"), "5.1.35", SECURITY_CATEGORY, 11); + private StringConnectionProperty enabledTLSProtocols = new StringConnectionProperty("enabledTLSProtocols", null, + Messages.getString("ConnectionProperties.enabledTLSProtocols"), "5.1.44", SECURITY_CATEGORY, 12); + private BooleanConnectionProperty enableEscapeProcessing = new BooleanConnectionProperty("enableEscapeProcessing", true, Messages.getString("ConnectionProperties.enableEscapeProcessing"), "5.1.37", PERFORMANCE_CATEGORY, Integer.MIN_VALUE); @@ -4948,6 +4951,14 @@ public class ConnectionPropertiesImpl implements Serializable, ConnectionPropert this.enabledSSLCipherSuites.setValue(cipherSuites); } + public String getEnabledTLSProtocols() { + return this.enabledTLSProtocols.getValueAsString(); + } + + public void setEnabledTLSProtocols(String protocols) { + this.enabledTLSProtocols.setValue(protocols); + } + public boolean getEnableEscapeProcessing() { return this.enableEscapeProcessing.getValueAsBoolean(); } diff --git a/src/com/mysql/jdbc/ExportControlled.java b/src/com/mysql/jdbc/ExportControlled.java index aa76d06..94516d4 100644 --- a/src/com/mysql/jdbc/ExportControlled.java +++ b/src/com/mysql/jdbc/ExportControlled.java @@ -77,6 +77,7 @@ import com.mysql.jdbc.util.Base64Decoder; public class ExportControlled { private static final String SQL_STATE_BAD_SSL_PARAMS = "08000"; + protected static boolean enabled() { // we may wish to un-static-ify this class this static method call may be removed entirely by the compiler return true; @@ -101,14 +102,39 @@ public class ExportControlled { try { mysqlIO.mysqlConnection = sslFact.connect(mysqlIO.host, mysqlIO.port, null); + String[] driverProtocols = new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" }; + String enabledTLSProtocols = mysqlIO.connection.getEnabledTLSProtocols(); + // allow TLSv1 and TLSv1.1 for all server versions by default + String[] configuredProtocols = new String[] {"TLSv1.1", "TLSv1"}; + // if enabledTLSProtocols configuration option is set, overriding the default + // TLS version restrictions. This allows enabling TLSv1.2 for self-compiled + // MySQL versions supporting it, as well as the ability for users to restrict + // TLS connections to approved protocols (e.g., prohibiting TLSv1) on the client + // side. + if(enabledTLSProtocols != null && enabledTLSProtocols.length() > 0){ + configuredProtocols = enabledTLSProtocols.split("\\s*,\\s*"); + } else { + // Note that it is problematic to enable TLSv1.2 on the client side when + // the server does not support it. This appears to be the original reason + // for this code, which is left as default behavior. When TLSv1.2 is enabled + // client-side, and not supported by the server, a SSLException is thrown + // during SSLSocket.startHandshake(). This closes the SSLSocket, and the + // handshake cannot be re-attempted with a different protocol list without + // establishing a new SSLSocket. + if (mysqlIO.versionMeetsMinimum(5, 6, 0) && Util.isEnterpriseEdition(mysqlIO.getServerVersion())) { + configuredProtocols = new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" }; + } + } + List enabledProtocols = new ArrayList(Arrays.asList(configuredProtocols)); List allowedProtocols = new ArrayList(); + List supportedProtocols = Arrays.asList(((SSLSocket) mysqlIO.mysqlConnection).getSupportedProtocols()); - for (String protocol : (mysqlIO.versionMeetsMinimum(5, 6, 0) && Util.isEnterpriseEdition(mysqlIO.getServerVersion()) - ? new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" } : new String[] { "TLSv1.1", "TLSv1" })) { - if (supportedProtocols.contains(protocol)) { + for (String protocol : driverProtocols) { + if (supportedProtocols.contains(protocol) && enabledProtocols.contains(protocol)) { allowedProtocols.add(protocol); } } + ((SSLSocket) mysqlIO.mysqlConnection).setEnabledProtocols(allowedProtocols.toArray(new String[0])); // check allowed cipher suites diff --git a/src/com/mysql/jdbc/LocalizedErrorMessages.properties b/src/com/mysql/jdbc/LocalizedErrorMessages.properties index 68dc602..b4d4efb 100644 --- a/src/com/mysql/jdbc/LocalizedErrorMessages.properties +++ b/src/com/mysql/jdbc/LocalizedErrorMessages.properties @@ -664,6 +664,7 @@ ConnectionProperties.detectCustomCollations=Should the driver detect custom char ConnectionProperties.dontCheckOnDuplicateKeyUpdateInSQL=Stops checking if every INSERT statement contains the "ON DUPLICATE KEY UPDATE" clause. As a side effect, obtaining the statement's generated keys information will return a list where normally it wouldn't. Also be aware that, in this case, the list of generated keys returned may not be accurate. The effect of this property is canceled if set simultaneously with 'rewriteBatchedStatements=true'. ConnectionProperties.readOnlyPropagatesToServer=Should the driver issue appropriate statements to implicitly set the transaction access mode on server side when Connection.setReadOnly() is called? Setting this property to 'true' enables InnoDB read-only potential optimizations but also requires an extra roundtrip to set the right transaction state. Even if this property is set to 'false', the driver will do its best effort to prevent the execution of database-state-changing queries. Requires minimum of MySQL 5.6. ConnectionProperties.enabledSSLCipherSuites=If "useSSL" is set to "true", overrides the cipher suites enabled for use on the underlying SSL sockets. This may be required when using external JSSE providers or to specify cipher suites compatible with both MySQL server and used JVM. +ConnectionProperties.enabledTLSProtocols=If "useSSL" is set to "true", overrides the TLS protocols enabled for use on the underlying SSL sockets. This may be used to restrict connections to specific TLS versions. ConnectionProperties.enableEscapeProcessing=Sets the default escape processing behavior for Statement objects. The method Statement.setEscapeProcessing() can be used to specify the escape processing behavior for an individual Statement object. Default escape processing behavior in prepared statements must be defined with the property 'processEscapeCodesForPrepStmts'. # diff --git a/src/com/mysql/jdbc/MultiHostMySQLConnection.java b/src/com/mysql/jdbc/MultiHostMySQLConnection.java index 86258da..c2dd87b 100644 --- a/src/com/mysql/jdbc/MultiHostMySQLConnection.java +++ b/src/com/mysql/jdbc/MultiHostMySQLConnection.java @@ -2479,6 +2479,14 @@ public class MultiHostMySQLConnection implements MySQLConnection { getActiveMySQLConnection().setEnabledSSLCipherSuites(cipherSuites); } + public String getEnabledTLSProtocols() { + return getActiveMySQLConnection().getEnabledTLSProtocols(); + } + + public void setEnabledTLSProtocols(String protocols) { + getActiveMySQLConnection().setEnabledTLSProtocols(protocols); + } + public boolean getEnableEscapeProcessing() { return getActiveMySQLConnection().getEnableEscapeProcessing(); } diff --git a/src/com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.java b/src/com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.java index c1a51be..9e5440b 100644 --- a/src/com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.java +++ b/src/com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.java @@ -2882,6 +2882,14 @@ public class ConnectionWrapper extends WrapperBase implements Connection { this.mc.setEnabledSSLCipherSuites(cipherSuites); } + public String getEnabledTLSProtocols() { + return this.mc.getEnabledTLSProtocols(); + } + + public void setEnabledTLSProtocols(String protocols) { + this.mc.setEnabledTLSProtocols(protocols); + } + public boolean getEnableEscapeProcessing() { return this.mc.getEnableEscapeProcessing(); } diff --git a/src/testsuite/regression/ConnectionRegressionTest.java b/src/testsuite/regression/ConnectionRegressionTest.java index 0c80056..1085a62 100644 --- a/src/testsuite/regression/ConnectionRegressionTest.java +++ b/src/testsuite/regression/ConnectionRegressionTest.java @@ -55,6 +55,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -8459,8 +8460,15 @@ public class ConnectionRegressionTest extends BaseTestCase { ResultSet rset = sslConn.createStatement().executeQuery("SHOW STATUS LIKE 'ssl_version'"); assertTrue(rset.next()); String tlsVersion = rset.getString(2); - System.out.println(tlsVersion); + System.out.println("TLS version: " + tlsVersion); System.out.println(); + System.out.println("MySQL version: " + ((MySQLConnection) sslConn).getServerVersion()); + String etp = ((MySQLConnection) sslConn).getEnabledTLSProtocols(); + System.out.println("enabledTLSProtocols: " + etp); + System.out.println(); + System.out.println("JVM version: " + Util.getJVMVersion()); + System.out.println(); + if (((MySQLConnection) sslConn).versionMeetsMinimum(5, 7, 10) && Util.getJVMVersion() > 6) { if (Util.isEnterpriseEdition(((MySQLConnection) sslConn).getServerVersion())) { @@ -8477,6 +8485,79 @@ public class ConnectionRegressionTest extends BaseTestCase { } /** + * Tests fix for Bug#87379. This allows TLS version to be overridden through a new configuration + * option - enabledTLSProtocols. When set to some combination of TLSv1, TLSv1.1, or TLSv1.2 (comma- + * separated, no spaces), the default behavior restricting the TLS version based on JRE and MySQL + * Server version is bypassed to enable or restrict specific TLS versions. + * + * This test requires community server (with yaSSL) in -Dcom.mysql.jdbc.testsuite.url and + * commercial server (with OpenSSL) in -Dcom.mysql.jdbc.testsuite.url.sha256default + * + * Test certificates from testsuite/ssl-test-certs must be installed on both servers. + * + * @throws Exception + * if the test fails. + */ + public void testEnableTLSVersion() throws Exception { + + final String[] testDbUrls; + Properties props = new Properties(); + props.setProperty("allowPublicKeyRetrieval", "true"); + props.setProperty("useSSL", "true"); + props.setProperty("requireSSL", "true"); + props.setProperty("trustCertificateKeyStoreUrl", "file:src/testsuite/ssl-test-certs/ca-truststore"); + props.setProperty("trustCertificateKeyStoreType", "JKS"); + props.setProperty("trustCertificateKeyStorePassword", "password"); + + if (this.sha256Conn != null && ((MySQLConnection) this.sha256Conn).versionMeetsMinimum(5, 5, 7)) { + testDbUrls = new String[] { BaseTestCase.dbUrl, sha256Url }; + } else { + testDbUrls = new String[] { BaseTestCase.dbUrl }; + } + + for (String testDbUrl : testDbUrls) { + System.out.println(testDbUrl); + System.out.println(System.getProperty("java.version")); + Connection sslConn = getConnectionWithProps(testDbUrl, props); + assertTrue(((MySQLConnection) sslConn).getIO().isSSLEstablished()); + List expectedProtocols = new ArrayList(); + expectedProtocols.add("TLSv1"); + if (Util.getJVMVersion() > 6 && ((MySQLConnection) sslConn).versionMeetsMinimum(5, 7, 10)) { + ResultSet rs = sslConn.createStatement().executeQuery("SELECT @@global.tls_version"); + assertTrue(rs.next()); + String supportedTLSVersions = rs.getString(1); + System.out.println("Server reported TLS version support: " + supportedTLSVersions); + expectedProtocols.addAll(Arrays.asList(supportedTLSVersions.split("\\s*,\\s*"))); + } + + String[] testingProtocols = { "TLSv1.2", "TLSv1.1", "TLSv1" }; + for (String protocol : testingProtocols) { + Properties testProps = new Properties(); + testProps.putAll(props); + testProps.put("enabledTLSProtocols", protocol); + System.out.println("Testing " + protocol + " expecting connection: " + expectedProtocols.contains(protocol)); + try { + Connection tlsConn = getConnectionWithProps(testProps); + if(!expectedProtocols.contains(protocol)){ + fail("Expected to fail connection with " + protocol + " due to lack of server support."); + } + ResultSet rset = tlsConn.createStatement().executeQuery("SHOW STATUS LIKE 'ssl_version'"); + assertTrue(rset.next()); + String tlsVersion = rset.getString(2); + assertEquals(protocol,tlsVersion); + tlsConn.close(); + } catch (Exception e) { + if (expectedProtocols.contains(protocol)) { + fail("Expected to be able to connect with " + protocol + " protocol, but failed."); + } + } + + } + + sslConn.close(); + } + } + /** * Tests fix for Bug#21286268 - CONNECTOR/J REPLICATION USE MASTER IF SLAVE IS UNAVAILABLE. */ public void testBug21286268() throws Exception {