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,另一个自研了一个轻量级国密网关。这里有些个人经验分享。

  1. 密钥共享不要硬编码:我记得第一次做网关时,图省事,把服务端的SM2私钥直接写在了配置文件里。后来安全扫描直接就挂了。正确的做法是用HSM(硬件安全模块)或者KMS(密钥管理服务)管理国密密钥,TLS 1.3握手时通过PKCS#11接口去签名。
  2. 伪随机函数(PRF)的重写:TLS 1.3使用HKDF作为密钥推导函数。国密体系下,你需要把HKDF-Extract里的基础哈希算法换成SM3。不是简单的替换,因为SM3的输出长度是256位,和SHA-256一致,但内部轮函数不同。我在项目中遇到过,直接用现成的OpenSSL的HKDF,把算法ID从SHA256换成SM3,结果因为SM3上下文处理不一样,导致Finished消息校验失败。
  3. HelloRetryRequest的应对:如果客户端没带key_share,或者带了但服务端不认,TLS 1.3会发HelloRetryRequest,要求客户端重新发起。这会增加一次RTT。对于国密场景,我建议客户端最好把主流的SM2曲线(比如curveSM2)加上一些备选曲线都放在key_share里,避免触发重试,这对移动端尤其重要。
⚠️ 重点警告:国密SM2的证书链中,中间CA证书的extendedKeyUsage里最好不要包含SM2签名与SM2密钥交换的混用。有些信创环境下的CA为了兼容,把签名和加密两个密钥用法都打上了。在TLS 1.3里,CertificateVerify消息明确要求签名证书的用法只能是数字签名。如果你用的证书是“既可以签名又可以加密”,服务器端校验时会通不过。我踩过这个坑,排查了两天才找到原因。

4.4 一张图看懂核心逻辑

下面这张图,基本把我前面讲到的TLS 1.3国密握手的关键路径画出来了。你可以把这张图当作“国密集成清单”来看——每一步都要检查。

TLS 1.3 国密握手核心节点 1. ClientHello (附带supported_groups扩展) 2. ServerHello (选择国密key_share) 3. Certificate Verify (SM2签名验证) 4. PSK + 国密衍生 (HKDF-Extract / SM3) 5. Finished (SM4-GCM解密验证) 安全数据传输 (SM4-GCM记录层) ⚠️ 避坑点 1. 国密key_share需同时 携带曲线信息。 2. PSK衍生必须用SM3。

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握手时服务端解析不出算法组,会直接中断。
💡 经验之谈:如果你是在BoringSSL或定制OpenSSL上进行改造,TLS 1.3的国密模块开发工作量其实不大。你只需要关注三个扩展的合理配置:supported_groups(包含国密曲线)、signature_algorithms(包含SM2签名)、key_share(包含对应公钥)。其他的,比如HelloRetryRequest的处理,底层框架已经帮你做了。我个人习惯是先写一个单连接测试脚本,用GnuTLS的--insecure带自定义曲线去验证,跑通了再改到生产级别的Nginx里。

嗯,话说到这里,TLS 1.3和国密套件的归纳总结基本就到位了。核心记住一点:TLS 1.3通过扩展驱动的机制,为国密提供了一条更干净的集成路径。别再像过去那样改密码套件表了——拥抱扩展,你后面的维护成本会低很多。