安全协议
HTTPS协议实际上是基于SSL/TLS的HTTP协议,位于应用层,简单的说,HTTPS=HTTP+SSL/TLS。SSL/TLS协议本身是带有加密信息的传输层协议,数字证书正是为这种协议提供相关的加密/解密信息。
1、安全协议概述
HTTPS协议和SSL/TLS协议分属TCP/IP参考模型中的应用层和传输层。简单地说,HTTPS就是附加了SSL/TLS协议的HTTP协议,就是HTTP安全版。HTTPS协议为数字证书提供了最佳的应用环境。
1.1、HTTPS协议
HTTPS (全称:Hypertext Transfer Protocol Secure),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面。
优点:
- 使用 HTTPS 协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
- HTTPS 协议是由 SSL+HTTP构建的可进行加密传输、身份认证的网络协议,要比 HTTP安全,可防止数据在传输过程中被窃取、改变,确保数据的完整性 。
3.HTTPS 是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
缺点:
3. 相同网络环境下,HTTPS 协议会使页面的加载时间延长近 50%,增加 10%到 20%的耗电。此外,HTTPS 协议还会影响缓存,增加数据开销和功耗。
4. HTTPS 协议的安全是有范围的,在黑客攻击、拒绝服务攻击和服务器劫持等方面几乎起不到什么作用。
5. 最关键的是,SSL 证书的信用链体系并不安全。特别是在某些国家可以控制 CA 根证书的情况下,中间人攻击一样可行。
6. 成本增加。部署 HTTPS 后,因为 HTTPS 协议的工作要增加额外的计算资源消耗,例如 SSL 协议加密算法和 SSL 交互次数将占用一定的计算资源和服务器成本。在大规模用户访问应用的场景下,服务器需要频繁地做加密和解密操作,几乎每一个字节都需要做加解密,这就产生了服务器成本。随着云计算技术的发展,数据中心部署的服务器使用成本在规模增加后逐步下降,相对于用户访问的安全提升,其投入成本已经下降到可接受程度。
1.2、SSL/TLS协议
SSL/TLS协议包含两个协议:SSL和TLS
- SSL(Secure Soacket Layer,安全套接字):由Netscape公司研发,位于TCP/IP参考模型中的网络传输层,作为为网络通信安全及数据完整性的一种安全协议。
- TLS(Transport Layer Security,传输层安全):基于SSL协议之上的通用化协议,它同样位于TCP/IP参考模型中的网络传输层,作为SSL协议的继任者,成为下一代网络安全性和数据完整性安全协议。
目前,SSL共有3个版本:SSL1.0、SSL2.0和SSL3.0。SSL3.0规范在1996年3月正式发布,较之前两个版本提供了更多的算法支持和安全特性。l999年,IETF(The Internet Engineering Task Force,互联网工程任务组)(详见http:/www.ietf.org/)在基于SSL3.0协议的基础上发布了
TLS1.0,TLS1.0与SSL3.0几乎是兼容的。因此,通常意义上我们提到的SSL/TLS协议指的是SSL3.0或TLS1.0的网络传输层安全协议。
SSL/TLS协议可分为两层:记录协议和握手协议。
- 记录协议(Record Protocol):建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
- 握手协议(Handshake Protocol):建立在SSL记录协议之上,用于在实际的数据传输开始前,通信双方进行身份认证、协商加密算法、交换加密密钥等。
握手协议较为底层,不好理解,好在目前主流计算机语言(如Java语言)的开发者已经将这些协议的处理封装得很透明了,无需我们关心。
SSL/TLS协议涉及多种加密算法,包含消息摘要算法、对称加密算法、非对称加密算法,以及数字签名算法。
- 消息摘要算法:MD5和SHA1。
- 对称加密算法:RC2、RC4、IDEA、DES、Triple DES和AES。
- 非对称加密算法:RSA和Diffie-Hellman(DH)。
- 数字签名算法:RSA和DSA。
SSL/TLS协议利用密码学算法在互联网上提供端点身份认证和通信保密,完全基于PKI,有较高的安全性。因此,SSL/TLS协议成了网络上最常用的网络传输层安全保密通信协议,众多电子邮件、网银、网上传真都通过SSL/TLS协议来保障数据传输安全。随着卫星和无线网络的发展,WAP安全逐渐得到重视。受限于手机及手持设备的处理和存储能力,WAP论坛(http:/www.wapforum.org/))在TLS的基础上进行了简化,制定了WTLS(Wireless Transport
Layer Security,无线传输层安全)协议。
2、模型分析
经过SSL/TLS握手协议交互后,数据交互双方确定了本次会话使用的对称加密算法以及密钥,由此开始基于对称加密算法的加密数据交互。
握手协议较为复杂,我们将分析主要环节,阐述服务器端和客户端构建加密交互的相关流程。
2.1、协商算法
服务器端和客户端在进行握手协议第一阶段时主要是商榷加密算法,主要包含以下几个步骤:
- 客户端产生随机数RNC(Random Number Client),这个随机数将为后续构建密钥做准备。
- 客户端将自身支持的SSL信息(版本和种类)、算法信息和随机数RNC发送到服务器端。
- 服务器端得到客户端请求后,产生相应的随机数RNS(Random Number Server),这个随机数为后续构建密钥做准备。
- 服务器端将自身支持的SSL信息(版本和种类)、算法信息、随机数RNS和其他信息回应到客户端。其他信息包括服务器证书,甚至包含获取客户端证书的请求。
这时,服务器端和客户端已经确认两方交互时所使用的加密算法。
2.2、验证证书
客户端验证服务器证书:
服务器端验证客户证书:
如果服务器端回复客户端时带有其他信息,则进入数字证书验证阶段。
服务器端下发服务器证书给客户端后,由客户端验证该证书,主要包含以下几个步骤:
- 服务器回复客户端响应时带有服务器证书。
- 客户端将该证书发送至认证机构。
- 认证机构验证该证书。
- 认证机构回应客户端验证结果,如果验证失败将同时得到警告信息。
这时,服务器端身份得以认证,客户端和服务器端可以进行以服务器端单向认证为基础的加密交互。如果服务器端对于客户端身份有要求,下发服务器证书的同时要求客户端提供证书。
2.3、产生密钥
当服务器端和客户端经过上述一系列操作后,开始密钥构建交互。
服务器端和客户端最初需要建立主密钥为构建会话密钥做准备,如图所示,主要包含以下几个步骤:
- 客户端产生随机数,作为预备主密钥(Pre-Master Secret,PMS)。
- 客户端使用服务器证书中的公钥对随机数PMS加密。
- 客户端将PMS加密信息发送到服务器端。
- 服务器使用私钥对信息解密获得PMS信息。
- 客户端使用随机数RNC、RNS和PMS构建主密钥(Master Secret,MS)。
- 服务器端使用随机数RNC、RNS和PMS构建主密钥MS。
上述步骤5、6不存在次序关系,实际操作中异步完成。
完成主密钥构建操作后,服务器端和客户端将建立会话密钥,即将完成握手协议:
服务器端和客户端分别构建主密钥后将构建会话密钥,并终止握手协议交互,主要步骤包括:
- 客户端使用主密钥构建会话密钥。会话密钥即对称加密算法中的秘密密钥。
- 客户端通知服务器端未来的信息将使用会话密钥加密。
- 客户端发送使用会话密钥加密的信息,终止握手。
- 服务器端使用主密钥构建会话密钥。
- 服务器端通知客户端未来的信息将使用会话密钥加密。
- 服务器发送使用会话密钥加密的信息,终止握手。
至此,服务器端和客户端完成了握手协议,开始进入正式会话阶段。
如果上述一系列操作中有任何一端受到外界因素干扰发生异常,则重新进入协商算法阶段。
2.4、加密交互
进入正式会话阶段后,服务器端和客户端将使用会话密钥进行加密交互,如图所示:
正式会话阶段的加密交互实际上是基于对称加密算法信息交互,会话密钥即秘密密钥,主要步骤如下:
- 客户端使用会话密钥对信息加密。
- 客户端向服务器端发送加密信息。
- 服务器使用会话密钥对请求信息解密。
- 服务器处理请求。
- 服务器完成请求处理后,使用会话密钥对回复信息加密。
- 服务器回复加密信息。
- 客户端使用会话密钥对信息解密。
握手协议交互着实让人难以理解,理解上述协议交互尚有难度,更别说这些协议的具体实现。智慧的先驱者早就想到了这一点,将上述实现封装在SSL(Security Socket Layer)层,我们只需要调用相应的API即可构建HTTPS协议。
3、单向认证服务
在上述模型分析中,我们提到了两种认证服务:单向认证服务和双向认证服务。
- 单向认证服务:仅需要服务器端服务器提供证书,验证服务器身份。
- 双向认证服务:需要服务器提供服务器证书的前提下,要求客户端提供客户证书,同时验证服务器和客户身份。
单向认证服务和双向认证服务是网络交互平台中最高级别的安全服务,广泛应用于电子商务等领域。
本节以配置Tomcat安全服务为例,介绍如何搭建基于单向认证服务和双向认证服务的网络交互平台。可以从Apache官网(http:/tomcat.apache.org/))下载Tomcat当前的最新版本。
为便于演示,本节使用KeyTool工具构建自签名证书,搭建单向认证服务。实际应用时,请使用经由CA机构签发的数字证书。
本节将使用密钥库文件acton.keystore和数字证书文件acton.cer。
3.1、准备工作
1)、生成证书和密钥库
创建密钥库和证书:
keytool -genkeypair -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 36000 -alias test -keystore test.keystore -dname "CN=www.test.com, OU=test, O=test, L=BJ, ST=BJ, C=CN" -storepass 123456
导出证书:
keytool -exportcert -alias test -keystore test.keystore -storepass 123456 -file test.cer -rfc
查看证书内容:
keytool -printcert -file test.cer
2)、绑定域名
在hosts文件中,将www.test.com
绑定到本机127.0.0.1
上,绑定成功后,可以使用ping命令测试。
ping www.test.com
3)、Tomcat服务器证书配置
将密钥库文件test.keystore
上传到tomcat的conf目录中,然后修改server.xml
文件:
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
keystoreFile="conf/test.keystore"
keystoreType="JKS"
keystorePass="123456"
clientAuth="false" sslProtocol="TLS" />
- SSLEnabled:开启SSL
- keystoreFile:密钥库文件
- keystoreType:密钥库类型
- keystorePass:密钥库密码
- clientAuth:采用单向认证,如果为true则是双向认证
- sslProtocol:使用TLS协议
此时使用浏览器访问https://www.test.com
:
4)、将证书导入浏览器
mac系统需要使用钥匙串导入:
添加信任:
再次访问https://www.test.com
:
单向认证成功!虽然这时我们连接的是本地服务器,但仍会感到联网有些迟钝。这是因为浏览器访问CA机构验证该证书,并初始化加密/解密信息等,这一系列动作致使访问延时。
3.2、服务验证
根据Tomcat官方文档提示,完成HTTPS协议架设后,可以从request的属性中获得有关加密的相关信息,如加密算法、密钥长度等。我们可以通过构建一个简单的应用来验证上述描述。
首先,我们需要构建一个JSP页面,遍历request的属性,并将其打印。
index.jsp:
<%@ page language="java" contentType="text/html; UTF-8" %>
<%@ page import="java.util.Enumeration" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>test.com</title>
</head>
<body>
<p>request attributes</p>
<pre>
<%
for (Enumeration en = request.getAttributeNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
out.print(name);
out.println(" = " + request.getAttribute(name));
out.println();
}
%>
</pre>
</body>
</html>
接下来,创建一个最普通的web.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID"
version="2.5">
<display-name>ssl</display-name>
<webcome-file-list>
<webcome-file>index.jsp</webcome-file>
</webcome-file-list>
</web-app>
最后,我们在Tomcat的webapps
目录下创建一个ssl
目录,并将上述index.jsp
文件放在这个目录下。同时,在ssl目录下创建一个WEB-INF
目录,并将web.xml
文件放置其中。
访问https://www.test.com/ssl
:
- javax.servlet.request.ssl_session_id:当前SSL/TLS协议的会话ID。
- javax.servlet.request…key_size:当前加密算法所使用的密钥长度。
- javax.servlet.request…cipher_.suite:当前SSL/TLS协议所使用的加密套件。
- org.apache.tomcat.util.secure_protocol_version:协议版本
可知本次连接所使用的协议为TLSv1.2
;密钥协商算法使用ECDHE
;签名算法使用RSA
;握手后的通信使用AES
对称算法,密钥长度256
位,分组模式是GCM
;摘要算法使用SHA384
。
对于使用何种协议、对称加密算法以及消息摘要算法,需由客户端与服务器端交互通过SSL/TLS握手协议确定。不同的客户端同一时间访问同一服务时,将有可能使用不同的协议或算法,唯一可以确定的是通过数字证书确定的非对称加密算法。
这里要说明一点,使用非对称加密算对数据进行加密/操作的效率相当低,而使用对称加密算法进行加密/操作的效率相当高。合理的解决办法是使用非对称加密算法传递对称加密算法的密钥,使用对称加密算法对数据加密。
3.3、Java代码验证
核心类:
- KeyManagerFactory:引擎类,管理密钥,即密钥管理工厂
- TrustManagerFactory:用于管理信任材料的管理器工厂
- SSLContext:引擎类,表示安全套接字上下文
- HttpsURLConnection:创建HTTPS连接
- SSLSession:用于保持SSL协议网络交互会话状态
- SSLSocketFactory:管理SSL套接字
- SSLSocket:安全套接字,用于设置加密套件、处理握手结束事件,并管理SSLSession
- SSLServerSocketFactory:服务端安全套接字工厂
- SSLServerSocket:服务端安全套接字
在这里密钥库与信任库可以指向同一个文件,即test.keystore文件。
public class HTTPSCoder {
/**
* 支持TLS和SSL协议
*/
public static final String PROTOCOL = "TLS";
/**
* 获得KeyStore
* @param keyStorePath 密钥库路径
* @param password 密码
* @return KeyStore 密钥库
* @throws Exception
*/
private static KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
//实例化密钥库
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
//获得密钥库文件流
FileInputStream is = new FileInputStream(keyStorePath);
//加载密钥库
ks.load(is, password.toCharArray());
//关闭密钥库文件流
is.close();
return ks;
}
/**
* 获得SSLSocketFactory
* @param password 密码(密钥库密码与信任库密码可能不同)
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @return SSLSocketFactory
* @throws Exception
*/
private static SSLSocketFactory getSSLSocketFactory(String password, String keyStorePath, String trustStorePath) throws Exception {
//实例化密钥工厂
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
//获得密钥库
KeyStore keyStore = getKeyStore(keyStorePath, password);
//初始化密钥工厂
keyManagerFactory.init(keyStore, password.toCharArray());
//实例化信任库
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//获得信任库
KeyStore trustStore = getKeyStore(trustStorePath, password);
//初始化信任库
trustManagerFactory.init(trustStore);
//实例化SSL上下文
SSLContext ctx = SSLContext.getInstance(PROTOCOL);
//初始化SSL上下文
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
//获得SSLSocketFactory
return ctx.getSocketFactory();
}
/**
* 为HttpsURLConnection配置SSLSocketFactory
* @param conn HttpsURLConnection
* @param password 密码
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @throws Exception
*/
public static void configSSLSocketFactory(HttpsURLConnection conn, String password, String keyStorePath, String trustStorePath) throws Exception {
//获得SSLSocketFactory
SSLSocketFactory sslSocketFactory = getSSLSocketFactory(password, keyStorePath, trustStorePath);
//设置SSLSocketFactory
conn.setSSLSocketFactory(sslSocketFactory);
}
}
public class HTTPSCoderTest {
//密钥库/信任库密码
private String password = "123456";
//密钥库文件路径
private String keyStorePath = "jks/test.keystore";
//信任库文件路径
private String trustStorePath = "jks/test.keystore";
//访问地址
private String httpsUrl = "https://www.test.com/ssl";
@Test
public void test() throws Exception {
//建立HTTPS链接
URL url = new URL(httpsUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
//打开输入输出流
conn.setDoInput(true);
//为HttpsURLConnection配置SSLSocketFactory
HTTPSCoder.configSSLSocketFactory(conn, password, keyStorePath, trustStorePath);
//鉴别内容长度
int length = conn.getContentLength();
byte[] data = null;
//读取内容
if (length != -1) {
DataInputStream dis = new DataInputStream(conn.getInputStream());
data = new byte[length];
dis.readFully(data);
dis.close();
//输出
System.out.println(new String(data));
}
//关闭连接
conn.disconnect();
}
}
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>test.com</title>
</head>
<body>
<p>request attributes</p>
<pre>
org.apache.tomcat.util.net.secure_protocol_version = TLSv1.2
javax.servlet.request.key_size = 256
javax.servlet.request.ssl_session_mgr = org.apache.tomcat.util.net.jsse.JSSESupport@48da8e2d
javax.servlet.request.cipher_suite = TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
javax.servlet.request.ssl_session_id = 656ed36ed921171b7132147bb4addc962cf5d70ccf296842fe3f637a99e90e8c
</pre>
</body>
</html>
通过代码访问https://www.test.com/ssl
页面,在控制台得到request相关属性与通过浏览器访问获得的内容有所不同。除了javax.servlet.request.ssl_session_id
属性必然发生变化外,javax.servlet.request.cipher_suite
属性也发生了变化,加密模式变为了CBC
模式,这说明不同的客户端在同一时间访问同一服务时,将有可能使用不同的协议或算法。
在上述代码中,我们并没有做过任何加密/解密操作。我们像使用HttpURLConnection类一样使用HttpsURLConnection类构建连接,无需关心加密/解密的具体实现。这些加密/解密的实现层位于传输层,对于应用层完全透明,极大地方便了我们的使用。
4、双向认证服务
双向认证服务与单向认证服务的主要差别在于双向认证服务增加了客户证书验证环节。这使得消息收发双方可以通过证书相互验证对方的身份,达到双向认证的作用。
双向认证服务需要根证书、服务器证书和客户证书共3项证书。我们可以通过OpenSSL工具构建这3项证书对应的个人信息交换文件(ca.pl2、server…pl2和client.pl2文件)。
为便于演示,本节使用OpenSSL工具构建自签名根证书、服务器证书和客户证书,搭建双向认证服务。实际应用时,应使用经由CA机构签发的根证书。
4.1、准备工作
1)、绑定域名
在hosts文件中,将127.0.0.1
和acton.com
绑定。
2)、构建证书
2.1、KeyTool方式(推荐)
构建密钥库和证书:
#ca根证书和密钥库
keytool -genkeypair -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 36000 -alias key_basic -keystore basic.keystore -dname "CN=*.acton.com, OU=test, O=test, L=BJ, ST=BJ, C=CN" -storepass 123456 -keypass 123456
# 服务器证书和密钥库
keytool -genkeypair -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 36000 -alias key_server -keystore server.keystore -dname "CN=acton.com, OU=test, O=test, L=BJ, ST=BJ, C=CN" -storepass 123456 -keypass 123456
# 客户端证书和密钥库
keytool -genkeypair -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 36000 -alias key_client -keystore client.keystore -dname "CN=client, OU=test, O=test, L=BJ, ST=BJ, C=CN" -storepass 123456 -keypass 123456
导出自签名的根证书:
# 导出根证书
keytool -exportcert -alias key_basic -keystore basic.keystore -storepass 123456 -file basic.cer -rfc
生成证书请求:
# 服务器证书请求
keytool -certreq -alias key_server -keystore server.keystore -file server_req.csr -storepass 123456
# 客户端证书请求
keytool -certreq -alias key_client -keystore client.keystore -file client_req.csr -storepass 123456
根证书签发:
# 根证书私钥对服务器证书请求签名
keytool -gencert -alias key_basic -infile server_req.csr -outfile server.cer -keystore basic.keystore -storepass 123456 -keypass 123456
# 根证书私钥对客户端证书请求签名
keytool -gencert -alias key_basic -infile client_req.csr -outfile client.cer -keystore basic.keystore -storepass 123456 -keypass 123456
安装证书回复(回复这个翻译也许不太好,反正这个意思就是:将CA颁发的公钥证书安装到server/client端证书库,前提条件是CA自己的公钥证书也需要先被安装):
1.先安装CA的公钥证书(这步不可以少,否则下面的证书回复没法安装):
# 将ca的公钥证书安装到服务端密钥库(根证书添加信任)
keytool -importcert -v -alias key_basic -file basic.cer -keystore server.keystore -storepass 123456
# 将ca的公钥证书安装到客户端密钥库(根证书添加信任)
keytool -importcert -v -alias key_basic -file basic.cer -keystore client.keystore -storepass 123456
2.安装server/client的公钥证书(安装证书回复(被CA签名过的)):
# 服务端的签名证书添加信任
keytool -importcert -alias key_server -file server.cer -keystore server.keystore -storepass 123456
# 客户端的签名证书添加信任
keytool -importcert -v -alias key_client -file client.cer -keystore client.keystore -storepass 123456
此时再查看下server/client证书:
keytool -list -v -keystore server.keystore -storepass 123456 -alias key_server
这时发现这个server证书变化挺大的,一是证书连变长了,变成2了,这个server证书附带了上一级证书的信息,其次是server的发布者变成了CA,这也就是说明成功的使用自签名的CA给server签名成功了。
# 导出服务器端证书
keytool -exportcert -alias key_server -keystore server.keystore -storepass 123456 -file server.cer
# 导出客户端证书
keytool -exportcert -alias key_client -keystore client.keystore -storepass 123456 -file client.cer
因为是双向认证,所以这时需要将客户端证书导入到服务端密钥库(添加信任),服务端证书添加到客户端密钥库(添加信任)。
# 服务器证书库添加信任客户端证书
keytool -importcert -alias key_client -keystore server.keystore -file client.cer -storepass 123456
# 客户端证书库添加信任服务端证书
keytool -importcert -alias key_server -keystore client.keystore -file server.cer -storepass 123456
# 服务端密钥库转为pkcs12
keytool -importkeystore -v -srckeystore server.keystore -destkeystore server.p12 -srcstoretype jks -deststoretype pkcs12 -srcstorepass 123456 -deststorepass 123456
# 客户端密钥库转为pkcs12
keytool -importkeystore -v -srckeystore client.keystore -destkeystore client.p12 -srcstoretype jks -deststoretype pkcs12 -srcstorepass 123456 -deststorepass 123456
最终,server.p12中包含了server证书,server私钥,可信任的ca证书、可信任的client证书。client.p12中包含了client证书,client私钥,可信任的ca证书、可信任的server证书。
2.2、OpenSSL方式
构建根证书:
# 根证书私钥
openssl genrsa -aes256 -passout pass:123456 -out private/ca.key.pem 2048
# 根证书请求
openssl req -new -key private/ca.key.pem -passin pass:123456 -out private/ca.csr -passout pass:123456 -subj "/C=CN/ST=BJ/L=BJ/O=acton/OU=acton/CN=*.acton.zhang"
# 自签名
openssl x509 -req -days 3650 -sha1 -extensions v3_ca -signkey private/ca.key.pem -in private/ca.csr -passin pass:123456 -out certs/ca.cer
构建服务器证书:
# 服务器证书私钥
openssl genrsa -aes256 -passout pass:123456 -out private/server.key.pem 2048
# 服务器证书请求
openssl req -new -key private/server.key.pem -passin pass:123456 -out private/server.csr -passout pass:123456 -subj "/C=CN/ST=BJ/L=BJ/O=acton/OU=acton/CN=www.acton.com"
# 根证书签发服务器证书请求
openssl x509 -req -days 3650 -sha1 -extensions v3_req -CA certs/ca.cer -CAkey private/ca.key.pem -passin pass:123456 -CAserial ca.srl -CAcreateserial -in private/server.csr -out certs/server.cer
构建客户端证书:
# 客户端证书私钥
openssl genrsa -aes256 -out private/client.key.pem -passout pass:123456 2048
# 客户端证书请求
openssl req -new -key private/client.key.pem -passin pass:123456 -out private/client.csr -passout pass:123456 -subj "/C=CN/ST=BJ/L=BJ/O=acton/OU=acton/CN=acton"
# 根证书签发客户端证书请求
openssl x509 -req -days 3650 -sha1 -extensions v3_req -CA certs/ca.cer -CAkey private/ca.key.pem -passin pass:123456 -CAserial ca.srl -CAcreateserial -in private/client.csr -out certs/client.cer
转换为PKCS12:
# 将服务端私钥和证书和ca证书打包为pkcs12
openssl pkcs12 -export -inkey private/server.key.pem -in certs/server.cer -passin pass:123456 -chain -CAfile certs/ca.cer -password pass:123456 -out certs/server.p12
# 将客户端私钥和证书和ca证书打包为pkcs12
openssl pkcs12 -export -inkey private/client.key.pem -in certs/client.cer -passin pass:123456 -chain -CAfile certs/ca.cer -password pass:123456 -out certs/client.p12
因为是双向认证,所以服务端要添加客户端证书信任,客户端要添加服务端证书信任:
# 生成服务端信任库(添加客户端)
keytool -importcert -trustcacerts -alias client -file certs/client.cer -keystore certs/server.p12 -storepass 123456
# 生成客户端端信任库:
keytool -importcert -trustcacerts -alias server -file certs/server.cer -keystore certs/client.p12 -storepass 123456
查看该信任库server.p12:
keytool -list -keystore certs/server.p12 -storetype pkcs12 -v -storepass 123456
3)、配置Tomcat
将server.p12
复制到conf目录下,然后修改web.xml配置文件:
<Connector port="443" protocol="HTTP/1.1"
maxThreads="150"
SSLEnabled="true"
scheme="https"
secure="true"
keystoreFile="conf/server.p12"
keystoreType="PKCS12"
keystorePass="123456"
truststoreFile="conf/a.keystore"
truststorePass="123456"
truststoreType="PKCS12"
clientAuth="true"
sslProtocol="TLS" />
此时访问https://acton.com
:
查看此证书:
4)、浏览器导入客户端证书
Mac系统通过钥匙串导入client.p12
个人信息交换文件:
client.p12中包含了根证书、server证书和client证书及私钥
所以需要给ca根证书添加信任,client和server证书才会符合标准(信任链)。
此时访问https://acton.com
:
4.2、服务验证
继续访问https://acton.com/ssl
:
多了一项request属性,该数组指向客户证书列表。
修改单向认证中的index.jsp页面:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ page import="java.util.Enumeration" %>
<%@ page import="java.security.cert.X509Certificate" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>test.com</title>
</head>
<body>
<p>request attributes</p>
<pre>
<%
for (Enumeration en = request.getAttributeNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
out.print(name);
out.println(" = " + request.getAttribute(name));
out.println();
}
%>
</pre>
<p>数字证书信息</p>
<pre>
<%
X509Certificate[] certs = (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
for (X509Certificate cert : certs) {
out.println("版本:\t\t" + cert.getVersion());
out.println("序列号:\t\t" + cert.getSerialNumber());
out.println("颁布者:\t\t" + cert.getIssuerDN().getName());
out.println("使用者:\t\t" + cert.getSubjectDN().getName());
out.println("签名算法:\t\t" + cert.getSigAlgName());
out.println("证书类型:\t\t" + cert.getType());
out.println("有效期从:\t" + cert.getNotBefore());
out.println("至:\t\t" + cert.getNotAfter());
}
%>
</pre>
</body>
</html>
重新部署后再次访问:https://acton.com/ssl
:
4.3、Java代码验证
因为使用PKCS#12格式的个人信息交换文件作为密钥库和信任库文件,所以在获取密钥库的时候,需要指定PKCS12
格式:
public class DoubleHTTPSCoder {
/**
* 支持TLS和SSL协议
*/
public static final String PROTOCOL = "TLS";
/**
* 获得KeyStore PKCS12格式的个人信息交换文件
* @param keyStorePath 密钥库路径
* @param password 密码
* @return KeyStore 密钥库
* @throws Exception
*/
private static KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
//实例化密钥库
KeyStore ks = KeyStore.getInstance("PKCS12");
//获得密钥库文件流
FileInputStream is = new FileInputStream(keyStorePath);
//加载密钥库
ks.load(is, password.toCharArray());
//关闭密钥库文件流
is.close();
return ks;
}
/**
* 获得SSLSocketFactory
* @param password 密码(密钥库密码与信任库密码可能不同)
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @return SSLSocketFactory
* @throws Exception
*/
private static SSLSocketFactory getSSLSocketFactory(String password, String keyStorePath, String trustStorePath) throws Exception {
//实例化密钥工厂
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
//获得密钥库
KeyStore keyStore = getKeyStore(keyStorePath, password);
//初始化密钥工厂
keyManagerFactory.init(keyStore, password.toCharArray());
//实例化信任库
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//获得信任库
KeyStore trustStore = getKeyStore(trustStorePath, password);
//初始化信任库
trustManagerFactory.init(trustStore);
//实例化SSL上下文
SSLContext ctx = SSLContext.getInstance(PROTOCOL);
//初始化SSL上下文
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
//获得SSLSocketFactory
return ctx.getSocketFactory();
}
/**
* 为HttpsURLConnection配置SSLSocketFactory
* @param conn HttpsURLConnection
* @param password 密码
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @throws Exception
*/
public static void configSSLSocketFactory(HttpsURLConnection conn, String password, String keyStorePath, String trustStorePath) throws Exception {
//获得SSLSocketFactory
SSLSocketFactory sslSocketFactory = getSSLSocketFactory(password, keyStorePath, trustStorePath);
//设置SSLSocketFactory
conn.setSSLSocketFactory(sslSocketFactory);
}
}
public class DoubleHTTPSTest {
//密钥库/信任库密码
private String password = "123456";
//密钥库文件路径
private String keyStorePath = "ssl/client.p12";
//信任库文件路径
private String trustStorePath = "ssl/client.p12";
//访问地址
private String httpsUrl = "https://acton.com/ssl";
@Test
public void test() throws Exception {
//建立HTTPS链接
URL url = new URL(httpsUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
//打开输入输出流
conn.setDoInput(true);
//为HttpsURLConnection配置SSLSocketFactory
HTTPSCoder.configSSLSocketFactory(conn, password, keyStorePath, trustStorePath);
//鉴别内容长度
int length = conn.getContentLength();
byte[] data = null;
//读取内容
if (length != -1) {
DataInputStream dis = new DataInputStream(conn.getInputStream());
data = new byte[length];
dis.readFully(data);
dis.close();
//输出
System.out.println(new String(data));
}
//关闭连接
conn.disconnect();
}
}
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>test.com</title>
</head>
<body>
<p>request attributes</p>
<pre>
org.apache.tomcat.util.net.secure_protocol_version = TLSv1.2
javax.servlet.request.key_size = 256
javax.servlet.request.ssl_session_mgr = org.apache.tomcat.util.net.jsse.JSSESupport@40d98132
javax.servlet.request.X509Certificate = [Ljava.security.cert.X509Certificate;@7e603abf
javax.servlet.request.cipher_suite = TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
javax.servlet.request.ssl_session_id = 65703af93686a3acc7d13077788a18669ff55599e71940380b4f45a7f335a8e0
</pre>
<p>数字证书信息</p>
<pre>
版本: 3
序列号: 934411646
颁布者: CN=*.acton.com, OU=test, O=test, L=BJ, ST=BJ, C=CN
使用者: CN=client, OU=test, O=test, L=BJ, ST=BJ, C=CN
签名算法: SHA256withRSA
证书类型: X.509
有效期从: Wed Dec 06 00:15:51 CST 2023
至: Tue Mar 05 00:15:51 CST 2024
版本: 3
序列号: 1658998103
颁布者: CN=*.acton.com, OU=test, O=test, L=BJ, ST=BJ, C=CN
使用者: CN=*.acton.com, OU=test, O=test, L=BJ, ST=BJ, C=CN
签名算法: SHA1withRSA
证书类型: X.509
有效期从: Wed Dec 06 00:15:07 CST 2023
至: Tue Jun 30 00:15:07 CST 2122
</pre>
</body>
</html>
5、实例
5.1、SSLSocket获取数字证书
在我们熟悉的一些应用中,有些应用基于Socket:通信。而且,为了客户端与服务器交互更加安全,甚至使用了SSLSocket。作为SSLSocket客户端,与SSLSocket)服务器进行交互的第一步,就是获取其数字证书。
大家所熟悉的苹果软件iTunes,就是通过这样一种安全网络机制与服务器进行交互的。本着学习的态度,我们将通过SSLSocket获取iTunes的数字证书链。
SSLSocketi通信需要复杂的握手协议,但Java底层已做过相当多的封装,对于它的使用者,我们只需要执行“握手”即可。而后,得到当前会话就能获取数字证书。
public class SSLSocketTest {
private String hostname;
private int port;
@Before
public void init() {
hostname = "itunes.apple.com";
port = 443;
}
@Test
public void test() throws Exception {
//debug模式
System.setProperty("javax.net.debug", "all");
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) factory.createSocket(hostname, port);
//握手,开始会话
socket.startHandshake();
SSLSession session = socket.getSession();
socket.close();
//获取服务器证书链
Certificate[] peerCertificates = session.getPeerCertificates();
for (int i = 0; i < peerCertificates.length; i++) {
FileOutputStream f = new FileOutputStream(i + ".cer");
DataOutputStream dos = new DataOutputStream(f);
dos.write(peerCertificates[i].getEncoded());
dos.flush();
dos.close();
}
}
}
执行代码后,会得到3个证书:
5.2、SSLSocket加密交互
获得数字证书后,我们就可以在服务器端和客户端建立起基于SSLSocket的安全网络交互了。这里模拟服务器端与客户端进行加密交互,由客户端发起用户名密码校验,服务器确认后返回“OK”。
服务端:
public class SSLSocketServerTest {
//加密端口
private int port;
//支持TLS和SSL协议
private String protocol;
//密钥库和信任库类型
private String keyStoreType;
//密钥库/信任库密码
private String password;
//密钥库文件路径
private String keyStorePath;
//信任库文件路径
private String trustStorePath;
@Before
public void init() {
port = 7070;
protocol = "TLS";
keyStoreType = "PKCS12";
password = "123456";
keyStorePath = "ssl/server.p12";
trustStorePath = "ssl/server.p12";
}
@Test
public void testServer() throws Exception {
SSLServerSocket serverSocket = null;
try {
//初始化SSLServerSocketFactory
SSLServerSocketFactory sslServerSocketFactory = getSSLSocketFactory(password, keyStorePath, trustStorePath);
//构建ServerSocket实例
serverSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(port);
} catch (Exception e) {
e.printStackTrace();
}
while (true) {
try {
//SSLServerSocket阻塞,等待客户端连接请求
SSLSocket socket = (SSLSocket) serverSocket.accept();
//获得输入流
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
//获得用户名密码
Map<String, String> map = (Map<String, String>) input.readObject();
String username = map.get("USERNAME");
String password = map.get("PASSWORD");
//验证用户名密码
Assert.assertNotNull(username);
Assert.assertEquals("123456", password);
//获得输出流
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
//输出OK
output.writeUTF("OK");
output.flush();
//关闭流,套接字
output.close();
input.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获得KeyStore
* @param keyStorePath 密钥库路径
* @param password 密码
* @return KeyStore 密钥库
* @throws Exception
*/
private KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
//实例化密钥库
KeyStore ks = KeyStore.getInstance(keyStoreType);
//获得密钥库文件流
FileInputStream is = new FileInputStream(keyStorePath);
//加载密钥库
ks.load(is, password.toCharArray());
//关闭密钥库文件流
is.close();
return ks;
}
/**
* 获得SSLServerSocketFactory
* @param password 密码(密钥库密码与信任库密码可能不同)
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @return SSLServerSocketFactory
* @throws Exception
*/
private SSLServerSocketFactory getSSLSocketFactory(String password, String keyStorePath, String trustStorePath) throws Exception {
//实例化密钥工厂
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
//获得密钥库
KeyStore keyStore = getKeyStore(keyStorePath, password);
//初始化密钥工厂
keyManagerFactory.init(keyStore, password.toCharArray());
//实例化信任库
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//获得信任库
KeyStore trustStore = getKeyStore(trustStorePath, password);
//初始化信任库
trustManagerFactory.init(trustStore);
//实例化SSL上下文
SSLContext ctx = SSLContext.getInstance(protocol);
//初始化SSL上下文
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
//获得SSLSocketFactory
return ctx.getServerSocketFactory();
}
}
客户端:
public class SSLSocktClientTest {
//域名
private String hostname;
//加密端口
private int port;
//支持TLS和SSL协议
private String protocol;
//密钥库和信任库类型
private String keyStoreType;
//密钥库/信任库密码
private String password;
//密钥库文件路径
private String keyStorePath;
//信任库文件路径
private String trustStorePath;
@Before
public void init() {
hostname = "localhost";
port = 7070;
protocol = "TLS";
keyStoreType = "PKCS12";
password = "123456";
keyStorePath = "ssl/client.p12";
trustStorePath = "ssl/client.p12";
}
@Test
public void testClient() {
try {
//构建SSLSocketFactory
SSLSocketFactory sslSocketFactory = getSSLSocketFactory(password, keyStorePath, trustStorePath);
//初始化SSL实例
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(hostname, port);
//获得输出流
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
//构建一个Map对象,用于记录用户名密码
Map<String, String> map = new HashMap<>();
map.put("USERNAME", "tom");
map.put("PASSWORD", "123456");
//输出
output.writeObject(map);
output.flush();
//获得输入流
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
//验证输入
String response = input.readUTF();
System.out.println(response);
Assert.assertEquals("OK", response);
//关闭流,套接字
output.close();
input.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获得KeyStore
* @param keyStorePath 密钥库路径
* @param password 密码
* @return KeyStore 密钥库
* @throws Exception
*/
private KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
//实例化密钥库
KeyStore ks = KeyStore.getInstance(keyStoreType);
//获得密钥库文件流
FileInputStream is = new FileInputStream(keyStorePath);
//加载密钥库
ks.load(is, password.toCharArray());
//关闭密钥库文件流
is.close();
return ks;
}
/**
* 获得SSLSocketFactory
* @param password 密码(密钥库密码与信任库密码可能不同)
* @param keyStorePath 密钥库路径
* @param trustStorePath 信任库路径
* @return SSLSocketFactory
* @throws Exception
*/
private SSLSocketFactory getSSLSocketFactory(String password, String keyStorePath, String trustStorePath) throws Exception {
//实例化密钥工厂
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
//获得密钥库
KeyStore keyStore = getKeyStore(keyStorePath, password);
//初始化密钥工厂
keyManagerFactory.init(keyStore, password.toCharArray());
//实例化信任库
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
//获得信任库
KeyStore trustStore = getKeyStore(trustStorePath, password);
//初始化信任库
trustManagerFactory.init(trustStore);
//实例化SSL上下文
SSLContext ctx = SSLContext.getInstance(protocol);
//初始化SSL上下文
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
//获得SSLSocketFactory
return ctx.getSocketFactory();
}
}