登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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多条消息共享一次会话长连接、批量记录、同一会话的消息流
HPKE Sender 与 Recipient 使用同一个 enc 和 info 按相同顺序连续处理两条消息的结构说明图
图1:HPKE 上下文消息序列说明图,展示 enc、info 与连续 Seal/Open 的对应关系;这是静态说明图,不是运行截图。

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。
HPKE 一次性 API、可复用上下文、nonce counter、AAD 和乱序重建错误边界的静态说明图
图2:HPKE 上下文生命周期边界图,比较一次性调用、顺序复用和乱序重建等路径;这是静态说明图,不是运行截图。

工程上可以把 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 或重新划分会话。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>