DEV Community

Cover image for Use JDBC to connect TiDB Cloud through TLS
Xiang Zhang for TiDB Cloud Ecosystem

Posted on • Edited on

Use JDBC to connect TiDB Cloud through TLS

At the time of writing, TiDB Cloud uses the default configurations 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

JDBC 8.0.26

Using the simplest DSN(with only username and password) to connect TiDB Cloud clusters

jdbc:mysql://<host>:4000/test?user=root&password=<password>
Enter fullscreen mode Exit fullscreen mode

throws error like

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)
Enter fullscreen mode Exit fullscreen mode

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. Let's see the critical code path

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]);

}
Enter fullscreen mode Exit fullscreen mode

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).

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;
        }
Enter fullscreen mode Exit fullscreen mode

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.

@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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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]);
}
Enter fullscreen mode Exit fullscreen mode

JDBC 5.1.49

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 };

        }
Enter fullscreen mode Exit fullscreen mode

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.

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.

Top comments (2)

Collapse
 
sabaping profile image
Yifan Xu

Very helpful!

Collapse
 
eskabore profile image
Jean-Luc KABORE-TURQUIN

Hello Yifan Xu,

Even though you've been part of this platform for quite some time (actually more than myself), it's great to see you engaging in a discussion.
I noticed you're a Senior SDE, and with such extensive experience, I'm sure you have a wealth of knowledge to share.

Dev.to is a fantastic place to share your insights, perspectives, and even challenges you face in your role.
We have a wonderfully supportive community here that appreciates learning from each other's experiences.

Whether it's sharing a code snippet, your thoughts on a new technology, or a step-by-step guide to a problem you've solved, every bit of information you share can make a significant difference to someone else's coding journey.

We would absolutely love to hear more from you, be it in the form of comments or posts.
Remember, every question you ask or piece of wisdom you share, no matter how small, can spark meaningful conversations and learning opportunities.

We're all here to support each other. Looking forward to reading more from you on dev.to!

Best,
Jean-Luc