Java SSL握手记录分析

公司项目当中经常使用CXF库连接WebService服务,而且我们自己提供的服务也是基于CXF的SOAP服务,经常需要指导客户怎么连接我们的服务。CXF库整个体系架构比较庞大,相关的类、知识点都比较多。尤其是连接SSL双向认证服务的时候,经常碰到问题。

使用双向SSL认证的时候,在Spring中配置起来非常简单,但是一旦出错就比较难找问题。经过多次尝试,最后发现还是分析SSL握手记录比较靠谱,比较容易找出真正的问题所在。特此记录一下自己的一些心得,以作备忘并希望能够帮到其他人。

1. Spring中的配置

使用CXF连接SOAP WebService,在Spring中配置起来非常简单。

1.1 spring配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...
<context:property-placeholder location="classpath:config.properties" />

<jaxws:client id="devicelinkService"
serviceClass="com.service.api_3_0.ApiCustomerService30"
address="${devicelink.url}">
</jaxws:client>

<http-conf:conduit name="${devicelink.serveraddress}/.*">
<!-- 客户端证书认证相关配置 //-->
<http-conf:tlsClientParameters disableCNCheck="true">
<sec:keyManagers keyPassword="${cert.file.pwd}">
<sec:keyStore type="PKCS12" password="${cert.file.pwd}" resource="${cert.file}"/>
</sec:keyManagers>
<sec:trustManagers>
<sec:keyStore type="JKS" password="changeit" resource="cacerts" />
</sec:trustManagers>
</http-conf:tlsClientParameters>

<http-conf:client Connection="Keep-Alive"
MaxRetransmits="1" AllowChunking="false"/>
<!-- 如果服务支持Basic认证,则可以使用下面的配置 //-->
<!--
<http-conf:authorization>
<sec:UserName>${devicelink.username}</sec:UserName>
<sec:Password>${devicelink.password}</sec:Password>

<sec:AuthorizationType>Basic</sec:AuthorizationType>
</http-conf:authorization>
//-->
</http-conf:conduit>
...

几个配置项:

  • jaxws:client 用来声明一个Spring Bean。可以在代码中使用@Autowired引用;
  • http-conf:conduit 用来配置认证相关的内容;
  • http-conf:tlsClientParameters 配置SSL认证相关的内容。可以配置Java SSL用的keyManager/trustManager;
  • http-conf:authorization 配置Basic认证相关信息,可以指定Basic认证所需的用户名、密码等。

上面的代码中使用了几个变量,他们是通过context:property-placeholder引入的property文件定义的。包括:

  • deviceLink.url 服务访问地址
  • deviceLink.serveraddress 服务器地址
  • cert.file 证书地址
  • cert.file.pwd 证书密码
  • deviceLink.username 访问服务的用户名
  • deviceLink.password 访问服务的密码

1.2 config.properties

这是properties文件中定义的变量,通过context:property-placeholder配置被引入Spring配置中。

1
2
3
4
5
6
devicelink.serveraddress=http://server.address
devicelink.url=http://server.address/api/CustomerApi30
devicelink.username=...
devicelink.password=...
cert.file.pwd=...
cert.file=....p12

使用CXF的相关配置就是这么多,确实比较简单。这是因为CXF隐藏了很多实现的细节。但是这也带来难以排错的弊端。下面将介绍一下如何通过SSL握手日志找到问题所在。

2. 握手过程

通常的握手过程是这样的:

Java SSL也是同样的步骤。粗略分为三步:

  • ClientHello
  • ServerHello
  • KeyExchange
  • 握手结束

3. 打开SSL握手日志

首先说一下如何打开SSL握手日志。方法是在Java启动命令行中增加-Djavax.net.debug=SSL或者-Djavax.net.debug=ALL即可打开SSL握手日志。

4. 日志分析

Java的握手日志中,使用***作为每一段内容的分隔符。使用这个分隔符可以把几步内容大体上分隔开,所以阅读起来还是比较方便的。

4.0. 准备阶段

4.0.1 找到客户端证书

当配置了客户端证书的时候,首先打印的就是找到了key。如下:

1
2
3
4
found key for : did.xwf-id.com key
chain [0] = [
[
...

如果没有配置客户端证书,或者配置有问题,则不会有这一部分日志出现。

4.0.2 添加信任证书

然后就是从系统中添加可信任的CA证书,这个过程会添加很多证书进来。来源主要有两个:

  • $JAVA_HOME/jre/lib/security/cacerts
  • 程序中配置的trustStore指定的证书库
1
2
3
adding as trusted cert:
Subject: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
...

4.1. ClientHello

准备阶段不算是正式的SSL握手过程,只是创建了SSL握手需要的环境(Context)。从ClientHello开始,SSL握手正式开始:

1
2
3
4
5
6
7
*** ClientHello, TLSv1
RandomCookie: ...
Session ID: {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,...]
Compression Methods: { 0 }
Extension elliptic_curves, curve names: {secp256r1, ...}
Extension ec_point_formats, formats: [uncompressed]

4.2. ServerHello

ClientHello消息被发送给服务提供方,然后收到的消息就是服务器返回的ServerHello消息报文。在Java的SSL握手日志中是看不到服务器上的处理流程的,能看到的只是Java处理这个报文的解析过程。这个一定要清楚:日志中显示的都是调用者处理的日志,并不是服务器端的实际处理顺序,所以顺序可能和服务器端不一致,这是正常的。

首先显示的是Server选中的加密算法,这里是TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA

1
2
3
4
5
6
*** ServerHello, TLSv1
RandomCookie: GMT: -1611070835 bytes = { ...}
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
Extension ec_point_formats, formats: [uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2]

收到ServerHello之后,就开始根据收到的内容创建Session,证书验证、交换密钥等操作了。

4.2.1 初始化Session

初始化Session:

1
2
3
***
%% Initialized: [Session-1, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
...

4.2.2 网站证书链

从之前添加的CA证书中找到了证书链,说明服务器证书是合法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*** Certificate chain
chain [0] = [
[
Version: V3
Subject: CN=*.xwf-id.com, OU=IT DEPT, O="Iraid Finance Information & Technology (Shanghai) Co.,Ltd", L=Shanghai, ST=Shanghai, C=CN
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]
chain [1] = [
[
Version: V3
Subject: CN=Symantec Class 3 Secure Server CA - G4, OU=Symantec Trust Network, O=Symantec Corporation, C=US
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]

然后就提示找到了可信任的证书,也就是上面chain [1]中对应的证书:

1
2
3
4
5
6
7
8
***
Found trusted certificate:
[
[
Version: V3
Subject: CN=Symantec Class 3 Secure Server CA - G4, OU=Symantec Trust Network, O=Symantec Corporation, C=US
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...

如果服务提供方的证书是自签名证书,那么要注意日志中是否出现了上述日志。如果没有,则很有可能握手失败。需要将提供方的证书添加到trustManager中对应的证书库中来解决问题。

4.2.3 ECDH ServerKeyExchange

密钥交换算法初始化,这里是ECDH算法的:

1
2
3
4
5
*** ECDH ServerKeyExchange
Server key: Sun EC public key, 256 bits
public x coord: 43374987853469150916971894311198082576230263269301289184949927385170106253395
public y coord: 92135670098354144912439154833828289482869500140098079447653500663810560580845
parameters: secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)

4.2.4 CertificateRequest

ServerHelloDone报文中包含了服务器可以接受的客户端证书的CA列表。客户端需要根据这个CA列表从本地筛选可用的客户端证书。

1
2
3
4
5
*** CertificateRequest
Cert Types: RSA, DSS, ECDSA
Cert Authorities:
<EMAILADDRESS=mailadmin@xwf-id.com, CN=devborgen.xwf-id.com, OU=Operation Department, O=iRaid, L=Shanghai, ST=Shanghai, C=CN>
<EMAILADDRESS=mailadmin@xwf-id.com, CN=devcap.xwf-id.com, OU=Development Department, O=iRaid, L=Shanghai, ST=Shanghai, C=CN>

如果服务器端没有要求客户端证书验证,则没有上述内容。

4.2.5 ServerHelloDone

ServerHello信息解析完毕。

1
2
3
4
5
6
7
8
9
10
11
12
*** ServerHelloDone
[read] MD5 and SHA1 hashes: len = 4
0000: 0E 00 00 00 ....
matching alias: did.xwf-id.com key
*** Certificate chain
chain [0] = [
[
[
Version: V3
Subject: CN=did.xwf-id.com
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
...

上面代表找到了客户端证书。如果服务器端开启了强制要求客户端证书,而本地没有配置,则会出现这样的提示:

1
2
3
4
5
6
*** ServerHelloDone
[read] MD5 and SHA1 hashes: len = 4
0000: 0E 00 00 00 ....
Warning: no suitable certificate found - continuing without client authentication
*** Certificate chain
<Empty>

出现了上述提示,就需要检查keyManager中的配置是否正确,查查为什么找不到客户端证书了。

4.3. ClientKeyExchange

最后一步就是密钥交换:

1
2
3
4
5
6
7
8
9
***
*** ECDHClientKeyExchange
ECDH Public value: { ... }
...
*** CertificateVerify
...
*** Finished
verify_data: { 234, 76, 169, 101, 128, 33, 222, 63, 185, 227, 150, 56 }
...

至此握手结束,生成了session并进行了缓存。

1
2
***
%% Cached client session: [Session-1, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]

通过上面的日志分析,可以有效地发现经常碰到的问题,也容易定位问题究竟出在哪儿。

附录A. 参考资料

附录B. 补充知识-证书链

配置服务端证书链时,有两点需要注意:1)证书是在握手期间发送的,由于 TCP 初始拥塞窗口的存在,如果证书内容太长可能会产生额外的往返开销;2)如果配置的证书没包含中间证书,大部分浏览器可以正常工作,方法是暂停验证并根据站点证书指定的父证书 URL 自己获取中间证书。这个过程会产生额外的 DNS 解析、建立 TCP 连接等开销,非常影响性能。

基于此,配置证书链的的最佳实践是只包含站点证书和中间证书,不要包含根证书,但不要漏掉中间证书。大部分证书都是「站点证书 – 中间证书 – 根证书」这样三级,这时服务端只需要发送前两个证书(站点证书 - 中间证书)即可。但也有的证书有四级,那就需要发送站点证书外加两个中间证书了。

热评文章