v2fly / v2fly-github-io

V2Fly Website & Documentation

Home Page:https://v2fly.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

VMessAEAD 设计文档

kslr opened this issue · comments

commented

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

  1. 进行重放检测,如果之前 120 秒内见过这个 EAuID 就终止检测,结果为重放错误。
  2. 针对每一个服务器上用户的 UUID。
  3. 根据 UUID 生成 CmdKey(用戶 UUID),并解密 EAuID。
  4. 校验 AuID.zero 如果 CRC32 结果错误终止第二步 2 的本轮循环,继续尝试下一个 UUID。
  5. 校验 AuID.Time 如果时间差超过允许的数值结果错误终止第二步 2 的本轮循环,继续尝试下一个 UUID。
  6. 如果进行至这步校验完成,终止检测 结果为 初步认证成功 本轮的 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 处理流程

  1. 读取 EAuID / 或原协议认证信息 16 字节。
  2. 根据 EAuID / 或原协议认证信息判断是否应该使用 AEAD 方法读取协议头。如果是旧协议的话就用原来的方法并返回结果。
  3. 继续读取 AuIDCheck,MaskedLength,Nonce。此时已经读取了 42 字节 <= 38 + 16 Drain 的初始值。
  4. 解除 MaskedLength 的混淆为 Length。
  5. 验证 AuIDCheck 数值,如果不正确则进行 Drain。
  6. 读取 AEADVMessHeader,解密为 VMessHeader 并进行相应处理并返回结果。

VMessAEAD 协议的返回头也会使用 AEAD 方式进行加密,方法类似请求包头。但是密钥数据来自连接密钥 + KDF。发送 AEAD 加密的 MaskedLength,之后是内容。

客户端必须记忆自己是否使用了 VMessAEAD 发送包头,在使用 VMessAEAD 替换掉了一些 MD5 等弱密码学函数,即使这些弱密码学函数尚不构成任何已知协议弱点。

注1:
阻止抓包 + 重放攻击者根据读取长度来确定服务器类型,如果需要读取过多数据,那么就会超出计算出的 Drain 值长度,无法通过 Drain 隐藏服务器处理流程。
为了保证 Drain 能正常运行,必须在读取 38 + 16 个字节前决定此连接是否来自真实客户端。

  • AuIDCheck 保证在读取 42 字节时已经得到 211 位校验熵,确定不是来自抓包 + 部分重放。