公司项目当中经常使用CXF库连接WebService服务,而且我们自己提供的服务也是基于CXF的SOAP服务,经常需要指导客户怎么连接我们的服务。CXF库整个体系架构比较庞大,相关的类、知识点都比较多。尤其是连接SSL双向认证服务的时候,经常碰到问题。
使用双向SSL认证的时候,在Spring中配置起来非常简单,但是一旦出错就比较难找问题。经过多次尝试,最后发现还是分析SSL握手记录比较靠谱,比较容易找出真正的问题所在。特此记录一下自己的一些心得,以作备忘并希望能够帮到其他人。
1. Spring中的配置
使用CXF连接SOAP WebService,在Spring中配置起来非常简单。
1.1 spring配置
1...
2<context:property-placeholder location="classpath:config.properties" />
3
4<jaxws:client id="devicelinkService"
5 serviceClass="com.service.api_3_0.ApiCustomerService30"
6 address="${devicelink.url}">
7</jaxws:client>
8
9<http-conf:conduit name="${devicelink.serveraddress}/.*">
10 <!-- 客户端证书认证相关配置 //-->
11 <http-conf:tlsClientParameters disableCNCheck="true">
12 <sec:keyManagers keyPassword="${cert.file.pwd}">
13 <sec:keyStore type="PKCS12" password="${cert.file.pwd}" resource="${cert.file}"/>
14 </sec:keyManagers>
15 <sec:trustManagers>
16 <sec:keyStore type="JKS" password="changeit" resource="cacerts" />
17 </sec:trustManagers>
18 </http-conf:tlsClientParameters>
19
20 <http-conf:client Connection="Keep-Alive"
21 MaxRetransmits="1" AllowChunking="false"/>
22 <!-- 如果服务支持Basic认证,则可以使用下面的配置 //-->
23 <!--
24 <http-conf:authorization>
25 <sec:UserName>${devicelink.username}</sec:UserName>
26 <sec:Password>${devicelink.password}</sec:Password>
27
28 <sec:AuthorizationType>Basic</sec:AuthorizationType>
29 </http-conf:authorization>
30 //-->
31</http-conf:conduit>
32...
几个配置项:
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配置中。
1devicelink.serveraddress=http://server.address
2devicelink.url=http://server.address/api/CustomerApi30
3devicelink.username=...
4devicelink.password=...
5cert.file.pwd=...
6cert.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。如下:
1found key for : did.xwf-id.com key
2chain [0] = [
3[
4 ...
如果没有配置客户端证书,或者配置有问题,则不会有这一部分日志出现。
4.0.2 添加信任证书
然后就是从系统中添加可信任的CA证书,这个过程会添加很多证书进来。来源主要有两个:
- $JAVA_HOME/jre/lib/security/cacerts
- 程序中配置的trustStore指定的证书库
1adding as trusted cert:
2 Subject: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
3...
4.1. ClientHello
准备阶段不算是正式的SSL握手过程,只是创建了SSL握手需要的环境(Context)。从ClientHello开始,SSL握手正式开始:
1*** ClientHello, TLSv1
2RandomCookie: ...
3Session ID: {}
4Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,...]
5Compression Methods: { 0 }
6Extension elliptic_curves, curve names: {secp256r1, ...}
7Extension ec_point_formats, formats: [uncompressed]
4.2. ServerHello
ClientHello消息被发送给服务提供方,然后收到的消息就是服务器返回的ServerHello消息报文。在Java的SSL握手日志中是看不到服务器上的处理流程的,能看到的只是Java处理这个报文的解析过程。这个一定要清楚:日志中显示的都是调用者处理的日志,并不是服务器端的实际处理顺序,所以顺序可能和服务器端不一致,这是正常的。
首先显示的是Server选中的加密算法,这里是TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
:
1*** ServerHello, TLSv1
2RandomCookie: GMT: -1611070835 bytes = { ...}
3Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
4Compression Method: 0
5Extension renegotiation_info, renegotiated_connection: <empty>
6Extension ec_point_formats, formats: [uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2]
收到ServerHello之后,就开始根据收到的内容创建Session,证书验证、交换密钥等操作了。
4.2.1 初始化Session
初始化Session:
1***
2%% Initialized: [Session-1, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
3...
4.2.2 网站证书链
从之前添加的CA证书中找到了证书链,说明服务器证书是合法的:
1*** Certificate chain
2chain [0] = [
3[
4 Version: V3
5 Subject: CN=*.xwf-id.com, OU=IT DEPT, O="Iraid Finance Information & Technology (Shanghai) Co.,Ltd", L=Shanghai, ST=Shanghai, C=CN
6 Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
7...
8]
9chain [1] = [
10[
11 Version: V3
12 Subject: CN=Symantec Class 3 Secure Server CA - G4, OU=Symantec Trust Network, O=Symantec Corporation, C=US
13 Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
14 ...
15]
然后就提示找到了可信任的证书,也就是上面chain [1]中对应的证书:
1***
2Found trusted certificate:
3[
4[
5 Version: V3
6 Subject: CN=Symantec Class 3 Secure Server CA - G4, OU=Symantec Trust Network, O=Symantec Corporation, C=US
7 Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
8...
如果服务提供方的证书是自签名证书,那么要注意日志中是否出现了上述日志。如果没有,则很有可能握手失败。需要将提供方的证书添加到trustManager中对应的证书库中来解决问题。
4.2.3 ECDH ServerKeyExchange
密钥交换算法初始化,这里是ECDH算法的:
1*** ECDH ServerKeyExchange
2Server key: Sun EC public key, 256 bits
3 public x coord: 43374987853469150916971894311198082576230263269301289184949927385170106253395
4 public y coord: 92135670098354144912439154833828289482869500140098079447653500663810560580845
5 parameters: secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)
4.2.4 CertificateRequest
ServerHelloDone报文中包含了服务器可以接受的客户端证书的CA列表。客户端需要根据这个CA列表从本地筛选可用的客户端证书。
1*** CertificateRequest
2Cert Types: RSA, DSS, ECDSA
3Cert Authorities:
4<EMAILADDRESS=mailadmin@xwf-id.com, CN=devborgen.xwf-id.com, OU=Operation Department, O=iRaid, L=Shanghai, ST=Shanghai, C=CN>
5<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*** ServerHelloDone
2[read] MD5 and SHA1 hashes: len = 4
30000: 0E 00 00 00 ....
4matching alias: did.xwf-id.com key
5*** Certificate chain
6chain [0] = [
7[
8[
9 Version: V3
10 Subject: CN=did.xwf-id.com
11 Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
12 ...
上面代表找到了客户端证书。如果服务器端开启了强制要求客户端证书,而本地没有配置,则会出现这样的提示:
1*** ServerHelloDone
2[read] MD5 and SHA1 hashes: len = 4
30000: 0E 00 00 00 ....
4Warning: no suitable certificate found - continuing without client authentication
5*** Certificate chain
6<Empty>
出现了上述提示,就需要检查keyManager中的配置是否正确,查查为什么找不到客户端证书了。
4.3. ClientKeyExchange
最后一步就是密钥交换:
1***
2*** ECDHClientKeyExchange
3ECDH Public value: { ... }
4...
5*** CertificateVerify
6...
7*** Finished
8verify_data: { 234, 76, 169, 101, 128, 33, 222, 63, 185, 227, 150, 56 }
9...
至此握手结束,生成了session并进行了缓存。
1***
2%% Cached client session: [Session-1, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA]
通过上面的日志分析,可以有效地发现经常碰到的问题,也容易定位问题究竟出在哪儿。
附录A. 参考资料
附录B. 补充知识-证书链
配置服务端证书链时,有两点需要注意:1)证书是在握手期间发送的,由于 TCP 初始拥塞窗口的存在,如果证书内容太长可能会产生额外的往返开销;2)如果配置的证书没包含中间证书,大部分浏览器可以正常工作,方法是暂停验证并根据站点证书指定的父证书 URL 自己获取中间证书。这个过程会产生额外的 DNS 解析、建立 TCP 连接等开销,非常影响性能。
基于此,配置证书链的的最佳实践是只包含站点证书和中间证书,不要包含根证书,但不要漏掉中间证书。大部分证书都是「站点证书 – 中间证书 – 根证书」这样三级,这时服务端只需要发送前两个证书(站点证书 - 中间证书)即可。但也有的证书有四级,那就需要发送站点证书外加两个中间证书了。