快捷搜索:  汽车  科技

quic协议是什么意思(QUIC协议详解之Initial包的处理)

quic协议是什么意思(QUIC协议详解之Initial包的处理)IV : InitialVector 初始化向量GCM : Galois/Counter Mode,伽罗瓦/计数器模式HKDF : HMAC-based Extract-and-Expand KeyDerivation Function,基于 HMAC 的提取扩展密钥衍生函数AEAD : authenticated encryption withassociated data 带有关联数据的认证加密ECB : Electronic codebook,电子密码本

quic协议是什么意思(QUIC协议详解之Initial包的处理)(1)

从服务器发起请求开始追踪,细说数据包在 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 初始化向量

基本概念介绍

quic协议是什么意思(QUIC协议详解之Initial包的处理)(2)

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 位来表示数据的长度,编码规则如下:

quic协议是什么意思(QUIC协议详解之Initial包的处理)(3)

举个例子:

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 握手过程就是这样。

猜您喜欢: