QUIC协议详解之Initial包的处理

从服务器发起请求开始追踪,细说数据包在 QUIC 协议中经历的每一步。大量实例代码展示,简明易懂了解 QUIC。

前言

本文介绍了在 QUIC 服务器在收到 QUIC 客户端发起的第一个 UDP 请求— Initial 数据包的分析、处理和解密过程,涉及Initial数据包的格式,数据包头部保护的去除, Packet Number 的计算,负载数据的解密,client hello 的解析,等等。本文的 C 实现采用 OpenSSL,并基于 IETFQUIC Draft-27。

术语

**PacketNumber :**数据包序号

**Initial Packet:**初始数据包

**Variable-length Integer Encode:**可变长度整型编码

**HMAC:**Hash-based messageauthencation code,基于 Hash 的验证信息码

**HKDF: **HMAC-based Extract-and-Expand KeyDerivation Function,基于 HMAC 的提取扩展密钥衍生函数

AEAD: authenticated encryption withassociated data, 带有关联数据的认证加密

ECB: Electronic codebook,电子密码本

GCM: Galois/Counter Mode,伽罗瓦/计数器模式

IV: InitialVector, 初始化向量

基本概念介绍

Initial 数据包的结构

Initial 包是长头部结构的数据包,结构如图 3.1 所示,在 CRYPTO 帧后面需要跟上 PADDING 帧,这是 QUIC 协议预防 UDP 攻击的手段之一。一般情况下,CRYPTO 帧太短了(确实也有比较长“一锅炖不下”的情况,可参阅 QTS-TLS 4.3节),服务端为了响应 CRYPTO, 必须发送数据长度大得多的握手包(Handshake Packet),这样就会造成所谓的反射攻击。

QUIC 使用三种方法来抑制此类攻击:

  • 含有 ClientHello 的数据包必须使用 PADDING 帧,达到协议要求的最小数据长度 1200 字节;

  • 当服务端响应未经验证原地址的请求,第一次(firstflight)发送数据时,不允许发送超过三个 UDP 数据报的数据;

  • 确认握手包是带验证的,盲攻击者无法伪造。

typedef struct {
  uint8_t flag;
  uint32_t version;
  uint8_t dcid_length;
  uint8_t *dcid;
  uint8_t scid_length;
  uint8_t *scid;
  uint64_t token_length;
  uint8_t *token;
  uint64_t packet_length;
  uint8_t *payload;} quic_long_header_packet_t;

Packet Number 三种上下文空间

Packet Number 为整型变量,其值在 0 到 2^62-1 之间,它也用于生成数据包加密所需的 nonce。通讯双方维护各自的 Packet Number 体系, 并且分为三个独立的上下文空间:

  • Initial 空间:所有的 Initial 数据包的 Packet Number 均在这个上下文空间里;

  • Handshake 空间:所有的握手数据包;

  • 应用数据空间:所有的 0-RTT 和 1-RTT 包。

所谓的 Packet Number 空间,指得是一种上下文关系,在这个上下文关系里,数据包被处理,被确认。换言之,初始数据包只能使用初始数据包专用的密钥,也只能确认初始数据包。类似的, 握手包只能使用握手包专用的密钥,也只能确认握手数据包。从 Initial 阶段进入 Handshake 阶段后, Initial 阶段使用的密钥就可以被丢弃了,Packet Number 也重新从 0 开始编号。

0-RTT 和 1-RTT 共享同一个 Packet Number 空间,这样做是为了更容易实现这两类数据包的丢包处理算法。

在同一连接同一个 Packet Number 空间里,你不能复用包号,包号必须是单调递增的,当然,具体实现的时候草案并不强制要求每次都递增1, 你可以递增 20,30。当 Packet Number 达到 2^62 -1 时,发送方必须关闭该连接。

通讯过程 Packet Number 的处理还有许多细节,比如重复抑制问题,这部分可以参考 QUIC-TLS 部分以及 RFC4303 的 3.4.3 节,这里就不深入展开讨论。

HKDF:基于 HMAC 的密钥衍生函数

密钥衍生函数(KDF)是加密系统最为基本核心的组件,它将初始密钥作为输入,生成一个或多个足够健壮的加密密钥。

HKDF 的提出一方面是为了给其他协议和应用程序提供基本的功能块,同时也为了解决各种不同机制的密钥衍生函数实现的激增问题。它采用“先提取再扩展(extract-and-expand)”的设计方式,逻辑上,一般采用两个步骤来完成密钥衍生。第一步,将输入的字符转换成固定长度的伪随机密钥。第二步,将其扩展成若干个伪随机密钥。一般人们把通过 Diffie-Hellman 交换的共享密文转换为指定长度的密钥,用于加密,完整性检查以及验证。具体原理可参考 RFC5869。

可变长度整型编码

QUIC 协议中大量使用可变长度整型编码,用首字节的高 2 位来表示数据的长度,编码规则如下:

举个例子:

0b00000011 01011110,0x035e => 2Bit=00,代表长度为 1,可用位数 6, 所以,Value = 3

0b01011001 01011110,0x595e => 2Bit=01,代表长度为 2,可用位数 14,所以,Value = 6494

代码如下:

  uint64_t Buffer_pull_uint_var(upai_buffer_t *buf, ssize_t *size)
  {
    CK_RD_BOUNDS(buf, 1)
    uint64_t value;
    switch (*(buf->pos) >> 6) {
    case 0:
        value = *(buf->pos++) & 0x3F;
        if (size != NULL) *size = 1;
        break;
    case 1:
        CK_RD_BOUNDS(buf, 2)
        value = (uint16_t)(*(buf->pos) & 0x3F) << 8 |
                (uint16_t)(*(buf->pos + 1));
        buf->pos += 2;
        if (size != NULL) *size = 2;
        break;
    case 2:
        CK_RD_BOUNDS(buf, 4)
        value = (uint32_t)(*(buf->pos) & 0x3F) << 24 |
                (uint32_t)(*(buf->pos + 1)) << 16 |
                (uint32_t)(*(buf->pos + 2)) <&
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值