QUIC協議詳解之Initial包的處理

從服務器發起請求開始追蹤,細說數據包在 QUIC 協議中經歷的每一步。大量實例代碼展現,簡明易懂了解 QUIC。html

前言

本文介紹了在 QUIC 服務器在收到 QUIC 客戶端發起的第一個 UDP 請求— Initial 數據包的分析、處理和解密過程,涉及Initial數據包的格式,數據包頭部保護的去除, Packet Number 的計算,負載數據的解密,client hello 的解析,等等。本文的 C 實現採用 OpenSSL,並基於 IETFQUIC Draft-27。git

術語

PacketNumber :數據包序號github

Initial Packet:初始數據包算法

Variable-length Integer Encode:可變長度整型編碼api

HMAC:Hash-based messageauthencation code,基於 Hash 的驗證信息碼服務器

HKDF: HMAC-based Extract-and-Expand KeyDerivation Function,基於 HMAC 的提取擴展密鑰衍生函數cookie

AEAD: authenticated encryption withassociated data, 帶有關聯數據的認證加密session

ECB: Electronic codebook,電子密碼本數據結構

GCM: Galois/Counter Mode,伽羅瓦/計數器模式app

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 協議簡析

相關文章
相關標籤/搜索