Https全揭秘系列 - 故障经验总结
Posted 框架那些事儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Https全揭秘系列 - 故障经验总结相关的知识,希望对你有一定的参考价值。
Https全揭秘系列 - 故障经验总结
今天总结下https握手失败的各种可能原因,下面列出的每种情况都是项目中实际遇到的。
1.基础篇
在第一章我将列举出一些比较容易排查的问题。
1.1 TLS版本不一致
我们曾经讲过Https握手协议有多个版本(SSL,TLSV1.0,TLSV1.1,TLSV1.2),那么这么多版本之前有什么区别呢?答案就是加密套件(Cipher Cuite)。
随着科技的发展,许多旧的加密算法被证明是不安全的,那么在新的tls协议中就将移除不安全的加密算法,增加新的安全的加密算法。那么当客户端和服务端使用不同的tls握手协议时就有可能导致客户端和服务端没有通用的加密套件,这时服务端就会主动断开连接。
这种情况排查非常容易,首先我们先模拟出这种场景,使用以前文章中的代码,将服务端tls版本设置为tlsv1.0(修改客户端版本同理),启动两端开始握手,在wireshark抓包中我们可以明显看到客户端在发送了client hello之后服务端没有返回正常的server hello包,而是直接返回失败。
我们查看服务端的日志发现jvm打印出了详细的错误原因,就是下面的信息。
*** ClientHello, TLSv1.2
RandomCookie: GMT: 1513150419 bytes = { 191, 25, 237, 2, 112, 204, 32, 184, 179, 42, 179, 139, 54, 111, 44, 128, 140, 165, 105, 126, 208, 50, 20, 118, 76, 252, 206, 115 }
Session ID: {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA256withDSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA
***
%% Initialized: [Session-1, SSL_NULL_WITH_NULL_NULL]
jetty-16, fatal error: 40: no cipher suites in common
javax.net.ssl.SSLHandshakeException: no cipher suites in common
%% Invalidated: [Session-1, SSL_NULL_WITH_NULL_NULL]
jetty-16, SEND TLSv1.1 ALERT: fatal, description = handshake_failure
jetty-16, WRITE: TLSv1.1 Alert, length = 2
jetty-16, fatal: engine already closed. Rethrowing javax.net.ssl.SSLHandshakeException: no cipher suites in common
jetty-16, called closeOutbound()
jetty-16, closeOutboundInternal()
注意到了no cipher suites in common这行信息了么。
关于tls版本还有一个需要特别注意的点,就是tls版本与jdk版本密切相关,jdk6某些版本只支持tlsv1.0,jdk7支持tlsv1.0,v1.1和v1.2,而jdk8只支持tlsv1.1和1.2。详细指定关系可参照下图。
1.2客户端不信任服务端发送的证书
这种情况其实就是客户端truststore中没有服务端的证书,有可能服务端提供的证书有误或者客户端导入有问题。
我们首先模拟出这种情况,使用正确的keystore和truststore,到jdk的bin目录下执行下面命令删除保存在客户端truststore中的服务端证书,然后客户端证书到客户端的truststore中(这一步是为了保证truststore不为空,truststore为空时在Https握手时会抛出异常:java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty),修改代码中的路径启动两端。
keytool -delete -alias server -keystore D:/https/client.truststore
keytool -importcert -alias client -file D://https/client.cer -keystore D://https/client.truststore
抓包如上图所示,服务端在完成server hello down发送完自己证书后客户端直接返回了报错信息关闭了连接,我们查看客户端的jvm输出信息可以看到下面字段
***
%% Invalidated: [Session-1, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256]
main, SEND TLSv1.2 ALERT: fatal, description = certificate_unknown
main, WRITE: TLSv1.2 Alert, length = 2
main, called closeSocket()
main, handling exception: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
至于发现这种情况后如何解决可以参照上一篇文章,对比包中的证书信息与truststore中是否一致,不一致则修改服务端或者客户端相关配置即可。
1.3服务端不信任客户端证书
这种与上面情况刚好相反,不过原理完全一样不再赘述。
使用正确的keystore和truststore执行下面命令删除保存在服务端和客户端证书,修改代码路径启动两端。
抓包如下,深色的为服务端发送的包,可以看到客户端校验服务端证书通过后发送了自己的证书,不过服务端紧接着直接返回了报错。
查看服务端JVM信息
*** Certificate chain
<Empty>
***
jetty-13, fatal error: 42: null cert chain
javax.net.ssl.SSLHandshakeException: null cert chain
%% Invalidated: [Session-4, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256]
jetty-13, SEND TLSv1.2 ALERT: fatal, description = bad_certificate
jetty-13, WRITE: TLSv1.2 Alert, length = 2
jetty-13, fatal: engine already closed. Rethrowing javax.net.ssl.SSLHandshakeException: null cert chain
jetty-13, called closeOutbound()
jetty-13, closeOutboundInternal()
2.提高篇
本篇中我将分享一个比较难排查,难以想象的jdk bug(写文章前一直以为是bug,最近发现也不算bug,只是一个极其隐晦的逻辑,oracle没有明确指出)
我们以前讲过keystore和truststore有很多种格式,其中jks格式为jdk默认的格式,常用的还有pkcs等等,我们这次使用pkcs12格式重新生成同样的keystore和truststore,生成命令如下:
keytool -genkey -alias server -keystore D://https/server.keystore -storetype PKCS12
keytool -export -alias server -keystore D://https/server.keystore -file D://https/server.cer
keytool -genkey -alias client -keystore D://https/client.keystore -storetype PKCS12
keytool -export -alias client -keystore D://https/client.keystore -file D://https/client.cer
keytool -importcert -alias server -file D://https/server.cer -keystore D://https/client.truststore -storetype PKCS12
keytool -importcert -alias client -file D://https/client.cer -keystore D://https/server.truststore -storetype PKCS12
现在,我们加载刚才生成的keystore和truststore使用JDK1.8启动服务端和客户端,那么一切都正常,握手成功。
现在我们将JDK换成1.7或者1.6再次启动服务端和客户端,我们会发现握手失败了,并且在客户端校验就失败了,也就是说客户端不信任服务端的证书了。
问题出在哪里呢,答案是truststore上,JDK1.6和JDK1.7并不支持PKCS12格式的truststore,因为对于PKCS12格式的truststore,除了提供包含公钥的证书还需要包含私钥才能被加载为可信任证书。
事实上,你不可能要求对端将自己的私钥也发送给自己,因为私钥是绝对保密的。这也就意味着对于JDK1.6和JDK1.7,可以使用PKCS12格式的keystore,但是不能使用PKCS12格式的truststore。
但是在JDK1.8,这个逻辑被修复了,也就是说你可以像使用JKS格式一样使用PKCS12格式的keystore和truststore。
我在我的HttpsExample项目中提供了今天文章所用到的所有keystore和truststore文件,有兴趣的可以下载下来验证一下。
到此为止,Https系列文章暂时告一段落,接下来我将开个Java集合类的坑。
以上是关于Https全揭秘系列 - 故障经验总结的主要内容,如果未能解决你的问题,请参考以下文章