你大概率没有亲眼见过但每次你打开一个 https 开头的网页在浏览器状态栏转圈的零点几秒里客户端和服务器已经悄悄完成了一次密钥协商。我以前抓 TLS 握手包调试的时候盯着 “Client Key Exchange” 这条消息看了很久才真正理解这几百字节背后藏着一个经典难题两个从没见过面、也没提前共享任何秘密的实体怎么在一根能全程监听的通道上各自算出一个完全相同的会话密钥这就是密钥交换更准确叫密钥协商要解决的事。这篇文章我会把密钥协商的基础原理、RSA 密钥交换为什么被逐步禁用、以及国密 SM2 密钥协商协议的完整流程一次说清楚还会附上能直接抄的配置步骤和排障经验。适合正在做 TLS 运维、等保合规改造以及刚入门密码学的开发同学。1. 密钥协商与密钥交换一次握手里到底发生了什么1.1 为什么不能在网络上直接传密钥先回到最朴素的问题。HTTPS 握手里最终要用的会话密钥是 AES 这类对称加密的密钥因为对称加密速度快适合加密海量数据。但对称加密的前提是通信双方必须持有同一把密钥那么问题来了这把密钥怎么给到对方最直觉的方案是“直接传”“嘿这是我的密钥接下来我们用这个加密。”这在真实网络里完全不可行。网络是开放的任何中间节点、任何旁路监听者都能看到这个消息密钥一旦被截获整个加密就形同虚设。可以想象成你要把一个装有贵重物品的保险箱寄给朋友但保险箱的钥匙只能放在同一个包裹里寄那等于没锁。密钥分发Key Distribution就是对称加密体系里最经典的“鸡生蛋”问题而密钥协商协议就是为了打破这个死循环而存在的。1.2 密钥协商和密钥交换一字之差场景完全不同很多人把密钥协商和密钥交换混用实际上在密码学里这是两种思路。密钥交换Key Exchange / Key Transport指的是一方生成密钥然后通过加密或其他手段安全地传送给另一方。最典型的例子是传统 TLS 里的 RSA 密钥传输客户端生成一个随机数预主密钥用服务器的 RSA 公钥加密后发过去服务器用私钥解出来。整个过程里密钥是“被传输”的它确实经过网络只是被公钥加密保护了。密钥协商Key Agreement则完全不同。通信双方各自生成随机数通过公开信道交换一些中间参数最终在本地计算出同一个密钥。最重要的是这个共同的密钥本身从来没有在网络上出现过。Diffie-Hellman 和 ECDHE 都属于这一类国密 SM2 的密钥协商协议也属于这一类。现实里 TLS 报文里的 “Server Key Exchange” 和 “Client Key Exchange” 消息用的算法大多是密钥协商而不是传统意义上的“交换”术语和实际行为经常对不上理解这一点后面排障会少走很多弯路。1.3 从 DH 到 ECDHE现代协商算法的演进路线Diffie-Hellman 密钥协商DH是现代密码学的基石之一。它的流程不复杂双方约定一个公开的大素数 p 和一个生成元 g甲方随机选一个私有数 a把 g^a 发给乙方乙方随机选私有数 b把 g^b 发给甲方。甲方算出 (g^b)^a g^(ab)乙方算出 (g^a)^b g^(ab)双方得到同一个值。监听者虽然能看到 g^a 和 g^b但无法在合理时间内算出 a 或 b也就算不出 g^(ab)这依赖的就是离散对数难题的困难性。很多人喜欢用颜料混合来类比双方各自拿一桶公开颜料和一个私密颜料混合后把结果公开交换再把自己那个私密颜料加进去最终得到同一桶颜色但旁观者无法拆解出私密颜料。这个类比很贴近 DH 的精髓。后来椭圆曲线密码出现以后DH 被搬到椭圆曲线群上就是 ECDH。椭圆曲线的优势在于同等安全强度下密钥尺寸小得多。而 ECDHE 中的 “E” 是 Ephemeral临时的意思是每次握手都临时生成一对私钥/公钥参与协商用完即弃这直接带来了前向保密。目前主流 HTTPS 配置里ECDHE 系列套件已经是绝对主力。国密 SM2 密钥协商从数学原理上看也属于椭圆曲线 DH 的范畴但它把曲线参数、杂凑算法、身份绑定和一致性校验流程都标准化了形成了独立的国标体系。2. 为什么越来越多系统要禁用 RSA 密钥交换2.1 RSA 密钥传输的硬伤私钥一丢历史全完在 TLS 1.2 及更早版本里有一大批套件叫 TLS_RSA_WITH_*它们的密钥交换方式就是 RSA 密钥传输。流程非常简单客户端随机生成一个预主密钥pre-master secret用服务器的 RSA 公钥加密通过 Client Key Exchange 消息发给服务器服务器用自己的 RSA 私钥解密得到预主密钥然后双方各自推导出会话密钥。这套机制的问题一下就暴露了所有会话的机密性都绑定在服务器的 RSA 私钥上。一旦服务器私钥泄露攻击者不仅能用它冒充服务器还能拿它解密曾经截获的所有历史流量。因为预主密钥是用同一个公钥加密的私钥不变历史会话就能批量还原。这等于一把万能钥匙开过的所有门都能再开一遍。我在项目里见过不少 2015 年前后的老设备配置到现在还开着 TLS_RSA_WITH_AES_128_CBC_SHA 这类套件。对这类设备做安全评估时“私钥泄露导致历史流量全量解密”是必报的高危问题。这也是业界这几年强烈要求禁用 RSA 密钥交换的最直接原因。2.2 TLS 1.3 的取舍只保留前向保密的协商方式密码学界和标准化组织在这件事上的态度非常明确。RFC 8446 发布 TLS 1.3 时直接移除了所有静态 RSA 密钥交换套件。TLS 1.3 里密钥交换只支持 (EC)DHE 和 PSKRSA 证书仍然可以用来做身份认证签名但绝不再承担密钥传输的任务。这一步等于从协议层面彻底焊死了“用 RSA 交换密钥”这条路。TLS 1.3 的套件命名也变了比如 TLS_AES_256_GCM_SHA384里面不再标注密钥交换算法因为密钥交换已经统一为 ECDHE。服务器证书即便是一张 RSA 证书在 TLS 1.3 里也只用于签名握手消息历史流量解密风险被直接消除。对运维来说只要能把协议版本提高到 TLS 1.3RSA 密钥交换就自然失效了。但对于兼容性要求高、还需要维持 TLS 1.2 的存量系统就必须从套件白名单里手工禁掉 RSA 密钥传输类套件。2.3 动手实操在 OpenSSL 与 Nginx 里禁用 RSA 密钥交换先用 OpenSSL 看看当前支持的套件情况openssl ciphers -v DEFAULT:!kRSA | grep -i rsa这个命令里的!kRSA是关键它表示剔除所有使用 RSA 密钥交换Key Exchange的套件。注意不要写成!RSA因为RSA会同时匹配 RSA 认证即证书签名和 RSA 密钥交换而 ECDHE-RSA-AES128-GCM-SHA256 这种套件里的 RSA 只是服务器证书签名算法密钥交换本身是 ECDHE它仍然前向保密是应该保留的。这个区分很多人搞混结果用!RSA把自己大部分安全套件都禁掉了。Nginx 里可以在 server 块这样配置server { listen 443 ssl; ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 套件只需要指定对称加密和摘要套件密钥交换固定为 ECDHE ssl_ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256; # TLS 1.2 套件只留 ECDHE并显式禁用 RSA 密钥交换 ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!kRSA; }验证是否生效最直接的是用 OpenSSL 客户端# 指定 RSA 密钥交换套件去连预期握手失败 openssl s_client -connect example.com:443 -tls1_2 -cipher RSA /dev/null # 指定 ECDHE 套件去连预期握手成功 openssl s_client -connect example.com:443 -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 /dev/null如果第一条命令返回握手失败或 no cipher match说明 RSA 密钥交换已经禁用成功第二条能正常连通说明配置没有被误伤。这套操作做完即使服务器私钥未来泄露攻击者也只能冒充服务器而无法解密已捕获的历史会话。3. 国密算法体系的密钥协商核心SM2 协议逐段拆解3.1 SM2 密钥交换的双方计算流程SM2 是国密体系里的椭圆曲线公钥密码算法曲线使用一条 256 比特的素数域曲线推荐参数由国标规定。SM2 同时支持签名、加密和密钥协商三种用途本身的算法结构比单纯的 ECDH 更完整。这里我把 SM2 密钥协商协议GB/T 32918.3的核心骨架拆开讲。假设用户 A 的密钥对是 (dA, PA)用户 B 的密钥对是 (dB, PB)双方共享椭圆曲线参数基点 G、阶 n。整个协商的流程可以分成三步第一步双方各自产生临时随机数并交换临时公钥。A 产生随机数 rA计算 RA [rA]G然后把 RA 和自己的身份杂凑值 ZA 发给 BB 同样产生随机数 rB计算 RB [rB]G把 RB 和 ZB 发给 A。第二步双方各自验证收到的点有效在曲线上、不是无穷远点然后计算一个共享点。A 侧计算 tA (dA x1 * rA) mod nx1 是对 RA 横坐标做截断加常数处理后的值再计算 U [tA](PB [x2]RB)B 侧对称地计算 tB (dB x2 * rB) mod n再计算 V [tB](PA [x1]RA)。因为椭圆曲线标量乘法满足交换律和结合律U 和 V 实际上是同一个点。第三步双方把共享点的横纵坐标拼接起来通过密钥派生函数 KDF 算出最终的会话密钥 K。想理解这个流程关键要抓住一点对方公钥 PB 是长期公开的对方临时公钥 RB 是这次握手公开的自己的私钥 dA 和自己临时随机数 rA 是保密的。组合起来双方都能算到同一个点但旁路监听者只知道 PA、PB、RA、RB缺少任一侧的私密标量就永远到不了那个共享点。协议里还有个容易被忽略的细节ZA 和 ZB 是双方可辨识标识、曲线参数和公钥的杂凑结果。把 ZA/ZB 放进后续哈希计算是为了把协商双方的“身份”牢牢绑到密钥上防止中间人篡改身份信息。实际项目里很多人只算一个裸 ECDH不绑定身份后来被打中间人测试打穿就是因为缺了这一步。3.2 SM3 在密钥协商里的两个关键角色SM3 是国密体系的密码杂凑算法输出 256 比特摘要。在 SM2 密钥协商里SM3 至少承担两个关键任务。第一个任务是生成身份杂凑值 ZA/ZB。ZA SM3(ENTLA || IDA || a || b || xG || yG || xPA || yPA)也就是把标识长度、用户标识、曲线参数、用户公钥全部杂凑在一起。默认的用户标识 IDA 是一串固定字符串很多库默认用 “1234567812345678” 这 16 个字节。这里有个工程上很常见的坑如果双方代码一个用默认 ID另一个用了自定义 IDZA/ZB 不一致最终协商出的密钥必然不一致。第二个任务是参与密钥派生函数 KDF。SM2 协商出来的共享点坐标不能直接拿来当会话密钥要通过 KDF 把它“搅拌”成所需长度的密钥。KDF 的核心操作就是循环调用 SM3把共享点坐标 Z 作为输入后面拼接一个 32 位的计数器每次取 SM3 的输出反复迭代直到累计输出长度达到需要的密钥长度。这个设计保证了即使共享点的某些比特有偏向最后的会话密钥依然均匀随机。SM2 和 SM3 不是独立存在的它们在同一套国密协议里天然配合。在国密 SSL 协议里握手消息的签名用 SM2协商出共享密钥后由 SM3 参与 KDF完整性校验也用 SM3数据加密则交给 SM1 或 SM4。3.3 SM1 / SM4协商出来的密钥最终用在哪每次密钥协商的目标是产出一个会话密钥但这个密钥本身并不是用来加密几百字节握手数据的它要交给对称加密算法去保护真正的业务流量。这就引出国密体系里的对称算法 SM1 和 SM4。SM1 是分组对称算法密钥长度 128 比特但它有个特点算法参数不公开只能通过国家批准的密码模块比如密码芯片、密码卡以硬件方式调用。也就是说软件层拿不到 SM1 的算法细节只能通过标准接口下发数据让它加解密。在很多安全等级要求高的场景里协商出来的会话密钥会直接下载到硬件密码模块里由硬件完成 SM1 加密会话密钥本身不出硬件安全性更强。SM4 则是公开的分组对称算法同样 128 比特分组和 128 比特密钥软件实现非常成熟是国密 SSL、国密 IPSec 等协议里最常见的记录层加密算法。协商得到的 K 就是给 SM4 的密钥。用一张表总结这几种算法在密钥协商生态里的位置算法类型密钥长度算法细节在密钥协商流程中的角色SM1分组对称128 比特不公开硬件模块调用协商完成后加密业务数据SM2椭圆曲线公钥256 比特公开签名、加密、密钥协商本身SM3密码杂凑输出 256 比特公开身份杂凑、KDF、完整性校验SM4分组对称128 比特公开协商完成后加密业务数据整个体系配合得很清楚SM2 负责把密钥“从无到有”协商出来SM3 负责在协商过程中做摘要和派生SM1/SM4 负责用协商出来的结果真正保护数据。这也是商用密码改造项目里的标准技术栈。4. 国密改造落地从证书到套件的工程实践4.1 双证书体系签名证书与加密证书各司其职在实际的国密 PKI 体系里一个用户或一台服务器往往持有两张 SM2 证书一张是签名证书一张是加密证书。签名证书的密钥对用途是 SM2 签名和验签用于实体身份认证和抗抵赖。服务器在 TLS 握手时用它签名握手消息客户端用它验证服务器确实是证书持有者。加密证书的密钥对用途是加密和密钥协商在需要静态密钥交换或解密加密数据的场景里使用。两把私钥物理分开、用途分开即使加密私钥泄露签名私钥还是安全的反之亦然。这个设计看起来多了一套证书管理工作但很值得。我在改造项目里见过有人图省事只做一张证书签名和协商都用同一对密钥。短期跑起来没问题一旦做密钥更新或私钥泄露事件处置就会面临“所有历史依赖全部要换”的窘境。双证书本质上是一种密钥隔离符合密码学里最小化影响的基本原则。Nginx 系里支持国密的版本比如 Tengine通常这样配置双证书server { listen 443 ssl; ssl_certificate sign.crt; # 签名证书 ssl_certificate_key sign.key; # 签名私钥 ssl_encrypted_certificate enc.crt; # 加密证书 ssl_encrypted_certificate_key enc.key; # 加密私钥 ssl_ciphers ECC-SM4-CBC-SM3:ECDHE-SM4-CBC-SM3; }具体套件名取决于你用的国密实现比如 BabaSSL、GmSSL 或者 Tengine 自带的补丁命名略有差异但双证书的配置模式大同小异。4.2 配置一套可用的国密 TLS 协商环境要真正跑通 SM2 密钥协商你得有一个支持国密的密码库或 Web 服务器。OpenSSL 1.1.1 起已经内置 SM2/SM3/SM4 支持GmSSL 和 BabaSSL 对国密套件支持更完整。先生成 SM2 签名密钥和证书# 生成 SM2 签名密钥 openssl ecparam -name SM2 -genkey -out sign.key # 生成签名证书请求注意要用 SM2 参数和 SM3 摘要 openssl req -new -key sign.key -out sign.csr -sm2 # 如果有 CA 证书签发签名证书 openssl x509 -req -in sign.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out sign.crt -sm2 -days 365同理生成加密密钥对enc.key/enc.crt。这个流程和传统 RSA 证书很相似关键差异是曲线名为 SM2、摘要算法用 SM3另外证书的 KeyUsage 扩展要按用途区分。配置好之后用 GmSSL 客户端验证协商# 输出握手套件和协商结果能看到使用的国密套件 gmssl s_client -connect example.com:443 -gmtls如果返回的套件名含 SM2/SM4/SM3说明 SM2 密钥协商链路已经跑通。需要提醒一句国密改造不是把 https 网址换成一套全新协议就完事还要考虑浏览器和客户端的兼容性。目前主流浏览器默认不支持国密套件很多项目会做双栈同时配置国际套件TLS 1.3 ECDHE和国密套件让支持国密的终端走 SM2 协商不支持的回退到国际算法。这是目前落地最常见也最稳的形态。4.3 性能调优与随机数质量两个容易被忽视的坑密钥协商是非对称运算尤其 SM2 和 ECDHE 的标量乘法比 AES 这类对称运算慢几个数量级。如果每个新连接都做一次完整协商高并发下 CPU 很快会成为瓶颈。解决方案有几条开启 TLS 会话缓存session cache和会话票据session ticket让同一客户端短时间内复用之前的协商结果条件允许时用硬件密码卡做 SM2 运算卸载再就是合理设置 keepalive让连接长期复用。另一个容易被忽视但影响致命的是随机数质量。密钥协商里的临时随机数 rA/rB 必须来源可靠如果随机数可预测攻击者就能算出会话密钥。好几个真实攻击案例的根因不是算法被破解而是随机数生成器被污染。我遇到过一台虚拟机频繁切换快照熵源不足导致随机数重复最终协商出相同的临时密钥。排查时先看系统熵cat /proc/sys/kernel/random/entropy_avail如果长期低于几百建议启用硬件熵源或确认 haveged 等方案配置合法合规。这个细节在测试环境里问题不大一旦上生产处理金融、政务类数据随机数质量就是合规审计里必查的项。5. 常见问题速查与我的排障笔记5.1 握手失败先查这四件事我调试密钥协商问题最喜欢用一句话打头先看交集再看证书。具体来说遇到握手失败按下面四步走基本能定位八成问题。第一确认客户端和服务端有没有共同的密码套件。用openssl ciphers -v对比两端套件列表重点看密钥交换算法是否匹配。比如服务端只开了 SM2 国密套件客户端是老版本 OpenSSL 不支持结果必然是 no shared cipher。第二确认证书类型和套件匹配。TLS 1.2 下用 ECDHE_RSA 套件证书要是 RSA 类型用国密 ECC_SM4_SM3 套件证书要是 SM2 类型。证书类型和套件错配是最常见的低级错误而且报错信息往往模棱两可只看日志很难发现。第三确认 TLS 协议版本范围没有互相排斥。服务端只开 TLS 1.3客户端只支持 TLS 1.2两边都会认为对方“协议版本不支持”。第四确认客户端、服务端都把对方支持的扩展纳入考虑。国密场景里很多实现需要客户端先声明国密扩展服务器才会选择国密套件这个扩展不是所有库都默认开启。5.2 我踩过的三轮坑第一个坑是配置禁用 RSA 密钥交换时用了!RSA而不是!kRSA。某次改完 nginx 配置刷新页面直接 HTTPS 打不开服务端日志显示一堆握手失败。检查之后才发现!RSA把 ECDHE-RSA 系列的套件全部禁光了而我的服务器证书恰好是 RSA 证书导致可用套件空集。改成!kRSA后立即恢复。这个教训就是禁用某个机制之前先搞清楚目标关键字精确匹配的是什么。第二个坑是 SM2 协商的密钥双方不一致。国产化平台对接时A 系统用默认用户标识 “1234567812345678”B 系统代码里自己写了一个用户 ID 字符串两边 ZA/ZB 不一样握手能完成但应用层解密的报文全是乱码。排查时先对比双方的 ZA/ZB 杂凑值再看代码里 SM2 的 ID 参数是否一致。后来我把 ID 参数统一抽到配置文件里两端共用同一份配置问题再没出现过。第三个坑是加密证书导入时 KeyUsage 不匹配。用原有 CA 签发加密证书时签发请求里没带 keyEncipherment 或 keyAgreement 用途导致部分客户端在握手时报证书用途非法。解决办法是生成加密证书请求时显式指定扩展openssl req -new -key enc.key -out enc.csr -sm2 -addext keyUsagekeyEncipherment,keyAgreement -addext extendedKeyUsageserverAuth这类证书元数据问题在验收测试里特别容易暴露提前加上可以省很多来回沟通的成本。5.3 给密钥协商方案选型的几点建议经历过这么多项目我对密钥协商方案选型的判断标准很朴素能用标准就用标准能走 TLS 1.3 就走 TLS 1.3能上 ECDHE 就别开 RSA 密钥传输。对公网业务系统优先把协议升到 TLS 1.3套件白名单只留 ECDHE 系列证书用 ECDSA 或 RSA 均可但一定要禁掉 TLS_RSA_WITH_* 老套件。对国密合规要求明确的系统走 SM2 协商 SM3 摘要 SM4 加密的完整国密链路证书体系按双证书部署客户端终端如果没有内置国密支持通过双栈兼容模式过渡。无论哪种场景都不要自己去实现一套“私有密钥协商算法”行业里所有标准协议都已经把安全性设计得很周全自己发明的协议几乎必然在某个角落漏掉身份绑定或前向保密。这几年做 TLS 调优和国密改造我最大的感受是密钥协商这件事算法选型和协议配置只是第一步真正决定安全下限的往往是证书管理、随机数质量和日志可见性。如果你只记得一条的话那就是永远不要自己发明协商协议用标准曲线、标准流程把降级到 RSA 密钥传输这条路彻底封死。后面哪怕遇到再复杂的兼容性问题至少在“协商过程本身是否安全”这件事上你可以放心交差。
阅读完成 · 觉得有帮助?