2019独角兽企业重金招聘Python工程师标准>>>
由于公司系统与其他系统进行通讯,时不时报出“peer not authenticated”这个错误,于是对这个错误进行分析。
一、场景重现
- 一台tomcat,自制证书,配置https方式访问,怎么用jdk生成密钥,怎么配置,网上很多文章,tomcat的server.xml配置如下(不需要放项目,空项目启动即可)
-
- 浏览器可以直接访问
- java客户端用httpclient访问(httpURLConnection一样的,底层都是socket)
- 客户端用jdk6访问,报错:peer not authenticated
- 客户端将jdk改为jdk8,可以成功请求。
二、先说解决方案
针对服务端用tomcat,客户端用jdk6造成这个报错的情况,先说下解决方案,主要有以下几个,1和2为客户端修改,3和4为服务端修改
- 客户端将jdk修改为1.8
- 客户端修改jdk源码,将sun.security.ssl.ProtocolVersion(jdk1.6)的DEFAULT_HELLO设置为“TLSv1”,并替换原有jdk中该类(1.8在sun.security.ssl包中),jdk8默认就是使用TSLv1
- 服务端改用nginx做代理,在nginx上配置https(亲测可行)
- 服务端tomcat的service.xml的connector中配置sslEnabledProtocols="TLSv1,TLSv1.1,TLSv1.2,SSLv2Hello",即增加SSLv2Hello的支持
(1.6的ProtocolVersion类,设置FIPS为true也可)
三、问题分析流程
1. 网上找资料,发现很多说要自己配置一个X509TrustManager跳过证书验证,实际上这一点已经做了,然而没有解决问题。
2. 分析https的socket工作原理流程图如下
(https通讯流程)
3. 经过堆栈查看,发现证书不存在,也就是说重写X509TrustManager是绕过证书验证,但问题是连服务器的证书都未获取到,如何绕过验证呢?所以网上这些文章未解决这个问题。(图1为jdk6时的堆栈信息,certs为null,图2为jdk8时候的堆栈信息,获取到了certs)
(jkd6 httpclient未获取到服务器证书)
(jdk8 httpclient获取到了服务器证书)
4. ok,那既然jdk6不行,jdk8可以,那么这两者发的消息有什么不一样呢?客户端请求代码中加上这一段:
System.setProperty("javax.net.debug","ssl");
将socket通讯记录打印出来,发现在client发送Hello请求的时候,
jdk6是这样的:
main, WRITE: TLSv1 Handshake, length = 81
main, WRITE: SSLv2 client hello message, length = 110
main, READ: TLSv1 Alert, length = 2
main, RECV TLSv1 ALERT: fatal, handshake_failure
(jdk6打印socket日志)
jdk8是这样的:
main, WRITE: TLSv1.2 Handshake, length = 235
main, READ: TLSv1 Handshake, length = 686;
(jdk8打印socket日志)
jdk6用的SSLv2,jdk8用的TLSv1
5. 那么服务端的tomcat是如何处理的呢?(服务端用的tomcat6+jdk6,经测试,tomcat8+jdk8是一样的情况)
jdk1.6发送过来的SSLV2Hello请求,被handleUnknowRecord这个方法处理,其中有个判断
if(this.helloVersion != ProtocolVersion.SSL20Hello){
throw new SSLHandshakeException("SSLv2Hello is disabled").
}
从这里抛出了异常,所以没有返回证书,握手失败。但是jdk1.8则走的readV3Record方法,正常执行,就不深入看代码了。
(jdk6请求时,服务端走handleUnknowRecord,可以看到,this.helloVersion是TLSv1,而不是SSLv2Hello,抛出异常,服务端不返回证书)
6. 那么就有疑问了,客户端发出的是SSLV2的握手请求,但是服务端说“你的握手请求,不是SSLV2的请求,所以不允许通过”,!!!!!???!?!?!??!?!?这不是自己打脸么。。
好吧,那么继续看服务端源代码解答疑问。
7. tomcat接收socket处理流程
1)tomcat接收socket,tomcat是采用2个线程在处理的,一个是接收线程,一个是任务处理线程,接收线程接收请求后进行初始化,然后交给任务线程去处理(两者处理的socket对象id相同),所以我们看到当有socket请求的时候,tomcat这边的堆栈是这样一个情况:
2)接收线程接收到后,要进行初始化,在com.sun.net.ssl.internal.ssl.InputRecord的setHelloVersion方法打个断点,跟进堆栈。接收者通过阻塞队列,接收到socket请求
3)进入serverSocketFactory.acceptSocket(serverSocket)方法,acceptSocket接收到socket,并执行doneConnect这个方法进行一些参数设置。
4)那么我们看下localSSLSocketImpl.doneConnect();这段代码中,localSSLSocketImpl的握手协议是什么?会发现,刚进入时候,握手协议是SSLv2Hello
5)一层层跟进,发现有一处对这个对象的helloVersion做了改动,代码如下:
6)那么这个localProtocolVersion是哪里来的呢?一层层往外找,发现最外层传进来的,为this.enabledProtocols,并且看到this.enabledProtocols里面的值即为TLSv1
8.修改下tomcat server.xml配置,增加sslEnabledProtocols="TLSv1,TLSv1.1,TLSv1.2,SSLv2Hello",明确使其支持SSLv2Hello协议,再调试一遍,发现this.enabledProtocols的helloVersion变为了SSLv2Hello
结论
通过分析发现
1、jdk6如果FIPS是false(干啥用的我也不清楚,其他时间再学习),默认用SSLv2Hello协议发送hello请求,而jdk8则默认用TLSv1
2、如果服务器是tomcat,tomcat如果未明确配置sslEnabledProtocols支持"SSLv2Hello",则默认为TLSV1,不支持SSLv2Hello
3、tomcat接收到客户端socket的hello请求后,强制将协议改为TLSV1
4、根据socket的握手报文数组中第一个元素的内容是否为20或22,jdk6发出来的请求将会调用handleUnknowRecord方法(jdk8发出来的走readV3Record方法,具体为何没必要继续研究)
5、handleUnknowRecord方法中会判断,如果协议非SSLv2Hello,则不允许通过,不会给客户端返回证书,服务端报错“SSLV2Hello is disabled”,客户端报错“peer not authenticated”
其他说明
1、tomcat源码运行,只需自己新建maven项目,并将下载的源码中conf和java包分别放入新项目的代码和conf中即可,并且需要配置conf的输出目录,tomcat有读文件的控制
2、推荐不要用tomcat配置证书,用nginx配置证书比较好
3、TLS可以理解为SSL的升级版本,更安全,SSL有漏洞,所以tomcat开启也不要开启对SSL的支持,而是只开启SSLv2Hello的支持