4. TLS 1.3与国密套件归纳总结
各位好,我们聊到今天这个章节,我心里其实挺感慨的。为什么呢?因为TLS 1.3和国密套件的结合,是我在信创项目里真正踩过坑、也真正尝到甜头的地方。你想想看,TLS 1.3本身就已经是大瘦身了,再塞进去SM2/SM3/SM4,组合得好是如虎添翼,组合不好就是一场灾难。
TLS 1.3的核心思路是什么?说白了,就是 删掉糊弄人的东西,逼着双方在第一条握手消息里就把家底亮出来。对于国密模块,这意味着我们不能像以前那样,在TLS 1.2里偷偷摸摸地协商了。国密必须从握手一开始就正大光明地登场。
4.1 为什么TLS 1.3是国密的最佳舞台?
我记得2018年刚开始做国密TLS改造时,团队还在用TLS 1.2改。那代码逻辑,唉,光是处理密钥交换的重协商,就多出了上千行状态机代码。TLS 1.3砍掉了这个历史包袱。
我个人习惯把TLS 1.3对国密套件的支持总结为三个关键点:
- 握手简化:从2-RTT降为1-RTT。国密模块的密钥生成和签名验证本来就要做SM2点乘,减少了来回次数,延迟明显下降。
- 算法惰性协商:客户端在
ClientHello里直接把支持的国密算法组列出来。服务端看到SM2签名、SM4-CBC、SM3-HMAC,就知道“噢,你要走国密”,直接挑一个套件,不用来回试探。 - 签名独立验证:国密的证书链条验证不再依赖ECIES或RSA加密,而是纯SM2签名的验证。TLS 1.3将ServerCertificate和CertificateVerify拆成两个独立消息,这正好契合国密体系的设计。
你肯定会问,那0-RTT模式呢?国密能不能用?嗯,这里要注意,0-RTT依赖于PSK(预共享密钥)。如果你们用国密做票据(ticket),SM4-GCM加密PSK是可以的。但早期握手必须还是1-RTT,第一次连接跑完才能拿PSK。我在项目中遇到过,有同事想第一次连接就直接0-RTT,结果协商失败,因为服务端根本没有这个PSK的上下文。这个坑,我先帮你避开了。
4.2 握手流程差异分析
咱们直接对比一下TLS 1.2和TLS 1.3在国密场景下的核心差异——别小看这个变化,它直接影响你实现国密模块时的代码结构。
| 特性 | TLS 1.2 + 国密 | TLS 1.3 + 国密 |
|---|---|---|
| 握手轮次 | 2-RTT(需额外ChangeCipherSpec) | 1-RTT(无ChangeCipherSpec) |
| 密钥交换 | 固定使用SM2密钥交换(类似ECDHE) | 使用HelloRetryRequest或直接协商key_share |
| 签名算法 | SM2-WITH-SM3作为签名算法标识 | 独立signature_algorithms扩展,SM2签名分开列举 |
| 国密支持深度 | 需要同时处理密码套件和ECC曲线 | 只依赖扩展(key_share + signature_algorithms) |
| 推荐实践 | 复杂度高,容易遗漏一方支持的曲线 | 结构清晰,推荐优先使用 |
从这个表你能看出来,TLS 1.3对国密的友好,本质上是把算法选择权从 静态套件 转移到了 动态扩展。举个例子,在TLS 1.2里,你要协商SM4-CBC,得先确保双方都认识某个特定的密码套件。而TLS 1.3只看ciphersuites扩展里的算法组合,模块化程度很高。
4.3 信创环境下的实践要点
好了,理论讲完了,咱们谈谈实际落地。我做过两个比较大的信创项目,一个用Nginx + TongSuo,另一个自研了一个轻量级国密网关。这里有些个人经验分享。
- 密钥共享不要硬编码:我记得第一次做网关时,图省事,把服务端的SM2私钥直接写在了配置文件里。后来安全扫描直接就挂了。正确的做法是用HSM(硬件安全模块)或者KMS(密钥管理服务)管理国密密钥,TLS 1.3握手时通过PKCS#11接口去签名。
- 伪随机函数(PRF)的重写:TLS 1.3使用HKDF作为密钥推导函数。国密体系下,你需要把HKDF-Extract里的基础哈希算法换成SM3。不是简单的替换,因为SM3的输出长度是256位,和SHA-256一致,但内部轮函数不同。我在项目中遇到过,直接用现成的OpenSSL的HKDF,把算法ID从SHA256换成SM3,结果因为SM3上下文处理不一样,导致Finished消息校验失败。
- HelloRetryRequest的应对:如果客户端没带key_share,或者带了但服务端不认,TLS 1.3会发HelloRetryRequest,要求客户端重新发起。这会增加一次RTT。对于国密场景,我建议客户端最好把主流的SM2曲线(比如curveSM2)加上一些备选曲线都放在key_share里,避免触发重试,这对移动端尤其重要。
CertificateVerify消息明确要求签名证书的用法只能是数字签名。如果你用的证书是“既可以签名又可以加密”,服务器端校验时会通不过。我踩过这个坑,排查了两天才找到原因。
4.4 一张图看懂核心逻辑
下面这张图,基本把我前面讲到的TLS 1.3国密握手的关键路径画出来了。你可以把这张图当作“国密集成清单”来看——每一步都要检查。
4.5 避坑与建议
最后,我把在多个环境下的调试经验浓缩成几条实用的检查清单——你在落地TLS 1.3国密时,可以直接拿着这个清单去校验代码。
- 签名算法扩展(signature_algorithms):确认服务端响应时,如果客户端只发了
SM2_SM3作为签名算法,服务端不能降级用RSA签名。否则它会发一个TLS 1.3的certificate_required报警。我曾经在一个信创项目里,直接用现有国密算法套件去对接TLS 1.3的伪随机函数,结果发现扩展密钥推导时的盐值长度不对。 - key_share扩展的优先级:我建议你把
SM2P256v1(也就是国密推荐的SM2曲线)放在key_share的第一位。有些商用网关会只选择第一个它能解析的出密钥交换算法。如果第一位是secp256r1,它就不会走国密,即使后面有国密曲线。 - 证书链的编码:国密SM2证书的
SubjectPublicKeyInfo内,曲线OID必须显式写出1.2.156.10197.1.301。如果有些CA图省事,把它写成未知的OID,TLS 1.3握手时服务端解析不出算法组,会直接中断。
supported_groups(包含国密曲线)、signature_algorithms(包含SM2签名)、key_share(包含对应公钥)。其他的,比如HelloRetryRequest的处理,底层框架已经帮你做了。我个人习惯是先写一个单连接测试脚本,用GnuTLS的--insecure带自定义曲线去验证,跑通了再改到生产级别的Nginx里。
嗯,话说到这里,TLS 1.3和国密套件的归纳总结基本就到位了。核心记住一点:TLS 1.3通过扩展驱动的机制,为国密提供了一条更干净的集成路径。别再像过去那样改密码套件表了——拥抱扩展,你后面的维护成本会低很多。