crypto/hpke 封装密钥与上下文复用的边界
来源:17golang原创
时间:2026-10-10 22:00:26 325浏览 收藏
crypto/hpke 的关键区别,是它既有适合单条消息的 Seal/Open,也有可以连续处理消息的 Sender/Recipient 上下文。后者不是普通的无状态对象:每次成功的 Seal 或 Open 都会推进内部 nonce 计数器,接收端还必须按发送端相同的顺序解密。
官方资料:https://pkg.go.dev/crypto/hpke
如果只传一条独立消息,用Seal/Open最省心;如果一条会话要传多条消息,就把enc、info、上下文生命周期和消息顺序一起设计,不能只把 Sender 指针放进全局变量。
先分清一次性 API 和有状态上下文
Seal 会创建一次发送上下文并立即完成一条消息,返回的密文包含封装出来的密钥材料;Open 则按同样的方式创建一次接收上下文并解密。它们适合消息之间互不关联的场景,例如每个对象都单独保存完整密文。
NewSender 和 NewRecipient 是另一条路径。发送端用接收方公钥创建 Sender,同时得到 enc;接收端拿着这个 enc 和自己的私钥创建 Recipient。之后每条消息只传密文与可选的 AAD,不必重复发送同一份封装材料。
| 路径 | 上下文生命周期 | 适合场景 |
|---|---|---|
Seal/Open | 一条消息一次 | 独立对象、离线文件、无需连续会话 |
Sender/Recipient | 多条消息共享一次会话 | 长连接、批量记录、同一会话的消息流 |

Sender 和 Recipient 为什么要成对复用
HPKE 的上下文由 KEM、KDF、AEAD、info 和封装结果共同确定。发送端的 enc 必须来自与接收端私钥匹配的公钥,info 也必须在两端保持一致。只复用 Sender、重新为每条消息创建 Recipient,或者只传密文而丢掉 enc,都无法保持同一条会话。
下面的示例把 enc 作为会话元数据返回,把每条业务消息的密文单独保存。这里使用 Go 1.26 标准库中的 X25519、HKDF-SHA256 和 AES-256-GCM 组合;实际项目可以根据协议约束选择其他受支持的组合,但不能让发送端和接收端自行漂移。
package main
import (
"fmt"
"crypto/hpke"
)
// Session 保存一条 HPKE 会话需要跨消息传递的封装结果和密文。
type Session struct {
Enc []byte
Ciphertext [][]byte
}
func newSession(pk hpke.PublicKey, messages [][]byte) (Session, error) {
// info 是公开的协议域分离标签,两端必须使用完全相同的字节序列。
info := []byte("17golang-demo/session-v1")
enc, sender, err := hpke.NewSender(pk, hpke.HKDFSHA256(), hpke.AES256GCM(), info)
if err != nil {
return Session{}, err
}
session := Session{Enc: enc, Ciphertext: make([][]byte, 0, len(messages))}
for _, message := range messages {
// 同一个 Sender 连续 Seal,内部 nonce 计数器会随成功调用推进。
ciphertext, err := sender.Seal(nil, message)
if err != nil {
return Session{}, err
}
session.Ciphertext = append(session.Ciphertext, ciphertext)
}
return session, nil
}
func openSession(sk hpke.PrivateKey, session Session) ([][]byte, error) {
// 接收端用发送端产生的 enc 建立匹配的 Recipient 上下文。
info := []byte("17golang-demo/session-v1")
recipient, err := hpke.NewRecipient(session.Enc, sk, hpke.HKDFSHA256(), hpke.AES256GCM(), info)
if err != nil {
return nil, err
}
plaintexts := make([][]byte, 0, len(session.Ciphertext))
for _, ciphertext := range session.Ciphertext {
// Open 必须按照发送端 Seal 的成功顺序调用,不能按业务时间戳乱序。
plaintext, err := recipient.Open(nil, ciphertext)
if err != nil {
return nil, err
}
plaintexts = append(plaintexts, plaintext)
}
return plaintexts, nil
}
func main() {
// 示例省略密钥生成与持久化,重点是 enc 和上下文的生命周期配对。
_ = fmt.Println
}
这个结构中,Session.Enc 只需要随会话元数据保存一次;每个 ciphertext 对应一个成功的 Seal。如果接收方需要从中间位置恢复,不能直接丢弃前面的消息后继续猜测计数器状态,而应按协议设计重建会话或使用新的会话密钥。
AAD、info 和 enc 不是可以随意复用的字符串
info 用于定义这条 HPKE 上下文的公开域,创建 Sender 和 Recipient 时必须一致;AAD 则可以在每次 Seal/Open 时绑定当前消息的公开元数据,例如记录编号、协议版本或方向标记。AAD 不需要保密,但接收端必须拿到完全相同的字节序列。
// SealMessage 把消息编号绑定到 AAD,防止把一条密文误放进另一条记录。
func SealMessage(sender *hpke.Sender, sequence uint64, plaintext []byte) ([]byte, error) {
// AAD 的编码必须是协议固定格式,不能一端用十进制字符串、另一端用二进制整数。
aad := []byte(fmt.Sprintf("record:%d", sequence))
return sender.Seal(aad, plaintext)
}
// OpenMessage 使用与发送端相同的序号编码恢复 AAD。
func OpenMessage(recipient *hpke.Recipient, sequence uint64, ciphertext []byte) ([]byte, error) {
// 序号既参与认证,也必须和当前 Open 的调用顺序保持一致。
aad := []byte(fmt.Sprintf("record:%d", sequence))
return recipient.Open(aad, ciphertext)
}
需要注意的是,AAD 不会替你管理消息顺序。即使业务层能从 AAD 读出记录编号,Recipient.Open 仍然按照内部计数器工作;收到序号 2 时不能跳过序号 1 直接调用第二次 Open,除非你的协议另有独立的会话或重放设计。
复用上下文时最容易越过的边界
有状态上下文的优势是减少重复封装和上下文初始化,但代价是生命周期变长、状态需要同步。下面几种做法尤其容易出错。
- 把一个 Sender 放到多个 goroutine 中并发调用,却没有把调用顺序和错误处理纳入同步策略。
- Sender 的 Seal 成功后才推进计数器,业务层却先把消息标成“已发送”,导致失败重试重复使用错误的业务序号。
- 接收端按网络到达顺序直接 Open,而发送端的 Seal 顺序与到达顺序不一致。
- 每条消息重新 NewSender,却继续复用旧会话的 enc 或让接收端沿用旧 Recipient。
- 把 AAD 当成可选装饰,发送端加入记录号后,接收端仍传 nil。

工程上可以把 Sender 和 Recipient 包装到会话对象中,用互斥锁、单写协程或严格的消息队列保证调用顺序;如果业务天然允许乱序,就为每条消息设计独立上下文,而不是强行共享一个有状态 Recipient。
一次性调用和复用方案怎么选
判断标准不是“复用一定更快”,而是消息是否真的属于同一条顺序会话。
| 业务特征 | 建议 | 原因 |
|---|---|---|
| 每条消息独立存储,可能脱离原会话读取 | 优先 Seal/Open | 密文自带一次性封装结果,生命周期清晰 |
| 长连接内按顺序传递多条消息 | 复用 Sender/Recipient | 一次建立上下文,显式维护顺序与状态 |
| 消息会乱序、重试或跨节点并行消费 | 拆分会话或重新设计协议 | 不要把有状态 nonce 计数器交给不受控的调度顺序 |
| 需要独立绑定记录元数据 | 固定 AAD 编码 | 让错误归属的密文在认证阶段失败 |
无论选哪条路径,都应把 KEM、KDF、AEAD、info 编码、enc 的传输方式和会话失效条件写进协议。尤其不要用“重新 NewRecipient 就能从任意位置继续”的假设替代恢复方案;上下文状态本身就是安全边界的一部分。
常见问题
同一个 enc 可以给多条消息使用吗?
可以,但前提是发送端和接收端分别复用与之匹配的 Sender、Recipient,并保持相同的 Seal/Open 成功顺序。若把 enc 和上下文拆开使用,不能把它当作普通静态公钥。
为什么 AAD 一样也不能解决乱序?
AAD 只参与当前消息的认证,不能重置或跳过 HPKE 上下文的 nonce 计数器。乱序协议应拆分独立上下文,或在更上层建立可重排的会话设计。
什么时候应该重新创建上下文?
当会话边界变化、密钥轮换、参与方变化、消息需要独立恢复,或业务无法保证顺序时,重新建立上下文通常比共享旧状态更清晰。重建时要同时生成新的 enc,并让接收端使用对应的新 Recipient。
crypto/hpke 的复用边界可以归纳成一句话:同一条有序会话里,enc、info、双方上下文和成功调用顺序必须成套维护;不满足这个条件,就退回一次性 API 或重新划分会话。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
369 收藏
-
292 收藏
-
337 收藏
-
245 收藏
-
122 收藏
-
484 收藏
-
Golang · Go教程 | 1小时前 | 加密 · Go教程 · crypto/hpke Go HPKE associated data aad Sender.Seal Recipient.Open417 收藏
-
434 收藏
-
131 收藏
-
198 收藏
-
115 收藏
-
325 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习