到底什么是 AEAD?再探究竟
到底什么是 AEAD?再探究竟
_ 2025年4月28日 _
你可能对这个问题很熟悉:我总是忘记 AEAD 到底是什么意思,以及为什么需要使用它。是的,我知道这个缩写代表 "Authenticated Encryption with Associated Data"(带有关联数据的认证加密),但这对理解有什么帮助呢?对我来说并没有,所以我最终决定坐下来写这篇博客文章,为未来的自己提供帮助……也为其他觉得 AEAD 难以记住的人提供帮助。
为什么需要关注 AEAD?
简单来说,AEAD 加密是当前的行业标准。听起来是个值得关注的好理由,至少如果你关心理解你的构建块的话。你不必相信我的话,下面是一些相关的数据点:
- 在 2018 年发布的 TLS 1.3 中,“_所有密码都被建模为带有关联数据的认证加密 (AEAD) _”(参见 RFC 8446)。
- 继 TLS 之后,QUIC 协议(它是 HTTP/3 的基础)也需要 AEAD(参见 RFC 9001)。
- Google 的 Tink 密码学库在加密数据时仅支持 AEAD 密码模式(参见 choose a primitive 和 list of available primitives)。
这个列表可以更长,但希望这足以证明 AEAD 将继续存在。正如 Thomas Ptacek 在他著名的 Cryptographic Right Answers 中所说的那样:“[AEAD] 是你在 2015 年唯一想使用的加密方式”。(是的,那是 10 年前的事情了。)
第一部分 - 认证加密
认证什么?
当我想到身份验证时,我脑海中的关联是登录网站。 然而,在密码学中,身份验证意味着_证明加密的消息是真实的_,即它在加密后没有被更改,因此完全来自有权访问密钥的人。
身份验证不仅仅是一个“锦上添花”的功能,正如你最初可能认为的那样。 它通常是系统安全的基本1条件2。 例如,在某些情况下,即使没有密钥,缺乏身份验证也会让拦截器解密消息!
走向更合理的默认值
当我第一次学习密码学时3,通常的做法是分步执行_加密_和_身份验证_。 你会选择一种加密方案(例如,AES-256 在 CBC 模式 中),一种身份验证方案(例如,HMAC-SHA256),并在你的代码中仔细地将它们组合在一起,以确保一切都经过适当的身份验证4。
以下伪代码显示了当时的加密和解密过程:
# Sender: encrypt and generate authentication tag
(nonce, ciphertext) = encrypt(key, "hello world")
tag = hmac(key, nonce + ciphertext)
send(nonce, ciphertext, tag)
# Receiver: verify authentication tag and decrypt
(nonce, ciphertext, tag) = receive()
assert tag == hmac(key, nonce + ciphertext)
assert decrypt(key, nonce, ciphertext) == "hello world"
相当冗长,不是吗? 不像仅仅调用 encrypt
和 decrypt
那么简单。 难怪人们经常搞砸,就像 Apple 的 iMessage 漏洞 是由于……完全未能包含身份验证步骤而引起的! 顺便说一句,即使你记得进行身份验证,你仍然需要以正确的顺序应用加密和身份验证,否则 The Cryptographic Doom Principle 会找上你。
幸运的是,在过去十年中,行业已经引入了更能抵抗误用的原语。 看一下下面的伪代码(libsodium 的 crypto_secretbox_easy
函数的简化版本):
# Sender: encrypt, including an authentication tag in the ciphertext
(nonce, ciphertext) = encrypt_auth(key, "hello world")
send(nonce, ciphertext).
# Receiver: verify message authenticity and decrypt
# (`decrypt` throws an exception if verification fails)
(nonce, ciphertext) = receive()
assert decrypt_auth(key, nonce, ciphertext) == "hello world"
很好,不是吗? 在底层,此 API 仍然对加密和身份验证使用单独的步骤,但是 API 的用户再也不会搞砸了。 对于像我这样,严重依赖 API 的设计来指导我编写正确的代码的人来说,这_远_比那些让你搬起石头砸自己的脚的旧 API 好。
第二部分 - 关联数据
但为什么?
我们已经采用了经过身份验证的加密。 这还不足以保证我们消息的秘密性吗? 所有关于“关联数据”的大惊小怪是什么? 为什么还要增加额外的复杂性?
经过身份验证的加密确实足以保证消息的秘密性,但事实证明,你经常需要将未加密的数据与加密的消息一起发送。 这段未加密的数据就是密码学家所说的“关联数据”。 让我用一个例子来说明这一点。
例如,假设你正在开发一个多用户聊天应用程序。 当两个用户进行对话时,他们会协商一个密钥并通过服务器开始交换消息。 正如你可能期望的那样,服务器无法看到消息的内容,因为它们已加密。 尽管如此,当发送新消息时,服务器需要访问接收者的用户 ID,以便正确地将消息路由给他们。 为此,当从客户端发送加密消息时,它还包含未加密的接收者用户 ID。 换句话说,接收者的用户 ID 作为加密消息的关联数据发送。
现在,如果中间人拦截该消息并将原始接收者的用户 ID 替换为其他用户 ID 会发生什么? 有两种可能性:
- 如果聊天协议验证用户 ID 而不仅仅是加密的消息:服务器将检测到用户 ID 已被篡改,并将该消息作为无效消息丢弃。
- 否则:服务器将不会检测到用户 ID 已被篡改,并将愉快地将消息转发给新的接收者。 这是意料之外的! 幸运的是,在这种情况下,最终接收者仍然无法读取该消息,因为它使用他们不知道的密钥加密。 然而,重点在于,对关联数据缺乏身份验证会让你暴露于攻击者的创造力之下,这可能会导致更严重的安全性问题。
让我们进行身份验证
与验证加密的字节数组类似,我们可以使用身份验证方案(例如,HMAC-SHA256
)来验证加密的消息及其关联数据。 像这样:
# Sender: encrypt and send together with tagged associated data
associated_data = "an unencrypted string"
(nonce, ciphertext) = encrypt(key, "hello world")
tag = hmac(key, nonce + ciphertext + associated_data)
send(nonce, ciphertext, associated_data, tag)
# Receiver: verify encrypted and associated data, then decrypt
(nonce, ciphertext, associated_data, tag) = receive()
assert tag == hmac(key, nonce + ciphertext + associated_data)
assert decrypt(key, nonce, ciphertext) == "hello world"
再次非常冗长,对吗? 实际上,在我看来,这看起来非常复杂,以至于我甚至不确定它是正确的……密码学库不能让我们的生活更轻松吗? 对于这样的事情,我宁愿信任它们而不是我自己的代码。
AEAD 来救援
正如我上面提到的,行业已经转向更能抵抗误用的原语。 我们之前提到的同一个 libsodium 库提供了验证加密位_和_关联数据的加密函数。 听起来很熟悉吗? 我们终于在讨论带Authenticated Encryption with Associated Data(AEAD)了!
让我们更详细地看一下。 下面的简化伪代码已从 libsodium 进行了调整,并在实践中说明了 AEAD 的用法:
# Sender: encrypt, including an authentication tag in the ciphertext
# The authentication tag applies to both the encrypted bits and the unencrypted associated data.
associated_data = "an unencrypted string"
(nonce, ciphertext) = encrypt_aead(key, "hello world", associated_data)
# Receiver: verify message authenticity and decrypt
# (`decrypt` throws an exception if verification fails for the encrypted bits or the associated data)
(nonce, ciphertext, associated_data) = receive()
assert decrypt_aead(key, nonce, ciphertext, associated_data) == "hello world"
正如你所看到的,该 API 现在“强制”我们验证加密位和关联数据,从而防止了各种错误。 如果你足够努力,仍然可以引入错误,但该 API 至少会将你引导到成功的陷阱。
第三部分 - 在库中使用 AEAD
如果你无法使用 libsodium 怎么办? 鉴于 AEAD 的普及,已经标准化了多种 AEAD 密码,这意味着你可以选择最适合你的密码,并在各种库和编程语言中使用它。 你可能已经见过像 AES256-GCM
和 ChaCha20-Poly1305
这样的名称,所以现在出现了一个显而易见的问题:我应该选择哪种 AEAD 原语?
我不是密码学家,因此除非我有非常特殊的要求,否则我会遵循 Tink 的 choose a primitive 页面所推荐的任何内容。 但是请记住,通用的密码学建议在定义上是有限的。 在某些情况下,即使是 Tink 的建议也需要谨慎对待。 希望你当地的密码学家可以帮助你提出明智的建议:)
结束
到底什么是 AEAD 呢? 恐怕我必须回到本文的开头并再次阅读一遍……
特别感谢@ctz 和 @cpu,他们审阅了本文的早期草稿,提出了改进意见,并验证了我的说法是准确的。 没有他们的审查,我不敢发布它! 任何遗留的错误显然都是我自己的。
- 此 StackExchange 评论 用简洁的语言表达了这一点:“默认情况下,加密应该是经过身份验证的加密,除非你确定不需要它”。 对于像我这样的凡人来说,这意味着在所有情况下都使用经过身份验证的加密,除非我信任的专家说服我否则。 ↩︎
- 本文 深入探讨了一些陷阱并详细解释了它们。 问题的核心是 malleability 的概念。 ↩︎
- 我很幸运能够拿到这本优秀的(尽管现在有些过时)Cryptography Engineering: Design Principles and Practical Applications。 ↩︎
- 根据此报告,这是 WhatsApp 用来保护消息的精确加密和身份验证组合。 ↩︎
- 在撰写本文时,Tink 建议你日常加密需求使用 AES128-GCM。 另一方面,Libsodium 警告不要使用 AES-GCM:尽管由于在 TLS 中使用,它是最流行的 AEAD 构造,但在不同的上下文中安全地使用 AES-GCM 非常棘手。 (……)除非你绝对需要 AES-GCM,否则请使用 AEGIS-256(……)代替。 阅读链接页面以获取更多信息。 ↩︎