一文学习和实践 当今互联网安全的基石 - TLS/SSL

[[N_OSI 模型.TCP 协议.封包]]
[[Java TLS Socket]]
[[TLS&SSL协议]]

What is SSL/TLS ?

TLS(Transport Layer Security)即传输层安全协议,SSL(Secure Sockets Layer)即安全套接层协议。它们都是用于在网络通信中保障数据安全和隐私的加密协议。

TLS(Transport Layer Security)传输层安全协议

  1. 发展历程
  • TLS 是 SSL 协议的继任者。由于 SSL 协议存在一些安全漏洞,并且随着网络安全需求的不断提高,IETF(Internet Engineering Task Force)对 SSL 3.0 进行了改进,推出了 TLS 1.0 协议,于 1999 年发布。TLS 协议在后续又不断更新版本,如 TLS 1.1、TLS 1.2 和 TLS 1.3,每一个新版本都在安全性和性能等方面有所提升。
  1. 工作原理
  • TLS 的工作原理与 SSL 类似,也包括握手阶段。在 TLS 握手过程中,客户端和服务器会协商协议版本、加密算法套件、交换密钥等。TLS 1.3 相比之前的版本,简化了握手过程,减少了握手消息的往返次数,从而提高了连接建立的速度,并且增强了安全性,如加强了密钥交换的安全性。
  • 例如,TLS 1.3 使用了更安全的密钥交换算法,如基于椭圆曲线的 Diffie - Hellman 密钥交换(ECDHE),这种算法能够有效抵抗中间人攻击等安全威胁。
  1. 应用场景
  • TLS 的应用范围非常广泛。它不仅用于 Web 安全,还用于电子邮件安全(如 IMAP、POP3 和 SMTP 协议的安全版本)、虚拟专用网络(VPN)等众多网络通信场景。在现代互联网中,几乎所有需要安全通信的地方都会优先考虑使用 TLS 协议。

SSL (Secure Sockets Layer)安全套接层协议

  1. 发展历程
  • SSL 最初是由网景公司(Netscape)在 1994 年开发的,目的是为了保障网络通信的安全,特别是在 Web 浏览器和服务器之间的通信安全。SSL 1.0 由于存在安全漏洞从未发布,SSL 2.0 在 1995 年发布,但也有安全问题。SSL 3.0 于 1996 年发布,它修复了许多 SSL 2.0 的问题,成为当时比较流行的安全协议。
  1. 工作原理
  • SSL 协议在应用层和传输层之间工作。它通过使用公钥和私钥加密技术来保证通信的保密性。当客户端(如浏览器)与服务器建立连接时,首先会进行 SSL 握手过程。
  • 在握手阶段,客户端向服务器发送客户端支持的加密算法列表等信息。服务器从列表中选择一种加密算法,并将服务器的证书发送给客户端。客户端验证服务器证书的合法性,例如检查证书是否由可信任的证书颁发机构(CA)颁发,证书是否过期等。
  • 如果证书验证通过,客户端和服务器就会协商出一个对称加密密钥。之后的数据传输就使用这个对称密钥进行加密,这样可以保证数据在传输过程中的保密性,因为只有客户端和服务器知道这个对称密钥。
  1. 应用场景
  • SSL 主要应用于 Web 安全。例如,在早期的 https(Hypertext Transfer Protocol Secure)网站访问中,就是通过 SSL 协议来加密浏览器和服务器之间的数据传输。这使得用户在网站上输入的敏感信息,如登录密码、信用卡信息等在传输过程中不会被窃取。

两者关系和区别

  1. 关系
  • TLS 可以看作是 SSL 的升级版。TLS 协议在设计上借鉴了 SSL 协议的很多理念,并且在 SSL 的基础上进行了改进和扩展,以应对不断出现的新的安全威胁和网络通信需求。
  1. 区别
  • 安全性:TLS 在安全性能上比 SSL 更优越。例如,SSL 3.0 存在 POODLE(Padding Oracle On Downgraded Legacy Encryption)攻击漏洞,而 TLS 协议通过不断更新版本来修复类似的安全漏洞。TLS 1.3 更是在密钥交换等关键环节采用了更先进的加密技术,提供了更强的安全性。
  • 兼容性:SSL 是比较早期的协议,在一些旧的系统和软件中有应用。但随着网络安全标准的提高,现代的应用和系统更倾向于使用 TLS 协议。TLS 在兼容性方面也在不断扩展,能够更好地与各种新的网络技术和应用场景相结合。

目前常用的是 TLS 1.2 和 TLS 1.3; SSL不多见了

HTTPS 工作流程

  1. Client 发起请求(端口443)
  2. Server 返回公钥证书
  3. Client 验证证书
  4. Client 生成对称密钥,用公钥加密后发给Server
  5. Server使用私钥解密,得到对称密钥
  6. C/S双方使用对称密钥:
    • 加密明文并发送
    • 解密密文得到明文

For TLS 1.2

Pasted image 20241218204449
Pasted image 20241218211322

For TLS 1.3

Pasted image 20241218204507

Pasted image 20241218203919

TLS

Pasted image 20231116112623

证书签名与验证

颁发证书的过程

  1. 撰写证书元数据: 包括 签发人(Issuer), 地址, 签发时间, 有效期 等, 还包括证书持有者(Owner)基本信息, 比如 DN(DNS Name, 即证书生效的域名), Owner 公钥 等信息
  2. 使用通用的 Hash 算法(如SHA-256)对证书元数据计算生成数字摘要
  3. 使用 签发人 Issuer (CA)的私钥 对该数字摘要进行加密, 生成一个加密的数字摘要, 也就是Issuer的数字签名
  4. 数字签名附加到数字证书上, 变成一个签过名的数字证书
    签过名的数字证书Issuer (CA)的公钥, 一同发给证书使用者(注意, 将公钥主动发给使用者是一个形象的说法, 通常系统或者浏览器都内置 Issuer (CA)的公钥 为可信任的根证书)

所以 CA的私钥是绝对不能被泄露, 它作为互联网被信任的基础

验证证书的过程

  1. 证书使用者获通过某种途径(如浏览器访问)获取到该数字证书, 解压后分别获得证书元数据数字签名,(Owner)的公钥
  2. 使用同样的Hash算法计算证书元数据的数字摘要
  3. 使用 签发人 Issuer (CA)的公钥 对数字签名进行解密, 得到 解密后的数字摘要

从第1步获得的数字摘要值, 再对比 2 和 3 两个步骤得到的数字摘要值; 如果相同, 则说明这个数字证书确实是被 签发人Issuer 验证过合法证书, 证书中的信息

最主要的是确定 Owner 的公钥是可信的, 此后通信 信任使用该公钥解密对方私钥加密的数据

in short 握手时服务端需要提供自己的公钥 和被 ca签发过的数字摘要; 关键在与客户端需要一个可信任 Issuer(CA)

证书链

在 chrome 证书信息-> 证书路径, 可以看到证书的签发链.

例, 百度的网站:

GlobalSign Root CA -> GlobalSign Organization Validation CA -> baidu.com
Pasted image 20231116113047

这个路径可以抽象为三个级别:

end-user: 即 baidu.com, 该证书包含百度的公钥, 访问者就是使用该公钥将数据加密后再传输给百度, 即在 HTTPS 中使用的证书

intermediates: 即上文提到的 签发人 Issuer, 用来认证公钥持有者身份的证书, 负责确认 HTTPS 使用的 end-user 证书确实是来源于百度; 这类 intermediates 证书可以有很多级, 也就是说 签发人 Issuer 可能会有有很多级

root: 可以理解为最高级别的签发人 Issuer, 负责认证 intermediates 身份的合法性

in short 其实就是一个可信任的链条, 最终的目的就是为了保证 end-user 证书是可信的, 该证书的公钥也就是可信的.

参考: 关于证书链的一点认知

TLS 握手步骤

(0) Client 与 Server 之间建立 (TCP 三次握手) 连接

(1) Client 向 Server 发送 "client hello" 消息, 里面包含了安全相关的信息, 例如SSL/TLS 版本号, Client 支持的加密套件 (CipherSuite); "client hello" 消息还包含了一个随机数client random, 用于通信密钥的计算;
(可选)SSL/TLS 协议还允许 "client hello" 消息包含 Client 所支持的压缩算法

(2) Server 回复一条 "server hello" 消息, 里面包含了加密套件(Server 从 "client hello" 消息的 CipherSuites 列表中选择其中一个),session id 和 另一个随机数server random; Server 还会在消息中附带自己的数字证书;

(可选)如果 Server 需要 Client 的数字证书进行客户端认证, 会向 Client 发送 "client certificate request" 请求消息, 里面包含了Server 所支持的证书类型认可的证书颁发机构 CA (双向认证, 同认证客户端同理)

(3) Client 收到 "server hello",验证 Server 端的数字证书, 并得到证书中Server 端的公钥
(关键这里客户端有可信任的证书签发者Issuer, 验证服务端的证书)

(4) Client 向 Server 发送第三个随机数 pre-master secret; 与之前不同,这次的随机数使用了 Server 的公钥加密 (非对称加密); 现在双方同时拥有这三个随机数client random,server random,premaster secret, 可以用来计算生成共同的通信密钥 master secret 用于加密后面传输的业务数据;

(5 - 可选) 如果收到 Server 端发来的 "client certificate request" 请求消息, Client 会向 Server 发送一个使用 Client 自己的私钥加密过的随机数 (暂时记作 secret-A), 附带 Client 的数字证书; 或者发送一个 "no digital certificate alert" 无证书警告, 这种情况下基本可以认为 SSL/TLS 握手失败;

(6 - 可选) Server 验证 Client 发送过来的数字证书, 并得到证书中公钥对 Client 进行身份认证 (通过公钥解密上面那个 secret-A);

(7) Client 向 Server 发送 "finished" 消息, 使用第 4 步中计算出来的密钥进行加密传输 (对称加密), 这表示 Client 端握手阶段已经完成;

(8) Server 也向 Client 发送 "finished" 消息, 使用第 4 步中计算出来的密钥进行加密传输 (对称加密), 这表示 Server 端握手阶段完成;

(9) SSL/TLS 握手阶段完成, 接下来双方通信的消息都会使用协商出来的密钥进行加密 (对称加密)
Content Type: Handshake (22)

SLLhandshanke

证书的应用之一 —— TCP&SSL通信实例及协议分析(中)

每一条消息都会包含有ContentType,Version,HandshakeType等信息;

ContentType 指示SSL通信处于哪个阶段, 是握手(Handshake), 开始加密传输(ChangeCipherSpec)还是正常通信(Application)等, 见下表

Hex Dec Type
0x14 20 ChangeCipherSpec
0x15 21 Alert
0x16 22 Handshake
0x17 23 Application

Handshake Type是在handshanke阶段中的具体哪一步, 见下表

Code Description
0 HelloRequest
1 ClientHello
2 ServerHello
11 Certificate
12 ServerKeyExchange
13 CertificateRequest
14 ServerHelloDone
15 CertificateVerify
16 ClientKeyExchange
20 Finished

关于双向认证

所有通信中,涉及两个端点,即浏览器和它所连接的网站(即客户端和服务器)。 在单向SSL身份认证过程中,仅验证一个端点(服务器)的身份。
双向认证是指在SSL握手过程中将同时验证客户端和服务器的身份,所以双向认证SSL证书至少包括两个或两个以上的证书,一个是服务器证书,另一个或多个是客户端证书(即个人认证证书)。

主流的证书格式

主流的SSL证书格式:
DER、CER,文件是二进制格式,只保存证书,不保存私钥。

PEM,一般是文本格式,可保存证书,可保存私钥。
Privacy Enhanced Mail,一般为文本格式,以 -----BEGIN... 开头,以 -----END... 结尾。中间的内容是 BASE64 编码。这种格式可以保存证书和私钥,有时我们也把PEM 格式的私钥的后缀改为 .key 以区别证书与私钥

CRT,可以是二进制格式,可以是文本格式,与 DER 格式相同,不保存私钥。
PFX P12,二进制格式,同时包含证书和私钥,一般有密码保护。
JKS,二进制格式,同时包含证书和私钥,一般有密码保护。

各服务器平台使用的证书格式:

Nginx pem
Apache pem
IIS pfx/pkcs12
Tomcat jks

Netty For TLS 实例

证书生成

CA 根证书生成

或者称为 Issuer , 它可以签发证书, 作为客户端和服务端共同信任者

  1. 创建一个私钥
    生成一个安全的私钥。这个私钥将用于签署根证书以及将来由该根CA签发的所有其他证书。为了确保安全性,使用足够长的密钥长度。
openssl genpkey -algorithm RSA -out root_ca.key -aes256 -pkeyopt rsa_keygen_bits:4096
# 不加密
# openssl genpkey -algorithm RSA -out root_ca.key -pkeyopt rsa_keygen_bits:2048

输入私钥密码

  1. 创建自签名的根证书
    使用刚刚生成的私钥来创建一个自签名的根证书。这一步骤不仅会生成证书本身,还会收集有关颁发者的详细信息,这些信息将嵌入到证书中。
openssl req -x509 -new -nodes -key root_ca.key -sha256 -days 36500 -out root_ca.crt
  • req 是用来处理证书请求的命令。
  • -x509 表示要创建一个X.509格式的自签名证书。
  • -new 表示这是一个新的证书请求。
  • -nodes 表示不加密私钥(仅适用于这次命令,因为私钥已经加密)。
  • -key root_ca.key 指定了用于签名的私钥。
  • -sha256 指定了哈希算法。
  • -days 3650 指定了证书的有效期为10年。
  • -out root_ca.crt 指定了输出文件名为 root_ca.crt

在运行该命令时,你会被要求填写一系列问题,这些问题的答案将作为证书的主题字段(Subject)的一部分。例如:

  • Country Name (2 letter code): 输入国家代码,如 CN
  • State or Province Name (full name): 输入省份或州的全名。
  • Locality Name (eg, city): 输入城市名。
  • Organization Name (eg, company): 输入组织名称。
  • Organizational Unit Name (eg, section): 输入部门名称。
  • Common Name (e.g. server FQDN or YOUR name): (注意这个) 输入通用名称,通常是组织的域名或描述性名称,对于根证书而言,可以是类似于 Root CA 的字符串。
  • Email Address: 可选,可以留空。
  1. 验证其内容
openssl x509 -in root_ca.crt -text -noout
  1. 客户端信任

至此

  • root_ca.crt 是CA 自签名的带公钥的证书
  • root_ca.key 是CA的私钥(绝对不能泄露)

为了让客户端信任这个新创建的根证书,需要将CA公钥安装到目标系统的受信任根证书存储中。
例如,在Windows上,你可以双击 root_ca.crt 文件,然后按照提示选择“安装证书”,并将安装位置设置为“受信任的根证书颁发机构

CA 签署/颁发服务端证书

  1. 生成目标实体的私钥
openssl genpkey -algorithm RSA -out server.key -aes256 -pkeyopt rsa_keygen_bits:2048
  1. 生成目标实体的签发请求 (CSR)
    使用刚刚生成的私钥来创建一个证书签名请求(CSR)。CSR包含了申请者的公钥以及一些识别信息,如组织名称、域名等。
openssl req -new -key server.key -out server.csr

# openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/C=CN/ST=State/L=City/O=Organization/CN=your.server.domain"
  1. 使用CA根证书的私钥 签署CSR
openssl x509 -req -in server.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out server.crt -days 7300 -sha256
# openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256

各文件说明

至此有6个文件分别是:
1. root_ca.crt (根CA证书公钥) - 自签名 公开 双方信任

  • 描述:这是由CA自身创建并签署的自签名证书,作为信任链的起点。根证书通常包含CA的公钥、名称和其他元数据,并且是由CA私钥签名的
  • 用途:根证书用于验证由该CA签发的所有下级证书的真实性。它通常被预装在操作系统、浏览器或其他应用程序的信任库中,作为受信任的根证书机构

2. root_ca.key (根CA证书私钥)

  • 描述:这是与根CA证书配对的私钥,用于签署其他证书。私钥是高度敏感的信息,不应公开或共享。
  • 用途:根CA私钥用于签署中间CA证书、服务器证书、客户端证书等。它是建立信任链的基础。
  • 安全性:私钥应存储在安全的地方,最好是在离线环境中,只有在需要签署新证书时才连接到网络。 它一旦被攻破,所有依赖它的证书都将失去信任。

3. root_ca.srl (序列号文件)

  • 描述:这是一个包含十六进制正整数的文件,用于记录已签发证书的序列号。每次签署新证书时,OpenSSL会从这个文件中读取当前的最大序列号,并为新证书分配一个递增的序列号。

4. server.crt (服务器公钥)

  • 描述:这是由CA签署的服务器证书,包含服务器的公钥以及关于证书的一些信息,如版本、序列号、签名算法、颁发者、持有者、有效期等。服务器证书用于加密客户端与服务器之间的通信,确保数据传输的安全性。
  • 用途:服务器证书安装在Web服务器上,用于HTTPS协议下的安全通信。客户端通过验证服务器证书的有效性来确认服务器的身份,并使用证书中的公钥进行加密通信。
  • 格式:常见的格式包括PEM(以 -----BEGIN CERTIFICATE----- 开头,-----END CERTIFICATE----- 结尾)和DER(二进制格式)。Linux系统通常使用 .crt 文件扩展名,而Windows系统可能使用 .cer

5. server.csr (证书签名请求)

  • 描述:这是证书申请者在申请数字证书时生成的文件,包含了公钥和标识名称(Distinguished Name, DN),如国家、省份、城市、组织名称、通用名称(通常是域名)等。CSR是以 -----BEGIN CERTIFICATE REQUEST----- 开头,-----END CERTIFICATE REQUEST----- 结尾的Base64编码格式。
  • 用途:CSR文件提交给CA后,CA会使用其根证书私钥对CSR中的信息进行签名,生成正式的数字证书。CSR文件本身不是证书,而是申请证书的请求。
  • 生成:CSR文件通常由服务器管理员使用OpenSSL等工具生成,同时生成相应的私钥。生成CSR时,必须确保私钥的安全性,因为CSR文件中不包含私钥

6. server.key (服务器私钥)

  • 描述:这是与服务器证书配对的私钥,用于解密客户端发送的加密信息。私钥是高度敏感的信息,不应公开或共享。
  • 用途:服务器私钥用于解密通过TLS/SSL协议传输的数据。在HTTPS通信中,客户端使用服务器证书中的公钥加密数据,服务器则使用私钥解密这些数据。
  • 格式:常见的格式包括PEM(以 -----BEGIN RSA PRIVATE KEY----- 或 -----BEGIN PRIVATE KEY----- 开头,-----END RSA PRIVATE KEY----- 或 -----END PRIVATE KEY----- 结尾)和DER(二进制格式)。Linux系统通常使用 .key 文件扩展名

每个角色验证需要

  1. **客户端需要
  • root_ca.crt:根 CA 的公钥证书。客户端将使用此证书来验证服务器的证书是否由可信的 CA 签发。
  1. **服务端端需要
  • server.key:服务器的私钥。服务器将使用此私钥来解密客户端发送的加密数据,并签署响应。
  • server.crt:服务器的公钥证书,由根 CA 签发。服务器将使用此证书向客户端证明其身份。
  • root_ca.crt(可选, 双向验证必选):如果你的服务器证书是由中间 CA 签发的,你可以将根 CA 证书或中间 CA 证书链一起加载到服务器的信任库中,以便客户端可以验证整个证书链。

在Netty 使用

证书的加载

  1. 公钥加载 .pem, .crt, .cer : 通常以 -----BEGIN CERTIFICATE----- 开头,以 -----END CERTIFICATE----- 结尾。它既可以包含公钥证书,也可以包含私钥。Java本身支持 pem

  2. 私钥加载 .key: openssl 默认给私钥加密了 , 将其转为不加密的, 可使用 bouncycastle加载之

# **转换为 PKCS#8 格式 (PEM 编码)**
openssl pkcs8 -topk8 -inform PEM -in server.key -outform PEM -nocrypt -out server_pkcs8.key

# **转换为 PKCS#8 格式 (DER 编码)** 二进制格式
openssl pkcs8 -topk8 -inform PEM -in server.key -outform DER -nocrypt -out server_pkcs8.der
  • 使用 bouncycastle 加载 pkcs8 私钥
implementation("cn.hutool:hutool-all:5.8.16")
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")


public static void main(String[] args) throws Exception {

	final String ca_cet = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\root_ca.crt";
	final String server_crt = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server.crt";

	final String server_key = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server.key";
	final String server_pkcs8_key = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server_pkcs8.key";
	
	PublicKey root_caKey = PemUtil.readPemPublicKey(FileUtil.getInputStream(ca_cet));
	System.out.println("root_caKey  ==> "+root_caKey );
	Certificate certificate = SecureUtil.readX509Certificate(FileUtil.getInputStream(server_crt));
	System.out.println("X509C  ==> "+root_caKey );
	
	PublicKey server_crtKey = PemUtil.readPemPublicKey(FileUtil.getInputStream(server_crt));
	System.out.println("server_crtKey  ==> "+server_crtKey );

	PrivateKey privateKey = PemUtil.readPemPrivateKey(FileUtil.getInputStream(server_pkcs8_key));
	System.out.println("server_pkcs8  ==> "+privateKey );
}
  • 使用 bouncycastle 如何 加载默认 openssl 加密的 server.key ?
public static PrivateKey loadEncryptedPrivateKeyFromPem(String pemFile, char[] password) throws Exception {
	try (Reader reader = new FileReader(pemFile);
		 PEMParser pemParser = new PEMParser(reader)) {

		Object object = pemParser.readObject();
		JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

		if (object instanceof PEMEncryptedKeyPair) {
			PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) object;
			InputDecryptorProvider decryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password);
			KeyPair kp = converter.getKeyPair(encryptedKeyPair.decryptKeyPair(decryptorProvider));
			return kp.getPrivate();
		} else {
			throw new IllegalArgumentException("Unsupported encrypted key type: " + object.getClass().getName());
		}
	}
}


单向验证的TLS

客户端单向了验证服务端

服务端

public static void main(String[] args) throws Exception {

	NioEventLoopGroup boosGroup = new NioEventLoopGroup();
	final ServerBootstrap serverBootstrap = new ServerBootstrap();
	int port = 19977;
	// 1. 加载 PEM 私钥,  2.  加载 证书
	final String server_crt = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server.crt";
	final String server_pkcs8_key = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server_pkcs8.key";

	Certificate certificate = SecureUtil.readX509Certificate(FileUtil.getInputStream(server_crt));
	PrivateKey privateKey = PemUtil.readPemPrivateKey(FileUtil.getInputStream(server_pkcs8_key));
	// 3. 创建 KeyStore 并将私钥和证书添加到其中
	KeyStore keyStore = KeyStore.getInstance("PKCS12");
	keyStore.load(null, null);
	keyStore.setKeyEntry("alias", privateKey, "password".toCharArray(), new Certificate[]{certificate});

	// 4. 创建 KeyManagerFactory 并初始化
	KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
	kmf.init(keyStore, "password".toCharArray());

	// 5. 构建 SslContext
	final SslContext sslContext = SslContextBuilder.forServer(kmf)
			.build();

	serverBootstrap
			.group(boosGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 1024)
			.childOption(ChannelOption.SO_KEEPALIVE, true)
			.childOption(ChannelOption.TCP_NODELAY, true)
			.childHandler(new ChannelInitializer<NioSocketChannel>() {
				@Override
				protected void initChannel(NioSocketChannel ch) {
					// 添加 SSL/TLS 处理器
					ch.pipeline().addLast(sslContext.newHandler(ch.alloc()));
					ch.pipeline().addLast(new StringDecoder())
							.addLast(new StringEncoder())
							.addLast(new ServerHandler());
				}
			});
	ChannelFuture f = serverBootstrap.bind(port).addListener(future -> {
		if (future.isSuccess()) {
			System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
		} else {
			System.err.println("端口[" + port + "]绑定失败!");
		}
	});
	//下面会进行阻塞, 等待服务器连接关闭之后 main 方法退出, 程序结束;
	f.channel().closeFuture().sync();
}

客户端

 public static void main(String[] args) throws Exception {
 
	Bootstrap bootstrap = new Bootstrap();
	NioEventLoopGroup group = new NioEventLoopGroup();

	String host = "192.168.20.130";
	int port = 19977;

	// 1. 加载 CA 证书
	final String ca_cet = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\root_ca.crt";
	Certificate certificate = SecureUtil.readX509Certificate(FileUtil.getInputStream(ca_cet));

	// 2. 创建 KeyStore 并将 CA 证书添加到信任库中
	KeyStore trustStore = KeyStore.getInstance("JKS");
	trustStore.load(null, null);
	trustStore.setCertificateEntry("ca", certificate);

	// 3. 创建 TrustManagerFactory 并初始化
	TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
	tmf.init(trustStore);

	// 4. 构建 SslContext
	SslContext build = SslContextBuilder.forClient()
			.trustManager(tmf)
			.build();

	bootstrap.group(group)
			.channel(NioSocketChannel.class)
			.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
			.option(ChannelOption.SO_KEEPALIVE, true)
			.handler( new SimpleChannelInboundHandler<String>() {
				@Override
				protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
					System.out.println("客户端接收: "+msg);
				}
			})
			.connect(host, port).addListener(new ChannelFutureListener() {
				@Override
				public void operationComplete(ChannelFuture future) throws Exception {
					future.channel().pipeline()
							.addFirst(new StringEncoder())
							.addFirst(new StringDecoder())
							.addFirst( build.newHandler(future.channel().alloc() ));
					final String helloToServer = "珠玉买歌笑,糟糠养贤才。方知黄鹄举,千里独徘徊。"+System.currentTimeMillis();
					future.channel().writeAndFlush(helloToServer);
				}
			});
//        group.shutdownGracefully();
    }

Android 上使用 java.security.KeyStore 时,可能会遇到 KeyStoreException: JKS not found 的错误。这是因为 Android 的安全框架与标准 Java 不同,Android 并不支持 JKS(Java KeyStore)格式。相反,Android 支持其他类型的密钥库,如 BKS(Bouncy Castle KeyStore)和 PKCS12。

private SslContext getSslContext() throws Exception {
	Certificate caCert = CertificateUtils.getCertificate();
	KeyStore trustStore = KeyStore.getInstance("BKS");  // 使用 BKS 或 PKCS12
	trustStore.load(null, null);
	trustStore.setCertificateEntry("ca", caCert);

	TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
	tmf.init(trustStore);

	SslContext build = SslContextBuilder.forClient()
			.trustManager(tmf)
			.build();
	return build;
}

抓包看看

Pasted image 20241219145126

双向验证的TLS

其实同服务端一样的逻辑 1.生成客户端私钥, 2.生成客户端CRS, 3.用CA根证书签发客户端的公钥

客户端证书准备

生成客户端私钥
openssl genpkey -algorithm RSA -out client.key -aes256 -pkeyopt rsa_keygen_bits:2048

转换为pkcs#8 格式

openssl pkcs8 -topk8 -inform PEM -in client.key -outform PEM -nocrypt -out client_pkcs8.key
创建客户端的证书签名请求 (CSR)
openssl req -new -key client.key -out client.csr
使用根 CA 签名客户端的 CSR
openssl x509 -req -in client.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out client.crt -days 7300 -sha256

服务端

public static void main(String[] args) throws Exception {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        final ServerBootstrap serverBootstrap = new ServerBootstrap();
        int port = 19977;
        // 1. 加载 PEM 私钥,  2.  加载 证书
        final String server_crt = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server.crt";
        final String server_pkcs8_key = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\server_pkcs8.key";

        Certificate certificate = SecureUtil.readX509Certificate(FileUtil.getInputStream(server_crt));
        PrivateKey privateKey = PemUtil.readPemPrivateKey(FileUtil.getInputStream(server_pkcs8_key));
        // 3. 创建 KeyStore 并将私钥和证书添加到其中
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(null, null);
        keyStore.setKeyEntry("alias", privateKey, "password".toCharArray(), new Certificate[]{certificate});

        // 4. 创建 KeyManagerFactory 并初始化
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, "password".toCharArray());

        //双向认证 CA根证书
        final String root_ca = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\root_ca.crt";
        X509Certificate caCertificate = (X509Certificate) SecureUtil.readX509Certificate(FileUtil.getInputStream(root_ca));

        // 5. 构建 SslContext
        final SslContext sslContext = SslContextBuilder.forServer(kmf)
                .trustManager(caCertificate)
                .clientAuth(io.netty.handler.ssl.ClientAuth.REQUIRE)//双向认证 > 配置要求客户端也认证
                .build();

        serverBootstrap
                .group(boosGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 添加 SSL/TLS 处理器
                        ch.pipeline().addLast(sslContext.newHandler(ch.alloc()));
                        ch.pipeline().addLast(new StringDecoder())
                                .addLast(new StringEncoder())
                                .addLast(new ServerHandler());
                    }
                });
        ChannelFuture f = serverBootstrap.bind(port).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
            } else {
                System.err.println("端口[" + port + "]绑定失败!");
            }
        });
        //下面会进行阻塞, 等待服务器连接关闭之后 main 方法退出, 程序结束;
        f.channel().closeFuture().sync();
    }

客户端

public static void main(String[] args) throws Exception {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        String host = "192.168.20.130";
        int port = 19977;

        // 1. 加载 CA 证书
        final String ca_cet = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\root_ca.crt";
        Certificate certificate = SecureUtil.readX509Certificate(FileUtil.getInputStream(ca_cet));

        // 2. 创建 KeyStore 并将 CA 证书添加到信任库中
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(null, null);
        trustStore.setCertificateEntry("ca", certificate);

        // 3. 创建 TrustManagerFactory 并初始化
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        // 双向认证 > 客户端证书
        final String client_crt = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\client.crt";
        final String client_pkcs8_key = "E:\\content-for-work\\2024-12XXXXAPP\\tls_test\\client_pkcs8.key";

        // 4. 构建 SslContext
        SslContext build = SslContextBuilder.forClient()
                .keyManager(FileUtil.file(client_crt), FileUtil.file(client_pkcs8_key))
                .trustManager(tmf)
                .build();

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler( new SimpleChannelInboundHandler<String>() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                        System.out.println("客户端接收: "+msg);
                    }
                })
                .connect(host, port).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        future.channel().pipeline()
                                .addFirst(new StringEncoder())
                                .addFirst(new StringDecoder())
                                .addFirst( build.newHandler(future.channel().alloc() ));
                        final String helloToServer = "珠玉买歌笑,糟糠养贤才。方知黄鹄举,千里独徘徊。"+System.currentTimeMillis();
                        future.channel().writeAndFlush(helloToServer);
                    }
                });
//        group.shutdownGracefully();
    }

最后, 抓包看看

Pasted image 20241220115443

posted @ 2026-01-07 21:18  daidaidaiyu  阅读(147)  评论(2)    收藏  举报