当前位置: 首页 > 科技观察

一次SSL握手异常,发现JDK发布版本不同

时间:2023-03-13 02:56:44 科技观察

介绍最近我们在多个机房部署了一个服务,调用者反映有问题。调用新加坡机房时正常,调用印度机房时SSL握手异常。查了些时间,积累了一些经验,记录一下。阅读本文后,您将了解以下内容:SSL握手过程中SSL握手异常时的排查思路与同版本工具的JDK有所不同。废话不多说,我们往下看...调用方调用印度机房的服务时,报错信息如下:Thisexceptionisacolleaguehasbeenlooking.经过查找,怀疑是JDK版本的问题。问了调用者,发现调用者版本是1.8.0_91-b14,所以同事打算下载这个版本的JDK进行本地测试。但是这个版本的JDK不好找,于是同事问了我,找了一会也没找到,于是打算从源码编译这个版本的JDK。过了一段时间,我通过源码编译了这个版本的jdk,同事也在网上找到了一个这个版本的JDK,如下:JDK源码:https://github.com/openjdk/jdk8u,tagselectjdk8u91-b14就可以了。在线JDK包:https://github.com/ojdkbuild/ojdkbuild/releases/download/1.8.0.91-3/java-1.8.0-openjdk-1.8.0.91-1.b14.el6.x86_64.zip后的JDK版本1.8.0_91-b14,我和同事测试过。奇怪的是同事在网上找的JDK重现了调用者报错,即新加坡机房正常,而印度机房与SSL握手失败,我却自己编译了。两个机房的JDK都是正常的,但是我们的JDK版本是一样的!好家伙,现在我有2个问题,如下:为什么新加坡机房正常,印度机房SSL握手报错?为什么自己编译同一个版本的JDK没有问题?为什么SSL握手报错?大致来说,SSL握手过程是这样的:客户端向服务器发送一个ClientHello报文,其中不仅包含密钥协商相关的数据,还告知自己支持的密码套件列表。服务端收到ClientHello报文后,会回复客户端一个ServerHello,其中还包含密钥协商数据和服务端选择了哪个密码套件。但是有一种情况,客户端在第一步发送的所有密码组都不被服务器支持,所以服务器会回复一个SSL握手异常包,这会导致客户端失败并报错。注:密码套件是指加密系统混合使用各种密码算法来实现各种安全需求,如TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,使用ECDHE实现密钥协商,RSA实现证书认证,AES实现加密,SHA256实现消息防篡改。如何确认是否是上述原因?我进行了如下测试:添加JVM参数-Djavax.net.debug=SSL,调用正常的新加坡机房,看SSL握手选择了什么密码套件。$bin/java-Djavax.net.debug=SSLSgSendRequest可以看到客户端提供了很多密码套件,服务端选择了TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,所以很有可能印度机房不支持这个密码套件,导致失败印度机房要求。使用curl确认:使用curl指定密码套件DHE-RSA-AES128-GCM-SHA256访问印度机房。$curl-vhttps://in.xxx.be.srv.com--ciphersDHE-RSA-AES128-GCM-SHA256可以发现印度机房不支持这个密码套件。注:jdkciphersuite的名称与curl的名称略有不一致。curl可以在这里找到https://curl.se/docs/ssl-ciphers.html也就是说这个JDK支持的密码套件和印度机房的密码套件是一样的支持的密码套件不重叠,服务器无法选择双方都支持的密码套件。可以进一步确认如下:jdk支持的密码套件可以通过SSLServerSocketFactory.getSupportedCipherSuites()获取。$bin/jrunscript-e"print(java.util.Arrays.toString(javax.net.ssl.SSLServerSocketFactory.getDefault().getSupportedCipherSuites()))"通过nmap扫描可以得到印度机房支持的密码套件,如下:$nmap--scriptssl-enum-ciphers-p443in.xxx.be.srv.com我查看后发现jdk的密码组和印度机房的密码组不重叠,而印度机房只支持一些较新的密码套件,这就是调用印度机房服务时SSL握手失败的原因。同样的方法,我也确认了新加坡机房,发现新加坡机房的密码组和jdk密码组重合,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256也在其中。解决这个问题也比较容易。要么让调用者升级jdk支持新的密码套件,要么请印度机房的SRE调整SSL配置支持旧的密码套件。我们选择了前者。那么,还有一个问题,为什么自己编译的同版本的JDK没有问题呢?为什么自己编译的JDK没有问题?有点疑惑,用上面同样的方法确认自己编译的JDK支持哪些包,如下:).getSupportedCipherSuites()))”可以发现自己编译的JDK支持ECDH系列新的密码套件。为什么?为了找出区别,我使用问题JDK进行调试,如下所示:.getInstance("ECDH");System.out.println(ka);}}在有问题的JDK中,会报如下异常:EcdhTest.main(EcdhTest.java:6)如果出现异常好办,只要顺着异常产生的过程调试,大概调试如下相关方法:sun.security.ssl.JsseJce.getKeyAgreement("ECDH")sun.security.ec.SunEC在调试到SunEC类时,发现加载sunec动态库会报错,如下:于是,我去问题jdk目录下找到这个动态库文件,动态库文件在Linux下一般以.so结尾,如下:grepsunec./jre/lib/ext/sunec.jar./jre/lib/amd64/libsunec.so_DISABLED./jre/lib/amd64/libsunec.diz糊涂了,在这个问题的JDK中,libsunec.so被改名了到libsunec.so_DISABLED,又看了看自己编译的JDK,这个文件没有改名!最后,第二个问题的原因也找到了。原来是在网上找到了JDK,通过改名libsunec.so禁用了EC系列算法。我可能看过JDK下载页面。这个JDK已经构建了很长时间了。是RedHat早期为CentOS6打造的一个JDK8版本,至于为什么要禁用EC系列算法,没有相关的解释,只能到此为止了。总结一下,这个问题在能够稳定复现错误的情况下并不难,但是排查思路和使用的工具值得分享,如下:客户端和服务端支持的密码套件不重叠,会导致SSL握手失败。使用-Djavax.net.debug=SSL调试java的SSL握手过程。通过curl--ciphers指定客户端密码套件访问服务器,可以确认服务器是否支持该密码套件。JDK支持的密码套件可以通过SSLServerSocketFactory.getSupportedCipherSuites()获取。使用nmap--scriptssl-enum-ciphers扫描出服务器支持的密码套件。同一版本的JDK,由不同的发布者发布,也可能存在差异。