RFC9001 A.2. Client Initial のQUICパケットの暗号化とQUICヘッダーの保護をやってみる

今回は、RFC9001 A.2. Client Initial にある、QUICのパケットの暗号化と、QUICヘッダ―の保護のサンプルを実際に行ってみます。

このサンプルでは、Initialパケットの暗号化とヘッダーの保護のサンプルを提示しています。

本記事では、最初にQUICパケットの暗号化とQUICヘッダーの保護がどのように行われているかを確認し、そのあとに実装を説明します。

Initialパケットのヘッダーは以下のようになっています。

c300000001088394c8f03e5157080000449e00000002

Initialパケットのペイロードは以下のようになっています。ここはCRYPTOフレームしか含まれていませんが、この後に1162バイトになるまでPADDINGフレームが含まれます。

060040f1010000ed0303ebf8fa56f129 39b9584a3896472ec40bb863cfd3e868
04fe3a47f06a2b69484c000004130113 02010000c000000010000e00000b6578
616d706c652e636f6dff01000100000a 00080006001d00170018001000070005
04616c706e0005000501000000000033 00260024001d00209370b2c9caa47fba
baf4559fedba753de171fa71f50f1ce1 5d43e994ec74d748002b000302030400
0d0010000e0403050306030203080408 050806002d00020101001c0002400100
3900320408ffffffffffffffff050480 00ffff07048000ffff08011001048000
75300901100f088394c8f03e51570806 048000ffff

全体の流れ

QUICパケットの保護は、QUICペイロードの暗号化、QUICヘッダーの保護の順番で適用されます。

QUICパケットの保護は、RFC-9001の5. Packet Protectionで説明されています

パケットの保護に使用する、キー、IV、HP(header protection key)の3つを使用します。Initialパケットにおけるこれらのキー生成については以前の記事で書きました。

like-cat.hatenablog.com

ペイロードの暗号化

ペイロードの暗号化には、Authenticated Encryption with Associated Data (AEAD)を使います。

AEADは、RFC5116で定義されています。

Authenticated Encryption with Associated Data (AEAD)

AEADは、シークレットキーK、ナンスN、プレインテキストP、関連データAを入力に取ります。 そして、暗号化されたテキストCを出力します。

QUICでは、TLS1.3で定義された暗号スイートの中で、TLS_AES_128_CCM_8_SHA256以外の任意のものが使えます。具体的には、TLS_AES_128_GCM_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256 の5つです。

また、TLS_AES_128_CCM_8_SHA256 以外のすべてのスキームに対して、ヘッダープロテクションが定義されています。

Initialパケットの場合は、AEAD_AES_128_GCM を使用すると決まっています。接続を試みる段階では暗号スイートの交渉は行われていないので、そうしていると考えられます。

シークレットキー

シークレットキーKは、生成したキーをそのまま使います。

ナンス

ナンスNは生成したIVとパケット番号をもとに計算します。

まず、62ビットのネットワークオーダーに再構築されたパケット番号を、IVのサイズまで0でパディングします。 そして、パディングしたパケット番号とIVのXORをとったものをナンスNとして使用します。

関連データ

関連データAは、QUICのヘッダーの最初のバイトから、保護されていないパケット番号まですべてを含めます。

平文P

QUICパケットのペイロードを入力の平文Pとして使います。

暗号アルゴリズム

QUICのInitialパケットの保護には、AEAD_AES_128_GCM を使います。

AEAD_AES_128_GCM を使う場合、暗号化されたテキストの後部に認証タグを追加します(参考 RFC-5116)。

このタグによって、暗号化される前のペイロードを比較して16 octets 長くなります。

そして、AEADの出力暗号文Cを通信に使用します。

ペイロードの暗号化を図にすると以下のようになります。

ペイロードの暗号化

ヘッダーの保護

ヘッダーの保護は、RFC9001の5.4. Header Protection で説明されています。

ヘッダーの保護はヘッダーの一部を、 ヘッダーの保護用のキーを使って保護します。このキーはパケットの暗号化に使用したキーやIVとは別のキーです。

ヘッダーの保護では、QUICヘッダーの最初の1バイトの下位ビットとパケット番号フィールドの2つを保護します。

Long Haderの場合は、最初の1バイトの下位4ビットが保護されます。これによって、リザーブドビットとパケット番号フィールドが保護されます。

Short Headerの場合は5個の下位ビットが保護されます。Short Headerの場合は、Key Phaseビットも保護します。

ちなみに、Retry パケットや Version Negotiation パケットにはこの保護は適用されないようです。

ヘッダーの保護の適用

ヘッダーの保護では、暗号アルゴリズムを使い、保護に使うマスクを生成します。

暗号アルゴリズムの入力は、暗号化されたペイロードからサンプリングしたデータと、ヘッダー保護用のキーを使用します。

暗号アルゴリズムはから生成されたヘッダー保護用のマスクの先頭5バイトがヘッダーの保護に使われます。

このマスクと、ヘッダーの保護したい部分のXORをとります。

RFC9001では以下のようなアルゴリズムが紹介されています。

mask = header_protection(hp_key, sample)

pn_length = (packet[0] & 0x03) + 1
if (packet[0] & 0x80) == 0x80:
   # Long header: 4 bits masked
   packet[0] ^= mask[0] & 0x0f
else:
   # Short header: 5 bits masked
   packet[0] ^= mask[0] & 0x1f

# pn_offset is the start of the Packet Number field.
packet[pn_offset:pn_offset+pn_length] ^= mask[1:1+pn_length]

このアルゴリズムheader_protection と書かれている部分は、暗号スイートによって違う方法でマスクを生成します。

次に、ショートヘッダーかロングヘッダーかに合わせて、保護が必要な部分を保護します。

ロングヘッダーの場合は下位4ビットをマスクするので、mask[0] & 0x0f で必要なビットを残します。

ショートヘッダーの場合は、下位5ビットをマスクするので、mask[0] & 0x1fで必要なビットを残します。

そして、QUICヘッダーの最初の1バイトとマスクでXORを計算します。

次に、パケット番号とマスクのXORを計算します。

参考までに、Initial パケットと1-RTTパケットは以下のようにヘッダーが保護されるとRFC9001に書かれています。

Initial Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 0,
  Reserved Bits (2),         # Protected
  Packet Number Length (2),  # Protected
  Version (32),
  DCID Len (8),
  Destination Connection ID (0..160),
  SCID Len (8),
  Source Connection ID (0..160),
  Token Length (i),
  Token (..),
  Length (i),
  Packet Number (8..32),     # Protected
  Protected Payload (0..24), # Skipped Part
  Protected Payload (128),   # Sampled Part
  Protected Payload (..)     # Remainder
}

1-RTT Packet {
  Header Form (1) = 0,
  Fixed Bit (1) = 1,
  Spin Bit (1),
  Reserved Bits (2),         # Protected
  Key Phase (1),             # Protected
  Packet Number Length (2),  # Protected
  Destination Connection ID (0..160),
  Packet Number (8..32),     # Protected
  Protected Payload (0..24), # Skipped Part
  Protected Payload (128),   # Sampled Part
  Protected Payload (..),    # Remainder
}

サンプリング

サンプリングにはQUICパケットのパケット番号フィールドから4バイト後から16バイト分のデータを使います。

ここで、4バイトを飛ばすのは、パケット番号がサンプリング対象に含まれないようにするためです。

パケット番号は可変長で最大4バイトあります。なので、4バイトスキップすることで安全にペイロードからサンプリングが行えます。

4バイト飛ばす理由

もし、サンプリングするデータにパケット番号が含まれてしまうと、パケットを受信した側でパケット保護の解除ができなくなります。なぜなら、パケット番号が保護されているためサンプリングができなくなり、パケットヘッダーの保護を解除できなくなるからです。

ペイロードからサンプリングすることで、パケット番号の長さが可変長でも、確実にパケット番号をスキップしてサンプリングをすることが可能になります。

パケット番号長によるサンプリング位置の違いを下記の図に載せておきました。

パケット番号長によるサンプリング位置の違い

マスク生成のアルゴリズム

AESの場合

暗号アルゴリズムがAESの場合のマスクの生成は5.4.3. AES-Based Header Protection に書かれています。

暗号スイートがAEAD_AES_128_GCM、または、AEAD_AES_128_CCM の場合、128ビットのAESをECBモードで使用します。

AEAD_AES_256_GCMが選ばれている場合は256ビットのAESをECBモードで使用します。

ヘッダーの保護のマスクは、サンプリングした16バイトとヘッダープロテクションキーを使用して生成します。

header_protection(hp_key, sample):
  mask = AES-ECB(hp_key, sample)
ChaCha20 の場合

暗号アルゴリズムがChaCha20の場合のマスクの生成は、5.4.4. ChaCha20-Based Header Protectionに書かれています。

AEAD_CHACHA20_POLY1305 を使う場合、生のChaCha20が使用されます。

サンプリングされた16バイトの最初の4バイトはcounterとして使用されます。

サンプリングされた残りの12バイトはナンスとして使用します。

これらとhp_key を入力としマスクを生成します。

header_protection(hp_key, sample):
  counter = sample[0..3]
  nonce = sample[4..15]
  mask = ChaCha20(hp_key, counter, nonce, {0,0,0,0,0})

ヘッダ―の保護は以下のような流れになります。

ヘッダ―の保護
ヘッダ―の保護

実装してみる

今回は、OpenSSLのEVP (The Digital EnVeloPe library) という暗号化関数を使用することができるAPIを使用して実装してみました。

パケットの暗号化

まずはパケットの暗号化部分全体です。

#include <stdio.h>
#include <string.h>
#include <openssl/ssl.h>
#include <openssl/aes.h>
#include <openssl/ssl.h>
#include <openssl/aes.h>

#define ENC 1
#define SSL_SUCCESS 1
#define SSL_FAILURE 0

#define SHORT_HEADER 0

#define LONG_HEADER 1

int packet_protection(unsigned char in[], int inl, 
        unsigned char header[], int header_sz,
        unsigned char payload[], int *payload_sz,
        unsigned char key[], int key_sz,
        unsigned char nonce[], int nonce_sz,
        unsigned char *associated_data, int associated_data_sz,
        unsigned char tag[AES_BLOCK_SIZE]
    ){
    EVP_CIPHER_CTX *evp = NULL;
    int mode = ENC;

    int payloadl;

    int tag_sz = AES_BLOCK_SIZE;

    if ((evp = EVP_CIPHER_CTX_new()) == NULL){
        fprintf(stderr, "ERROR: EVP_CIPHER_CTX_new\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

    if (EVP_CipherInit(evp, EVP_aes_128_gcm(), key, nonce, mode) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_EncryptInit\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

    if (EVP_CipherUpdate(evp, NULL, &payloadl, associated_data, associated_data_sz) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherUpdate\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

    if (EVP_CipherUpdate(evp, payload, &payloadl, in, inl) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherUpdate\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }
    *payload_sz = payloadl;

    if (EVP_CipherFinal(evp, payload, &payloadl) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherFinal\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

    if (EVP_CIPHER_CTX_ctrl(evp, EVP_CTRL_AEAD_GET_TAG, tag_sz, tag) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CIPHER_CTX_ctrl(ENC)\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;            
    }

    EVP_CIPHER_CTX_free(evp);

    return 0;
}

int header_protection(unsigned char header[], int header_sz,
                    unsigned char payload[], int payload_sz,
                    unsigned char key[]

                    ){
    int header_type;//q
    int packet_number_length;
    unsigned char sample[16];
    int sample_sz = 16;

    if ((header[0] & 0x80) != 0){
        header_type = LONG_HEADER;
        // Initial Packet
        if ((header[0]&0x30) == 0){
            packet_number_length = (0x03&header[0]) + 1;
            int sample_begin = 4 - packet_number_length;
            for(int i = 0;i < sample_sz;i++){
                sample[i] = payload[sample_begin+i];
            }
        }
    } else {
        // short header
    }

    int mode = ENC;
    EVP_CIPHER_CTX *evp = NULL;

    if ((evp = EVP_CIPHER_CTX_new()) == NULL){
        fprintf(stderr, "ERROR: header_protection EVP_CIPHER_CTX_NEW\n");
        return 0;
    }

    if (EVP_CipherInit(evp, EVP_aes_128_ecb(), key, NULL, mode) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_EncryptionInit\n");
        return 0;
    }

    unsigned char out[16] = {0};
    int out_length;
    if (EVP_CipherUpdate(evp, out, &out_length, sample, sample_sz) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_CpiherUpdate\n");
        return 0;
    }

    header[0] ^= out[0] & 0x0f;
    for(int i = 18, mask=1;i <=21 && mask <= 4;i++, mask++){
        header[i] ^= out[mask];
    }

    EVP_CIPHER_CTX_free(evp);

    return 0;
}

int main(){
    unsigned char in_payload[1162] = {
        0x06,0x00,0x40,0xf1,0x01,0x00,0x00,0xed,0x03,0x03,0xeb,0xf8,0xfa,0x56,0xf1,0x29,0x39,0xb9,0x58,0x4a,0x38,0x96,0x47,0x2e,0xc4,0x0b,0xb8,0x63,0xcf,0xd3,0xe8,0x68,0x04,0xfe,0x3a,0x47,0xf0,0x6a,0x2b,0x69,0x48,0x4c,0x00,0x00,0x04,0x13,0x01,0x13,0x02,0x01,0x00,0x00,0xc0,0x00,0x00,0x00,0x10,0x00,0x0e,0x00,0x00,0x0b,0x65,0x78,0x61,0x6d,0x70,0x6c,0x65,0x2e,0x63,0x6f,0x6d,0xff,0x01,0x00,0x01,0x00,0x00,0x0a,0x00,0x08,0x00,0x06,0x00,0x1d,0x00,0x17,0x00,0x18,0x00,0x10,0x00,0x07,0x00,0x05,0x04,0x61,0x6c,0x70,0x6e,0x00,0x05,0x00,0x05,0x01,0x00,0x00,0x00,0x00,0x00,0x33,0x00,0x26,0x00,0x24,0x00,0x1d,0x00,0x20,0x93,0x70,0xb2,0xc9,0xca,0xa4,0x7f,0xba,0xba,0xf4,0x55,0x9f,0xed,0xba,0x75,0x3d,0xe1,0x71,0xfa,0x71,0xf5,0x0f,0x1c,0xe1,0x5d,0x43,0xe9,0x94,0xec,0x74,0xd7,0x48,0x00,0x2b,0x00,0x03,0x02,0x03,0x04,0x00,0x0d,0x00,0x10,0x00,0x0e,0x04,0x03,0x05,0x03,0x06,0x03,0x02,0x03,0x08,0x04,0x08,0x05,0x08,0x06,0x00,0x2d,0x00,0x02,0x01,0x01,0x00,0x1c,0x00,0x02,0x40,0x01,0x00,0x39,0x00,0x32,0x04,0x08,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x05,0x04,0x80,0x00,0xff,0xff,0x07,0x04,0x80,0x00,0xff,0xff,0x08,0x01,0x10,0x01,0x04,0x80,0x00,0x75,0x30,0x09,0x01,0x10,0x0f,0x08,0x83,0x94,0xc8,0xf0,0x3e,0x51,0x57,0x08,0x06,0x04,0x80,0x00,0xff,0xff
    };
    // set PADDING frame
    for(int i = 245;i < 1162;i++){
        in_payload[i] = 0;
    }
    int in_payload_sz = sizeof(in_payload);

   unsigned char header[] = {
        0xc3,0x00,
        0x00,0x00,
        0x01,0x08,
        0x83,0x94,
        0xc8,0xf0,
        0x3e,0x51,
        0x57,0x08,
        0x00,0x00,
        0x44,0x9e,
        0x00,0x00,
        0x00,0x02
    };
    int header_sz = sizeof(header);

    unsigned char protected_payload[1162];
    int protected_payload_sz = 1162;

    unsigned char key [] = {
        0x1f,0x36,0x96,0x13,0xdd,0x76,0xd5,0x46,0x77,0x30,0xef,0xcb,0xe3,0xb1,0xa2,0x2d,
    };
    int key_sz = sizeof(key);

    int packet_number = 2;
    unsigned char iv[] = {
        0xfa,0x04,0x4b,0x2f,0x42,0xa3,0xfd,0x3b,0x46,0xfb,0x25,0x5c
    };
    int iv_sz = sizeof(iv);

    // The 62 bits of the reconstructed QUIC packet number in network byte order 
    // are left-padded with zeros to the size of the IV.
    // The exclusive OR of the padded packet number and the IV forms the AEAD nonce.
    iv[iv_sz-1] ^= packet_number;

    unsigned char tag[AES_BLOCK_SIZE];
 
    // Associated data is QUIC header.
    // The associated data, A, for the AEAD is the contents of the QUIC header, 
    // starting from the first byte of either the short or long header, 
    // up to and including the unprotected packet number.
    unsigned char *associated_data = new unsigned char[header_sz];
    memcpy(associated_data, header, header_sz);
    int associated_data_sz = header_sz;

    packet_protection(in_payload, in_payload_sz, header, sizeof(header), protected_payload, &protected_payload_sz, key, key_sz, iv, iv_sz, associated_data, associated_data_sz, tag);
}

入力のペイロードは、RFC9001のサンプルからコピペしてきます。PADDINGフレームは値が0の1バイトのデータなので、入力のペイロードの残りの部分をすべて0で埋めます。

    unsigned char in_payload[1162] = {
     060040f1010000ed0303ebf8fa56f129 (中略) 048000ffff
   };
    // set PADDING frame
    for(int i = 245;i < 1162;i++){
        in_payload[i] = 0;
    }
    int in_payload_sz = sizeof(in_payload);

ヘッダー部分も、RFC9001のサンプルをコピペします。

   unsigned char header[] = {
        0xc3,0x00,
        0x00,0x00,
        0x01,0x08,
        0x83,0x94,
        0xc8,0xf0,
        0x3e,0x51,
        0x57,0x08,
        0x00,0x00,
        0x44,0x9e,
        0x00,0x00,
        0x00,0x02
    };
    int header_sz = sizeof(header);

出力のペイロードを確保するための領域を確保します。インプレースで更新する方法がわからなかったので別に用意しています。

unsigned char protected_payload[1162];
    int protected_payload_sz = 1162;

シークレットキーKは、生成されたキーを使います。

    unsigned char key [] = {
        0x1f,0x36,0x96,0x13,0xdd,0x76,0xd5,0x46,0x77,0x30,0xef,0xcb,0xe3,0xb1,0xa2,0x2d,
    };
    int key_sz = sizeof(key);

ナンスNは、IVとパケット番号(このサンプルでは2)のXORで生成します。今回はパケット番号が2なので、特に考えずに最後のバイトのXORをとります。

    int packet_number = 2;
    unsigned char iv[] = {
        0xfa,0x04,0x4b,0x2f,0x42,0xa3,0xfd,0x3b,0x46,0xfb,0x25,0x5c
    };
    int iv_sz = sizeof(iv);
    // The 62 bits of the reconstructed QUIC packet number in network byte order 
    // are left-padded with zeros to the size of the IV.
    // The exclusive OR of the padded packet number and the IV forms the AEAD nonce.
    iv[iv_sz-1] ^= packet_number;

タグを確保するための領域を確保します

    unsigned char tag[AES_BLOCK_SIZE];

関連データAはQUICヘッダーをコピーします。

// Associated data is QUIC header.
    // The associated data, A, for the AEAD is the contents of the QUIC header, 
    // starting from the first byte of either the short or long header, 
    // up to and including the unprotected packet number.
    unsigned char *associated_data = new unsigned char[header_sz];
    memcpy(associated_data, header, header_sz);
    int associated_data_sz = header_sz;

これで、必要なK、N、P、Aがそろったので、パケット暗号化の関数を呼び出します。

packet_protection(in_payload, in_payload_sz, header, sizeof(header), protected_payload, &protected_payload_sz, key, key_sz, iv, iv_sz, associated_data, associated_data_sz, tag);

パケット暗号化

まず、OpenSSLの暗号化のためのコンテキストを初期化します。

    if ((evp = EVP_CIPHER_CTX_new()) == NULL){
        fprintf(stderr, "ERROR: EVP_CIPHER_CTX_new\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

次に、暗号アルゴリズム、キー、IV、モード(暗号化モード)をセットします。

    if (EVP_CipherInit(evp, EVP_aes_128_gcm(), key, nonce, mode) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_EncryptInit\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

次に、関連データを設定します。

    if (EVP_CipherUpdate(evp, NULL, &payloadl, associated_data, associated_data_sz) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherUpdate\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

OpenSSLのドキュメントによると、EVP_CipherUpdate はAEADの時に出力を空に設定すると、関連データの設定ができるようです。

AEAD INTERFACE The EVP interface for Authenticated Encryption with Associated Data (AEAD) modes are subtly altered and several additional ctrl operations are supported depending on the mode specified. To specify additional authenticated data (AAD), a call to EVP_CipherUpdate(), EVP_EncryptUpdate() or EVP_DecryptUpdate() should be made with the output parameter out set to NULL.

www.openssl.org

次に暗号化処理を呼び出します。

    if (EVP_CipherUpdate(evp, payload, &payloadl, in, inl) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherUpdate\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }
    *payload_sz = payloadl;
    
if (EVP_CipherFinal(evp, payload, &payloadl) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CipherFinal\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;
    }

最後に、タグを取り出します。

    if (EVP_CIPHER_CTX_ctrl(evp, EVP_CTRL_AEAD_GET_TAG, tag_sz, tag) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: EVP_CIPHER_CTX_ctrl(ENC)\n");
        EVP_CIPHER_CTX_free(evp);
        return 0;            
    }

このようにすることで、パケットの暗号化が行えました。

ヘッダーの保護

int header_protection(unsigned char header[], int header_sz,
                    unsigned char payload[], int payload_sz,
                    unsigned char key[]

                    ){
    int header_type;//q
    int packet_number_length;
    unsigned char sample[16];
    int sample_sz = 16;

    if ((header[0] & 0x80) != 0){
        header_type = LONG_HEADER;
        // Initial Packet
        if ((header[0]&0x30) == 0){
            packet_number_length = (0x03&header[0]) + 1;
            int sample_begin = 4 - packet_number_length;
            for(int i = 0;i < sample_sz;i++){
                sample[i] = payload[sample_begin+i];
            }
        }
    } else {
        // short header
    }

    int mode = ENC;
    EVP_CIPHER_CTX *evp = NULL;

    if ((evp = EVP_CIPHER_CTX_new()) == NULL){
        fprintf(stderr, "ERROR: header_protection EVP_CIPHER_CTX_NEW\n");
        return 0;
    }

    if (EVP_CipherInit(evp, EVP_aes_128_ecb(), key, NULL, mode) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_EncryptionInit\n");
        return 0;
    }

    unsigned char out[16] = {0};
    int out_length;
    if (EVP_CipherUpdate(evp, out, &out_length, sample, sample_sz) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_CpiherUpdate\n");
        return 0;
    }

    header[0] ^= out[0] & 0x0f;
    for(int i = 18, mask=1;i <=21 && mask <= 4;i++, mask++){
        header[i] ^= out[mask];
    }

    EVP_CIPHER_CTX_free(evp);

    return 0;
}

int main(){
     // パケットの暗号化
    unsigned char hp_key [] = {
        0x9f,0x50,0x44,0x9e,0x04,0xa0,0xe8,0x10,0x28,0x3a,0x1e,0x99,0x33,0xad,0xed,0xd2
    };
    header_protection(header, header_sz, protected_payload, protected_payload_sz, hp_key);

    return 0;
}

メイン部分は、ヘッダー保護用のキーを用意して、QUICヘッダー、先ほど生成した暗号化されたペイロードを一緒に関数に渡します。

    unsigned char hp_key [] = {
        0x9f,0x50,0x44,0x9e,0x04,0xa0,0xe8,0x10,0x28,0x3a,0x1e,0x99,0x33,0xad,0xed,0xd2
    };
    header_protection(header, header_sz, protected_payload, protected_payload_sz, hp_key);

ヘッダーを保護する際に、まず、ヘッダーがLong Headerか、Short Headerかに基づきパケット番号長を解析をします。 ここでの実装では、QUICヘッダーとQUICパケットを別のデータとして持っています。

なので、ペイロードの開始地点が分かるため、QUICヘッダーを詳細に解析する必要はありません。もし一つのバッファに両方のデータが含まれているなら、ペイロードの開始位置を正しく解析する必要があります。

QUICヘッダーのパケット番号長に合わせて、暗号化されたペイロードからサンプリングを行います。

if ((header[0] & 0x80) != 0){
        header_type = LONG_HEADER;
        // Initial Packet
        if ((header[0]&0x30) == 0){
            packet_number_length = (0x03&header[0]) + 1;
            int sample_begin = 4 - packet_number_length;
            for(int i = 0;i < sample_sz;i++){
                sample[i] = payload[sample_begin+i];
            }
        }
    } else {
        // short header
    }

コンテキストの初期化は先ほどと同様に行います。

    if ((evp = EVP_CIPHER_CTX_new()) == NULL){
        fprintf(stderr, "ERROR: header_protection EVP_CIPHER_CTX_NEW\n");
        return 0;
    }

次に、128ビットAESをECBモードで使用する設定を行います。

    if (EVP_CipherInit(evp, EVP_aes_128_ecb(), key, NULL, mode) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_EncryptionInit\n");
        return 0;
    }

サンプリングされたデータを暗号関数の入力とし、マスクを生成します。

unsigned char out[16] = {0};
    int out_length;
    if (EVP_CipherUpdate(evp, out, &out_length, sample, sample_sz) != SSL_SUCCESS){
        fprintf(stderr, "ERROR: header_protection EVP_CpiherUpdate\n");
        return 0;
    }

生成したマスクを適用します。

    header[0] ^= out[0] & 0x0f;
    for(int i = 18, mask=1;i <=21 && mask <= 4;i++, mask++){
        header[i] ^= out[mask];
    }

このようにすると、クライアントが最初に送るInitialパケットの暗号化ができました。

確認用コード

RFC9001には出力のサンプルがあるので、正しく処理できたかを確認することができます。

    // merge header and payload for decoding
    unsigned char *packet = new unsigned char[header_sz + protected_payload_sz + AES_BLOCK_SIZE]; // add header + payload
    int p = 0;
    for(int i = 0;i < header_sz;i++){
        packet[p++] = header[i];
    }
    for(int i = 0;i < protected_payload_sz;i++){
        packet[p++] = protected_payload[i];
    }
    for(int i = 0;i < AES_BLOCK_SIZE;i++){
        packet[p++] = tag[i];
    }
unsigned char packet_answer[] = {
        0xc0,0x00,0x00,0x00,0x01,0x08,0x83,0x94,0xc8,0xf0,0x3e,0x51,0x57,0x08,0x00,0x00,0x44,0x9e,0x7b,0x9a,0xec,0x34,0xd1,0xb1,0xc9,0x8d,0xd7,0x68,0x9f,0xb8,0xec,0x11,0xd2,0x42,0xb1,0x23,0xdc,0x9b,0xd8,0xba,0xb9,0x36,0xb4,0x7d,0x92,0xec,0x35,0x6c,0x0b,0xab,0x7d,0xf5,0x97,0x6d,0x27,0xcd,0x44,0x9f,0x63,0x30,0x00,0x99,0xf3,0x99,0x1c,0x26,0x0e,0xc4,0xc6,0x0d,0x17,0xb3,0x1f,0x84,0x29,0x15,0x7b,0xb3,0x5a,0x12,0x82,0xa6,0x43,0xa8,0xd2,0x26,0x2c,0xad,0x67,0x50,0x0c,0xad,0xb8,0xe7,0x37,0x8c,0x8e,0xb7,0x53,0x9e,0xc4,0xd4,0x90,0x5f,0xed,0x1b,0xee,0x1f,0xc8,0xaa,0xfb,0xa1,0x7c,0x75,0x0e,0x2c,0x7a,0xce,0x01,0xe6,0x00,0x5f,0x80,0xfc,0xb7,0xdf,0x62,0x12,0x30,0xc8,0x37,0x11,0xb3,0x93,0x43,0xfa,0x02,0x8c,0xea,0x7f,0x7f,0xb5,0xff,0x89,0xea,0xc2,0x30,0x82,0x49,0xa0,0x22,0x52,0x15,0x5e,0x23,0x47,0xb6,0x3d,0x58,0xc5,0x45,0x7a,0xfd,0x84,0xd0,0x5d,0xff,0xfd,0xb2,0x03,0x92,0x84,0x4a,0xe8,0x12,0x15,0x46,0x82,0xe9,0xcf,0x01,0x2f,0x90,0x21,0xa6,0xf0,0xbe,0x17,0xdd,0xd0,0xc2,0x08,0x4d,0xce,0x25,0xff,0x9b,0x06,0xcd,0xe5,0x35,0xd0,0xf9,0x20,0xa2,0xdb,0x1b,0xf3,0x62,0xc2,0x3e,0x59,0x6d,0x11,0xa4,0xf5,0xa6,0xcf,0x39,0x48,0x83,0x8a,0x3a,0xec,0x4e,0x15,0xda,0xf8,0x50,0x0a,0x6e,0xf6,0x9e,0xc4,0xe3,0xfe,0xb6,0xb1,0xd9,0x8e,0x61,0x0a,0xc8,0xb7,0xec,0x3f,0xaf,0x6a,0xd7,0x60,0xb7,0xba,0xd1,0xdb,0x4b,0xa3,0x48,0x5e,0x8a,0x94,0xdc,0x25,0x0a,0xe3,0xfd,0xb4,0x1e,0xd1,0x5f,0xb6,0xa8,0xe5,0xeb,0xa0,0xfc,0x3d,0xd6,0x0b,0xc8,0xe3,0x0c,0x5c,0x42,0x87,0xe5,0x38,0x05,0xdb,0x05,0x9a,0xe0,0x64,0x8d,0xb2,0xf6,0x42,0x64,0xed,0x5e,0x39,0xbe,0x2e,0x20,0xd8,0x2d,0xf5,0x66,0xda,0x8d,0xd5,0x99,0x8c,0xca,0xbd,0xae,0x05,0x30,0x60,0xae,0x6c,0x7b,0x43,0x78,0xe8,0x46,0xd2,0x9f,0x37,0xed,0x7b,0x4e,0xa9,0xec,0x5d,0x82,0xe7,0x96,0x1b,0x7f,0x25,0xa9,0x32,0x38,0x51,0xf6,0x81,0xd5,0x82,0x36,0x3a,0xa5,0xf8,0x99,0x37,0xf5,0xa6,0x72,0x58,0xbf,0x63,0xad,0x6f,0x1a,0x0b,0x1d,0x96,0xdb,0xd4,0xfa,0xdd,0xfc,0xef,0xc5,0x26,0x6b,0xa6,0x61,0x17,0x22,0x39,0x5c,0x90,0x65,0x56,0xbe,0x52,0xaf,0xe3,0xf5,0x65,0x63,0x6a,0xd1,0xb1,0x7d,0x50,0x8b,0x73,0xd8,0x74,0x3e,0xeb,0x52,0x4b,0xe2,0x2b,0x3d,0xcb,0xc2,0xc7,0x46,0x8d,0x54,0x11,0x9c,0x74,0x68,0x44,0x9a,0x13,0xd8,0xe3,0xb9,0x58,0x11,0xa1,0x98,0xf3,0x49,0x1d,0xe3,0xe7,0xfe,0x94,0x2b,0x33,0x04,0x07,0xab,0xf8,0x2a,0x4e,0xd7,0xc1,0xb3,0x11,0x66,0x3a,0xc6,0x98,0x90,0xf4,0x15,0x70,0x15,0x85,0x3d,0x91,0xe9,0x23,0x03,0x7c,0x22,0x7a,0x33,0xcd,0xd5,0xec,0x28,0x1c,0xa3,0xf7,0x9c,0x44,0x54,0x6b,0x9d,0x90,0xca,0x00,0xf0,0x64,0xc9,0x9e,0x3d,0xd9,0x79,0x11,0xd3,0x9f,0xe9,0xc5,0xd0,0xb2,0x3a,0x22,0x9a,0x23,0x4c,0xb3,0x61,0x86,0xc4,0x81,0x9e,0x8b,0x9c,0x59,0x27,0x72,0x66,0x32,0x29,0x1d,0x6a,0x41,0x82,0x11,0xcc,0x29,0x62,0xe2,0x0f,0xe4,0x7f,0xeb,0x3e,0xdf,0x33,0x0f,0x2c,0x60,0x3a,0x9d,0x48,0xc0,0xfc,0xb5,0x69,0x9d,0xbf,0xe5,0x89,0x64,0x25,0xc5,0xba,0xc4,0xae,0xe8,0x2e,0x57,0xa8,0x5a,0xaf,0x4e,0x25,0x13,0xe4,0xf0,0x57,0x96,0xb0,0x7b,0xa2,0xee,0x47,0xd8,0x05,0x06,0xf8,0xd2,0xc2,0x5e,0x50,0xfd,0x14,0xde,0x71,0xe6,0xc4,0x18,0x55,0x93,0x02,0xf9,0x39,0xb0,0xe1,0xab,0xd5,0x76,0xf2,0x79,0xc4,0xb2,0xe0,0xfe,0xb8,0x5c,0x1f,0x28,0xff,0x18,0xf5,0x88,0x91,0xff,0xef,0x13,0x2e,0xef,0x2f,0xa0,0x93,0x46,0xae,0xe3,0x3c,0x28,0xeb,0x13,0x0f,0xf2,0x8f,0x5b,0x76,0x69,0x53,0x33,0x41,0x13,0x21,0x19,0x96,0xd2,0x00,0x11,0xa1,0x98,0xe3,0xfc,0x43,0x3f,0x9f,0x25,0x41,0x01,0x0a,0xe1,0x7c,0x1b,0xf2,0x02,0x58,0x0f,0x60,0x47,0x47,0x2f,0xb3,0x68,0x57,0xfe,0x84,0x3b,0x19,0xf5,0x98,0x40,0x09,0xdd,0xc3,0x24,0x04,0x4e,0x84,0x7a,0x4f,0x4a,0x0a,0xb3,0x4f,0x71,0x95,0x95,0xde,0x37,0x25,0x2d,0x62,0x35,0x36,0x5e,0x9b,0x84,0x39,0x2b,0x06,0x10,0x85,0x34,0x9d,0x73,0x20,0x3a,0x4a,0x13,0xe9,0x6f,0x54,0x32,0xec,0x0f,0xd4,0xa1,0xee,0x65,0xac,0xcd,0xd5,0xe3,0x90,0x4d,0xf5,0x4c,0x1d,0xa5,0x10,0xb0,0xff,0x20,0xdc,0xc0,0xc7,0x7f,0xcb,0x2c,0x0e,0x0e,0xb6,0x05,0xcb,0x05,0x04,0xdb,0x87,0x63,0x2c,0xf3,0xd8,0xb4,0xda,0xe6,0xe7,0x05,0x76,0x9d,0x1d,0xe3,0x54,0x27,0x01,0x23,0xcb,0x11,0x45,0x0e,0xfc,0x60,0xac,0x47,0x68,0x3d,0x7b,0x8d,0x0f,0x81,0x13,0x65,0x56,0x5f,0xd9,0x8c,0x4c,0x8e,0xb9,0x36,0xbc,0xab,0x8d,0x06,0x9f,0xc3,0x3b,0xd8,0x01,0xb0,0x3a,0xde,0xa2,0xe1,0xfb,0xc5,0xaa,0x46,0x3d,0x08,0xca,0x19,0x89,0x6d,0x2b,0xf5,0x9a,0x07,0x1b,0x85,0x1e,0x6c,0x23,0x90,0x52,0x17,0x2f,0x29,0x6b,0xfb,0x5e,0x72,0x40,0x47,0x90,0xa2,0x18,0x10,0x14,0xf3,0xb9,0x4a,0x4e,0x97,0xd1,0x17,0xb4,0x38,0x13,0x03,0x68,0xcc,0x39,0xdb,0xb2,0xd1,0x98,0x06,0x5a,0xe3,0x98,0x65,0x47,0x92,0x6c,0xd2,0x16,0x2f,0x40,0xa2,0x9f,0x0c,0x3c,0x87,0x45,0xc0,0xf5,0x0f,0xba,0x38,0x52,0xe5,0x66,0xd4,0x45,0x75,0xc2,0x9d,0x39,0xa0,0x3f,0x0c,0xda,0x72,0x19,0x84,0xb6,0xf4,0x40,0x59,0x1f,0x35,0x5e,0x12,0xd4,0x39,0xff,0x15,0x0a,0xab,0x76,0x13,0x49,0x9d,0xbd,0x49,0xad,0xab,0xc8,0x67,0x6e,0xef,0x02,0x3b,0x15,0xb6,0x5b,0xfc,0x5c,0xa0,0x69,0x48,0x10,0x9f,0x23,0xf3,0x50,0xdb,0x82,0x12,0x35,0x35,0xeb,0x8a,0x74,0x33,0xbd,0xab,0xcb,0x90,0x92,0x71,0xa6,0xec,0xbc,0xb5,0x8b,0x93,0x6a,0x88,0xcd,0x4e,0x8f,0x2e,0x6f,0xf5,0x80,0x01,0x75,0xf1,0x13,0x25,0x3d,0x8f,0xa9,0xca,0x88,0x85,0xc2,0xf5,0x52,0xe6,0x57,0xdc,0x60,0x3f,0x25,0x2e,0x1a,0x8e,0x30,0x8f,0x76,0xf0,0xbe,0x79,0xe2,0xfb,0x8f,0x5d,0x5f,0xbb,0xe2,0xe3,0x0e,0xca,0xdd,0x22,0x07,0x23,0xc8,0xc0,0xae,0xa8,0x07,0x8c,0xdf,0xcb,0x38,0x68,0x26,0x3f,0xf8,0xf0,0x94,0x00,0x54,0xda,0x48,0x78,0x18,0x93,0xa7,0xe4,0x9a,0xd5,0xaf,0xf4,0xaf,0x30,0x0c,0xd8,0x04,0xa6,0xb6,0x27,0x9a,0xb3,0xff,0x3a,0xfb,0x64,0x49,0x1c,0x85,0x19,0x4a,0xab,0x76,0x0d,0x58,0xa6,0x06,0x65,0x4f,0x9f,0x44,0x00,0xe8,0xb3,0x85,0x91,0x35,0x6f,0xbf,0x64,0x25,0xac,0xa2,0x6d,0xc8,0x52,0x44,0x25,0x9f,0xf2,0xb1,0x9c,0x41,0xb9,0xf9,0x6f,0x3c,0xa9,0xec,0x1d,0xde,0x43,0x4d,0xa7,0xd2,0xd3,0x92,0xb9,0x05,0xdd,0xf3,0xd1,0xf9,0xaf,0x93,0xd1,0xaf,0x59,0x50,0xbd,0x49,0x3f,0x5a,0xa7,0x31,0xb4,0x05,0x6d,0xf3,0x1b,0xd2,0x67,0xb6,0xb9,0x0a,0x07,0x98,0x31,0xaa,0xf5,0x79,0xbe,0x0a,0x39,0x01,0x31,0x37,0xaa,0xc6,0xd4,0x04,0xf5,0x18,0xcf,0xd4,0x68,0x40,0x64,0x7e,0x78,0xbf,0xe7,0x06,0xca,0x4c,0xf5,0xe9,0xc5,0x45,0x3e,0x9f,0x7c,0xfd,0x2b,0x8b,0x4c,0x8d,0x16,0x9a,0x44,0xe5,0x5c,0x88,0xd4,0xa9,0xa7,0xf9,0x47,0x42,0x41,0xe2,0x21,0xaf,0x44,0x86,0x00,0x18,0xab,0x08,0x56,0x97,0x2e,0x19,0x4c,0xd9,0x34,0xe2,0x21,0xaf,0x44,0x86,0x00,0x18,0xab,0x08,0x56,0x97,0x2e,0x19,0x4c,0xd9,0x34
    };
    for(int i = 0;i < header_sz + protected_payload_sz + AES_BLOCK_SIZE;i++){
        if (packet_answer[i] != packet[i]){
            printf("incorrect at %d\n", i);
            return 1;
        }
    }
    printf("correct\n");

まとめ

今回は、RFC9001 A.2 のClient InitialのQUICパケットの暗号化とQUICヘッダーの保護をやってみました。

ここから先に実際の接続に進むには、TLS1.3をちゃんと勉強する必要がありそうです。。。