乐趣区

关于又拍云: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)) << 8 |
            (uint32_t)(*(buf->pos + 3));
    buf->pos += 4;
    if (size != NULL) *size = 4;
    break;

default:
    CK_RD_BOUNDS(buf, 8)
    value = (uint64_t)(*(buf->pos) & 0x3F) << 56 |
            (uint64_t)(*(buf->pos + 1)) << 48 |
            (uint64_t)(*(buf->pos + 2)) << 40 |
            (uint64_t)(*(buf->pos + 3)) << 32 |
            (uint64_t)(*(buf->pos + 4)) << 24 |
            (uint64_t)(*(buf->pos + 5)) << 16 |
            (uint64_t)(*(buf->pos + 6)) << 8 |
            (uint64_t)(*(buf->pos + 7));
    buf->pos += 8;
    if (size != NULL) *size = 8;
    break;
}
return value;}

Initial 包的处理过程

头部明文信息解析

这部分比较简单,间接上代码:

uapi_err_t pull_quic_header(upai_buffer_t buf, quic_header_packet_t header){

int32_t retcode = 0;
CK_RET(Buffer_pull_uint8(buf, &(header->flag)),
    UPAI_ERR_HEADER|1))

header->is_long_header = (header->flag & PACKET_LONG_HEADER) == 0 ? -1 : 1;

if (header->is_long_header > 0) {CK_RET(Buffer_pull_uint32(buf, &(header->version)),
        UPAI_ERR_HEADER|2)
    CK_RET(Buffer_pull_uint8(buf, &(header->dcid_length)),
        UPAI_ERR_HEADER|3)
    CK_RET(Buffer_pull_bytes(buf, header->dcid_length, 
        &(header->dcid)),
        UPAI_ERR_HEADER|4)
    CK_RET(Buffer_pull_uint8(buf, &(header->scid_length)),
        UPAI_ERR_HEADER|5)
    CK_RET(Buffer_pull_bytes(buf, header->scid_length , 
        &(header->scid)),
        UPAI_ERR_HEADER|6)

    if (header->version == PROTO_NEGOTIATION) {header->packet_type = 0;} else {header->packet_type = header->flag & PACKET_TYPE_MASK;}

    if (header->packet_type == PACKET_TYPE_INITIAL) {
        CK_RET(Buffer_pull_uint_var(buf, NULL, 
            &(header->token_length)),
            UPAI_ERR_HEADER|7)
        CK_RET(Buffer_pull_bytes(buf, header->token_length, 
            &(header->token)),
            UPAI_ERR_HEADER|8)
        CK_RET(Buffer_pull_uint_var(buf, NULL, 
            &(header->packet_length)),
            UPAI_ERR_HEADER|9)

        header->packet_number_offset = buffer_tell(buf);

        CK_RET(Buffer_pull_bytes(buf, header->packet_length, 
            &(header->payload)),
            UPAI_ERR_HEADER|10)
    } else if (header->packet_type == PACKET_TYPE_RETRY) {//TODO: deal with retry packet parsing} else {
        CK_RET(Buffer_pull_uint_var(buf, NULL, 
            &(header->packet_length)),
            UPAI_ERR_HEADER|11)
        CK_RET(Buffer_pull_bytes(buf, header->packet_length, 
            &(header->payload)),
            UPAI_ERR_HEADER|12)
    }
} else {//TODO: short header parse}
return UPAI_RES_OK;}

生成 KEY, IV, HP

QUIC 协定定义了 4 组加密密钥集,对应四个不同的加密层级,这与 Packet Number 空间有相似的意思,他们是:

  • Initial 密钥集
  • Early Data(0-RTT)密钥集
  • Handshake,握手密钥集
  • Application Data(1-RTT),利用数据密钥集

QUIC 的 CRYPTO 帧和 TCP 上的 TLS 最大不不同点在于,一个 QUIC 数据包里可能含有多个数据帧,协定标准自身也要求,只有在同一加密密钥层里,一个数据包就应该尽可能的多放入数据帧。

解密 Initial 数据包,应用的便是 Initial 密钥集。进入某个加密层级,须要三样货色:

  • 初始密钥
  • AEAD 函数
  • HKDF 函数

QUIC 的 Initial 包的初始秘密(Initial secrets)同版本号,指标 Connection ID 相干,加密算法固定为 AES-128-GCM,Initial secrets 的提取形式如下:

uint32_t algorithm_digest_size = _get_algorithm_digest_size(ctx->cipher_name);//SHA256 的长度是 32
const uint8_t initial_salt_d27 []= {0xc3,0xee,0xf7,0x12,

                   0xc7,0x2e,0xbb,0x5a,
                   0x11,0xa7,0xd2,0x43,
                   0x2b,0xb4,0x63,0x65,
                   0xbe,0xf9,0xf5,0x02};//Draft-27 的 salt

uint8_t initial_secrets = (uint8_t )upai_mem_pool_alloc(algorithm_digest_size);
ret = upai_HKDF_Extract(_get_hash_method(ctx->cipher_name), //SHA256

initial_salt_d27, 
sizeof(initial_salt_d27), 
initial_packet.dcid, 
initial_packet.dcid_length, 
initial_secrets);

CK_KG_RET(ret, UPAI_KG_ERR | 1)

提取出 Initial Secrets 之后,便是扩大出 Key,IV 和 HP 了,在这之前,于服务端,须要先扩大出接管秘密(receive secrets),须要用“client in”作为标签。标签函数大抵长这样:

static uapi_err_tupai_hkdf_label(

upai_memory_pool_t *m,
const uint8_t * label,
uint32_t sz_label,
const uint8_t * hash_value,
uint32_t sz_hash_value,
uint32_t sz,
uint8_t **out,
uint32_t *sz_out){
uint32_t full_size = 10 + sz_label + sz_hash_value;
if (sz_out != NULL)
    *sz_out = full_size;
*out = (uint8_t *)upai_mem_pool_alloc(m, full_size);
(*out)[0] = (uint8_t)((uint16_t)(sz >> 8));
(*out)[1] = (uint8_t) sz;
(*out)[2] = 6 + sz_label;
memcpy(*out+3, "tls13", 6);
memcpy(*out + 9, label, sz_label);
(*out)[sz_label + 9] = sz_hash_value;
memcpy(*out + 9 + sz_label + 1, hash_value, sz_hash_value);
return UPAI_RES_OK;}

有了 receive secrets,接下来就是由它再扩大出以“quic key”为标签的 Key,以“quiciv”为标签的 IV 和以“quic hp”为标签的 HP。前两个用于解密负载,后一个用于去除数据包头部掩码。代码如下所示:

uint8_t *recv_label;
uint32_t sz_recv_label;
uint32_t sz_defined_key = = _get_algorithm_key_size(ctx->cipher_name);
upai_hkdf_label(m, “client in”, 9, “”, 0, algorithm_digest_size, &recv_label, &sz_recv_label);
uint8_t recv_secrets = (uint8_t )upai_mem_pool_alloc(ctx->mem, algorithm_digest_size);
ret = upai_HKDF_Expand(_get_hash_method(ctx->cipher_name),

    initial_secrets,
    sz_initial_secrets,
    recv_label,
    sz_recv_label,
    recv_secrets,
    algorithm_digest_size);

CK_KG_RET(ret, UPAI_KG_ERR | 2)
uint8_t key, iv, *hp;uint32_t sz_key, sz_iv, sz_hp;
upai_hkdf_label(m, “quic key”, 8, “”, 0, sz_defined_key, &key, &sz_key);
upai_hkdf_label(m, “quic iv”, 7, “”, 0, AEAD_NONCE_LENGTH, &iv, &sz_iv);
upai_hkdf_label(m, “quic hp”, 7, “”, 0, sz_defined_key, &hp, &sz_hp);
uint8_t *key_for_client = upai_mem_pool_alloc(ctx->mem, sz_defined_key);
uint8_t *iv_for_client = upai_mem_pool_alloc(ctx->mem, AEAD_NONCE_LENGTH);
uint8_t *hp_for_client= upai_mem_pool_alloc(ctx->mem, sz_defined_key);
ret = upai_HKDF_Expand(_get_hash_method(ctx->cipher_name), //Initial 包的 Hash 函数是 SHA256

    recv_secrets, algorithm_digest_size, key, sz_key, key_for_client, sz_defined_key);

CK_KG_RET(ret, UPAI_KG_ERR | 3)
ret = upai_HKDF_Expand(_get_hash_method(ctx->cipher_name),

    recv_secrets, 
    algorithm_digest_size, iv, sz_iv, 
    iv_for_client, AEAD_NONCE_LENGTH);

CK_KG_RET(ret, UPAI_KG_ERR | 4)
ret = upai_HKDF_Expand(_get_hash_method(ctx->cipher_name),

    recv_secrets, 
    algorithm_digest_size, hp, sz_hp, 
    hp_for_client, sz_defined_key);

CK_KG_RET(ret, UPAI_KG_ERR | 5)

去除头部爱护

QUIC 协定的 Initial 数据包头部第一个字节的后 4~5 比特,以及头部的 PacketNumber 域是通过 AES-128-ECB 混同的,其中第一字节的最初两位批示了 Packet Number 的存储长度,使得数据包的 Pakcet Number 长度不可见。不确定 Packet Number 的长度,负载的解密也无从谈起。加密这两局部的密钥由初始化向量 IV 以及爱护密钥衍生而来。该密钥应用“quic hp”作为标签(生成形式可参考上一节),作用于头部第一字节的最低无效位和 Packet Number 域,如果是长头部,则加密 4 位;若是短头部则加密最低 5 位。不过版本协商包和重试包不须要做头部加密。

以下代码初始化 crypto_context,并执行 remove header protection 操作:

upai_memory_pool_t *m = upai_create_memory_pool(MEM_POOL_SIZE);// 创立内存池
//…..
// 此处省略若干无关代码
//…..
uint8_t *plain_header;
uint32_t plain_header_len, truncated_pn, pn_length;
upai_crypto_ctx_t * crypt_ctx = upai_create_quic_crypto(m);
crypt_ctx->initialize(crypt_ctx,

"AES-128-ECB", // 去除头部混同用的算法
"AES-128-GCM", // 负载局部的加解密算法
key_for_client, sz_key, //Key
iv_for_client, sz_iv,   //IV
hp_for_client, sz_hp);  //HP

crypt_ctx->remove_hp(crypt_ctx,

Buffer_get_base(quic_buffer), //QUIC 数据包存储首地址
Buffer_get_size(quic_buffer), // 长度
initial_packet.packet_number_offset, //Packet Number 域的偏移地位
&plain_header, // 输入的纯文本头部
&plain_header_len, // 长度
&truncated_pn, // 编码后的 Packet Number
&pn_length);//PN 存储长度

以下为 crypt_ctx->initialize 函数的头部爱护去除初始化局部代码

//header protection init
int res = EVP_CipherInit(ctx->hp_ctx,
EVP_get_cipherbyname(hp_cipher_name), NULL, NULL, 1);
CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO | 1)
res = EVP_CIPHER_CTX_set_key_length(ctx->hp_ctx, hp_len);
CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO | 2)
res = EVP_CipherInit_ex(ctx->hp_ctx, NULL, NULL, hp, NULL, 1);
CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO | 3)

解密头部爱护的代码如下

//remove_hp 次要代码 u
int8_t mask[32] = {0}, buffer[PACKET_LENGTH_MAX] = {0};
int32_t outlen;
uint8_t *sample = packet_buffer + packet_number_offset + PACKET_NUMBER_LENGTH_MAX;
int32_t res = EVP_CipherUpdate(ctx->hp_ctx, mask, &outlen, sample, SAMPLE_LENGTH);
CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO | 4)
memcpy(buffer, packet_buffer, packet_number_offset + PACKET_NUMBER_LENGTH_MAX);
if (buffer[0] & 0x80) // 长头部数据包,后 4 位去混同
{

buffer[0] ^= mask[0] & 0x0f;

} else // 短头部数据包,后 5 位去混同
{

buffer[0] ^= mask[0] & 0x1f;

}
int pn_length = (buffer[0] & 0x03) + 1;// 第一字节的最低 2 位批示 Packet Number 的长度
*truncated_pn = 0;
for (int i = 0; i < pn_length; ++ i) {
buffer[packet_number_offset + i] ^= mask[i + 1];
truncated_pn = buffer[packet_number_offset + i] | (truncated_pn) << 8);
}
plain_header =(uint8_t ) upai_mem_pool_alloc(ctx->mem, packet_number_offset + pn_length);
memcpy(*plain_header, buffer, packet_number_offset + pn_length);
*plain_header_len = packet_number_offset + pn_length;
*packet_number_len = pn_length;

计算 Packet Number

Packet numbers 是大小为 0-2^62-1 之间的整型数值,枯燥递增,示意数据包的先后顺序,然而放入 QUIC 数据包头部时却编码成 1-4 字节的数据。通过抛弃 packet number 的高位数据 接管方通过上下文复原 packet number,这样一来就达到缩减数据长度的目标。

发送端的 packet number 数据存储容量,个别要求是其最近确认收到的数据包的 packet number 与正要发送的数据包的 packet number 之差的两倍以上,如此接收端方能正确解码。

举个例子,如果通信的某一方收到对方的确认帧,确认己方收回的 packetnumber 为 0xabe8bc 的数据包已收到,那么如果要发送 packetnumber 为 0xac5c02 的数据包,则至多须要(0xac5c02- 0xabe8bc) 2 = 0xe68c, 16 位的编码空间,如果发送 packet number 是 0xace8fe,则至多须要(0xace8fe – 0xabe8bc)2= 0x20084, 24 位的编码空间。

接收端必须得去掉包头爱护,再能力进行 packet number 的解码工作。头部爱护去掉后就能够拿到编码过的 packet number 亦即 truncatedpacket number,需依据肯定算法还原实在数字。其中 expected 为解码端预期的包号,即已接管的最大包号值加 1。举个例子,以后最大的包号是 0xa82f30ea,那么如果接管到的编码包号是 16 位数据 0x9b32,那么最终解码进去的 packet number 是 0xa82f9b32。

实现代码如下所示。

uint64_t decode_packet_number(uint32_t truncated, uint8_t num_bits, uint64_t expected){

uint64_t window = 1L << num_bits;
uint64_t half_window = (uint64_t)(window/2);
uint64_t candidate = (expected & ~(window - 1)) | truncated;
const uint64_t pn_max = 1L << 62;
if (((int64_t)candidate <= (int64_t)(expected - half_window))
  && (candidate < (pn_max - window))) {return candidate + window;} else if ((candidate > expected + half_window)&&(candidate >= window)) {return candidate - window;} else {return candidate;}}

解密负载内容

Initial 数据包的负载采纳的是 AES-128-GCM 加密算法。首先初始化 OpenSSL EVP:

res = EVP_CipherInit_ex(ctx->decrypt_ctx,

EVP_get_cipherbyname(aead_cipher_name), //Cipher name=AES-128-GCM
NULL, NULL, NULL, 0);

CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|6)
res = EVP_CIPHER_CTX_set_key_length(ctx->decrypt_ctx, key_len);
CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|7)
res = EVP_CIPHER_CTX_ctrl(ctx->decrypt_ctx,

EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL);

CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|8)

解密负载时,IV 局部还须要 PacketNumber 参加计算最终生成 nonce,

uint8_t nonce[AEAD_NONCE_LENGTH] = {0};
memcpy(nonce, ctx->iv, AEAD_NONCE_LENGTH);
*plain_payload_len = 0;
*plain_payload = NULL;
uint8_t *data = packet_buffer + plain_header_len;
uint32_t data_len = packet_buffer_len – plain_header_len;
uint8_t buffer_payload[PACKET_LENGTH_MAX] = {0};
for (int i = 0; i < 8; i++) {

nonce[AEAD_NONCE_LENGTH - 1 - i] ^= (uint8_t)(packet_number >> 8 * i);
}

int32_t res = EVP_CipherInit_ex(ctx->decrypt_ctx,

NULL, NULL, ctx->key, nonce, 0);

res = EVP_CIPHER_CTX_ctrl(ctx->decrypt_ctx,

    EVP_CTRL_GCM_SET_TAG,
    AEAD_TAG_LENGTH,
    (void *)(data + (data_len-AEAD_TAG_LENGTH)));

CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|10)
int32_t outlen, outlen2;
res = EVP_CipherUpdate(ctx->decrypt_ctx, NULL, &outlen,

    plain_header,
    plain_header_len);

CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|11)
res = EVP_CipherUpdate(ctx->decrypt_ctx, buffer_payload, &outlen,

    data,
    data_len - AEAD_TAG_LENGTH);

CRYPTO_CK_RET(res, UPAI_ERR_CRYPTO|12)
res = EVP_CipherFinal_ex(ctx->decrypt_ctx, NULL, &outlen2);
if (res == 0) {

return UPAI_ERR_CRYPTO|14;

} else {

*plain_payload = (uint8_t *) upai_mem_pool_alloc(ctx->mem, outlen);
memcpy(*plain_payload, buffer_payload, outlen);
*plain_payload_len = outlen;
return UPAI_RES_OK;

}

解析 ClientHello

上一节咱们拿到了负载的明文, 这个区域存储的是至多一个或者一个以上的数据帧。Initial 数据包负载区第一帧个别是 CRYPTO 数据帧,FrameType 值为 0x06。以下代码获取了 CRYPTO 帧的四个数据段:FrameType,Offset,Length,CryptoData。其中,Offset,为变长整型数值,批示数据在该帧中的字节偏移地位,Length 段,为变长整型数值,批示 Crypto Data 的长度。

uint64_t frame_type, frame_length, frame_offset;
uint8_t *crypto_data;
Ref_buffer(m, payload_buffer, 0, plain_payload, plain_payload_len);
Buffer_pull_uint_var(payload_buffer, NULL, &frame_type);
if (frame_type == FRAME_TYPE_CRYPTO) {

Buffer_pull_uint_var(plain_payload_buffer, NULL, &frame_offset);
Buffer_pull_uint_var(plain_payload_buffer, NULL, &frame_length);
Buffer_pull_bytes(plain_payload_buffer, frame_length, &crypto_data);

}

获得 Crypto Data 后,接着是对该段数据的解析。第一个字节是 HandshakeType,定义如下:

typedef enum {

client_hello = 1,
server_hello = 2,
new_session_ticket = 4,
end_of_early_data = 5,
encrypted_extensions = 8,
certificate = 11,
certificate_request = 13,
certificate_verify = 15,
finished = 20,
key_update = 24,
message_hash = 254} handshake_type_t;

不言而喻,Initial 包里该段的类型值为 0x01,表明是 ClientHello 数据。接下来便是解析 TLS1.3 的 ClientHello 数据结构。

以下为 RFC8446 的 ClientHello 构造体:

uint16_t ProtocolVersion;opaque Random[32];
uint8 CipherSuite[2];
struct {

  ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
  Random random;
  opaque legacy_session_id<0..32>;
  CipherSuite cipher_suites<2..2^16-2>;
  opaque legacy_compression_methods<1..2^8-1>;
  Extension extensions<8..2^16-1>;
  } ClientHello;

解释一下为什么 legacy_version 是 0x0303: 在 TLS 的前一个版本中,该字段用于版本协商,也示意客户端能反对到的最高版本号。实践证明许多服务器并没有很好地实现版本协商性能,导致了所谓的“版本不宽容”的问题,只有此版号高于服务器能反对的, 它就会连带着回绝其余它它能承受的 ClientHello,在 TLS1.3 中,客户端能够在 ClientHello 扩大信息的“supported_versions”字段中申明它版本反对的优先级,因而,为兼容性思考,legacy_version 就必须设为 0x0303,示意版本 TLS1.2。如此一来,通过将 legacy_version 等于 0x0303,并在 supported_versions 字段中设 0x0304 为最高优先版本,就能够表明,此 ClientHello 为 TLS1.3 了。

简略的实现代码如下:

uint8_t handshake_type;
uint8_t h_length;
uint16_t l_length;
uint16_t tls_version;
uint8_t *random_value;
uint8_t session_id_length;
uint8_t *session_id;
uint16_t cipher_suites_length;
uint16_t ciphers[256];
uint8_t compression_length;
uint8_t *compression_methods;
Buffer_pull_uint8(plain_payload_buffer, &handshake_type);
Buffer_pull_uint8(plain_payload_buffer, &h_length);
Buffer_pull_uint16(plain_payload_buffer, &l_length);
Buffer_pull_uint16(plain_payload_buffer, &tls_version);
Buffer_pull_bytes(plain_payload_buffer, 32, &random_value);
Buffer_pull_uint8(plain_payload_buffer, &session_id_length);
Buffer_pull_bytes(plain_payload_buffer, session_id_length, &session_id);
Buffer_pull_uint16(plain_payload_buffer, &cipher_suites_length);
for (int i = 0; i < cipher_suites_length/2;i++){

Buffer_pull_uint16(plain_payload_buffer, ciphers + i);
}

Buffer_pull_uint8(plain_payload_buffer, &compression_length);
Buffer_pull_bytes(plain_payload_buffer, compression_length, &compression_methods);

最初,咱们来看看 Extension 的构造,援用自 RFC8446。

struct {

ExtensionType extension_type;
opaque extension_data<0..2^16-1>;} Extension;

enum {

server_name(0),                             /* RFC 6066 */
max_fragment_length(1),                     /* RFC 6066 */
status_request(5),                          /* RFC 6066 */
supported_groups(10),                       /* RFC 8422, 7919 */
signature_algorithms(13),                   /* RFC 8446 */
use_srtp(14),                               /* RFC 5764 */
heartbeat(15),                              /* RFC 6520 */
application_layer_protocol_negotiation(16), /* RFC 7301 */
signed_certificate_timestamp(18),           /* RFC 6962 */
client_certificate_type(19),                /* RFC 7250 */
server_certificate_type(20),                /* RFC 7250 */
padding(21),                                /* RFC 7685 */
pre_shared_key(41),                         /* RFC 8446 */
early_data(42),                             /* RFC 8446 */
supported_versions(43),                     /* RFC 8446 */
cookie(44),                                 /* RFC 8446 */
psk_key_exchange_modes(45),                 /* RFC 8446 */
certificate_authorities(47),                /* RFC 8446 */
oid_filters(48),                            /* RFC 8446 */
post_handshake_auth(49),                    /* RFC 8446 */
signature_algorithms_cert(50),              /* RFC 8446 */
key_share(51),                              /* RFC 8446 */
(65535)} ExtensionType;

总结

到这里,QUIC 协定的解析总算是走出了万里长征的第一步,作为服务端,得回复 ACK 帧,告知客户端“你方申请曾经收到”,而后回复 ServerHello,放入 CRYPTO 帧,把该交代的事件交代分明,该协商的事件协商明确,这两个帧塞在同一个数据包发给客户端,而后,单方就能够欢快的步入 Handshake 的殿堂了。是的,1-RTT 握手过程就是这样。

参考资料

https://tools.ietf.org/html/d…
https://datatracker.ietf.org/…

https://github.com/aiortc/aio…

https://github.com/carlescufi…

https://tools.ietf.org/html/r… TLS1.2

https://tools.ietf.org/html/r… TLS1.3

https://tools.ietf.org/html/r… HKDF

https://tools.ietf.org/html/r… IPEncapsulating Security Payload

举荐浏览

从新冠疫情登程,漫谈 Gossip 协定

QUIC/HTTP3 协定简析

退出移动版