前言
最近因公司某个服务器应用升级以及互联网安全要求,近期将禁用对TLSv1.0、TLSv1.1的访问支持,要求所有访问该服务的客户端项目升级到TLSv1.2,否则到期TLSv1.2生效后,将影响客户端的正常访问,于是安排工作着手对部份老项目进行升级改造。
TLS协议说明
百度百科的描述
安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。
传输层安全性协议(英语:Transport Layer Security,缩写作TLS),及其前身安全套接层(Secure Sockets Layer,缩写作SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。网景公司(Netscape)在1994年推出首版网页浏览器,网景导航者时,推出HTTPS协议,以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件。随后又公布RFC 5246 (2008年8月)与RFC 6176(2011年3月)。在浏览器、邮箱、即时通信、VoIP、网络传真等应用程序中,广泛支持这个协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连线,发送数据。目前已成为互联网上保密通信的工业标准。
SSL包含记录层(Record Layer)和传输层,记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密钥作为会谈密钥(Session key)。这个会谈密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。
简单来说,TLS是一种应用于互联网通讯的安全协议,可以在客户端与服务端之间提供安全、信任的通讯模式,是基于HTTP封装的应用层协议;
目前主要流行TLS版本有TLSv1.0,TLSv1.1,TLSv1.2,SSLv3,SSLv2Hello,具体通讯协议需要视服务端设定而定,并且不同JDK版本对协议的使用存在差异;
需要注意的是客户端与服务端通讯,需要使用TLS一致相同的版本才能通讯,否则版本不同,服务端将关闭客户端的连接;
官方解释如下:
由于存在各种版本的 TLS(1.0、1.1、1.2 和可能的未来版本)和 SSL,TLS 协议提供了一种内置机制来协商要使用的特定协议版本。当客户端连接到服务器时,它会宣布它可以支持的最高版本,然后服务器会以实际用于连接的协议版本进行响应。如果服务器选择的版本不受客户端支持或不被客户端接受,则客户端终止协商并关闭连接。例如,如果客户端支持 TLS 1.2,但服务器只支持 TLS 1.0,他们将使用 TLS 1.0 进行通信;但是,如果客户端不支持 TLS 1.0,它会立即关闭连接。
在实践中,有些服务器没有正确实现,不支持协议版本协商。例如,仅支持 TLS 1.0 的服务器可能会简单地拒绝客户端对 TLS 1.2 的请求。即使客户端能够支持 TLS 1.0,也不会建立连接。这是一个服务器错误,通常称为“版本不兼容”。
基本通讯过程:
JDK7对TLS版本支持
开始改造前,查询到oracle官方有描述JDK各版本对TLS的支持说明,如下:
参见: https://blogs.oracle.com/java/post/diagnosing-tls-ssl-and-https
下表描述了每个 JDK 版本支持的协议和算法:
JDK 8 (2014 年 3 月至今) |
JDK 7 (2011 年 7 月至今) |
JDK 6 (2006 年至 2013 年公共更新结束 ) |
|
TLS 协议 | TLSv1.2(默认) TLSv1.1 TLSv1 SSLv3 |
TLSv1.2 TLSv1.1 TLSv1(默认) SSLv3 |
TLS v1.1( JDK 6 更新 111 及更高版本) TLSv1(默认) SSLv3 |
JSSE 密码: | JDK 8 中的密码 | JDK 7 中的密码 | JDK 6 中的密码 |
参考: | JDK 8 JSSE | JDK 7 JSSE | JDK 6 JSSE |
Java Cryptography Extension,无限强度(稍后解释) | JDK 8 的 JCE | JDK 7 的 JCE | JDK 6 的 JCE |
JDK1.8默认TLS协议版本为TLSv1.2,但JDK1.7默认TLS协议版本为TLSv1,因此需要更改TLS关键默认选项;
由于因历史原因,大部份项目是基于JDK1.7版本进行升级,由于考虑到历史项目直接升级JDK致1.8存在风险:生产环境项目运行稳定情况下,没经过大量测试与验证,以及引入了很多第三方JAR包,直接升级会带来未知问题;因此不考虑升级JDK1.8;
通过查询各类资源,发现最快捷并且无代码侵入的方式,可以通过JDK启动参数-Dhttps.protocols来指定当前环境的TLS协议版本;配置如下:
// -Djavax.net.debug=ssl:handshake 是用来打印协议日志,可以不用加
-Dhttps.protocols=TLSv1.2 -Djavax.net.debug=ssl:handshake
在JDK启动命令中添加
java -Dhttps.protocols=TLSv1.2 -Djavax.net.debug=ssl:handshake com.youApp.Main
JDK启动参数TLSv1.2不生效
通过在JDK启动参数中加-Dhttps.protocols=TLSv1.2指定TLS协议后,测试验证发现并未生效,还是使用的TLSv1,通过查阅文档与资源,得到信息,如下:
https.protocols :控制 Java 客户端使用的协议版本,这些客户端通过使用 HttpsURLConnection 类或通过 URL.openStream() 操作获得 https 连接。对于旧版本,如果您的 Java 7 客户端想要使用 TLS 1.2 作为其默认值,这可以更新默认值。
示例:-Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2
也就是说 -Dhttps.protocols 主要用在通过使用 HttpsURLConnection 类或通过 URL.openStream() 操作获得 https 连接时,控制 Java 客户端使用的TLS协议版本;
基于上述线索,对使用的HttpUtils工具类进行源码查看,发现使用的是apache-commons-httpclient-3.1版本,提供http和https访问操作;
通过查看httpclient源码,其是通过HttpConnection类创建连接对象,进行https连接,是org.apache.commons.httpclient包中自行基于socket的封装的实现类;而HttpsURLConnection是JDK中javax.net.ssl包自带实现类,其中有对SSL做进一步的封装处理,因此HttpsURLConnection能够通过JDK启动参数-Dhttps.protocols指定TLSv1.2为网络环境通讯协议;(个人粗略见解,如有错误欢迎指正)
httpclient-3.1版本对TLSv1.2支持
项目中的引入apache-commons-httpclient-3.1版本的pom描述如下:
commons-httpclient
commons-httpclient
3.1
在JDK添加启动参数-Dhttps.protocols=TLSv1.2无效后,只能重新进行分析,官方的最新版本httpclient-4.5.x已经有了SSLConnectionSocketFactory类可以直接扩展指定TLS版本,小量改动就能非常的方便的实现;
但还是考虑到历史项目的代码比较年长,直接升级httpclient版本,会造成引用的新、旧方法产生冲突或需要较多工作量改造完成,因此也放弃直接升级httpclient版本到最新的方案;
通过查询网上大量资料,发现httpclient-3.1指定到TLSv1.2的信息非常少,官方也未找到相关资源可参考;
于是经过不断的尝试与摸索,在我的之前写的一篇关于https基于ssl双向证书的博文中,总算找到实现方式;
参考: https://my.oschina.net/u/437309/blog/4414762
通过创建https连接时指定SSL协议默认版本来控制TLSv1.2;
通过简单改造后,运行项目进行验证,加上JDK命令启动参数-Djavax.net.debug=ssl:handshake打印https访问的协议日志,终于成功看到了客户端通过apache-commons-httpclient-3.1版本jar包指定TLSv1.2协议用https模式访问服务端的日志;
整理后示例如下:
package com.task.demo;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
public class HttpsTLSv1_2Test {
public static void main(String[] args) throws Exception {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
javax.net.ssl.SSLSocketFactory factory = context.getSocketFactory();
SSLSocket socket = (SSLSocket) factory.createSocket();
//查看受支持的协议(supported protocols)
String[] protocols = socket.getSupportedProtocols();
System.out.println(Arrays.asList(protocols));
//启用的协议(enabled protocols)
protocols = socket.getEnabledProtocols();
System.out.println(Arrays.asList(protocols));
String url = "https://www.xxxx.com/service/openapi/info.do?id=1234";
String result = get(url, "UTF-8", 3000, 3000);
System.out.println("result:" + result);
}
// 以get方式发送http请求
private static String get(String url, String encoding, int connectionTimeout, int soTimeout) {
GetMethod getMethod = new GetMethod(url);
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, soTimeout);
HttpClient httpClient = new HttpClient();
//指定TLSv1.2协议访问https
try {
TrustManager[] trustAllCerts =new TrustManager[1];
TrustManager tm = new HttpTest.SslManager();
trustAllCerts[0] = tm;
SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, trustAllCerts, null);
SSLContext.setDefault(sc);
}catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
}catch (KeyManagementException e) {
e.printStackTrace();
}
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(connectionTimeout);
try {
httpClient.getParams().setContentCharset(encoding);
httpClient.executeMethod(getMethod);
return getMethod.getResponseBodyAsString() ;
} catch (HttpException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
getMethod.releaseConnection();
}
}
//TrustManager是JSSE 信任管理器的基接口,管理和接受提供的证书,通过JSSE可以很容易地编程实现对HTTPS站点的访问
//X509TrustManager此接口的实例管理使用哪一个 X509 证书来验证远端的安全套接字
public static class SslManager implements TrustManager, X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}
添加JVM启动参数-Djavax.net.debug=ssl:handshake后,打印的https访问的协议日志;
..... 略
Server write key:
0000: 6A 7D C5 54 EA 57 7D F0 3A B2 38 06 F1 7A EE 61 j..
0010: 13 C1 CE CF 4C EE 5E 40 9D EB 9B 38 6E C8 E6 68 ....
... no IV derived for this protocol
main, WRITE: TLSv1.2 Change Cipher Spec, length = 1
*** Finished
verify_data: { 139, 74, 71, 182, 253, 192, 240, 99, ............ }
***
main, WRITE: TLSv1.2 Handshake, length = 96
main, READ: TLSv1.2 Change Cipher Spec, length = 1
main, READ: TLSv1.2 Handshake, length = 96
*** Finished
verify_data: { 87, 184, 46, 252, 141, 177, 200, 223, .......... }
***
%% Cached client session: [Session-1, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384]
main, WRITE: TLSv1.2 Application Data, length = 192
main, READ: TLSv1.2 Application Data, length = 1408
result:Apache Tomcat/7.0.82 - ..... 略
Disconnected from the target VM, address: '127.0.0.1:53442', transport: 'socket'
Process finished with exit code 0
其中日志中明确打印为main主程WRITE(写)和READ(读)都是TLSv1.2,其中写入字符长度为192,读取字符长度为1408;
main, WRITE: TLSv1.2 Application Data, length = 192
main, READ: TLSv1.2 Application Data, length = 1408
参考
https://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html (阮一峰:图解SSL/TLS协议)
https://blogs.oracle.com/java/post/diagnosing-tls-ssl-and-https (oracle官方关于TLS、SSL和HTTPS)
https://chenyongjun.vip/articles/77 (TLS版本不适配问题)