mbedTLSでTLS 1.3を使ってみる

こんにちは。GMOインターネット 新里です。
組込み・IoT的な端末でよく利用されているmbedTLSの最新バージョンを使って、TLS 1.3〜QUICの暗号化まで実際にESP32を利用して行ってみます。

mbedTLS

mbedTLS は組込み機器などで使われるコンパクトなサイズのSSL/TSLライブラリです。よく知られているSSLライブラリではOpenSSL/BoringSSLなどがありますね。ただ、IoT的な端末などではスペックが限られたものになります。例えば、僕が利用する端末だとアプリケーションを組込める領域がわずか256Kbyteというサイズとかだったりします。
そういったIoT的な機器や組込み機器のようなスペックで、PCやサーバーで利用されているようなSSL/TLSライブラリを組込んで使うのはスペック的にも難しいです。そこで使われるのが組込み端末向けのコンパクトなライブラリというわけです。

mbedTLSの他にも組込み機器向けをターゲットにしたSSL/TSLライブラリに wolfSSL もあります。個人的にはmbedTLS、wolfSSLは組込み機器向けOSS SSL/TLSでよく見るものです。

気がついたら…

そんなmbedTLSを組込みで利用していて、気がついたらバージョンが3.1.0になっていました。他にもバージョンが何個かあって、現時点では…

・2.16.x :Long Time Supportの最後のバージョン。2.28系への移行を勧める。
・2.28.x :2024年末までバグ・セキュリティフィックスのサポートが行われる。
・3.1.x  :最新のバグ・セキュリティフィックス、機能追加が行われる。

僕が個人的にメンテしているライブラリでは2.16系を使っていたので、とりあえず2.28.0にアップデートしました。このとき、PSA(Platform Security Architecture)というモジュールセットが追加されて全体的にドラスティックに構成が変わっていて、これらを組込むのが地味に面倒でした。
ただ、2.28.x系はあと2年弱でサポート対象から外れてしまうという事もあって、今後のアップデートを考えると最新の3.1系を追いかけた方が良さそうです。

mbedTLS 3.1系を見てみる

mbedTLSはconfig(ヘッダファイル)で利用するアルゴリズムを設定することが出来ます。例えば、DTLSを使いたかったら”#define MBEDTLS_SSL_PROTO_DTLS”を書いておくといった感じです。mbedTLS 3.1系では諸々の設定が書いてあるのは mbedtls_confi.h になっていました。ざっと見てみて個人的におや?と思った点はこの辺です。

・MBEDTLS_SSL_PROTO_TLS1_3が入ってきた。
・TLS 1.0/1.1, 3DES, MD2, MD5, RC4, Blowfish, XTEAが無くなった。
・PSA周りの設定が増えていた。

MBEDTLS_SSL_PROTO_TLS1_3_EXPERIMENTALだったのが、EXPERIMENTALが取れていました。これから組込みもTLS 1.3に向かっていくぞ!!という気持ちを感じます。

TLS 1.3をPC上で使ってみる

現時点での最新版 mbedTLS 3.10 をとりあえずビルドしてサンプルプログラム経由で、www.google.com にTLS 1.3でアクセスしてみます。(環境はMacを利用しています。ビルドエラーで必要な物は適宜brewで入れます。)

wget https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v3.1.0.tar.gz
tar -zxvf v3.1.0.tar.gz
cd mbedtls-3.1.0
# include/mbedtls/mbedtls_config.h から
# MBEDTLS_SSL_PROTO_TLS1_3
# MBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE
# をコメントアウトしてビルド
make

# www.google.com に接続してみる。Root CAをダウンロード
wget https://pki.goog/repo/certs/gtsr1.pem

# force_version=tls13 を付けてTLS 1.3でアクセス
./programs/ssl/ssl_client2 server_name=www.google.com server_port=443 ca_file=./gtsr1.pem force_version=tls13

このとき、本当にTLS 1.3でアクセスしているのか?Wiresharkを使って同時に観測します。

TLS 1.3でアクセスしているようです。1-RTTで終わるはずが、なぜかクライアント側からChange Cipher Specが飛んでいます。これはMBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE の説明にある通り(RFC 8446 D.4. Middlebox Compatibility Mode)、ミドルボックスの下位互換性のために投げている空のChange Cipher Specですね。

ESP32からTLS 1.3を使ってみる

とりあえずTLS 1.3で通信が出来たので、実際に組込みで使いたくなるのは人の性でしょう。
ここではESP32 Devboardからアクセスして使ってみます。というのも、ESP32 IDF(Arduino IDEでも)ではmbedTLSが利用されていて、とりあえず動かしてみるのにはちょうど良かったのもあります。
ここで利用するのは、ESP-WROOM-32ブレイクアウトSD+です。

ESP-IDF のmbedTLSの組込み実装を見てみたところ、リポジトリの最新は既に3.1.0向けになっていました。releaseバージョンではまだですが、今後はESP32のTLSでもmbedTLS 3.1系が利用されることが想像されます。
とりあえず esp-idf 経由で先程と同じく www.google.com にTLS1.3でアクセスしてみます。

# release バージョンではなく、最新のrepoから全部取得してくる。
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf

# esp-idf の環境構築手順
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/linux-macos-setup.html
# 手順に従って環境を準備する。
# 必要なパッケージをインストール・手順に従ってビルド環境を整備する。
# ここからが本番
cp -r examples/protocols/https_mbedtls/ . 
cd https_mbedtls
idf.py set-target esp32
idf.py menuconfig

PC上ではmbedTLSのconfigを直接変更しましたが、esp idfではmenuconfigから設定することが出来ます。menuconfigの ”Component config” -> “mbedTLS” -> “mbedTLS v3.x related” -> “Support TLS 1.3 protocol” を有効にします。この時、WiFiの接続設定も ”Example Connection Configuration” から行って、NWに接続できるようにしておく。

次にソースコードを少し修正します。このままだとTLS 1.2で接続するので、サンプルの https_mbedtls_example_main.c を以下のように修正します。サンプルコードなので全体を載せておきます。

#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "esp_netif.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

#include "mbedtls/platform.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/esp_debug.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#include "esp_crt_bundle.h"


// 接続先はGoogle
#define WEB_SERVER "www.google.com"
#define WEB_PORT "443"
#define WEB_URL "/"

static const char *TAG = "example";

static const char *REQUEST = "GET " WEB_URL " HTTP/1.0\r\n"
    "Host: "WEB_SERVER"\r\n"
    "User-Agent: esp-idf/1.0 esp32\r\n"
    "\r\n";

// GTS Root R1 のPEMを取ってきて置いとく
#define GTSR1_CA_PEM                                              \
"-----BEGIN CERTIFICATE-----\r\n"  \
"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\r\n"  \
"bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c\r\n"  \
"-----END CERTIFICATE-----"
const unsigned char gtsr1_pem[] = GTSR1_CA_PEM;

static void https_get_task(void *pvParameters) {
    char buf[512];
    int ret, flags, len;

    mbedtls_entropy_context entropy;
    mbedtls_ctr_drbg_context ctr_drbg;
    mbedtls_ssl_context ssl;
    mbedtls_x509_crt cacert;
    mbedtls_ssl_config conf;
    mbedtls_net_context server_fd;

    mbedtls_ssl_init(&ssl);
    mbedtls_x509_crt_init(&cacert);
    mbedtls_ctr_drbg_init(&ctr_drbg);
    ESP_LOGI(TAG, "Seeding the random number generator");

    mbedtls_ssl_config_init(&conf);

    mbedtls_entropy_init(&entropy);
    if((ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
                                    NULL, 0)) != 0) {
        ESP_LOGE(TAG, "mbedtls_ctr_drbg_seed returned %d", ret);
        abort();
    }

    ESP_LOGI(TAG, "Attaching the certificate bundle...");

    ret = esp_crt_bundle_attach(&conf);

    if(ret < 0) {
        ESP_LOGE(TAG, "esp_crt_bundle_attach returned -0x%x\n\n", -ret);
        abort();
    }

    ESP_LOGI(TAG, "Setting hostname for TLS session...");

     /* Hostname set here should match CN in server certificate */
    if((ret = mbedtls_ssl_set_hostname(&ssl, WEB_SERVER)) != 0) {
        ESP_LOGE(TAG, "mbedtls_ssl_set_hostname returned -0x%x", -ret);
        abort();
    }

    ESP_LOGI(TAG, "Setting up the SSL/TLS structure...");

    if((ret = mbedtls_ssl_config_defaults(&conf,
                                          MBEDTLS_SSL_IS_CLIENT,
                                          MBEDTLS_SSL_TRANSPORT_STREAM,
                                          MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
        ESP_LOGE(TAG, "mbedtls_ssl_config_defaults returned %d", ret);
        goto exit;
    }

    /* MBEDTLS_SSL_VERIFY_OPTIONAL is bad for security, in this example it will print
       a warning if CA verification fails but it will continue to connect.

       You should consider using MBEDTLS_SSL_VERIFY_REQUIRED in your own code.
    */
    mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_OPTIONAL);

    // root caの読み込みとセッションチケットを有効にしておいた
    // あとはTLSの鍵交換モードの設定、re-negotiationはとりあえず無効に
    // TLSの最大・最小バージョンをTLS 1.3固定
    // 上記の記載のとおり、サンプルなので、本来はちゃんとVERIFYすること!!
    mbedtls_x509_crt_parse( &cacert, gtsr1_pem, sizeof(gtsr1_pem) );
    mbedtls_ssl_conf_session_tickets( &conf, MBEDTLS_SSL_SESSION_TICKETS_ENABLED );
    mbedtls_ssl_conf_tls13_key_exchange_modes( &conf, MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_ALL );
    mbedtls_ssl_conf_renegotiation( &conf, MBEDTLS_SSL_RENEGOTIATION_DISABLED );
    mbedtls_ssl_conf_min_version( &conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_4 );
    mbedtls_ssl_conf_max_version( &conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_4 );

    mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL);
    mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
#ifdef CONFIG_MBEDTLS_DEBUG
    mbedtls_esp_enable_debug_log(&conf, CONFIG_MBEDTLS_DEBUG_LEVEL);
#endif

    if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) {
        ESP_LOGE(TAG, "mbedtls_ssl_setup returned -0x%x\n\n", -ret);
        goto exit;
    }

    while(1) {
        mbedtls_net_init(&server_fd);

        ESP_LOGI(TAG, "Connecting to %s:%s...", WEB_SERVER, WEB_PORT);

        if ((ret = mbedtls_net_connect(&server_fd, WEB_SERVER,
                                      WEB_PORT, MBEDTLS_NET_PROTO_TCP)) != 0) {
            ESP_LOGE(TAG, "mbedtls_net_connect returned -%x", -ret);
            goto exit;
        }

        ESP_LOGI(TAG, "Connected.");

        mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);

        ESP_LOGI(TAG, "Performing the SSL/TLS handshake...");

        while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) {
            if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
                ESP_LOGE(TAG, "mbedtls_ssl_handshake returned -0x%x", -ret);
                goto exit;
            }
        }

        ESP_LOGI(TAG, "Verifying peer X.509 certificate...");

        if ((flags = mbedtls_ssl_get_verify_result(&ssl)) != 0) {
            /* In real life, we probably want to close connection if ret != 0 */
            ESP_LOGW(TAG, "Failed to verify peer certificate!");
            bzero(buf, sizeof(buf));
            mbedtls_x509_crt_verify_info(buf, sizeof(buf), "  ! ", flags);
            ESP_LOGW(TAG, "verification info: %s", buf);
        } else {
            ESP_LOGI(TAG, "Certificate verified.");
        }

        ESP_LOGI(TAG, "Cipher suite is %s", mbedtls_ssl_get_ciphersuite(&ssl));
        ESP_LOGI(TAG, "Writing HTTP request...");

        size_t written_bytes = 0;
        do {
            ret = mbedtls_ssl_write(&ssl,
                                    (const unsigned char *)REQUEST + written_bytes,
                                    strlen(REQUEST) - written_bytes);
            if (ret >= 0) {
                ESP_LOGI(TAG, "%d bytes written", ret);
                written_bytes += ret;
            } else if (ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret != MBEDTLS_ERR_SSL_WANT_READ) {
                ESP_LOGE(TAG, "mbedtls_ssl_write returned -0x%x", -ret);
                goto exit;
            }
        } while(written_bytes < strlen(REQUEST));

        ESP_LOGI(TAG, "Reading HTTP response...");

        do {
            len = sizeof(buf) - 1;
            bzero(buf, sizeof(buf));
            ret = mbedtls_ssl_read(&ssl, (unsigned char *)buf, len);

            if(ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
                continue;

            if(ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
                ret = 0;
                break;
            }

            if(ret < 0) {
                ESP_LOGE(TAG, "mbedtls_ssl_read returned -0x%x", -ret);
                break;
            }

            if(ret == 0) {
                ESP_LOGI(TAG, "connection closed");
                break;
            }

            len = ret;
            ESP_LOGD(TAG, "%d bytes read", len);
            /* Print response directly to stdout as it is read */
            for(int i = 0; i < len; i++) {
                putchar(buf[i]);
            }
        } while(1);

        mbedtls_ssl_close_notify(&ssl);

    exit:
        mbedtls_ssl_session_reset(&ssl);
        mbedtls_net_free(&server_fd);

        if(ret != 0) {
            mbedtls_strerror(ret, buf, 100);
            ESP_LOGE(TAG, "Last error was: -0x%x - %s", -ret, buf);
        }

        putchar('\n'); // JSON output doesn't have a newline at end

        static int request_count;
        ESP_LOGI(TAG, "Completed %d requests", ++request_count);
        printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size());

        for(int countdown = 10; countdown >= 0; countdown--) {
            ESP_LOGI(TAG, "%d...", countdown);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
        ESP_LOGI(TAG, "Starting again!");
    }
}

void app_main(void) {
    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

    xTaskCreate(&https_get_task, "https_get_task", 8192, NULL, 5, NULL);
}

あとはESP32にフラッシュして実際にシリアル経由で書き込んで、実際にTLS 1.3で www.google.com にアクセスできることを確認します。

どうやらバッチリアクセスできているようです。この時はWindows PCのモバイルホットスポットを利用して、Wiresharkでパケットキャプチャーをしましたが、こちらもTLS 1.3でアクセスしているログが取れました。

QUICの暗号化を行う

ここまでやったら、mbedTLS を使ってQUICの暗号化・復号化をしたくなるのは人の性ですね。ここでは、RFC 9001のAppendix Aにあるサンプルパケットを使ってやってみます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "common.h"

#include "mbedtls/platform.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/hmac_drbg.h"
#include "mbedtls/x509.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"
#include "mbedtls/timing.h"
#include "mbedtls/base64.h"
#include "mbedtls/hkdf.h"
#include "mbedtls/gcm.h"

#include <ssl_misc.h>
#include "ssl_tls13_keys.h"
#define BUFFER_SIZE   2000

// RFC 9001, 5.2. Initial Secrets, salt
unsigned char salt[20] = {
0x38,0x76,0x2c,0xf7,0xf5,0x59,0x34,0xb3,0x4d,0x17,0x9a,0xe6,0xa4,0xc8,0x0c,0xad,
0xcc,0xbb,0x7f,0x0a};

// RFC 9001, Appendix A.2. Client Initial QUIC long header
// c300000001088394c8f03e5157080000449e00000002
// c3 = long header(1byte)
// 00 00 00 01 = version (4byte)
// 08 = DCID Len (1byte)
// 83 94 c8 f0 3e 51 57 08 = Destination Connection ID (8byte)
// 00 = SCID Len(1byte)
// 00 = Token Length (1byte)
// 449e = length = 1182 (2byte)
// 00 00 00 02 = packet number = 2(pn_length)
unsigned char plane_header[22] = {
0xc3,0x00,0x00,0x00,0x01,0x08,0x83,0x94,0xc8,0xf0,0x3e,0x51,0x57,0x08,0x00,0x00,
0x44,0x9e,0x00,0x00,0x00,0x02};

// RFC 9001, Appendix A.2. Client Initial payload
unsigned char plane_payload[245] = {
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};

// 16進ダンプ用
void keydump(unsigned char *key, int length) {
    for (int i = 0; i < length; i++) {
        if (i != 0 && i % 32 == 0) printf("\n");
        else if (i != 0 && i % 16 == 0) printf(" ");
        printf ("%02x", key[i]);
    }
    printf("\n");
}

// mbedTLSを使ってAES128_GCM 暗号化(とりあえずサンプルなのでretはスルー)
int encrypt_payload(unsigned char *key, size_t keylen,
            unsigned char *iv, size_t ivsize,
            unsigned char *ad, size_t adsize,
            unsigned char *from, size_t fromsize,
            unsigned char *to, size_t tosize, size_t *olen) {
    int ret;
    mbedtls_gcm_context ctx;
    mbedtls_cipher_id_t cipher = MBEDTLS_CIPHER_ID_AES;
    int output_len;
    unsigned char tag_output[16];
    size_t tag_len = sizeof(tag_output);
    size_t tmpolen;

    // 鍵・Associated Data、電文を設定して暗号化
    mbedtls_gcm_init(&ctx);
    ret = mbedtls_gcm_setkey(&ctx, cipher, key,  keylen);

    // mbedtls_gcm_crypt_and_tag と同等の処理
    ret = mbedtls_gcm_starts(&ctx, MBEDTLS_GCM_ENCRYPT, iv, ivsize); 
    ret = mbedtls_gcm_update_ad(&ctx, ad, adsize);
    ret = mbedtls_gcm_update(&ctx, from, fromsize, to, tosize, olen);
    ret = mbedtls_gcm_finish(&ctx, NULL, 0, &tmpolen, tag_output, sizeof(tag_output) );

    // 最後にtagを付与する
    memcpy(to + *olen, tag_output, sizeof(tag_output));
    *olen = *olen + 16;

    mbedtls_gcm_free(&ctx);
    return ret;
}

int main() {
    int ret = 0;
    unsigned char enc_payload[BUFFER_SIZE + 1];

    // デバッグレベルを3にしておく
    mbedtls_debug_set_threshold( 3 );

    // client secret/key/iv/hp を salt, did(distinatin connection id) から取得
    const mbedtls_md_info_t *md = mbedtls_md_info_from_type( MBEDTLS_MD_SHA256 );
    unsigned char initial_secret[mbedtls_md_get_size(md)];
    ret = mbedtls_hkdf_extract( md, salt, 20, plane_header+6, 8, &initial_secret);

    unsigned char client_secret[32];
    unsigned char client_key[16];
    unsigned char client_iv[12];
    unsigned char client_hp[16];

    mbedtls_ssl_tls13_hkdf_expand_label(MBEDTLS_MD_SHA256, initial_secret, 32, "client in", 9, NULL, 0, &client_secret, 32);
    mbedtls_ssl_tls13_hkdf_expand_label(MBEDTLS_MD_SHA256, client_secret, 32, "quic key", 8, NULL, 0, &client_key, 16);
    mbedtls_ssl_tls13_hkdf_expand_label(MBEDTLS_MD_SHA256, client_secret, 32, "quic iv", 7, NULL, 0, &client_iv, 12);
    mbedtls_ssl_tls13_hkdf_expand_label(MBEDTLS_MD_SHA256, client_secret, 32, "quic hp", 7, NULL, 0, &client_hp, 16);

    keydump(initial_secret, mbedtls_md_get_size(md));
    keydump(client_secret, 32);
    keydump(client_key, 16);
    keydump(client_iv, 12);
    keydump(client_hp, 16);

    // 暗号化するペイロードの準備をする
    int padding = 1200  // QUIC UDP initialize packet size
                    - 1 // Flags
                    - 4 // version
                    - 1 // Destination Connectin ID length
                    - 8 // Destination Connectin ID 
                    - 1 // Source Connection ID length
                    - 0 // Source Connection ID
                    - 1 // Token Length
                    - 2 // length len
                    - ((plane_header[0] & 0x03) + 1) // packet number length
                    - sizeof(plane_payload)
                    - 16;
    uint32_t packet_number = *(plane_header + 18) << 24
                             | *(plane_header + 19) << 16
                             | *(plane_header + 20) << 8
                             | *(plane_header + 21) << 0;
    unsigned char payload[padding + sizeof(plane_payload)];

    // 実際に送信するデータから0パディングする
    memset(payload, 0, sizeof(payload));
    memcpy(payload, plane_payload, sizeof(plane_payload));

    // 暗号化するためのnonceを用意する RFC9001 5.3. AEAD Usage
    unsigned char nonce[12];
    unsigned int olen;
    memcpy(nonce, client_iv, sizeof(client_iv));
    nonce[11] = nonce[11] ^ packet_number;
    keydump(nonce, 12);

    // ペイロードの暗号化実行
    encrypt_payload(client_key, sizeof(client_key)*8,
            nonce, sizeof(nonce),
            plane_header, sizeof(plane_header),
            payload, sizeof(payload),
            enc_payload, sizeof(enc_payload), &olen);
    keydump(enc_payload, olen);

    // QUICヘッダの保護のためのマスクを用意 RFC9001 5.4.2. Header Protection Sample
    // sample_offset : 22(payloadの先頭16byte)
    mbedtls_aes_context aes;
    unsigned char header_mask[16];
    mbedtls_aes_init(&aes);
    mbedtls_aes_setkey_enc( &aes, (const unsigned char*) client_hp, sizeof(client_hp) * 8 );
    mbedtls_aes_crypt_ecb( &aes, MBEDTLS_AES_ENCRYPT, (const unsigned char*)enc_payload, header_mask);
    mbedtls_aes_free( &aes );
    keydump(header_mask, 16);

    // ヘッダにマスク
    plane_header[0] ^= header_mask[0] & 0x0f;
    for (int i = 0; i < 4; i++)
        plane_header[18 + i] ^= header_mask[1 + i];
    keydump(plane_header, sizeof(plane_header));

    // [保護されたヘッダ + 暗号化されたペイロード] にしてダンプ
    unsigned char encpacket[olen + sizeof(plane_header)];
    memcpy(encpacket, plane_header, sizeof(plane_header));
    memcpy(encpacket + sizeof(plane_header), enc_payload, olen);
    keydump(encpacket, olen + sizeof(plane_header));

    return 0;
}

これで”保護されたヘッダ+暗号化されたペイロード”の保護されたパケットが(enc_payload)、”c000000001088 …. 018ab0856972e194cd934″ とA.2. Client Initialの結果と合致しているはずです。復号化はこの逆をやればOKといった感じです。この暗号化・復号化もesp-idf(ESP32)での動作確認も出来ました。

実際にプロトコルを理解するには、先達の実装やサンプルを参考にしたり、RFCを読みながらサンプルコードを書いて動かして、Wiresharkでパケットを観察して…という地道な方法が良いかなと思っていたりします。暗号化・復号化をmbedTLSを使って出来そうなので、次は組込み端末上でQUICで通信を行う所までやってみようかなと。
ただし、よく僕が利用する端末だと時刻を持っていなかったり(gettime系などは自作する)、通信系のインタフェイスが独自だったり(lwipはよく見ますが)、利用する組込み環境にうまく合わせて…といった感じになってきますね。

ブログの著者欄

新里 祐教

GMOインターネットグループ株式会社

プログラマー。GMOインターネットグループにて開発案件・新規事業開発に携わる。またオープンソースの開発や色々なアイデアを形にして展示をするなどの活動を行っている。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック