[C言語で] TLS通信を実装してみて通信内容を見てみたい！

:::note info
とてもとても（4ヶ月）遅刻してしまいましたが、この記事は[DeNA 25新卒 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/dena-25-newgrad)の記事です。他の方々の記事もぜひご覧ください！
:::

みなさんは、暗号化通信（SSL/TLS）は使っていますか？

たぶん使っていると思います。

HTTPSのサイトにアクセスするなどで、普段から意識せずに触れている暗号化通信について、ふと意識してみたいなと思ったので、その通信の内容を見てみようと思います。

この記事では、C言語でTLSを使うサーバを作成し、サーバ・クライアント間の暗号化した通信を行ってみます。それから、その通信内容をキャプチャして、せっかくなら復号してその通信の中身を見ることにも挑戦したいと思います。

:::note alert
SSLなどの暗号化技術について、まだ私自身も知識不足な点が多々あります。

この記事で紹介する実装についても、一旦動作するように実装はしたものの、書かれた内容全てを理解しているという訳ではなく、誤った内容を含む可能性もありますのでご理解ください。

また、検証のためにセキュリティ機能をスキップしたりしています。実際のインターネット上で利用できるプログラムとはなっていない箇所もありますのでご注意ください。

暗号化通信の動作を見る1つのデモとしてご覧いただければ幸いです。
:::

## 先にまとめ

<details>
<summary>サーバのコード</summary>

```c:server.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
  // SSL_CTXを作成する
  SSL_CTX *ctx = NULL;
  ctx = SSL_CTX_new(TLS_server_method());
  if (ctx == NULL) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // 証明書と秘密鍵の読み込み
  if (SSL_CTX_use_certificate_file(ctx, "./server.crt", SSL_FILETYPE_PEM) <=
      0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  if (SSL_CTX_use_PrivateKey_file(ctx, "./server.key", SSL_FILETYPE_PEM) <= 0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // 受け入れ用BIOの設定
  BIO *acceptor_bio;
  const char *hostport = "12345";
  acceptor_bio = BIO_new_accept(hostport);
  if (acceptor_bio == NULL) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  BIO_set_bind_mode(acceptor_bio, BIO_BIND_REUSEADDR);
  if (BIO_do_accept(acceptor_bio) <= 0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // サーバーループ
  printf("Start Server:\n");
  for (;;) {
    unsigned char buf[8192];
    size_t nread;
    size_t nwritten;
    size_t total = 0;

    // クライアントの接続を待ち続ける
    if (BIO_do_accept(acceptor_bio) <= 0) {
      continue;
    }

    // 新しいクライアント接続がきたら、SSLハンドシェイクの準備をする
    BIO *client_bio;
    client_bio = BIO_pop(acceptor_bio);
    printf("New client connection accepted\n");

    SSL *ssl;
    if ((ssl = SSL_new(ctx)) == NULL) {
      ERR_print_errors_fp(stderr);
      printf("Error creating SSL handle for new connection\n");
      BIO_free(client_bio);
      continue;
    }
    SSL_set_bio(ssl, client_bio, client_bio);

    // SSLハンドシェイクを行う
    if (SSL_accept(ssl) <= 0) {
      ERR_print_errors_fp(stderr);
      printf("Error performing SSL handshake with client\n");
      SSL_free(ssl);
      continue;
    }

    // クライアントとの接続が閉じられるまで、クライアントの入力をエコーバックする
    while (SSL_read_ex(ssl, buf, sizeof(buf), &nread) > 0) {
      if (SSL_write_ex(ssl, buf, nread, &nwritten) > 0 && nwritten == nread) {
        total += nwritten;
        continue;
      }
      printf("Error echoing client input\n");
      break;
    }
    printf("Client connection closed, %zu bytes sent\n", total);
    SSL_free(ssl);
  }
}
```

</details>

<details>
<summary>クライアントのコード</summary>

```c:client.c
#include <arpa/inet.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

void keylog_callback(const SSL *ssl, const char *line) {
  FILE *fp;

  fp = fopen("keylog.log", "a");
  if (fp == NULL) {
    fprintf(stderr, "Can't open keylog file.\n");
    return;
  }

  fprintf(fp, "%s\n", line);

  fclose(fp);
}

int main(int argc, char *argv[]) {
  // SSL_CTXの作成
  SSL_CTX *ctx = NULL;
  ctx = SSL_CTX_new(TLS_client_method());
  if (ctx == NULL) {
    printf("Failed to create the SSL_CTX\n");
    return EXIT_FAILURE;
  }

  // サーバ証明書検証の設定
  // FIXME: 検証用に`SSL_VERIFY_NONE`で証明書の検証を行わないよう設定
  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

  // キーログファイルの記録用コールバックを設定
  SSL_CTX_set_keylog_callback(ctx, keylog_callback);

  // SSLオブジェクトの作成
  SSL *ssl = NULL;
  ssl = SSL_new(ctx);
  if (ssl == NULL) {
    printf("Failed to create the SSL object\n");
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  // ソケットの作成
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    printf("Failed to create socket\n");
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  struct sockaddr_in server;
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr("127.0.0.1");
  server.sin_port = htons(12345);

  if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
    printf("failed to connect to the server\n");
    close(sock);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  if (!SSL_set_fd(ssl, sock)) {
    perror("Error: SSL_set_fd()\n");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // TLSハンドシェイクを行う
  if (SSL_connect(ssl) < 1) {
    printf("TLS handshake failed\n");
    ERR_print_errors_fp(stderr);

    // 認証エラーの場合は原因を出力する
    if (SSL_get_verify_result(ssl) != X509_V_OK)
      printf("Verify error: %s\n",
             X509_verify_cert_error_string(SSL_get_verify_result(ssl)));

    close(sock);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  // サーバにデータを送る
  const char *message = "Hello, TLS!!";
  SSL_write(ssl, message, strlen(message));
  printf("Send message to server\n");

  // サーバからデータを受信する
  char buff[256] = {};
  SSL_read(ssl, buff, sizeof(buff));
  printf("Receive message from server: \"%s\"\n", buff);

  // クリーンアップ処理
  SSL_shutdown(ssl);
  close(sock);
  SSL_free(ssl);
  SSL_CTX_free(ctx);

  return EXIT_SUCCESS;
}
```

</details>

ソースコードのリポジトリ

https://github.com/mochi-yu/qiita-material/tree/main/clang-tls-communication

## 動機
友人と話していて、ふと暗号化通信の仕組みって気になるなーということになりました。

暗号化の技術は、普段の生活で仕組みまで意識することはほぼ無いですが、今の情報化社会では無くてはならない技術の1つと言っても過言ではないでしょう。

そこで、C言語を用いて簡単な暗号化通信を行うクライアント・サーバを実装し、その通信を覗いてみることで暗号化通信の世界へ少し脚を踏み入れてみることにしました。

## 実装と動作確認
### 準備
実行環境として、私のPCのWSL上の`Ubuntu-20.04`を使っていきます（ぼちぼちサポート終了なので更新しましょう（自戒））。まずOpenSSLのライブラリをC言語のプログラムから利用するために、`sudo apt install libssl-dev`でダウンロードしておきます。

それから、暗号化通信をキャプチャして内容を見てみたいので、`sudo apt install tshark`でUbuntu上にTSharkをインストールしておきます。TSharkはパケット解析でおなじみのWiresharkでも利用されている、パケットキャプチャのCLIツールです。

私の環境だと、そのままでは実行権限が無かったので、`sudo usermod ユーザ名 -a -G wireshark`で自身を`wireshark`グループに追加し実行権限を渡しておきます。

通信内容の表示については、Windowsの方にインストールしているWiresharkのGUIで行います。こちらについては、各自でインストールをお願いします。

また、TLSの暗号化にサーバのSSL証明書と秘密鍵が必要ですので、以下の記事を参考に作っておきます。

この記事ではオレオレ認証局を作っていますが、今回は簡単にするために**サーバ証明書についても自己署名によるものにしておきます**。記事の手順のうち、「CA証明書の作成」の手順で作成したものを、サーバの証明書として利用する形です。

https://qiita.com/sotoiwa/items/35f42d969210e0fcd62e

私が実際に実行したコマンドは以下です。これで生成される`server.crt`がサーバ証明書、`server.key`がサーバの秘密鍵です。

```shell
openssl genrsa 2048 > server.key
openssl req -x509 -new -nodes -key ca.key -subj "/CN=server" -days 10000 -out server.crt
```

### サーバの実装
まずはTLSを使ったサーバを実装していきます。この記事での実装は動作に最低限必要な部分のみを実装しています。詳細については以下の公式ドキュメントも参照してそれに沿った部分もありますが、内容については余り理解できていないといった状態です。

https://docs.openssl.org/master/man7/ossl-guide-tls-server-block/

<details>
<summary>サーバの実装の全文</summary>

```c:server.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
  // SSL_CTXを作成する
  SSL_CTX *ctx = NULL;
  ctx = SSL_CTX_new(TLS_server_method());
  if (ctx == NULL) {
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // 証明書と秘密鍵の読み込み
  if (SSL_CTX_use_certificate_file(ctx, "./server.crt", SSL_FILETYPE_PEM) <=
      0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  if (SSL_CTX_use_PrivateKey_file(ctx, "./server.key", SSL_FILETYPE_PEM) <= 0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // 受け入れ用BIOの設定
  BIO *acceptor_bio;
  const char *hostport = "12345";
  acceptor_bio = BIO_new_accept(hostport);
  if (acceptor_bio == NULL) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  BIO_set_bind_mode(acceptor_bio, BIO_BIND_REUSEADDR);
  if (BIO_do_accept(acceptor_bio) <= 0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // サーバーループ
  printf("Start Server:\n");
  for (;;) {
    unsigned char buf[8192];
    size_t nread;
    size_t nwritten;
    size_t total = 0;

    // クライアントの接続を待ち続ける
    if (BIO_do_accept(acceptor_bio) <= 0) {
      continue;
    }

    // 新しいクライアント接続がきたら、SSLハンドシェイクの準備をする
    BIO *client_bio;
    client_bio = BIO_pop(acceptor_bio);
    printf("New client connection accepted\n");

    SSL *ssl;
    if ((ssl = SSL_new(ctx)) == NULL) {
      ERR_print_errors_fp(stderr);
      printf("Error creating SSL handle for new connection\n");
      BIO_free(client_bio);
      continue;
    }
    SSL_set_bio(ssl, client_bio, client_bio);

    // SSLハンドシェイクを行う
    if (SSL_accept(ssl) <= 0) {
      ERR_print_errors_fp(stderr);
      printf("Error performing SSL handshake with client\n");
      SSL_free(ssl);
      continue;
    }

    // クライアントとの接続が閉じられるまで、クライアントの入力をエコーバックする
    while (SSL_read_ex(ssl, buf, sizeof(buf), &nread) > 0) {
      if (SSL_write_ex(ssl, buf, nread, &nwritten) > 0 && nwritten == nread) {
        total += nwritten;
        continue;
      }
      printf("Error echoing client input\n");
      break;
    }
    printf("Client connection closed, %zu bytes sent\n", total);
    SSL_free(ssl);
  }
}
```
</details>

一部のみ、比較的理解できている箇所を中心に抽出して説明します。

以下の箇所では、先ほど作成したサーバ証明書とサーバの秘密鍵を設定しています。`SSL_CTX_use_certificate_file`の部分について、公式ドキュメントでは`SSL_CTX_use_certificate_chain_file`を呼び出していました。おそらく認証局などが絡んでくる箇所と思われ、理解しきれなかったため、他の記事を参考にこの実装では`SSL_CTX_use_certificate_file`の関数を使うことにしました。

```c:server.c（一部抜粋）
  if (SSL_CTX_use_certificate_file(ctx, "./server.crt", SSL_FILETYPE_PEM) <=
      1) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  if (SSL_CTX_use_PrivateKey_file(ctx, "./server.key", SSL_FILETYPE_PEM) <= 0) {
    SSL_CTX_free(ctx);
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }
```

それと、プログラム中で`BIO *acceptor_bio;`という変数を定義して使っていますが、この`BIO`という概念についてもまだ理解しきれておらず、公式ドキュメントの言うがままに使っています。

forループの中では、以下のような処理を書いています。

- クライアントの接続を待ち続ける
- クライアントが接続をリクエストしてきたらSSLハンドシェイクで接続を確立する
- クライアントの入力を待ち、受け取ったデータをそのままオウム返しする（いわゆるエコーサーバ）

### クライアントの実装
こちらについても、以下にあげたOpenSSLの公式ドキュメントと記事を参考にしながら実装しました。

https://docs.openssl.org/master/man7/ossl-guide-tls-client-block/

https://e-penguiner.com/encrypted-socket-programming-c-cpp/#index_id3

<details>
<summary>クライアントの実装の全文</summary>

```c:client.c
#include <arpa/inet.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  // SSL_CTXの作成
  SSL_CTX *ctx = NULL;
  ctx = SSL_CTX_new(TLS_client_method());
  if (ctx == NULL) {
    printf("Failed to create the SSL_CTX\n");
    return EXIT_FAILURE;
  }

  // サーバ証明書検証の設定
  // FIXME: 検証用に`SSL_VERIFY_NONE`で証明書の検証を行わないよう設定
  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

  // SSLオブジェクトの作成
  SSL *ssl = NULL;
  ssl = SSL_new(ctx);
  if (ssl == NULL) {
    printf("Failed to create the SSL object\n");
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  // ソケットの作成
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    printf("Failed to create socket\n");
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  struct sockaddr_in server;
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr("127.0.0.1");
  server.sin_port = htons(12345);

  if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
    printf("failed to connect to the server\n");
    close(sock);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  if (!SSL_set_fd(ssl, sock)) {
    perror("Error: SSL_set_fd()\n");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }

  // TLSハンドシェイクを行う
  if (SSL_connect(ssl) < 1) {
    printf("TLS handshake failed\n");
    ERR_print_errors_fp(stderr);

    // 認証エラーの場合は原因を出力する
    if (SSL_get_verify_result(ssl) != X509_V_OK)
      printf("Verify error: %s\n",
             X509_verify_cert_error_string(SSL_get_verify_result(ssl)));

    close(sock);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  // サーバにデータを送る
  const char *message = "Hello, TLS!!";
  SSL_write(ssl, message, strlen(message));
  printf("Send message to server\n");

  // サーバからデータを受信する
  char buff[256] = {};
  SSL_read(ssl, buff, sizeof(buff));
  printf("Receive message from server: \"%s\"\n", buff);

  // クリーンアップ処理
  SSL_shutdown(ssl);
  close(sock);
  SSL_free(ssl);
  SSL_CTX_free(ctx);

  return EXIT_SUCCESS;
}
```
</details>

こちらも一部を抜粋して説明します。

以下の箇所では、サーバ証明書の検証方法について設定しています。今回は検証のための実装なので、`SSL_VERIFY_NONE`で検証を行わないように設定しました。

https://docs.openssl.org/master/man3/SSL_CTX_set_verify/

```c:client.c（抜粋）
  // サーバ証明書検証の設定
  // FIXME: 検証用に`SSL_VERIFY_NONE`で検証を行わないよう設定
  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
```

---

以下の部分では、サーバとの接続の確立をしています。上にあげた公式ドキュメントでは、`BIO_lookup_ex()`や`BIO_connect()`などの関数を利用していましたが、先ほども言ったように`BIO`の概念が理解しきれていないため、ここはシンプルな`socket()`関数などを使いました。

```c:client.c（抜粋）
  // ソケットの作成
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    printf("Failed to create socket\n");
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  struct sockaddr_in server;
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr("127.0.0.1");
  server.sin_port = htons(12345);

  if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
    printf("failed to connect to the server\n");
    close(sock);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  if (!SSL_set_fd(ssl, sock)) {
    perror("Error: SSL_set_fd()\n");
    ERR_print_errors_fp(stderr);
    exit(EXIT_FAILURE);
  }
```

その後、以下の箇所でサーバにメッセージを送信します。ここでは`"Hello, TLS!!"`という文字列をサーバに送ります。

```c:client.c（抜粋）
  // サーバにデータを送る
  const char *message = "Hello, TLS!!";
  SSL_write(ssl, message, strlen(message));
  printf("Send message to server\n");
```

### コンパイルと実行
以上までで作成したプログラムのコンパイル時には、`-lcrypto`と`-lssl`のオプションを指定し以下のように実行します。このオプションで、OpenSSLのライブラリをリンクしてコンパイルすることができます。

```shell
gcc client.c -o client -lcrypto -lssl
gcc server.c -o server -lcrypto -lssl
```

実行は1つのターミナルで`./server`としてサーバを立ち上げておき、別のターミナルから`./client`を実行します。上手く通信ができると、以下のようにサーバとクライアントそれぞれに通信のログが見えます。

サーバの出力

![サーバの出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/452ae63f-1d45-3b93-afc8-599d6b8907c5.png)

クライアントの出力

![クライアントの出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/98defaed-50c3-a326-bc42-c91702b7a1e6.png)

## パケットの確認
以上までで、暗号化通信を行うクライアントとサーバを用意することができました。次はその通信パケットをキャプチャしてみて、どのようなものなのかを見ていきたいと思います。

先ほどもお伝えした通り、パケットのキャプチャには`TShark`を使います。

https://www.wireshark.org/docs/man-pages/tshark.html

サーバを起動しておいて、別のターミナルで以下のコマンドでキャプチャを開始します。ここでは、サーバ側のポート12345に絡んでくる通信をキャプチャするようにしました。

```shell
tshark -i any -w out.pcap -f "port 12345"
```

クライアントのプログラムを1度実行して、ctrl+Cなどでキャプチャを停止すると、`out.pcap`というファイル名でキャプチャファイルが生成されました。

これをWiresharkで見たものが以下です。分かりずらいですが、サーバのポート`12345`とポート`41362`で通信をしていることが記録されています。

![Wiresharkでキャプチャ結果を確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/2382cc96-ddb0-e758-02c1-9995080d34a1.png)

個人的には、もう、この表示だけで色々感動がありました。

授業やIPAの試験対策で覚えた知識としてのSYK => SYK, ACK => ACKからなるTCPの3ウェイハンドシェイクが良く見えていたり、TLSのClient HelloとServer Helloのハンドシェイクが見えていたりします。

初めにこれを見たときは、知識が「生きた知識」になったという感動があったのを覚えています。

で、本題はそれではなく、今回やり取りしたデータを見てみたいのですが、もちろんこの通信は暗号化されているので見えませんでした。

先ほどの画像のNo.9～No.11でInfoが`Application Data`のものがありますが、パケット本体を見てもデータは暗号化されていると書かれています。

![データは暗号化されていた](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/8a689074-8764-2a60-b698-37aaa4ed0621.png)

### 暗号化をといてみる
では最後に、この暗号化を解いてみようと思います。「wireshark tls 復号」などで調べてみると、どうも`keylog_file`というものがあれば、それを使って復号ができるようです。

以下の記事を参考に、クライアント側のプログラムを修正します。どうやら、キーログファイルを記録するためのコールバック関数を設定するだけで済むようです。

https://www.bit-hive.com/articles/20221204

https://docs.openssl.org/3.2/man3/SSL_CTX_set_keylog_callback/

```diff_c:client.c（抜粋）
～～（省略）～～

#include <sys/socket.h>
#include <unistd.h>

+ void keylog_callback(const SSL *ssl, const char *line) {
+   FILE *fp;
+ 
+   fp = fopen("keylog.log", "a");
+   if (fp == NULL) {
+     fprintf(stderr, "Can't open keylog file.\n");
+     return;
+   }
+ 
+   fprintf(fp, "%s\n", line);
+ 
+   fclose(fp);
+ }

int main(int argc, char *argv[]) {
  // SSL_CTXの作成
  SSL_CTX *ctx = NULL;
  ctx = SSL_CTX_new(TLS_client_method());
  if (ctx == NULL) {
    printf("Failed to create the SSL_CTX\n");
    return EXIT_FAILURE;
  }

  // サーバ証明書検証の設定
  // FIXME: 検証用に`SSL_VERIFY_NONE`で検証を行わないよう設定
  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

+   // キーログファイルの記録用コールバックを設定
+   SSL_CTX_set_keylog_callback(ctx, keylog_callback);

  // SSLオブジェクトの作成
  SSL *ssl = NULL;
  ssl = SSL_new(ctx);
  if (ssl == NULL) {
    printf("Failed to create the SSL object\n");
    SSL_CTX_free(ctx);
    return EXIT_FAILURE;
  }

  ～～（省略）～～

}
```

再度コンパイルをして、サーバを起動、キャプチャを開始して、クライアントプログラムを実行します。今度は、`out.pcap`の他に`keylog.log`というファイルも生成されたかと思います。

### WiresharkでTLSの暗号化を解く
先ほどのキャプチャファイルをWiresharkで開き、以下の記事の「Wireshark 設定」を参考に設定していきます。

https://qiita.com/Shumei_Ito/items/044cb40f5b02682ed8c1#wireshark-%E8%A8%AD%E5%AE%9A

上部タブから「編集」→「設定」→「Protocols」→「TLS」と選んでいき、この設定の中で「(Pre)-Master-Secret log filename」という所に先ほどの`keylog.log`を設定します。それで「適用」してみると、、、見えました！

![TLS通信をふくごうできた](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/b924b281-d7a8-c6fb-8002-c6833fbae9cf.png)

先ほど`Encrypted Application Data`となっていた所の下に、復号されたデータがあり、バイナリで送信した`Hello, TLS!!`という文字列が見えています。

こんな簡単に復号できちゃうんだ･･･。

### `keylog_file`の正体を考えてみる
:::note warn
この項に書く内容は、私の推測を**特に多く含みます**。

不正確な場合もあるかと思いますので、あらかじめご了承ください。もしお詳しい方が居ればぜひコメントなどで教えてください🙇
:::

少し処理を変えてたった1つの`keylog_file`が取得できるだけで、TLSが復号できてしまうことは非常に驚きでした。色々と記事を見てみると、Chromeなどのブラウザでも、特別に`keylog_file`を出力するような設定に変更でき、HTTPSの通信が復号できてしまうようです。もちろんセキュリティ的なリスクは相当あるでしょうから、むやみに出力する設定にするのはマズいでしょう。

そんな最強の`keylog_file`って何なんだと思ったわけですが、あまりぱっとした説明に出会えず、寄せ集めの情報と知識とChatGPTとの問答などから推測したのが以下になります。

調べて出てくる記事には`keylog_file`は「TLS通信時に使われる鍵情報のダンプ」などと書かれている事があります。でも、その鍵って何なんでしょうか。今回、`keylog_file`の出力を仕込んだのはクライアント側であり、サーバにある復号用の秘密鍵はもちろん見えません。また、サーバの公開鍵を使ったとして、暗号化はできてももちろん復号はできません。

そんなときに、そういえば「**ハイブリッド暗号方式**」なんてものを、大学の授業やIPAの試験勉強で習ったなーと思い出しました。ざっくり言えば、コネクションを作るときに、その通信専用の共通鍵を作って公開鍵暗号方式でセキュアに共有し、以後はその共通鍵を使って暗号化通信を行うことで、暗号処理の負担を軽くする、みたいなものだったと記憶しています。この**共通鍵**が記録されているなら、クライアント側でそれを記録しパケットが復号できるのも納得がいきます。

その認識であっていると確認するのも大変だったので、ChatGPTに「TLSにおけるSSLKEYLOGFILEとは、ハイブリッド暗号化方式の共通鍵の写しですか？」と質問してみました。回答は以下です。

> いいえ、TLSにおける`SSLKEYLOGFILE`に記録される内容は、ハイブリッド暗号方式の共通鍵そのものではありません。しかし、**共通鍵を生成する際の重要な情報である「セッション鍵」や「プリマスターシークレット」**が記録されるため、結果的にTLS通信の内容を復号するのに必要な情報を提供します。

また、以下のようにも言っていました。

> `SSLKEYLOGFILE`はハイブリッド暗号方式の「共通鍵の写し」ではなく、**共通鍵を生成するために必要な基礎情報（プリマスターシークレットなど）**が記録されています。これにより、通信の暗号化を解読可能となりますが、ファイル自体には直接セッション鍵は含まれていません。

まとめると、**ハイブリッド暗号方式の共通鍵自体では無いが、その共通鍵を生成できるだけの情報が記録されている**と言ったところでしょうか。この回答の信憑性も確実なものでは無いですが、`keylog_file`の実際の中身を見てみても、非常に納得のいく回答のように感じました。

知識だけあった「**ハイブリッド暗号方式**」が、また1つ生きた知識になった瞬間でした。そもそもハイブリッド暗号方式というものが実際のTLSで使われているとは知らなかったし思っていませんでした。IPv6みたいに、良さはあるけどあまり普及してはいない部類のものなのかという誤解をしていました…。

## ふりかえり
ひょんな興味からやってみた今回の暗号化通信のキャプチャですが、SSL/TLSに関する仕様やその実装についての自分の無知を身に染みて感じた機会であった一方、TCPの3ウェイハンドシェイクやSSLのハンドシェイクを実際に見ることができ、知識が生きた知識になった貴重な経験でもありました。こういう普段意識していないことに、ちょっと好奇心の目を向けて見るというのを、これから働きだして忙しくなってしまっても大事にしていきたいなと思います！

あと、久しぶりにC言語を書きましたが、普段書いているGo言語に慣れすぎていて、書きづらいなぁなんて思ってしまいました。ということで、次の記事では、このTLSの通信内容のキャプチャをGo言語を使って行っていきたいと思います！

## 参考文献

https://qiita.com/sotoiwa/items/35f42d969210e0fcd62e

https://www.bit-hive.com/articles/20221204

https://e-penguiner.com/encrypted-socket-programming-c-cpp/

https://qiita.com/Shumei_Ito/items/044cb40f5b02682ed8c1