VMessAEAD 设计文档
kslr opened this issue · comments
1. 现状
目前 VMess 已可根据协议头自动协商使用 AEAD 加密并认证传输的流量。但 VMess 协议头部仍然使用 MD5 + AES-CFB ,不能保证协议头部自身的完整性。
2. 定义
KDF: KDF 接受一个字节数组形式的主密钥,和若干字节数组形式的路径,生成一个子密钥
func KDF(key []byte, path [][]byte) []byte {
oKDF = HMAC(SHA256, "VMess AEAD KDF")
for in_v := range path {
oKDF = HMAC(oKDF, in_v)
}
return oKDF(key)
}
CmdKey: 由用户 UUID 产生的一个字节数组形式密钥
const IDBytesLen = 16
type ID struct {
uuid uuid.UUID
cmdKey [IDBytesLen]byte
}
func (id ID) CmdKey() []byte {
return id.cmdKey[:]
}
3. EAuID
为了保持和原有协议,配置兼容,继而保证全部用户都可以享受到这个更新,必须要通过且仅通过最开始16个字节计算出客户端的UUID信息。
3.1 EAuID
EAuID 明文为 [Timestamp 8B][Rand 4B][CRC 4B]
Timestamp 为以秒为单位的64位 Unix 时间戳
Rand 为随机数
CRC 为 CRC32([Timestamp, Rand])
IEEE 多项式
EAuID 密钥为 KDF(CmdKey, ["AES Auth ID Encryption"])
EAuID 使用 AES-128 块加密
3.2 ELength
ELength 是经过 AES-128-GCM 加密的 16 位大端序 VMess 包头部长度。
加密密钥为 KDF(CmdKey, ["VMess Header AEAD Key_Length", EAuID, Nonce])
不重数为 KDF(CmdKey, ["VMess Header AEAD Nonce_Length", EAuID, Nonce])
附加数据为 EAuID
3.3 EHeader
经过 AES-128-GCM 加密的 VMess 标准首部
加密密钥为 KDF(CmdKey, ["VMess Header AEAD Key", EAuID, Nonce])
不重数为 KDF(CmdKey, ["VMess Header AEAD Nonce", EAuID, Nonce])
附加数据为 EAuID
3.4 整体格式
[EAuID][ELength][Rand 8B][EHeader]
Rand 是 64 位随机数
3.5 验证流程
对于每个客户端发来的 EAuID
- 进行重放检测,如果之前 120 秒内见过这个 EAuID 就终止检测,结果为重放错误。
- 针对每一个服务器上用户的 UUID。
- 根据 UUID 生成 CmdKey(用戶 UUID),并解密 EAuID。
- 校验 AuID.zero 如果 CRC32 结果错误终止第二步 2 的本轮循环,继续尝试下一个 UUID。
- 校验 AuID.Time 如果时间差超过允许的数值结果错误终止第二步 2 的本轮循环,继续尝试下一个 UUID。
- 如果进行至这步校验完成,终止检测 结果为 初步认证成功 本轮的 UUID 为认证用户的 UUID。
4. VMessAEAD 包头
4.1 ALength
AES-128-GCM 加密的 16 位大端序包头部长度。
加密密钥为 KDF(CmdKey, ["VMess Header AEAD Key_Length", EAuID, Nonce])[:16]
不重数为 KDF(CmdKey, ["Vmess Header AEAD Nonce_Length", EAuID, Nonce])[:12]
附加数据为 EAuID
目的在与尽早完成对客户端的验证(注1)
- 必须发送长度否则不知道要读取多少信息
- 必须验证 EAuID,防止攻击者抓取阻止新请求后用新 EAuID 和旧的 Nonce,MaskedLength 一起构造重放攻击
- 必须验证 Length,防止攻击者修改长度,后面的 MaskedLength 没有独立的认证
- 必须验证 Nonce,如果 Nonce 错误解密会失败,要尽早验证 (注1)
- 必须加盐,此用户 UUID (CmdKey) 还有其他用途
4.2 Aheader
经过 AES-128-GCM 加密的 VMess AEAD 首部
加密密钥为 KDF(CmdKey, ["VMess Header AEAD Key", EAuID, Nonce])[:16]
不重数为 KDF(CmdKey, ["VMess Header AEAD Nonce", EAuID, Nonce])[:12]
附加数据为 EAuID
数据是VMess的整个未加密头部,从版本号到校验数据。目的是简化支持AEAD的难度,减少非必要的协议修改。
AES-GCM 要求 Key-Nonce 组合不重复。由于其中使用了 EAuID, Nonce 作为Key,Nonce 的 KDF 输入,有96位随机熵输入不会重复。因为使用用户 UUID 作为输入无法被攻击者解密。 这两个 AEAD 输入都必须加盐因为用户 UUID 还有其他用途。
4.3 整体格式
[EAuID][ALength][Rand 8B][Aheader]
Rand 是 64 位随机数
4.4 VMessAEAD 处理流程
- 读取 EAuID / 或原协议认证信息 16 字节。
- 根据 EAuID / 或原协议认证信息判断是否应该使用 AEAD 方法读取协议头。如果是旧协议的话就用原来的方法并返回结果。
- 继续读取 AuIDCheck,MaskedLength,Nonce。此时已经读取了 42 字节 <= 38 + 16 Drain 的初始值。
- 解除 MaskedLength 的混淆为 Length。
- 验证 AuIDCheck 数值,如果不正确则进行 Drain。
- 读取 AEADVMessHeader,解密为 VMessHeader 并进行相应处理并返回结果。
VMessAEAD 协议的返回头也会使用 AEAD 方式进行加密,方法类似请求包头。但是密钥数据来自连接密钥 + KDF。发送 AEAD 加密的 MaskedLength,之后是内容。
客户端必须记忆自己是否使用了 VMessAEAD 发送包头,在使用 VMessAEAD 替换掉了一些 MD5 等弱密码学函数,即使这些弱密码学函数尚不构成任何已知协议弱点。
注1:
阻止抓包 + 重放攻击者根据读取长度来确定服务器类型,如果需要读取过多数据,那么就会超出计算出的 Drain 值长度,无法通过 Drain 隐藏服务器处理流程。
为了保证 Drain 能正常运行,必须在读取 38 + 16 个字节前决定此连接是否来自真实客户端。
- AuIDCheck 保证在读取 42 字节时已经得到 211 位校验熵,确定不是来自抓包 + 部分重放。