前言

最近因公司某个服务器应用升级以及互联网安全要求,近期将禁用对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,也不会建立连接。这是一个服务器错误,通常称为“版本不兼容”。

基本通讯过程:

  • 1.客户端访问服务端先发送支持的TLS版本清单。
  • 2.服务端选择其中一个包含的指定或默认TLS协议版本并在响应中返回所选版本;
  • 3.客户端和服务端使用所选版本继续完成连接设置;

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 JSSEJDK 7 JSSEJDK 6 JSSE
Java Cryptography Extension,无限强度(稍后解释)JDK 8 的 JCEJDK 7 的 JCEJDK 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描述如下:

<dependency>
    <groupId>commons-httpclient</groupId>
    <artifactId>commons-httpclient</artifactId>
    <version>3.1</version>
</dependency>

在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:<html><head><title>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版本不适配问题)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐