Mqtt 系列:SSL
这篇文章讲述在 Java
中如何使用 SSL
进行通信。
基本概念
SSL
通信的目的是构建一个安全可靠的通信通道,它涉及到数据加解密、数字证书等知识。本文对加密算法,如对称加密、非对称加密及摘要算法不再赘述,只讲述与 Java
体系相关的知识点。
KeyStore
KeyStore
,一个存储密钥及证书的存储设备或数据库,它用于证明服务器及客户端身份。 KeyStore 的形态可以是一个文件,也可以是一个物理设备,它可以存储三种类型的条目,根据 KeyStore
类型的不同,存储的条目可能不一样。
三种类型的条目如下:
- 私钥:存储非对称算法的私钥,处于安全的考虑,访问该条目,需要提供密码;
- 证书:证书包含一个公钥及签名,用于验证服务器或客户端的身份;
- 密钥:存储对称算法的加密密钥。
1. KeyStore Alias
在 KeyStore
中,每个条目都对应一个别名,这个别名惟一对应了一个条目。可以使用别名查询 KeyStore
的内容。
2. KeyStore 类型
根据存储的条目类型及存储方式,在 Java
中,KeyStore
有一些不同的类型:JKS
, JCEKS
, PKCS12
, PKCS11
, DKS
.
- JKS:
Java Key Store
的首字母简写,它的实现类是sun.security.provider.JavaKeyStore
.JKS
是与Java
语言相关的KeyStore
,不能被其它语言使用。它可以存储私钥和证书,但不能存储对称密钥,另外,它的私钥在Java
中不能被提取; - JCEKS:
JCE key store(Java Cryptography Extension KeyStore)
, 它是JKS
的一个超集,包含了更多的算法支持,实现类是com.sun.crypto.provider.JceKeyStore
.JCEKS
可以存储私钥,证书和密钥三种类型的条目,它使用Triple DES
加密算法对私钥存储进行了加强保护。JCEKS
由SunJCE
提供,于Java 1.4
版本中引入,在Java 1.4
之前的版本中,只有JKS
可用; - PKCS12: 这是一种标准的
KeyStore
,可以被Java
或其它语言使用,它扩展了p12 or pfx
, 其实现类是sun.security.pkcs12.PKCS12KeyStore
.PKCS12
也可以存储三种类型的条目,不同于JKS
,它的私钥可以被其它语言如 C, C++ or C# 提取。另外,在 Java 9 版本之前默认的KeyStore
是JKS
, Java 9 之后改为JCEKS
. 可以在$JRE/lib/security/java.security
中查看默认的KeyStore
; - PKCS11: 它是一种硬件类型的
KeyStore
, 它为 Java 库连接硬件KeyStore
提供了一套接口,其实现类是sun.security.pkcs11.P11KeyStore
.
TrustKeyStore
TrustKeyStore
, 专门存储受信任的证书条目的 KeyStore
. Java 自带了一个 TrustKeyStore cacerts
, 它位于 $JAVA_HOME/jre/lib/security
目录下,包含了默认的受信任的证书。不过,可以通过 javax.net.ssl.trustStore
属性覆盖默认的 TrustKeyStore
,也可以通过 javax.net.ssl.trustStorePassword
和 javax.net.ssl.trustStoreType
属性指定其密码和类型。
说明:
在程序中,存放私钥、己方证书的 KeyStore
和存放第三方证书的 TrustKeyStore
可以是同一个。
证书类型
常用的证书包括如下类型:
- DER,CER:文件是二进制格式,只保存证书,不保存私钥,用于 Java 和 Windows 服务器中;
- PEM:一般是文本格式,可保存证书和私钥,分别使用两个文件保存,用于 Nginx 或 Apache 中;
- CRT: 文件可以是二进制格式,也可以是文本格式,与 DER 格式相同,不保存私钥;
- PFX P12: 文件是二进制格式,同时包含证书和私钥,一般有密码保护,用于 Java 语言或 Windows IIS 中;
- JKS: 二进制格式,同时包含证书和私钥,一般有密码保护,用于 Java 语言。
keytool
keytool
是 JDK
提供的一个管理 KeyStore
工具,常用的命令包括:
- genkeypair: 生成非对称算法的公私钥密码对;
- exportcert: 导出证书;
- importcert: 导入证书;
- printcertreq: 打印输出证书;
- list: 列出
KeyStore
内容。
1. genkeypair 参数
1 | -v: 输出详细日志; |
For the -keypass option, if you do not specify the option on the command line, then the keytool command first attempts to use the keystore password to recover the private/secret key. If this attempt fails, then the keytool command prompts you for the private/secret key password.
实例:
1 | $ keytool -genkeypair -v -alias mqtt-broker -keyalg RSA -keystore ./server_ks -dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass 123456 -keypass 123456 |
2. exportcert 参数
1 | -v: 输出详细日志; |
实例:
1 | $ keytool -exportcert -v -alias mqtt-broker -keystore ./server_ks -storepass 123456 -file server_key.cer |
3. importcert 参数
1 | -trustcacerts: 指定条目类型为“受信任的证书类型”; |
实例:
1 | $ keytool -importcert -trustcacerts -v -alias mqtt-broker -file ./server_key.cer -storepass 123456 -keystore ./client_ks |
4. printcertreq 参数
1 | -v: 输出详细日志; |
实例:
1 | $ keytool -printcertreq -v -file server_key.cer |
5. list 参数
1 | -v: 输出详细日志; |
实例:
1 | $ keytool -list -v -keystore ./server_ks -storepass 123456 -storetype jks |
详细命令参数可参见keytool官方文档.
双向认证实例
现在有这样一个场景,Client 和 Server 通过 SSL 通信且需要双向认证,双向认证是指 Client 和 Server 两端都要验证对方的证书。完成这个场景需要如下步骤(使用自签名证书):
- 生成 Client 和 Server 端公私钥对;
- 导出各自的证书,并导入到对方的
TrustKeyStore
中; - 将
KeyStore
加载到程序中,初始化SSLContext
对象,并生成对应的Socket
对象,完成通信。
生成公私密钥
使用 keytool -genkeypair
生成 Client 和 Server 端公私钥对,别名分别是 mqtt-broker
和 mqtt-client
, 域名使用 localhost
.
1 | # Server 端 |
导出证书
Server 和 Client 端各自使用一个 KeyStore
来存放私钥、己方证书和第三方证书。
1 | # 导出 Server 端证书 |
查看 Server 端 KeyStore
文件 server_ks
, 它存储有两个条目,一个是别名为 mqtt-client
类型为 trustedCertEntry
的条目,它是导入的 client 端证书,还有一个别名为 mqtt-broker
类型为 PrivateKeyEntry
的条目,它便是 Server 端的私钥。
1 | $ keytool -list -v -keystore ./server_ks -storepass 123456 |
初始化 SSLContext 对象
将生成的 KeyStore
加载到程序中,生成 SSLContext 对象
1 | String keyStoreFile = "E:\\lab\\bell-labs\\ssl-lab\\src\\main\\resources\\cert\\server_ks"; |
不同 JDK 版本支持的 SSL 协议可能不一样,可以通过以下代码查看支持的协议。
1 | System.out.println("Suported SSL Protocols : " + String.join(" ", |
在 Win10 系统 JDK 8 的环境下,支持的协议为:SSLv2Hello SSLv3 TLSv1 TLSv1.1 TLSv1.2
.
生成 Socket 对象
使用 SSLContext 对象生成 Server 和 Client 端 Socket 对象。
1 | // 生成 Server 端 ServerSocket 对象 |
输出 SSL 日志
在开发阶段,通过设置 javax.net.debug
参数输出 SSL 相关的日志,方便定位问题。
1 | System.setProperty("javax.net.debug", "ssl,handshake"); |
总结
通过上面的描述,可以知道在 Java
代码中引入 SSL 的步骤。不过,在 Tomcat 或 Jetty 容器中,或在 Spingboot 框架中引入 SSL 无需那么复杂,它们已经封装了这些步骤,只需要配置 KeyStore
文件位置和访问密码即可。
参考:
1. keytool
2. Difference Between a Java Keystore and a Truststore
3. Java-JSSE-SSL/TLS编程代码实例-双向认证
4. Different types of keystore in Java – Overview