日常密码学的设计
摘要
本文讨论了设计既安全又易用的密码协议的挑战,以‘诺曼门’概念作为类比,并重点介绍了密钥封装机制(KEMs)作为用户友好型密码接口的一个好例子。
<p><a href="https://lobste.rs/s/fdqnii/design_everyday_cryptography">评论</a></p>
查看缓存全文
缓存时间: 2026/07/27 11:44
# 日常密码学的设计
原文: https://www.dlp.rip/everyday-cryptography/
密码学家(以及应用密码学家)喜欢定义优美、健壮、完整的协议来解决有趣的问题。而且,如果你把足够多的密码学家(或应用密码学家)聚集在一个房间里,他们就会定义出能解决*很多*有趣问题的协议。
幸运的话,其中至少有一些有趣的问题终将对某个系统的安全产生影响。
不幸的是,任何能够解决足够多且足够有趣问题的密码协议,对于仅仅作为普通用户的人来说,都几乎不可能安全使用。它将成为密码协议界的**诺曼门**(https://uxdesign.cc/intro-to-ux-the-norman-door-61f8120b6086)。
## KEM(密钥封装机制)(https://www.dlp.rip/everyday-cryptography/#kems)
一个易用的密码接口的典型例子是 **KEM**(https://csrc.nist.gov/pubs/sp/800/227/final),即密钥封装机制。
来自 FIPS 203 的 KEM 示意图
我最喜欢的 KEM 示意图来自上面提到的 **FIPS 203**(https://csrc.nist.gov/pubs/fips/203/final)。我们也可以将 KEM 接口用代码表示:
```python
def kem_keygen(seed: bytes, parms: ParameterSet) -> tuple[PublicKey, PrivateKey]:
# 使用深奥的代数将给定的随机种子扩展为实际的密钥对。
...
return (public_key, private_key)
def kem_encaps(pub: PublicKey) -> tuple[SharedSecret, Ciphertext]:
# 生成一个随机的共享秘密以及封装该秘密的密文。
# 可选地,可能存在一个仅用于测试的"去随机化"版本,
# 例如用于 Wycheproof 测试用例。
...
return (ss, ct)
def kem_decaps(priv: PrivateKey, ct: Ciphertext) -> SharedSecret:
# 解封密文,获取其中的共享秘密。
...
return ss
```
作为这个接口的潜在用户,我很容易明白应该使用 `kem_keygen` 来生成密钥,然后可以将公钥用于 `kem_encaps`,私钥用于 `kem_decaps`。检查这些函数的输入,我会面临以下问题:
- 我需要什么样的 `seed`?
- 我应该使用哪个 `parms`?
- 我应该如何处理我的 `SharedSecret`?
对于特定的算法(例如 **ML-KEM**(https://csrc.nist.gov/pubs/fips/203/final)),前两个问题答案很少。例如,ML-KEM 使用一个 64 字节的种子 `d || z`,并且有 3 个针对不同安全强度的参数集。
第三个问题不应该由你的 KEM 来回答,而是由你的对称密码原语(图中未给出)来回答。
请注意,我不需要为 `kem_encaps` 或 `kem_decaps` 选择任何输入。这些函数只接受由其他函数提供给我的东西。对于熟悉 RSA 加密或 IES 的人来说,你无法决定**加密什么**可能会带来一些文化冲击。但这实际上是一种解放。你不需要担心如果你加密了一个低熵值会发生什么,因为你根本无法选择要加密的值。相反,你会得到一个 `SharedSecret`,可以插入到你选择的认证加密原语中。
还要注意,每个输入和输出都属于具有某种有限最大大小的类型(例如,ML-KEM 的 `PublicKey` 最大大小为 1568)。
看起来相当简单。让我们来看一个不那么美好的情况:现代数字签名。
## 签名(https://www.dlp.rip/everyday-cryptography/#signatures)
尝试使用现代数字签名时,你必须跨越的一个大坑是 **预哈希(pre-hashing)**。
> 预哈希是一个很奇怪的话题。一半密码学界坚称它绝对必要,而另一半则从未听说过它。通常,从未听说过它的那一半是理论密码学家。
>
> — Sophie Schmieg(https://keymaterial.net/2026/05/13/so-you-want-to-deploy-fn-dsa)
更多智慧见解:参见 Sophie 的精彩文章 《HashML-DSA 被认为有害》(https://keymaterial.net/2024/11/05/hashml-dsa-considered-harmful/)。
预哈希问题增加了额外的多样性(从**控制论**(https://en.wikipedia.org/wiki/Variety_(cybernetics)) 的意义上说),这种多样性必须在接口层面用额外的多样性来处理。
我们可以创建 `pure` 和 `prehash` 两种变体的 `sign` 和 `verify` 函数。
```python
def dsa_keygen(seed: bytes, parms: ParameterSet) -> tuple[PublicKey, PrivateKey]:
# 使用深奥的代数将给定的随机种子扩展为实际的密钥对。
...
return (pub, priv)
def dsa_sign_pure(priv: PrivateKey, ctx: bytes, msg: bytes) -> Signature:
# 使用密钥计算上下文和消息的纯签名。
# 可选地,可能存在一个仅用于测试的"去随机化"版本,
# 例如用于 Wycheproof 测试用例。
...
return sig
def dsa_sign_prehash(priv: PrivateKey, ctx: bytes, hash_alg: HashAlgorithm, hash: bytes) -> Signature:
# 使用密钥计算上下文和哈希的预哈希签名。
# 可选地,可能存在一个仅用于测试的"去随机化"版本,
# 例如用于 Wycheproof 测试用例。
...
return sig
def dsa_verify_pure(pub: PublicKey, ctx: bytes, msg: bytes, sig: Signature) -> bool:
# 使用密钥验证纯签名。
...
return ok
def dsa_verify_prehash(pub: PublicKey, ctx: bytes, hash_alg: HashAlgorithm, hash: bytes, sig: Signature) -> bool:
# 使用密钥验证预哈希签名。
...
return ok
```
我差点忘了“外部 Mu”技巧(例如 **FIPS 204**(https://csrc.nist.gov/pubs/fips/204/final),算法 7,第 6 行),它等同于纯签名,但允许在将消息传递给签名者之前计算固定长度的消息代表哈希(包含 ctx),这有助于解决下面的问题。让我们把它加上……
```python
def dsa_sign_pure_mu(priv: PrivateKey, mu: bytes) -> Signature:
# 使用密钥计算 mu 值的签名。
...
return sig
def dsa_verify_pure_mu(priv: PrivateKey, mu: bytes, sig: Signature) -> bool:
# 使用密钥验证 mu 值的签名。
...
return sig
```
与 KEM 的情况不同,这些函数的输入**不是**固定长度的。`msg` 可以是 100 字节,也可以是 100 GB。这给嵌入式密码接口(如硬件令牌或 TPM)带来了明显的问题,因为它们往往对请求或响应的最大大小有限制。预哈希本应有助于解决这个问题,但由于 HashML-DSA 和 HashSLH-DSA 是与 ML-DSA 和 SLH-DSA 不同的算法,因此帮助不大。嵌入式密码领域的人明白,他们会被要求支持与其他所有人相同的协议,而“完全修改你的协议以使用不同的密码算法”并不是一个好的答案。
所以,我们必须实现 start/update/finish 风格的 API(类似于哈希使用的那些):
```python
def dsa_sign_pure_start(priv: PrivateKey, ctx: bytes) -> SignatureSequence:
# 使用给定的密钥和上下文开始签名。
...
return seq
def dsa_sign_pure_update(seq: SignatureSequence, data: bytes):
# 将给定的数据添加到签名中。
...
def dsa_sign_pure_complete(seq: SignatureSequence) -> Signature:
# 完成签名的生成。
...
return sig
def dsa_verify_pure_start(pub: PublicKey, ctx: bytes) -> VerifySequence:
# 使用给定的密钥和上下文开始验证签名。
...
return seq
def dsa_verify_pure_update(seq: VerifySequence, data: bytes):
# 将给定的数据添加到签名中。
...
def dsa_verify_pure_complete(seq: VerifySequence, sig: Signature) -> bool:
# 完成签名的检查。
...
return ok
```
天啊。所以,与 KEM 相比(那里我们有 3 个 API 和 2 个输入决策),我们有 13 个 API 和以下 5 个决策:
- (与 KEM 相同)我需要什么样的 `seed`?
- (与 KEM 相同)我应该使用哪个 `parms`?
- 我应该使用纯签名还是预哈希签名?
- (如果使用预哈希)我应该使用哪种哈希算法?
- 我应该把什么放入 `ctx`?
所有这些多样性都是有意引入的,以提供某种(真实的或感知的)安全优势:
- 纯签名允许签名的安全性不依赖于所选哈希算法的抗碰撞性(但仍然需要抗第二原像性)
- `ctx`(上下文字符串)将域分离(例如,如果一个密钥签署两种不同类型的消息)直接放入签名本身(而不是在使用签名的高级协议中)
为什么 KEM 如此简单,而签名如此复杂?一个原因是密钥封装机制在 `SharedSecret` 中有一个非常好的 **间接层**。KEM 不关心你用那个秘密做什么。你可以用它来做一些对称密码学。它不在乎。你想要加密的实际数据与你的 KEM 无关。
相比之下,现代数字签名更像是微观管理者。你想要签署的实际数据**仍然是**签名的事务。
### 让签名更易用(https://www.dlp.rip/everyday-cryptography/#making-signatures-more-usable)
有什么可以帮忙的吗?作为一名软件工程师,我不得不建议:也许再来一层间接?
想象一下,如果签名是基于固定大小的**消息代表(message representatives)**:
```python
def dsa_keygen(seed: bytes, parms: ParameterSet) -> tuple[PublicKey, PrivateKey]:
# 使用深奥的代数将给定的随机种子扩展为实际的密钥对。
...
return (pub, priv)
def dsa_sign_mrep(priv: PrivateKey, mrep: MessageRepresentative) -> Signature:
# 使用密钥计算消息代表的签名。
# 可选地,可能存在一个仅用于测试的"去随机化"版本,
# 例如用于 Wycheproof 测试用例。
...
return sig
def dsa_verify_mrep(pub: PublicKey, mrep: MessageRepresentative, sig: Signature) -> bool:
# 使用密钥验证消息代表的签名。
...
return ok
```
`MessageRepresentative` 的实际长度取决于签名原语(例如,ML-DSA 为 64 字节,ECDSA-P256 为 32 字节,RSA-2048 为 256 字节)。
一个**独立的**密码协议可以定义要输入到 XOF(如 SHAKE256)中的内容,以计算 `mrep`:
- 包含一个唯一前缀,用于标识 `mrep` 方案本身
- 包含一个可选的上下文字符串
- 包含公钥哈希(以提供例如 **超不可伪造性**(https://eprint.iacr.org/2020/1525.pdf) 属性,针对那些最初没有考虑这些属性的签名)
- 包含用于对冲(例如,针对侧信道风险)的随机性
## 通用原则(https://www.dlp.rip/everyday-cryptography/#general-principles)
签名不会因为互联网上某个怪人的随机博客文章而重新设计(至少这个怪人不行)。但希望上面关于签名的讨论有助于阐明一些原则,这些原则可以在从原语到高级协议的所有级别的密码接口设计中遵循:
### 协议应当有主见
不要向用户提供不必要的旋钮。用户会转动它们并找到伤害自己的方法。你怎么知道一个旋钮是否必要?
- **必要**:两个不同的现实威胁模型证明了两个不同值的合理性。
- **不必要**:能够改变这个值会很有趣。
### 协议应当只有一个职责
遵循**单一职责原则**(https://en.wikipedia.org/wiki/Single-responsibility_principle)。一个协议不需要做所有事情。制造由小的、单一用途的协议(例如 KEM + 认证加密)组成的可组合系统。
### 协议应当可以进行已知答案测试
密码协议很容易出错(甚至更糟,稍微出错)。使用测试向量(这些输入 -> 那些输出)使实现易于测试。
### 协议应当可升级
无论一个协议设计得多么仔细,将来都需要更新。无论是由于严重缺陷,还是仅仅为了新的优化或用例,你都需要能够改变一些东西。考虑当两个支持不同协议版本的方尝试相互通信时会发生什么。
相似文章
混淆:打造密码学的终极Boss
一篇文章讨论混淆作为一种强大的密码学原语,因其理论和实践上的挑战,可能成为密码学的‘终极Boss’。
后量子密码学现状
详细概述了向后量子密码学的过渡进程,涵盖NIST对ML-KEM和ML-DSA的标准化、“现在收集、以后解密”威胁、混合密钥交换的采用,以及PKI分裂为MTC和基于ML-DSA的X.509。
回归构建模块的构建模块
本文类比了C/C++中的安全漏洞与Verilog中的安全漏洞,指出硬件描述语言的设计导致了缺陷,并认为行业应投资于更安全的替代方案,类似于软件领域对内存安全编程语言的推动。
如果你喜欢MITM攻击,就别管DNSSEC
文章认为,忽略DNSSEC会让用户暴露在中间人攻击之下,并以电子邮件、Matrix和XMPP为例,将其与历史上对HTTPS的抵制进行类比。
使用Claude发现密码学弱点
Anthropic的研究人员使用Claude Mythos,通过大量提示使其持续工作并找到可发布的结果,发现了HAWK和一种弱化版AES变体中的数学弱点,API调用花费约10万美元。该工作还产生了一个新的密码分析基准:CryptanalysisBench。