SSL/TLS协议
SSL,Secure Socket Layer,安全套接层
TLS,Transport Layer Security,传输层安全协议
TLS是SSL的最终完善版本,一般也可称为SSL协议
SSL是负责传输层安全,确定传输层数据如何封装的一套协议
SSL协议阶段
- 算法协商
- 数字证书验证
- 数据通信
SSL协议核心内容
- 事先准备:
- 服务端生成自己的私钥和公钥
- 服务端向CA机构申请SSL证书
- CA向服务器颁发数字证书,该证书通过CA私钥加密,包含了服务端域名和公钥,用于证明服务端身份
- 算法协商阶段:
- 客户端和服务端分别生成随机数RNC和RNS,连同SSL算法参数一起发送给对方
- 数字证书验证阶段:
- 服务端将自己的数据进行摘要,再通过私钥加密,生成数字签名
- 服务端将数据+数字签名+数字证书,一同发往客户端
- 客户端通过CA公钥解密数字证书,确认证书与域名一致后,拿到服务端公钥(保证了公钥未被篡改)
- 客户端通过服务端公钥解开数字签名,得到数据摘要(保证了数据来自服务端)
- 客户端对数据部分进行摘要,与数字签名中的摘要进行对比(保证了数据完整性)
- 客户端生成随机数PMS(Pre Master Secret),通过服务端公钥加密,发给服务端
- 服务端通过私钥解密出PMS
- 双方根据RNC+RNS+PMS构建主密钥MS(Master Secret),完成对称秘钥协商
- 数据通信阶段:
- 服务端和客户端通过主密钥MS,加密解密通信数据
- 双向认证:
- 如果服务端也要求客户端持有SSL证书,则增加一步服务端验证客户端证书的过程
- 秘钥协商算法:
- 以上流程使用的是DH秘钥协商算法
- 如果用的是RSA算法,则直接将PMS作为MS使用,并且由客户端发送给服务端
SSL协议应用
- 最常见的是应用于Https协议
- 此外也可应用于Socket和WebSocket
SSL核心类
- KeyManager,用于管理自己的私钥和证书,多用于服务端
- TrustManager,用于验证收到的证书是否可信,多用于客户端
- 服务端也可以要求客户端也发送安全证书,即双向认证,此时双方都要同时设置KeyManager+TrustManager
- KeyStore,秘钥仓库,用来存储私钥、公钥、证书等数据,可设置密码
- TrustStore,KeyStore的一种,存储CA证书,用于验证服务端身份正确性,密码公开
- KeyStore和TrustStore的区别,仅在于存储的内容不同,它们可以是同一个文件,但是不建议这么做
- Java中比较常见的KeyStore文件格式是JKS,此外还有CRT、PEM、P12、KEY等格式,私钥和证书也可能分开保存
- KeyManagerFactory,用于创建KeyManager,一般通过algorithm和keystore文件来初始化
- TrustManagerFactory,用于创建TrustManager,一般通过algorithm和keystore文件来初始化
- SSLContext,用于整体管理SSL相关事务,一般通过KeyManager和TrustManager来初始化
- 当KeyManager和TrustManager未指定时,SSLContext会从系统已安装的SecurityProvider中,搜索合适的Provider来处理对应工作
- SSLServerSocketFactory,用于创建带SSL功能的ServerSocket,可通过SSLContext创建
- SSLSocketFactory,用于创建带SSL功能的ClientSocket,可通过SSLContext创建
- 也可以不使用Factory和KeyStore,通过自定义的方式来创建KeyManager和TrustManager
- SSLSessionContext,维护所有会话信息,可通过SSLContext.getSessionContext获得
- SSLSession,可通过SSLSessionContext或SSLSocket来获得
- SSLServerSocket可以接收来自多个客户端的连接,SSLSocket只能连接指定的服务器和端口
- SSLEngine,用于实现SSL握手,通过SSLContext创建,一般不用自己去实现,属于偏内部的Class
通过KeyTool生成KeyStore
学习SSL首先得有用于测试的SSL证书库,所以我们先来看看如何生成KeyStore
private.keystore和public.truststore本质上都是JKS文件,只是为了区分作用而换了后缀
# 生成私钥证书,首个名称请输入域名或ip
keytool -genkeypair-aliasalias-keyalg RSA -validity365-keystore private.keystore
# 生成公钥证书
keytool -export-aliasalias-keystore private.keystore -rfc-file public.cer
# 公钥证书转为JKS格式
keytool -import-aliasalias-file public.cer -keystore public.truststore
KeyStore格式转换
JKS是Java专用的秘钥仓库格式
不同平台和语言的对KeyStore的支持性和使用习惯并不一样
比如安卓就不支持JKS格式,安卓上的证书一般使用P12格式
我们可以通过
KeyStore Explorer
软件来转换
https://keystore-explorer.org/downloads.html
使用带SSL的TcpSocket
import java.io.*import java.security.KeyStore
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLServerSocket
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManagerFactory
constval serverKeyStore ="resources/private.keystore"constval clientKeyStore ="resources/public.truststore"constval passphrase ="123456"constval serverPort =18001funmain(){Thread(::launchServerSocket).start()
Thread.sleep(500)Thread(::launchClientSocket).start()}funlaunchServerSocket(){// load key manager from key storeval keyManagerFactory = KeyManagerFactory.getInstance("SunX509")val keyStore = KeyStore.getInstance("JKS")
keyStore.load(FileInputStream(serverKeyStore), passphrase.toCharArray())
keyManagerFactory.init(keyStore, passphrase.toCharArray())val keyManagers = keyManagerFactory.keyManagers
// init ssl contextval context = SSLContext.getInstance("TLS")
context.init(keyManagers,null,null)// create server socketval serverSocketFactory = context.serverSocketFactory
val serverSocket = serverSocketFactory.createServerSocket(serverPort)as SSLServerSocket
serverSocket.needClientAuth =false// accept client sessionval socket = serverSocket.accept()as SSLSocket
// communication with clientwhile(true){
Thread.sleep(500)
socket.getOutputStream().write("Hello World".encodeToByteArray())
socket.getOutputStream().flush()}}funlaunchClientSocket(){// load trust manager from trust storeval trustManagerFactory = TrustManagerFactory.getInstance("SunX509")val trustStore = KeyStore.getInstance("JKS")
trustStore.load(FileInputStream(clientKeyStore), passphrase.toCharArray())
trustManagerFactory.init(trustStore)val trustManagers = trustManagerFactory.trustManagers
// init ssl contextval context = SSLContext.getInstance("TLS")
context.init(null, trustManagers,null)// create client socket and auto connectval sslSocketFactory = context.socketFactory
val socket = sslSocketFactory.createSocket("localhost", serverPort)as SSLSocket
// communication with serverval buffer =ByteArray(1024)while(true){val len = socket.getInputStream().read(buffer)if(len >0){val message =String(buffer,0, len)println("Client Received: $message")}}}
使用带SSL的HttpServer
大多网络编程框架,都是默认支持Https协议的,但仅限于由CA机构颁发的可信任证书
对于人工签发,未经CA机构授权的自签名证书,必须由开发者自己去实现验证逻辑
import com.sun.net.httpserver.HttpsConfigurator
import com.sun.net.httpserver.HttpsServer
import java.io.*import java.net.InetSocketAddress
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.security.KeyStore
import java.util.*import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
constval serverKeyStore ="resources/private.keystore"constval clientKeyStore ="resources/public.truststore"constval passphrase ="123456"constval serverPort =18001funmain(){Thread(::launchHttpServer).start()
Thread.sleep(1500)Thread(::launchHttpClient).start()}funlaunchHttpServer(){// load key manager from key storeval keyManagerFactory = KeyManagerFactory.getInstance("SunX509")val keyStore = KeyStore.getInstance("JKS")
keyStore.load(FileInputStream(serverKeyStore), passphrase.toCharArray())
keyManagerFactory.init(keyStore, passphrase.toCharArray())val keyManagers = keyManagerFactory.keyManagers
// configure ssl contextval context = SSLContext.getInstance("TLS")
context.init(keyManagers,null,null)// create https serverval server = HttpsServer.create(InetSocketAddress(serverPort),10)// configure httpsval configurator =HttpsConfigurator(context)
server.httpsConfigurator = configurator
// create service
server.createContext("/home"){ exchange ->val response =Date().toString().encodeToByteArray()
exchange.sendResponseHeaders(200, response.size.toLong())
exchange.responseBody.write(response)
exchange.responseBody.close()}// start https server
server.start()}funlaunchHttpClient(){// load trust manager from trust storeval trustManagerFactory = TrustManagerFactory.getInstance("SunX509")val trustStore = KeyStore.getInstance("JKS")
trustStore.load(FileInputStream(clientKeyStore), passphrase.toCharArray())
trustManagerFactory.init(trustStore)val trustManagers = trustManagerFactory.trustManagers
// init ssl contextval context = SSLContext.getInstance("TLS")
context.init(null, trustManagers,null)// create http clientval uri = URI.create("https://localhost:18001/home")val client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).sslContext(context).build()// send requestval request = HttpRequest.newBuilder().GET().uri(uri).build()// get responseval response = client.send(request, HttpResponse.BodyHandlers.ofString())println(response.body())}
使用带SSL的WebSocket
这里我们使用比较出名的
Java-WebSocket
库来实现WebSocket服务端和客户端功能
api("org.java-websocket:Java-WebSocket:1.5.1")
为WebSocketServer设置SSL
funsetServerSSL(server: WebSocketServer){// load key manager from key storeval keyManagerFactory = KeyManagerFactory.getInstance("SunX509")val keyStore = KeyStore.getInstance("JKS")
keyStore.load(FileInputStream(serverKeyStore), passphrase.toCharArray())
keyManagerFactory.init(keyStore, passphrase.toCharArray())val keyManagers = keyManagerFactory.keyManagers
// configure ssl contextval context = SSLContext.getInstance("TLS")
context.init(keyManagers,null,null)// configure server sslval websocketServerFactory =DefaultSSLWebSocketServerFactory(context)
server.setWebSocketFactory(websocketServerFactory)}
为WebSocketClient设置SSL
funsetClientSSL(client: WebSocketClient){// load trust manager from trust storeval trustManagerFactory = TrustManagerFactory.getInstance("SunX509")val trustStore = KeyStore.getInstance("JKS")
trustStore.load(FileInputStream(clientKeyStore), passphrase.toCharArray())
trustManagerFactory.init(trustStore)val trustManagers = trustManagerFactory.trustManagers
// configure ssl contextval context = SSLContext.getInstance("TLS")
context.init(null, trustManagers,null)// configure client sslval sslSocketFactory = context.socketFactory
client.setSocketFactory(sslSocketFactory)}
在OkHttp中自定义KeyManager和TrustManager
以上案例,都是通过KeyStore来实现KeyManager和TrustManager的管理功能
现在我们不用KeyStore,通过自定义规则,来实现秘钥管理和证书验证功能
我们以OkHttp框架为例
import okhttp3.OkHttpClient
import java.security.cert.X509Certificate
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
import javax.net.ssl.X509TrustManager
funsetOkHttpSSL(builder: OkHttpClient.Builder){val trustManager =object: X509TrustManager {overridefuncheckClientTrusted(chain: Array<out X509Certificate>, authType: String){}overridefuncheckServerTrusted(chain: Array<out X509Certificate>, authType: String){}overridefungetAcceptedIssuers(): Array<X509Certificate>=emptyArray()}val trustManagers =arrayOf(trustManager)val hostnameVerifier = HostnameVerifier { hostname: String, session: SSLSession ->true}val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustManagers,null)val socketFactory = sslContext.socketFactory
builder.sslSocketFactory(socketFactory, trustManager)
builder.hostnameVerifier(hostnameVerifier)}
现在我们用OkHttp来替换上面的HttpClient来访问服务器
funlaunchHttpClient(){val url ="https://localhost:18001/home"// create okhttp clientval builder = OkHttpClient.Builder()setOkHttpSSL(builder)val client = builder.build()// create requestval request = Request.Builder().url(url).get().build()// execute callval call = client.newCall(request)val response = call.execute()// print responseval responseBody = response.body.string()println(responseBody)}
这里我们为了演示,不让问题复杂化,只是简单地信任了所有的证书,并不能起到实际的安全作用
关于KeyManager,TrustManager,HostnameVerifier的正式用法,可以参考以下类的源码
SunX509KeyManagerImpl
X509TrustManagerImpl
OkHostnameVerifier
SSL证书格式
- JKS,二进制格式存储,Java专属格式一般为私钥+证书+密码,或只有公钥的组合常用于Tomcat服务器
- PEM,文本格式存储可保存私钥和证书,一般以
BEGIN
开头,END
结尾,中间为BASE64编码字符串常用于Apache或Nginx服务器 - CER/DER,二进制格式存储只能保存证书常用于Windows服务器
- CRT,只是一个后缀名,可以是PEM编码,也可以是CER编码一般只用来保存证书,不存储私钥可以将私钥单独保存,以KEY作为后缀,来区分秘钥文件和证书文件KEY文件可以是PEM编码,也可以是CER编码
- P12/PKCS12/PFX,二进制格式存储一般同时包含私钥和证书,有密码保护常用于Windows IIS服务器
- CSR,证书请求文件这个不是证书,而是通过私钥向CA申请公钥的请求文件
- 最后,要注意的是,文件后缀和证书格式之间没有必然关系,还是以文件内容的实际存储格式为准
SSL证书转换工具
- OpenSSL
- KeyTool
- KeyStore Explorer
OpenSSL指令
genrsa
,生成秘钥req
,创建自签名根证书,或生成证书请求文件x509
,查看,创建,或转换证书-in
,输入文件-out
,输出文件-inform
,输入文件格式-outform
,输出文件格式-CA
,指定根证书-CAkey
,指定根证书私钥-CAserial
,指定CA证书-CAcreateserial
,创建下级CA证书
OpenSSL制作证书
# 创建根证书
openssl req -new-x509-days365-extensions v3_ca -keyout crt/ca.key -out crt/ca.crt
# 颁发服务端证书
openssl genrsa -out crt/server.key 2048
openssl req -out crt/server.csr -key crt/server.key -new
openssl x509 -req-in crt/server.csr -CA crt/ca.crt -CAkey crt/ca.key -CAcreateserial-out crt/server.crt -days365# 颁发客户端证书
openssl genrsa -out crt/client.key 2048
openssl req -out crt/client.csr -key crt/client.key -new
openssl x509 -req-in crt/client.csr -CA crt/ca.crt -CAkey crt/ca.key -CAcreateserial-out crt/client.crt -days365# 查看服务端证书
openssl x509 -in crt/server.crt -text-noout
End
到此为止,基本涵盖了Java SSL的所有核心内容,已经足以满足大家日常开发需要
更高阶的用法,可能在专业的领域才能用得到,希望大家遇到时,能勇于自己去研究!
版权归原作者 ByteFlys 所有, 如有侵权,请联系我们删除。