第三章 密钥边界划分:握手密钥与应用密钥的隔离
做TLS实现的人都知道,密码学里最怕的不是算法弱,而是密钥乱用。我在早期参与一个国密改造项目时,遇到过这么个情况:对方把握手过程中算出来的临时密钥直接拿去加密业务数据,结果密钥复用导致安全审计不过。说白了,密钥边界没划清楚。
这一章我们就来聊聊,国密TLS握手过程中,密钥到底是怎么一步步生成、隔离、使用的。你理解了这个边界,实现时才能避免“密钥穿墙”的坑。
密钥从哪里来?又到哪里去?
国密TLS握手遵循GM/T 0024-2014标准,密钥的诞生路径非常清晰。先看整体流程:
SM2密钥交换生成预主密钥
在整个TLS握手流程中,预主密钥是根基。它由客户端和服务端通过SM2密钥交换协议(SM2 Key Exchange,简称SM2KE)协商产生。
我记得第一次看GM/T 0024标准时,发现它要求预主密钥必须是48字节,和传统TLS一样。但生成方式完全不同——国密走的是SM2的KDF(密钥派生函数,基于SM3哈希)。
PRF将预主密钥扩展为主密钥
预主密钥不会直接使用。TLS的伪随机函数(PRF)基于SM3,把48字节的预主密钥扩展成同样48字节的主密钥。为什么长度不变?因为主密钥后续还要进行第二轮PRF扩展。
- 输入: 预主密钥 + 客户端随机数 + 服务端随机数
- 算法: 基于SM3的HMAC迭代
- 输出: 48字节主密钥
这里有个小细节:国标里的PRF标签字符串必须严格按照GM/T 0024附录A来写,我之前因为写错了标签,导致和开源库无法互连。排查了两天,血泪教训。
主密钥到密钥块的“边界分水岭”
主密钥继续通过PRF扩展出密钥块(Key Block)。密钥块的长度取决于密码套件需要的密钥总数。举个例子,如果使用SM4-GCM + SHA256,需要:
| 密钥类型 | 长度(字节) | 用途 |
|---|---|---|
| 客户端写密钥(SM4) | 16 | 加密客户端发送的数据 |
| 服务端写密钥(SM4) | 16 | 加密服务端发送的数据 |
| 客户端写IV | 12 | GCM模式初始向量 |
| 服务端写IV | 12 | GCM模式初始向量 |
| 客户端写MAC密钥 | 32 | GMAC认证 |
| 服务端写MAC密钥 | 32 | GMAC认证 |
这个密钥块被一条看不见的“边界线”一分为二:前一半用于握手阶段的消息加密,后一半用于应用数据的传输加密。
握手密钥的应用范围
握手密钥只服务于ChangeCipherSpec之后、Finished消息之前的握手记录。在国密TLS中,一旦收到对方的Finished消息,握手密钥立刻销毁。
你可能会问:那握手中的早期数据(如CertificateVerify)呢?嗯,CertificateVerify也是用握手密钥加密的,因为它产生在握手结束之前。
- 包含哪些消息: ServerHello、Certificate、ServerKeyExchange、ClientCertificate、ClientKeyExchange、CertificateVerify、Finished
- 加密算法: SM4-GCM 或 SM4-CCM,取决于密码套件
- 生命周期: 从ChangeCipherSpec消息发送起,到双方Finished验证通过止
应用密钥的严格隔离
应用密钥在握手完成之后才启用。注意,虽然它和握手密钥来自同一个Key Block,但代码层面必须互斥。标准规定:
在我做的一个物联网网关项目中,为了节省内存,工程师试图在握手完成后复用握手密钥的SM4上下文,结果被安全评审拦住了。说白了,密钥的生命周期和内存上下文必须解耦。
完整的密钥边界检查清单
每次实现国密TLS握手后,我都会按下面的清单做自检。你也试试:
- 预主密钥在生成后是否立即进行下一次PRF,不保留原始值?
- 主密钥是否只用于派生Key Block,不参与其他操作?
- Key Block中的握手部分是否在收到Finished后立即clear?
- 应用密钥是否在握手完成前被设置为NULL(不可用)?
- 密钥派生函数使用的HMAC标签是否与标准一致?
这一章内容比较密集,核心就是记住密钥边界存在于预主密钥 → 主密钥 → 密钥块 → 分离的两个分支。你只要在代码里用不同的结构体或者不同的内存区域来管理,基本就不会出大问题。嗯,我这里多提醒一句:千万别在日志里打印密钥信息,即使只是调试阶段。我在一个项目中见识过打印出预主密钥的日志,虽然只是开发环境,但规范意识要养成。