Skip to content

关于OpenJDK禁用TLS 1.0与1.1的分析

wxiang edited this page Jul 19, 2021 · 1 revision

作者:江莎

1. SSL/TLS"捡屎"

TLS的前身SSL由Netscape开发。其中SSL 1.0没有正式公开发布,SSL 2.0在1995年被发布,但安全漏洞太多,在1996年被SSL 3.0取代。

TLS 1.0对SSL 3.0进行了升级,作为IETF的正式规范(RFC 2246)在1999年被发布。其后,IETF又在2006年和2008年分别发布了TLS 1.1(RFC 4346)和TLS 1.2(RFC 5246)。这些更新均没有在握手方式上作出实质的修改,主要是修补安全漏洞,并使用更为安全的密码学算法。2018年发布的TLS 1.3(RFC 8446)是目前的最新版本。它对握手方式作出了重大改进,并废除了所有被认为不安全的密码学算法,以提高效率并增强安全性。

值得注意的是,IETF在2011年发布了RFC 6176以废除SSL 2.0;在2015年发布了RFC 7568以废除SSL 3.0;在2020年发布了RFC 8996以废除TLS 1.0和1.1。一切都是为了安全!

2. OpenJDK禁用TLS 1.0和1.1

IETF 2021年3月发布RFC 8996《Deprecating TLS 1.0 and TLS 1.1》正式宣布弃用TLS 1.0和1.1这两个协议。OpenJDK紧跟IETF规范,在2021年4月发布的Update Release中就默认禁用了上述两个协议(详见JDK-8254713)。即,从OpenJDK 8u292和11.0.11开始,应用程序,无论是服务器端,还是客户端,均默认禁止使用TLS 1.0或1.1协议去构建安全连接。

以OpenJDK 11.0.11为例,默认的java.security现在作出了如下设定:

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

可见,TLS 1.0和TLS 1.1已经被"默认"禁用了。

其实,这并不是OpenJDK第一次废除安全连接协议了。早在2015年,OpenJDK就废除了SSL 3.0协议。《Oracle JRE and JDK Cryptographic Roadmap》对已经弃用的安全算法(包括SSL/TLS协议)及其适用范围作了明确的表述,也对未来会废除的安全算法作了规划。

另外,各主流浏览器厂商(Apple,MS,Google和Mozilla)在2018年就已经宣布了各自弃用TLS 1.0和1.1协议的计划。

3. 对业务的影响

如果一个Java业务应用处于持续良性的迭代,更新,维护的过程之中,那么OpenJDK的这一变化对该应用可能没有影响,或者影响极其有限。因为该应用程序所依赖的各种框架,工具库,Web服务器,数据库等等,也会随着时间的推移进行更新,以适应当前的潮流。比如,它们默认不再使用TLS 1.1及其之前的SSL/TLS版本,或者是将偏好的TLS最高版本设置为至少1.2。

但不可否认,生产环境对稳定性的苛刻要求,使得开发时使用的最新依赖并不一定能及时地更新到生产环境中去。

然而无论是什么情况,当将JDK(如KonaJDK 8u252)升级到最新的KonaJDK 8u292/11.0.11时,需要特别关注这一变化可能产生的影响。

当一个基于OpenJDK 8u292/11.0.11的Java客户端试图发起一个TLS 1.0/1.1连接时,该客户端会报如下错误:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

该异常表示客户端指定的TLS协议并不受OpenJDK支持,客户端试图发起TLS握手失败。即,此时还没有发生TLS握手。

当一个基于OpenJDK 8u292/11.0.11的Java客户端试图去连接一个最低仅支持TLS 1.0/1.1的服务器端(无论它使用何种语言实现)时,该客户端会报如下错误:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version

该异常代表一个TLS握手失败,表明服务器不支持客户端所支持的全部协议。

当一个仅支持TLS 1.0/1.1的客户端(无论它使用何种语言实现)试图去连接一个基于OpenJDK 8u292/11.0.11的Java服务器端时,该服务器端会报类似如下的错误:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: Client requested protocol TLSv1.1 is not enabled or supported in server context

很明显,该异常表示该服务器端不支持或未启用客户端请求的协议(此处为TLS 1.1)。

4. 问题诊断

4.1. 可用的SSL/TLS协议

其实OpenJDK仍然支持全部的SSL/TLS协议,但只启用了其中的一部分,其它部分则被禁用。被禁用的部分就是由java.security配置中的参数jdk.tls.disabledAlgorithms(见第2节)设定。

确定使用的JDK支持和启用了哪些协议,可以帮助我们定位问题。下面的程序可以用于展示OpenJDK的TLS服务器端与客户端分别所支持/启用的SSL/TLS协议。

public static void showProtocols() throws Exception {
    try (SSLServerSocket serverSocket = (SSLServerSocket) SSLServerSocketFactory
            .getDefault().createServerSocket()) {

        System.out.println("##### Server supported protocols #####");
        for (String protocol : serverSocket.getSupportedProtocols()) {
            System.out.println(protocol);
        }

        System.out.println("##### Server enabled protocols #####");
        for (String protocol : serverSocket.getEnabledProtocols()) {
            System.out.println(protocol);
        }
    }

    System.out.println();

    try (SSLSocket socket = (SSLSocket) SSLSocketFactory
            .getDefault().createSocket()) {

        System.out.println("##### Client supported protocols #####");
        for (String protocol : socket.getSupportedProtocols()) {
            System.out.println(protocol);
        }

        System.out.println("##### Client enabled protocols #####");
        for (String protocol : socket.getEnabledProtocols()) {
            System.out.println(protocol);
        }
    }
}

针对KonaJDK 8u292执行上述程序,会得到如下输出:

##### Server supported protocols #####
TLSv1.3
TLSv1.2
TLSv1.1
TLSv1
SSLv3
SSLv2Hello
##### Server enabled protocols #####
TLSv1.3
TLSv1.2
SSLv2Hello

##### Client supported protocols #####
TLSv1.3
TLSv1.2
TLSv1.1
TLSv1
SSLv3
SSLv2Hello
##### Client enabled protocols #####
TLSv1.2

可以看到,服务器端与客户端确实依然支持所有的SSL/TLS协议,但服务器端只启用了TLS 1.2和1.3,而客户端只启用了TLS 1.2。需要了解的是,在OpenJDK 11.0.11上执行同样程序的结果会稍有不同,它的客户端默认启用了TLS 1.2和1.3,而不仅仅是TLS 1.2。

4.2. 连接测试

将一个基于KonaJDK 8u292/11.0.11的Java TLS服务器端/客户端与目标TLS客户端/服务器端直接进行连接测试,可能是更为快速的定位问题的方法。

下面两个方法分别是启动TLS服务器与客户端的示例程序。请根据实际的测试环境,修改其中的IP,端口和各端要启用的TLS协议。

public static void server() throws Exception {
    try (SSLServerSocket serverSocket = (SSLServerSocket) SSLServerSocketFactory
            .getDefault().createServerSocket(8443)) {
        serverSocket.setEnabledProtocols(new String[]{"TLSv1.3"});
        System.out.println("Listening " + serverSocket.getLocalPort());
        try (SSLSocket socket = (SSLSocket) serverSocket.accept()) {
            try(InputStream in = socket.getInputStream()) {
                System.out.println((char) in.read());
            }

            try (OutputStream out = socket.getOutputStream()) {
                out.write('S');
            }
        }
    }
}
public static void client() throws Exception {
    SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault()
            .createSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1.2" });
    socket.connect(new InetSocketAddress("localhost", 8443));

    try (OutputStream out = socket.getOutputStream()) {
        out.write('C');
    }

    try(InputStream in = socket.getInputStream()) {
        System.out.println((char) in.read());
    }
}

在上面的测试用例中,client就会收到server发送的fatal alert: protocol_version。因为client希望使用TLS 1.2进行握手,且它也只支持TLS 1.2;而server不支持TLS 1.2,仅支持TLS 1.3。

必须注意的是,上述测试程序均没有使用合适的keystore和trustore,所以它们在与实际的TLS服务器端/客户端进行交互时,可能最终仍然会握手失败,但这与本文所关注的问题无关。

4.3 开启debug日志

其实第3节中所列异常的根因可能会有多种,短时间内也许无法快速地定位问题。此时可以开启TLS的debug日志,以暴露更多的细节。

为了启用该日志,可以在程序的最开始处加上如下语句,

System.setProperty("javax.net.debug", "all");

或者在java命令中加上如下系统属性,

-Djavax.net.debug=all

当一个仅支持TLS 1.1客户端去连接一个不支持TLS 1.1的服务器端时,该服务器端可能会输出类似下面的debug日志,

javax.net.ssl|FINE|01|main|2021-07-14 24:05:05.360 CST|ClientHello.java:689|Consuming ClientHello handshake message (
"ClientHello": {
 "client version" : "TLSv1.1",
...
}
...
javax.net.ssl|SEVERE|01|main|2021-07-14 24:19:29.958 CST|TransportContext.java:316|Fatal (PROTOCOL_VERSION): Client requested protocol TLSv1.1 is not enabled or supported in server context (
...
javax.net.ssl|FINE|01|main|2021-07-14 24:19:29.959 CST|SSLSocketOutputRecord.java:71|WRITE: TLS12 alert(protocol_version), length = 2
...

它表示服务器端收到了客户端的握手消息ClientHello,该消息要求使用TLS 1.1进行通信。但服务器端并不支持这个协议,然后它向客户端发出了致命警告protocol_version以中断当前握手。

5. 案例

一个使用MySQL JDBC驱动(msyql-connector-java)的Java客户端应用程序在连接MySQL服务器时,如果连接URL中没有使用参数useSSL(jdbc:mysql://:/),或者使用了useSSL=true(jdbc:mysql://:/?useSSL=true),那么该客户端就(可能)会发起TLS连接(参见Connector/J的文档)。

对这样的业务应用升级JDK到KonaJDK 8u292/11.0.11时,就需要注意TLS 1.0和1.1被禁用所带来的影响。因为特定版本的MySQL JDBC驱动针对特定版本的MySQL数据库会试图发起TLS 1.1的握手消息。但由于KonaJDK 8u292/11.0.11已经不支持该协议,就会导致客户端抛出异常:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

如果这样的JDBC应用程序并不想启用TLS连接,那么应该总是显示地设置useSSL=false;如果确实要启用TLS连接,可以在JDBC URL中加上参数enabledTLSProtocols=TLSv1.2。

冰雪聪明如你,肯定想到了另一个解决方案:修改JDK中自带的或是通过系统属性java.security.properties提供另一个java.security文件,将TLS 1.1从jdk.tls.disabledAlgorithms参数中去除,这样就可以让这个协议复活了。但,这是非常不被推荐的做法,还是将它封在"瓶子"里吧!