RFC 9180混合公钥加密的笔记
摘要:本文对IETF的RFC规定的9180混合公钥加密(Hybrid Public Key Encryption,HPKE)进行梳理和研究。
关键词:加密,混合加密,数字信封,可鉴别加密,AEAD,秘密共享,密钥协商
对标文档:
- [RFC 9180] Hybrid Public Key Encryption. 2022-02.
https://www.rfc-editor.org/rfc/rfc9180.html
0. 文档思路
混合公钥加密(Hybrid Public Key Encryption,HPKE),即一种标准化的数字信封加密方案,其核心思路如下:
第1步:基于DH秘密协商实现秘密共享/秘密封装DHKEM。见密钥封装/秘密共享KEM。
第2步:使用共享秘密生成加解密上下文KeySchedule,包括密钥、Nonce等。见X.X上下文派生KeySchedule<ROLE>。
第3步:使用加解密上下文(内含密钥、Nonce),执行可鉴别加密AEAD算法的加密/解密。见5.2 加解密。
注:加解密上下文状态需维护,因为内部有个单调递增的计数器需确保每次使用后累加1。

3. 记号
- pk<ROLE>和sk<ROLE>:分别表示角色ROLE的公私钥,ROLE可以为发送方S、接收方R、临时密钥对E。
- I2OSP(n, w):将整数n转为w字节长的大端表示的字节串。
- Seal(key, nonce, aad, pt):可鉴别加密AEAD的加密算法。key的长度为Nk字节,nonce的长度为Nn字节,生成的MAC长度为Nt字节。
- Open(key, nonce, aad, ct):可鉴别加密AEAD的解密算法。
4. 密码基础部件
4.1 基于Diffie-Hellman协商的密钥封装DHKEM
KDF组件:LabeledExtract + LabeledExpand + ExtractAndExpand
KDF的Extract(salt, ikm)
功能:生成长度固定(Nh字节)的伪随机密钥
输入:
- salt:盐值,任意长度
- ikm:密钥材料
输出:
- 伪随机密钥(Nh字节)
例如:为SM3算法,则固定派生Nh = 32字节的密钥。
KDF的Expand(prk, info, L)
功能:派生任意长度的伪随机密钥
输入:
- prk:密钥材料
- info:任意消息串,任意长度
- L:期望的密钥字节长度
输出:
- 伪随机密钥(L字节)
例如:为SM2-KDF算法。
LabeledExtract(salt, label, ikm)
功能:带标签的固定长度密钥派生,KDF-Extract的进一步封装,以限制/设定一些特定参数
输入:
- salt:盐值,任意长度
- label:标签,任意长度
- ikm:密钥材料
输出:
- 伪随机密钥(Nh字节)
执行流程见图 4-1。
LabeledExpand(prk, label, info, L)
功能:带标签的任意长度密钥派生,KDF-Expand的进一步封装,以限制/设定一些特定参数
输入:
- prk:密钥材料
- label:标签,任意长度
- info:任意消息串,任意长度
- L:期望的密钥字节长度
输出:
- 伪随机密钥(L字节)
执行流程见图 4-1。
ExtractAndExpand(dh, kem_context)
功能:专用的秘密派生函数(主要使用函数)
输入:
- dh:密钥材料,即LabeledExtract的ikm
- kem_context:任意消息串,即LabeledExpand的info
输出:
- shared_secret:共享秘密信息(固定为Nsecret字节,例如Nsecret = 32)
执行流程见图 4-1。

图 4-1 shared_secret = ExtractAndExpand(dh, kem_context)
suit_id
suite_id在LabeledExtract、LabeledExpand、ExtractAndExpand中使用,为
suite_id = "KEM" || I2OSP(kem_id, 2))
其中,I2OSP(kem_id, 2)表示把7.1节表2的kem_id转为2字节长的字节串。
密钥封装/秘密共享KEM
密钥封装方案的目的:共享一个秘密值,基于临时公私钥对、对方公钥,可选地基于己方私钥(AuthEncap+AuthDecap),其核心思想是DH方案:
[skE]pkR = [skE・skR]G = [skR]pkE
[skS]pkR = [skS・skR]G = [skR]pkS
其中:pk<ROLE>和sk<ROLE>分别表示角色ROLE的公私钥,ROLE可以为发送方S、接收方R、临时密钥对E。
有两种方案:
|
方案 |
执行 |
特点 |
|
不使用发送方公私钥的方案 |
发送方Encap 接收方Decap |
无法提供发送方真实性 |
|
使用发送方公私钥的方案 |
发送方AuthEncap 接收方AuthDecap |
提供发送方真实性 |
Encap不使用发送方私钥的秘密封装
shared_secret, enc = Encap(pkR)
功能:秘密封装(不使用发送方私钥)
输入:
- pkR:接收方公钥
输出:
- shared_secret:共享的秘密信息,长度固定为Nsecret字节。
- enc:临时公钥的编码数据,长度为Nenc字节。
步骤:如下图。

图 shared_secret, enc = Encap(pkR)
注:文中提到GenerateKeyPair可以使用DeriveKeyPair(random(Nsk))的方式实现,后者的实现可见7.1.3节。个人认为可以自由发挥。
Decap不使用发送方公钥的秘密解封
shared_secret = Decap(enc, skR, pkR)
功能:秘密解封(不使用发送方公钥)
输入:
- enc:临时公钥的编码数据(发送方提供)
- skR:接收方私钥
- pkR:接收方公钥
输出:
- shared_secret:共享的秘密信息
步骤:如下图。

图 shared_secret = Decap(enc, skR, pkR)
AuthEncap使用发送方私钥的秘密封装
shared_secret, enc = AuthEncap(pkR, skS)
功能:秘密封装(使用发送方私钥)
输入:
- pkR:接收方公钥
- skS:发送方私钥
输出:
- shared_secret:共享的秘密信息
- enc:临时公钥的编码数据
步骤:如下图。

图 shared_secret, enc = AuthEncap(pkR, skS)
AuthDecap使用发送方公钥的秘密解封
shared_secret =AuthDecap(enc, skR, pkR, pkS)
功能:秘密解封(使用发送方公钥)
输入:
- enc:临时公钥的编码数据(发送方提供)
- skR:接收方私钥
- pkR:接收方公钥
- pkS:发送方公钥
输出:
- shared_secret:共享的秘密信息
步骤:如下图。

图 shared_secret = AuthDecap(enc, skR, pkR, pkS)
5. 混合公钥加密方案
5.1 创建加解密的上下文Setup
创建加解密的上下文为Setup<MODE><ROLE>,其中
- <MODE>:包括如下四种模式(default_psk = "",default_psk_id = "")
|
MODE |
使用己方私钥 |
使用预共享密钥 |
秘密共享方案 |
上下文派生方案 |
|
Base (5.1.1) |
× |
× |
Encap + Decap |
KeySchedule default_psk和default_psk_id |
|
PSK (5.1.2) |
× |
√ |
Encap + Decap |
KeySchedule psk和psk_id |
|
Auth (5.1.3) |
√ |
× |
AuthEncap + AuthDecap |
KeySchedule default_psk和default_psk_id |
|
AuthPSK (5.1.4) |
√ |
√ |
AuthEncap + AuthDecap |
KeySchedule psk和psk_id |
- <ROLE>:发送方S或接收方R。
注意:
- KeySchedule创建生成的上下文仅用于单向传输——发送方仅用它加密,接收方仅用它解密。如果要双向传输,可以使用5.3节的秘密导出/派生方案得到另一个方向传输加密AEAD的key和nonce。
X.X上下文派生KeySchedule<ROLE>
本节内容见文献的5.1节(至5.1.1节前)的内容,Page14。
Context<ROLE>(key, base_nonce, seq = 0, exporter_secret) = KeySchedule<ROLE>(mode, shared_secret, info, psk, psk_id)
输入:
- mode:密钥派生模式,为四种之一,mode_base(0x00)、mode_psk(0x01)、mode_auth(0x02)、mode_auth_psk(0x03)
- shared_secret:DHKEM建立的共享秘密值
- info:应用层提供的信息(可选,默认为空)
- psk:预共享密钥(可选,默认为空)
- psk_id:预共享密钥的ID(可选,默认为空)
输出:
- key:可鉴别加密AEAD的密钥
- base_nonce:可鉴别加密AEAD的nonce基线值(用此值和计数器生成AEAD的nonce)
- seq:序列号,被初始化为零
- exporter_secret:仅用于秘密导出/派生的信息(见5.3节)
步骤:如下图。

图 Context<ROLE>(key, base_nonce, seq = 0, exporter_secret) =
KeySchedule<ROLE>(mode, shared_secret, info, psk, psk_id)
5.1.1—5.1.4 具体方案
表 5.1.1—5.1.4的创建加解密上下文方案
(表中default_psk = "",default_psk_id = "")
|
发送方 |
接收方 |
|
|
5.1.1. Encryption to a Public Key |
SetupBaseS(pkR, info):
|
SetupBaseR(enc, skR, info):
|
|
5.1.2. Authentication Using a Pre-Shared Key |
SetupPSKS(pkR, info, psk, psk_id):
|
SetupPSKR(enc, skR, info, psk, psk_id):
|
|
5.1.3. Authentication Using an Asymmetric Key |
SetupAuthS(pkR, info, skS):
|
SetupAuthR(enc, skR, info, pkS):
|
|
5.1.4. Authentication Using Both a PSK and an Asymmetric Key |
SetupAuthPSKS(pkR, info, psk, psk_id, skS):
|
SetupAuthPSKR(enc, skR, info, psk, psk_id, pkS):
|
5.2 加解密
加密 ct = ContextS.Seal(aad, pt)
- 执行AEAD加密,密钥和nonce在上下文中(ComputeNonce是计算AEAD用的nonce):
ct = Seal(self.key, self.ComputeNonce(self.seq), aad, pt)
- 维护状态,让计数器累加1,self.IncrementSeq()。
- 返回密文return ct。
解密pt = ContextR.Open(aad, ct)
- 执行AEAD的解密,密钥和nonce在上下文中(ComputeNonce是计算AEAD用的nonce):
pt = Open(self.key, self.ComputeNonce(self.seq), aad, ct)
- 核验明文正确性,if pt == OpenError: raise OpenError。
- 维护状态,让计数器累加1,self.IncrementSeq()。
- 返回明文return pt。
计算AEAD用nonce,nonce = Context<ROLE>.ComputeNonce(seq)
- 计数器seq转字节串,seq_bytes = I2OSP(seq, Nn) 。
- 输出nonce基线值与seq_bytes异或的结果,return xor(self.base_nonce, seq_bytes) 。
计数器累加1 Context<ROLE>.IncrementSeq()
- 判断计数器是否溢出,if self.seq >= (1 << (8*Nn)) - 1: raise MessageLimitReachedError。
- 计数器累加1,self.seq += 1。
5.3 秘密导出/派生
Context.Export(exporter_context, L)
- return LabeledExpand(self.exporter_secret, "sec", exporter_context, L)
5.1节建立的上下文是单向的,要建立双向的,可以使用本节的秘密导出/派生方案生成另一个上下文的密钥和nonce:
key = context.Export("response key", Nk)
nonce = context.Export("response nonce", Nn)
6. 一步式API
6.1 一步式加密 / 一步式解密
一步式加密Seal<MODE>(…) /一步式解密Open<MODE>(...)
第1步:执行enc, ctx = Setup<MODE><ROLE>,其中<MODE>为5.1描述的四种模式,<ROLE>为发送方S或接收方R。
第2步:执行加密ct = ctx.Seal(aad, pt)或解密pt= ctx.Open(aad, ct),其中Seal和Open分别为可鉴别式加密AEAD的加密算法和解密算法。
第3步:返回密文 / 明文。
6.2 一步式派生
发送方 / 接收方一步式派生SendExport<MODE>(...) / ReceiveExport<MODE>(...)
第1步:执行enc, ctx = Setup<MODE><ROLE>,其中<MODE>为5.1描述的四种模式,<ROLE>为发送方S或接收方R。
第2步:执行exported = ctx.Export(exporter_context, L)。
第3步:返回enc, exported。
9. 安全性
- 如果使用量子安全的KEM(使用抗量子密码算法),HPKE的认证模式(Auth模式)可以提供量子安全性。
- HPKE可以实现IND-CCA2安全性。
- 隐藏明文长度:HPKE不会隐藏明文长度,应用层需要实现额外的填充机制。
- 双向加密:HPKE支持单向加密,双向加密需要应用层实现。
附录A. 测试向量
见原文,略。

浙公网安备 33010602011771号