Use JDBC to connect TiDB Cloud through TLS
Tags: jdbc, java, tidb, mysql
Published: 2022-09-05 02:48:16

At the time of writing, [TiDB Cloud](https://pingcap.com/tidb-cloud/) uses the [default configurations](https://docs.pingcap.com/tidb/dev/enable-tls-between-clients-and-servers#supported-tls-versions) to deploy clusters(dedicated tier), supporting TLSv1.1, TLSv1.2 and TLSv1.3. When users use MySQL Connector/J to connect TiDB Cloud, they need to take care of which JDK version and JDBC driver version are in use. In this article, JDK 17 is used for test.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fp1hznx2rk51d6gcpibx.png)

## JDBC 8.0.26
Using the simplest DSN(with only username and password) to connect TiDB Cloud clusters
```Java
jdbc:mysql://<host>:4000/test?user=root&password=<password>
```
throws error like
```Java
Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
        at java.base/sun.security.ssl.HandshakeContext.<init>(HandshakeContext.java:172)
        at java.base/sun.security.ssl.ClientHandshakeContext.<init>(ClientHandshakeContext.java:103)
        at java.base/sun.security.ssl.TransportContext.kickstart(TransportContext.java:240)
        at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:443)
        at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:421)
        at com.mysql.cj.protocol.ExportControlled.performTlsHandshake(ExportControlled.java:320)
        at com.mysql.cj.protocol.StandardSocketFactory.performTlsHandshake(StandardSocketFactory.java:194)
        at com.mysql.cj.protocol.a.NativeSocketConnection.performTlsHandshake(NativeSocketConnection.java:101)
        at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:308)
```
The reason for the error is that JDBC 8.0.26 will by default use TLSv1 and TLSv1.1 against low version MySQL server. Although TiDB Cloud supports TLSv1.1, but [high version JDK doesn't support TLSv1.1 any more](https://aws.amazon.com/cn/blogs/opensource/tls-1-0-1-1-changes-in-openjdk-and-amazon-corretto/). Let's see the critical code path
```Java
private static String[] getAllowedProtocols(PropertySet pset, ServerVersion serverVersion, String[] socketProtocols) {
    String[] tryProtocols = null;

    // 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.
    String enabledTLSProtocols = pset.getStringProperty(PropertyKey.enabledTLSProtocols).getValue();
    if (enabledTLSProtocols != null && enabledTLSProtocols.length() > 0) {
        tryProtocols = enabledTLSProtocols.split("\\s*,\\s*");
    }
    // It is problematic to enable TLSv1.2 on the client side when the server is compiled with yaSSL. When client attempts to connect with
    // TLSv1.2 yaSSL just closes the socket instead of re-attempting handshake with lower TLS version. So here we allow all protocols only
    // for server versions which are known to be compiled with OpenSSL.
    else if (serverVersion == null) {
        // X Protocol doesn't provide server version, but we prefer to use most recent TLS version, though it also means that X Protocol
        // connection to old MySQL 5.7 GPL releases will fail by default, user must use enabledTLSProtocols=TLSv1.1 to connect them.
        tryProtocols = TLS_PROTOCOLS;
    } else if (serverVersion.meetsMinimum(new ServerVersion(5, 7, 28))
            || serverVersion.meetsMinimum(new ServerVersion(5, 6, 46)) && !serverVersion.meetsMinimum(new ServerVersion(5, 7, 0))
            || serverVersion.meetsMinimum(new ServerVersion(5, 6, 0)) && Util.isEnterpriseEdition(serverVersion.toString())) {
        tryProtocols = TLS_PROTOCOLS;
    } else {
        // allow only TLSv1 and TLSv1.1 for other server versions by default
        tryProtocols = new String[] { TLSv1_1, TLSv1 };
    }

    List<String> configuredProtocols = new ArrayList<>(Arrays.asList(tryProtocols));
    List<String> jvmSupportedProtocols = Arrays.asList(socketProtocols);

    List<String> allowedProtocols = new ArrayList<>();
    for (String protocol : TLS_PROTOCOLS) {
        if (jvmSupportedProtocols.contains(protocol) && configuredProtocols.contains(protocol)) {
            allowedProtocols.add(protocol);
        }
    }
    return allowedProtocols.toArray(new String[0]);

}
```
`getAllowedProtocols` calculates the possible TLS protocols used in the handshake phase. The version TiDB Cloud returns is `5.7.25-TiDB-v6.1.0`, so the code finally goes into the `else` branch, protocols being TLSv1 and TLSv1.1(`enabledTLSProtocols` parameter in DSN is also treated here).
```Java
private static List<ProtocolVersion> getActiveProtocols(
        List<ProtocolVersion> enabledProtocols,
        List<CipherSuite> enabledCipherSuites,
        AlgorithmConstraints algorithmConstraints) {
    boolean enabledSSL20Hello = false;
    ArrayList<ProtocolVersion> protocols = new ArrayList<>(4);
    for (ProtocolVersion protocol : enabledProtocols) {
        if (!enabledSSL20Hello && protocol == ProtocolVersion.SSL20Hello) {
            enabledSSL20Hello = true;
            continue;
        }

        if (!algorithmConstraints.permits(
                EnumSet.of(CryptoPrimitive.KEY_AGREEMENT),
                protocol.name, null)) {
            // Ignore disabled protocol.
            continue;
        }
``` 
`getActiveProtocols` calculates the final protocol and cipher. No matter TLSv1 or TLSv1.1, it will be rejected in `algorithmConstraints.permits` and then `getActiveProtocols` returns an empty list.
```Java
@Override
public boolean permits(Set<CryptoPrimitive> primitives,
        String algorithm, AlgorithmParameters parameters) {

    boolean permitted = true;

    if (peerSpecifiedConstraints != null) {
        permitted = peerSpecifiedConstraints.permits(
                                primitives, algorithm, parameters);
    }

    if (permitted && userSpecifiedConstraints != null) {
        permitted = userSpecifiedConstraints.permits(
                                primitives, algorithm, parameters);
    }

    if (permitted) {
        permitted = tlsDisabledAlgConstraints.permits(
                                primitives, algorithm, parameters);
    }

    if (permitted && enabledX509DisabledAlgConstraints) {
        permitted = x509DisabledAlgConstraints.permits(
                                primitives, algorithm, parameters);
    }

    return permitted;
}
```
TLSv1.1 will be rejected at `tlsDisabledAlgConstraints.permits(primitives, algorithm, parameters);`. `tlsDisabledAlgConstraints` is checked against `jdk.tls.disabledAlgorithms` in `java.security` file. For JDK 11, its value is
```
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \
     DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \
     include jdk.disabled.namedCurves
```
## JDBC 8.0.29
Compared with 8.0.26, `getAllowedProtocols` changed in 8.0.29, returns TLSv1.2 and TLSv1.3. So normally you could connect successfully to TiDB Cloud.
```Java
private static String[] getAllowedProtocols(PropertySet pset, @SuppressWarnings("unused") ServerVersion serverVersion, String[] socketProtocols) {
    List<String> tryProtocols = null;

    RuntimeProperty<String> tlsVersions = pset.getStringProperty(PropertyKey.tlsVersions);
    if (tlsVersions != null && tlsVersions.isExplicitlySet()) {
        // If tlsVersions configuration option is set then override the default TLS versions restriction.
        if (tlsVersions.getValue() == null) {
            throw ExceptionFactory.createException(SSLParamsException.class,
                    "Specified list of TLS versions is empty. Accepted values are TLSv1.2 and TLSv1.3.");
        }
        tryProtocols = getValidProtocols(tlsVersions.getValue().split("\\s*,\\s*"));
    } else {
        tryProtocols = new ArrayList<>(Arrays.asList(VALID_TLS_PROTOCOLS));
    }

    List<String> jvmSupportedProtocols = Arrays.asList(socketProtocols);
    List<String> allowedProtocols = new ArrayList<>();
    for (String protocol : tryProtocols) {
        if (jvmSupportedProtocols.contains(protocol)) {
            allowedProtocols.add(protocol);
        }
    }
    return allowedProtocols.toArray(new String[0]);
}
```
## JDBC 5.1.49
```Java
protected static void transformSocketToSSLSocket(MysqlIO mysqlIO) throws SQLException {
    SocketFactory sslFact = new StandardSSLSocketFactory(getSSLSocketFactoryDefaultOrConfigured(mysqlIO), mysqlIO.socketFactory, mysqlIO.mysqlConnection);

    try {
        mysqlIO.mysqlConnection = sslFact.connect(mysqlIO.host, mysqlIO.port, null);

        String[] tryProtocols = null;

        // If enabledTLSProtocols configuration option is set then override 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.
        // Note that it is problematic to enable TLSv1.2 on the client side when the server is compiled with yaSSL. When client attempts to connect with
        // TLSv1.2 yaSSL just closes the socket instead of re-attempting handshake with lower TLS version.
        String enabledTLSProtocols = mysqlIO.connection.getEnabledTLSProtocols();
        if (enabledTLSProtocols != null && enabledTLSProtocols.length() > 0) {
            tryProtocols = enabledTLSProtocols.split("\\s*,\\s*");
        } else if (mysqlIO.versionMeetsMinimum(5, 7, 28) || mysqlIO.versionMeetsMinimum(5, 6, 46) && !mysqlIO.versionMeetsMinimum(5, 7, 0)
                || mysqlIO.versionMeetsMinimum(5, 6, 0) && Util.isEnterpriseEdition(mysqlIO.getServerVersion())) {
            // allow all known TLS versions for this subset of server versions by default
            tryProtocols = TLS_PROTOCOLS;
        } else {
            // allow TLSv1 and TLSv1.1 for all server versions by default
            tryProtocols = new String[] { TLSv1_1, TLSv1 };

        }
```
Although name is different, but 5.1.49 shares the same logic with 8.0.26, goes into the `else` branch and returns only TLSv1/TLSv1.1.
## Conclusion
For failed versions, we could concat `enabledTLSProtocols=TLSv1.2,TLSv1.3` in the DSN to force JDBC driver choose higher version TLS protocols. To connect consistently, you could always add the parameter. But from 8.0.28, `enabledTLSProtocols` is changed to `tlsVersions` and remains an alias. It might be removed in the future.

## UPDATE on 2022/09/19:

TiDB Cloud dedicated clusters recently provide their private CA for users to download and verify. They recommend using `verify_identity` argument in DSN. One thing to note is that the certs of the clusters use SAN, to do identity verification, you need to use at least MySQL Connector/J 8.0.22. [From this version, SAN is supported.](https://dev.mysql.com/doc/relnotes/connector-j/8.0/en/news-8-0-22.html)

## UPDATE on 2024/01/04

TiDB kernel advertise their MySQL version as `8.0.11` from *v7.4.0*. So if you connect to versions later than this, even *JDBC 8.0.26* could connect easily. TiDB Serverless currently use TiDB kernel v7.1.0, but it advertise its MySQL version as `5.7.28` to provide more convenience for old version JDBC drivers.