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

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

<!-- TODO: リンクの設定 -->
前回の記事にてC言語でTLSサーバを構築し、そのパケットをキャプチャしてみるといったことをしました。ただ、自分は普段はGo言語のプログラムを書くことが多く、C言語だとどうしても描きにくいなーという気持ちでした。

そこで、今回の記事では、前回の記事と同様のTLSによる暗号化通信を行うクライアント・サーバのプログラムを**Go言語**で実装し、再度パケットのキャプチャと復号をおこなってみたいと思います。

## 先にまとめ

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

```go:server/main.go
package main

import (
	"bufio"
	"crypto/tls"
	"log"
	"net"
)

func main() {
	cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal("loadkeys: ", err)
	}

	config := &tls.Config{Certificates: []tls.Certificate{cer}}
	ln, err := tls.Listen("tcp", ":12345", config)
	if err != nil {
		log.Fatal("listen: ", err)
	}
	defer ln.Close()

	log.Println("Start Server:")

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept: ", err)
		}

		log.Println("New client connection accepted")
		go func(conn net.Conn) {
			s := bufio.NewScanner(conn)
			for s.Scan() {
				msg := s.Text()
				log.Printf("Accept Message: `%s`", msg)
				_, err := conn.Write([]byte(msg))
				if err != nil {
					log.Print("write: ", err)
				}
			}
			conn.Close()
			log.Println("Client connection closed")
		}(conn)
	}
}
```

</details>

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

```go:client/main.go
package main

import (
	"crypto/tls"
	"log"
	"os"
)

func main() {
	w, err := os.Create("keylog.log")
	if err != nil {
		log.Fatal("open file: ", err)
	}

	conf := &tls.Config{
		InsecureSkipVerify: true, // 動作確認のためにTLSの検証をスキップ
		KeyLogWriter:       w,
	}

	conn, err := tls.Dial("tcp", "127.0.0.1:12345", conf)
	if err != nil {
		log.Fatal("dial: ", err)
	}
	defer conn.Close()

	_, err = conn.Write([]byte("Hello, golang TLS!!\n"))
	if err != nil {
		log.Fatal("write: ", err)
	}

	buf := make([]byte, 100)
	n, err := conn.Read(buf)
	if err != nil {
		log.Fatal("read: ", err)
	}

	log.Printf("Accept Message: `%s`", buf[:n])
}
```

</details>

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

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

## 実装と動作確認
### 準備

<!-- TODO: リンクの設定 -->

まずはツールなど準備ですが、Goで実装する場合はほとんど何も必要ありません。「Goの実行環境(Dockerでもローカルでも)」と「`tshark`」が使えれば問題ありません。Ubuntuの環境では`tshark`は`apt`経由のインストールと権限の設定が必要でした。こちらは前回の記事をご参照ください。

C言語だと標準のパッケージに加えて`OpenSSL`のライブラリを用意する必要がありましたが、Goの場合全て標準パッケージで一連の処理が実装できてしまいます。この標準での多機能さがやはり魅力的ですね。

それから、今回もサーバ証明書とサーバの秘密鍵が必要なので、前回と同じ以下のコマンドで作成しておきます。

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

今回は以下のようなフォルダ構成で`server/main.go`にサーバの実装を、`client/main.go`にクライアントの実装を書いていきます。今後のコマンドは、全て`go.mod`のあるディレクトリで実行していきます。

```
.
├── .gitignore
├── Makefile
├── go.mod
├── client
│   └── main.go
├── server
│   └── main.go
├── server.crt
└── server.key
```

### サーバの実装

まずはサーバの実装です。以下のgistなどを参考にさせていただきました。

[参考のGist](https://gist.github.com/denji/12b3a568f092ab951456)

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

```go:server/main.go
package main

import (
	"bufio"
	"crypto/tls"
	"log"
	"net"
)

func main() {
	cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal("loadkeys: ", err)
	}

	config := &tls.Config{Certificates: []tls.Certificate{cer}}
	ln, err := tls.Listen("tcp", ":12345", config)
	if err != nil {
		log.Fatal("listen: ", err)
	}
	defer ln.Close()

	log.Println("Start Server:")

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept: ", err)
		}

		log.Println("New client connection accepted")
		go func(conn net.Conn) {
			s := bufio.NewScanner(conn)
			for s.Scan() {
				msg := s.Text()
				log.Printf("Accept Message: `%s`", msg)
				_, err := conn.Write([]byte(msg))
				if err != nil {
					log.Print("write: ", err)
				}
			}
			conn.Close()
			log.Println("Client connection closed")
		}(conn)
	}
}
```

</details>

ぱっと見、C言語での実装と比べて非常にシンプルに済ませることができました。自分がGo言語に慣れていることもあるのでしょうが、標準パッケージのみで実装でき、その処理で何をしているのか分からないという箇所もありませんでした。

まず、以下の箇所では最初に作成したサーバの証明書と秘密鍵のペアを読み込んでいます。

```go:server/main.go（抜粋）
	cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal("loadkeys: ", err)
	}
```

次に、上で読み込んだキーペアを渡して、`*tls.Config`のオブジェクトを作り、さらにそれを渡して`tls.Listen("tcp", ":12345", config)`で特定のポートをリッスン（＝サーバを起動）します。

```go:server/main.go（抜粋）
	config := &tls.Config{Certificates: []tls.Certificate{cer}}
	ln, err := tls.Listen("tcp", ":12345", config)
	if err != nil {
		log.Fatal("listen: ", err)
	}
	defer ln.Close()
```

C言語でバッファの用意などを書いていたのが全く不要です。もちろん、パッケージの内部的な処理として隠蔽されているために見える部分が簡潔になっているだけなのでしょうが、こうして標準パッケージだけでパパッと処理を作れるのは本当に楽だなと感じます。

その後のforループがクライアントの接続を待つループです。`ln.Accept()`で新しいコネクションを受け付けたら、その後のgoルーチンで受信の処理を行います。

```go:server/main.go（抜粋）
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept: ", err)
		}
```

goルーチンの関数が以下です。

先ほどの`ln.Accept()`の1つ目の戻り値だった`net.Conn`型のオブジェクトが`io.Reader`のインタフェースを実装しているようで、そのまま`bufio.NewScanner(conn)`と渡すことで`*bufio.Scanner`のオブジェクトが得られました。

そうしたら`for s.Scan() { ... }`で入力があるだけループを回し、`s.Text()`で文字列として読み込んで出力。`conn.Write([]byte(msg))`でクライアントに同じ文字列を返信します。

```go:server/main.go（抜粋）
		go func(conn net.Conn) {
			s := bufio.NewScanner(conn)
			for s.Scan() {
				msg := s.Text()
				log.Printf("Accept Message: `%s`", msg)
				_, err := conn.Write([]byte(msg))
				if err != nil {
					log.Print("write: ", err)
				}
			}
			conn.Close()
			log.Println("Client connection closed")
		}(conn)
```

バッファの処理などが標準パッケージの`bufio.Scanner`で処理できてしまっているので、ほんとにシンプルに記述ができました。

### クライアントの実装
続いてクライアントの実装です。こちらもサーバの実装と同じく以下のgistを参考にさせていただきました。

[参考のGist](https://gist.github.com/denji/12b3a568f092ab951456)

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

```go:client/main.go
package main

import (
	"crypto/tls"
	"log"
	"os"
)

func main() {
	w, err := os.Create("keylog.log")
	if err != nil {
		log.Fatal("open file: ", err)
	}

	conf := &tls.Config{
		InsecureSkipVerify: true, // 検証のためにTLSの検証をスキップ
		KeyLogWriter:       w,
	}

	conn, err := tls.Dial("tcp", "127.0.0.1:12345", conf)
	if err != nil {
		log.Fatal("dial: ", err)
	}
	defer conn.Close()

	_, err = conn.Write([]byte("Hello, golang TLS!!\n"))
	if err != nil {
		log.Fatal("write: ", err)
	}

	buf := make([]byte, 100)
	n, err := conn.Read(buf)
	if err != nil {
		log.Fatal("read: ", err)
	}

	log.Printf("Accept Message: `%s`", buf[:n])
}
```

</details>

まずは以下の箇所で`*tls.Config`のオブジェクトを作ります。`InsecureSkipVerify: true`とすることで、TLSの検証をスキップしています。C言語の実装でいう`SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);`にあたるかと思われます。

```go:client/main.go（抜粋）
	conf := &tls.Config{
		InsecureSkipVerify: true, // 動作確認のためにTLSの検証をスキップ
	}
```

次に以下でサーバに接続し、`tls.Conn`のオブジェクトを受け取ります。

```go:client/main.go（抜粋）
	conn, err := tls.Dial("tcp", "127.0.0.1:12345", conf)
	if err != nil {
		log.Fatal("dial: ", err)
	}
	defer conn.Close()
```

確立されたコネクションに対して、以下でサーバにメッセージを送信します。

```go:client/main.go（抜粋）
	_, err = conn.Write([]byte("Hello, golang TLS!!\n"))
	if err != nil {
		log.Fatal("write: ", err)
	}
```

その後、以下でサーバからのレスポンスを受け取ります。理由は後述しますが、ここでは`bufio.Scanner`オブジェクトではなく`tls.Conn`オブジェクトの`Read`メソッドを使ってデータを読み込みました。

```go:client/main.go（抜粋）
	buf := make([]byte, 100)
	n, err := conn.Read(buf)
	if err != nil {
		log.Fatal("read: ", err)
	}
```

### 少し詰まったところ：`bufio.Scanner.Scan()`で処理が詰まる
詰まったところとして`bufio.Scanner.Scan()`の取り扱いがありましたので、備忘録も兼ねてここに書いておきます。

`bufio.Scanner`オブジェクトは**改行で区切られた文字列などを読み込む**のに適したオブジェクトです。参考にしたgistでは`bufio.Reader`オブジェクトを用いていましたが、挙動の確認のために公式ドキュメントを読んでいて[`Reader.ReadString('\n')`を用いるなら`bufio.Scanner`オブジェクトの方が便利だ](https://pkg.go.dev/bufio#Reader.ReadString)と書かれておりこれを利用するように書き換えました。

ここで注意したいのが、**改行区切り**で文字列を扱うという部分です。`client/main.go`のサーバへの送信処理である`conn.Write([]byte("Hello, golang TLS!!\n"))`のうち、末尾の`\n`を消して`conn.Write([]byte("Hello, golang TLS!!"))`のように送信するとサーバで処理が詰まってしまいました。

完全に挙動を理解した訳ではないのですが、区切り文字（＝改行）が来ないためまだデータが来るものと思って`Scanner.Scan()`の部分を保留しているのかなと考えています（間違っていたら教えてください）。

クライアント側の送信処理は送る文字列に改行を追加することで済みました。今度、サーバ側の処理では、文字列を`Scanner.Text()`で受け取ってそのまま返しています。ただ、ここで受け取る文字列は末尾の区切り文字（＝改行）が削除されているようです。

したがって、サーバ側の`conn.Write([]byte(msg))`で送られる文字列には改行が含まれません。そのため、クライアント側でサーバのレスポンスを`Scanner.Scan()`を使って読もうとするとまたここで処理が詰まってしまいました。

これに対して、サーバ側で送る文字列に改行を付与するという手順もありましたが、今回は色々なパターンを試してもみたいのでクライアント側の受け取り方で対応しました。その結果が先に示した`buf := make([]byte, 100)`と	`n, err := conn.Read(buf)`で読み取る形です。バッファを用意してそこにデータを読み込んでいます。

このように、`bufio.Scanner`を使う際には、区切り文字が**必ず**含まれているのかという点を気にする必要がありそうでした。

### 実行
実行は一方のターミナルで`go run ./server/main.go`としてサーバを起動し、別のターミナルで`go run ./client/main.go`と実行します。

これで以下のようにサーバ・クライアント双方で文字列が送り合えていれば成功です！

サーバの出力

![サーバの出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/c1dc03ee-78bb-4767-8507-13b79ae14f82.png)

クライアントの出力

![クライアントの出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/754c0323-087b-44b3-93e9-b218f5f7ce70.png)

## パケットの確認とキーログファイルの保存
パケットの確認は前回のC言語のときにも行いましたので、今回はキーログファイルの設定までまとめて行ってしまいます。

キーログファイルの保存設定は、まずクライアントのプログラムで保存先のファイルの`*os.File`オブジェクトを取得しておき、`*tls.Config`オブジェクトの設定項目としてそのポインターを渡します。これだけで設定が済んでしまうので、本当にシンプルですね。

```diff_go:client/main.go（抜粋）
func main() {
+	w, err := os.Create("keylog.log")
+	if err != nil {
+		log.Fatal("open file: ", err)
+	}

	conf := &tls.Config{
		InsecureSkipVerify: true, // 検証のためにTLSの検証をスキップ
+		KeyLogWriter:       w,
	}
```

これでクライアントを実行すると、`keylog.log`ファイルが保存されるようになりました。

あとはC言語のときと同じように、`tshark -i any -w out.pcap -f "port 12345"`と実行してサーバのポートを監視しておき、クライアントの処理を実行して`out.pcap`も取得します。

### 暗号化通信の復号
何もせずに`out.pcap`をWiresharkで開いてみると、案の定、通信の内容は暗号化されていて読めないかと思います。

![通信が暗号化されていて見えない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/519513e1-316d-4edf-9c8b-672d315b148a.png)

複合の手順はC言語の際と全く同じです。上部タブから「編集」→「設定」→「Protocols」→「TLS」と選んでいき、この設定の中で「(Pre)-Master-Secret log filename」という所に先ほどの`keylog.log`を設定します。

これで通信を復号すると、以下の通り今回もやり取りしている通信の内容を見ることができました！

![暗号化が解除された](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/c6c5ea4a-45b7-4b4c-b37f-ac69439bbfe8.png)

## まとめ
この記事では先日C言語で実装したTLSサーバをGo言語で実装していきました。全体的な所感としては、Go言語だとやはりシンプルかつ手軽に処理が書けるなといったことを感じています。ざっくりと**コード量はC言語のときの4分の1程度**になっていました。

ただ、本文中でも少し触れましたが、こうしてシンプルに手軽に書けるのは、標準パッケージが充実していてその内部で実装を済ませてくれているからであり、本当の意味で処理を理解しようとすると、苦しみながらもC言語で低レベルから書いた方が良いのかなとも思いました。今回の記事の`bufio.Scanner`で気を付ける必要があったことにも関連しますが、Goで手軽に書くにも、**その背後で起こっている処理や動作原理をしっかり把握し理解した上で実装を行う**必要があるなと改めて感じています。

## 参考文献

[さんｋGist](https://gist.github.com/denji/12b3a568f092ab951456)

https://pkg.go.dev/net

https://pkg.go.dev/bufio

https://pkg.go.dev/crypto/tls