[インデックス 12880] ファイルの概要
このコミットは、Go言語の標準ライブラリである crypto/tls
パッケージにおけるテストスクリプトの生成方法を更新するものです。具体的には、TLS接続のテストにおいて、これまでのGnuTLSのデバッグ出力とPythonスクリプトを用いたバイト列の抽出方法から、Go言語のコード自身でTLS接続を記録し、そのバイト列をテストに利用する方式へと変更しています。これにより、テストの信頼性と保守性が向上し、特にECDHE-AESクライアントのテストが追加されています。
コミット
commit 7247dcab92e4be1c0d2eec76823e27c60c9735ba
Author: Adam Langley <agl@golang.org>
Date: Wed Apr 11 12:55:57 2012 -0400
crypto/tls: update how we create testing scripts.
crypto/tls is tested, in part, by replaying recorded TLS connections
and checking that the bytes sent by the Go code haven't changed.
Previously we used GnuTLS's debug output and extracted the bytes of
the TLS connection using a Python script. That wasn't great, and I
think GnuTLS removed that level of debugging in a more current
release.
This change records the connection with Go code and adds a test for
ECDHE-AES clients generating using this method.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5988048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7247dcab92e4be1c0d2eec76823e27c60c9735ba
元コミット内容
crypto/tls: update how we create testing scripts.
crypto/tls
パッケージは、記録されたTLS接続をリプレイし、Goコードによって送信されたバイトが変更されていないことを確認することで部分的にテストされています。
以前は、GnuTLSのデバッグ出力を使用し、Pythonスクリプトを用いてTLS接続のバイトを抽出していました。これはあまり良い方法ではなく、より新しいリリースではGnuTLSがそのレベルのデバッグ機能を削除したと考えられます。
この変更は、Goコードで接続を記録し、この方法で生成されたECDHE-AESクライアントのテストを追加します。
変更の背景
Go言語の crypto/tls
パッケージは、セキュアな通信を実現するためのTLS/SSLプロトコルを実装しています。このパッケージの正確性と堅牢性を保証するためには、厳密なテストが不可欠です。特に、TLSハンドシェイクのような複雑なプロトコルにおいては、バイトレベルでの正確な動作確認が求められます。
このコミット以前は、TLS接続のテストデータ(バイト列)を生成するために、外部のTLS実装であるGnuTLSのデバッグ機能を利用していました。具体的には、GnuTLSのデバッグ出力を取得し、それをPythonスクリプト (parse-gnutls-cli-debug-log.py
) でパースして、TLS接続の生バイト列を抽出していました。この抽出されたバイト列が、Goの crypto/tls
パッケージのテストにおける「リプレイデータ」として使用されていました。
しかし、このアプローチにはいくつかの問題がありました。
- 外部依存性: テストデータの生成がGnuTLSという外部ライブラリに依存しているため、GnuTLSのバージョンアップや仕様変更によってテストが不安定になる可能性がありました。コミットメッセージにも「GnuTLSがそのレベルのデバッグ機能をより新しいリリースで削除した」という懸念が示されており、実際にテストデータの生成が困難になるリスクがありました。
- 保守性: Pythonスクリプトを介してデバッグ出力をパースするという方法は、GnuTLSの出力フォーマットに強く依存しており、GnuTLS側の変更があった場合にPythonスクリプトの更新が必要となるなど、保守の負担がありました。
- Goエコシステムとの統合: Goのコードをテストするために、Go以外の言語(Python)と外部ライブラリ(GnuTLS)を組み合わせることは、Goのエコシステム内での一貫性を損ない、開発者がテスト環境を構築する際の複雑さを増していました。
これらの背景から、テストデータの生成プロセスをGo言語自身で完結させることで、外部依存性を排除し、テストの安定性、保守性、そしてGoエコシステムとの統合性を向上させる必要がありました。また、当時比較的新しい暗号スイートであったECDHE-AESのテストカバレッジを向上させることも目的の一つでした。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間でデータの機密性、完全性、認証を提供します。
TLSハンドシェイク
TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に実行する一連のメッセージ交換です。このプロセス中に、以下のことが行われます。
- プロトコルバージョンのネゴシエーション: クライアントとサーバーがサポートするTLSのバージョン(例: TLS 1.2, TLS 1.3)を決定します。
- 暗号スイートのネゴシエーション: データの暗号化、認証、鍵交換に使用するアルゴリズムの組み合わせ(暗号スイート)を決定します。
- サーバー認証: サーバーが自身の身元を証明するためにデジタル証明書を提示します。クライアントはこの証明書を検証します。
- 鍵交換: クライアントとサーバーが、セッション鍵(対称鍵)を安全に共有するための鍵交換アルゴリズムを実行します。この鍵は、その後のデータ暗号化に使用されます。
- クライアント認証(オプション): サーバーがクライアントの身元を認証する必要がある場合、クライアントも証明書を提示します。
暗号スイート
暗号スイートは、TLS接続で使用される暗号アルゴリズムのセットを定義します。通常、以下の要素を含みます。
- 鍵交換アルゴリズム: セッション鍵を共有する方法(例: RSA, Diffie-Hellman, ECDHE)。
- 認証アルゴリズム: サーバー(およびオプションでクライアント)の身元を検証する方法(例: RSA, DSA, ECDSA)。
- 対称暗号アルゴリズム: 実際のデータを暗号化する方法(例: AES, RC4)。
- ハッシュ関数: メッセージの完全性を保証する方法(例: SHA-256)。
このコミットで言及されている暗号スイートは以下の通りです。
- TLS_RSA_WITH_RC4_128_SHA: RSA鍵交換、RC4-128ビット対称暗号、SHAハッシュ関数を使用する暗号スイート。RC4は現在では脆弱性が指摘されており、非推奨です。
- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) 鍵交換、RSA認証、AES-128ビットCBCモード対称暗号、SHAハッシュ関数を使用する暗号スイート。ECDHEは前方秘匿性(Forward Secrecy)を提供する点で優れています。
リプレイテスト
リプレイテストは、事前に記録された通信データ(この場合はTLSハンドシェイクのバイト列)をシステムに再入力し、そのシステムの応答が期待通りであるかを確認するテスト手法です。TLSの文脈では、特定のハンドシェイクシーケンスがGoの crypto/tls
パッケージによって正しく処理され、期待されるバイト列が生成されるか、または受信されたバイト列が正しく解釈されるかを検証するために使用されます。これにより、プロトコル実装の正確性を保証します。
GnuTLS
GnuTLSは、TLS/SSLプロトコルを実装したフリーソフトウェアライブラリです。多くのLinuxディストリビューションやオープンソースプロジェクトで利用されています。このコミット以前は、Goの crypto/tls
パッケージのテストデータ生成にGnuTLSのデバッグ出力が利用されていました。
技術的詳細
このコミットの核心は、TLS接続のテストデータを生成するメカニズムの変更にあります。
旧来のテストデータ生成方法(GnuTLS + Pythonスクリプト):
- GnuTLSのサーバー (
gnutls-serv
) とクライアント (gnutls-cli
) を起動し、TLS接続を確立します。 - GnuTLSのデバッグレベルを高く設定し、TLSハンドシェイク中に送受信される生バイト列を含む詳細なログを出力させます。
- このログをPythonスクリプト (
parse-gnutls-cli-debug-log.py
) にパイプします。 - Pythonスクリプトは、GnuTLSのデバッグログからTLSレコードプロトコルのバイト列を抽出し、Goのテストコードが利用できる形式(
[][]byte
のGoスライス)に変換して標準出力に出力します。 - この出力されたバイト列が、
handshake_client_test.go
やhandshake_server_test.go
内のrc4ClientScript
やrc4ServerScript
といった変数にハードコードされ、リプレイテストに使用されていました。
この方法は、GnuTLSの内部実装やデバッグ出力フォーマットに依存しており、GnuTLSのバージョンアップによってデバッグ出力の形式が変わると、Pythonスクリプトの修正が必要になるという脆弱性がありました。コミットメッセージにあるように、実際にGnuTLSが特定のデバッグレベルを削除した可能性があり、これがテストデータ生成の障害となることが懸念されていました。
新しいテストデータ生成方法(Goコードによる記録): このコミットでは、上記の問題を解決するために、Go言語自身でTLS接続のバイト列を記録するメカニズムを導入しています。
-
recordingConn
構造体の導入:handshake_server_test.go
にrecordingConn
という新しい型が追加されました。これはnet.Conn
インターフェースをラップし、Read
およびWrite
メソッドをオーバーライドします。recordingConn
は、そのRead
およびWrite
メソッドを通じて送受信されるすべてのバイト列を内部のflows [][]byte
スライスに記録します。WriteTo(w io.Writer)
メソッドが追加され、記録されたバイト列をGoのソースコード形式(var changeMe = [][]byte { ... }
のような形式)で指定されたio.Writer
(通常はos.Stdout
) に出力できるようになりました。
-
テストスクリプト生成フローの変更:
TestRunClient
およびTestRunServer
関数が修正され、TLS接続を確立する際に通常のnet.Conn
の代わりにrecordingConn
を使用するように変更されました。- これにより、Goの
crypto/tls
パッケージが実際に送受信するバイト列がrecordingConn
によって記録されます。 - 接続終了後、
recordingConn.WriteTo(os.Stdout)
を呼び出すことで、記録されたバイト列がGoのテストコードに直接貼り付け可能な形式で標準出力に出力されます。
-
ECDHE-AESクライアントテストの追加:
- 新しいテスト
TestHandshakeClientECDHEAES
がhandshake_client_test.go
に追加されました。 - このテストは、新しい記録方法で生成されたECDHE-AES暗号スイートを使用するクライアントのTLSハンドシェイクバイト列 (
ecdheAESClientScript
) をリプレイし、Goのcrypto/tls
パッケージが正しくハンドシェイクを処理できることを検証します。
- 新しいテスト
-
Pythonスクリプトの削除:
parse-gnutls-cli-debug-log.py
ファイルが削除されました。これは、GnuTLSに依存するテストデータ生成方法が不要になったためです。
この変更により、Goの crypto/tls
パッケージのテストは、外部ツールやスクリプトに依存することなく、Go自身でテストデータを生成・管理できるようになり、テストの信頼性、保守性、そしてGoエコシステム内での一貫性が大幅に向上しました。
コアとなるコードの変更箇所
src/pkg/crypto/tls/handshake_client_test.go
TestHandshakeClientECDHEAES
関数が追加されました。これは、新しいecdheAESClientScript
を使用してECDHE-AES暗号スイートのクライアントハンドシェイクをテストします。TestRunClient
関数が修正され、net.Dial
で取得したnet.Conn
をrecordingConn
でラップし、TLS接続のバイト列を記録してos.Stdout
に出力するように変更されました。これにより、新しいテストスクリプトを生成するためのユーティリティとして機能します。ecdheAESClientScript
という新しい[][]byte
変数が追加され、ECDHE-AESクライアントハンドシェイクの記録されたバイト列が格納されています。- コメントが更新され、テストスクリプトの生成方法がGnuTLSからGoコードによる記録に変更されたことが明記されました。
src/pkg/crypto/tls/handshake_server_test.go
recordingConn
構造体とそのメソッド (Read
,Write
,WriteTo
) が追加されました。これは、ネットワーク接続の送受信バイトを記録するための汎用的なラッパーです。GetTestConfig
関数が追加され、テスト設定の取得ロジックが分離されました。TestRunServer
関数が修正され、net.Listen
で取得したnet.Conn
をrecordingConn
でラップし、TLS接続のバイト列を記録してos.Stdout
に出力するように変更されました。これにより、新しいテストスクリプトを生成するためのユーティリティとして機能します。- コメントが更新され、テストスクリプトの生成方法がGnuTLSからGoコードによる記録に変更されたことが明記されました。
src/pkg/crypto/tls/parse-gnutls-cli-debug-log.py
- このファイルは完全に削除されました。
コアとなるコードの解説
recordingConn
構造体 (src/pkg/crypto/tls/handshake_server_test.go
に追加)
// recordingConn is a net.Conn that records the traffic that passes through it.
// WriteTo can be used to produce Go code that contains the recorded traffic.
type recordingConn struct {
net.Conn
lock sync.Mutex
flows [][]byte
currentlyReading bool
}
func (r *recordingConn) Read(b []byte) (n int, err error) {
// ... (バイトを読み込み、flowsに記録するロジック) ...
}
func (r *recordingConn) Write(b []byte) (n int, err error) {
// ... (バイトを書き込み、flowsに記録するロジック) ...
}
// WriteTo writes Go source code to w that contains the recorded traffic.
func (r *recordingConn) WriteTo(w io.Writer) {
fmt.Fprintf(w, "var changeMe = [][]byte {\\n")
for _, buf := range r.flows {
fmt.Fprintf(w, "\\t{\\")
for i, b := range buf {
if i%8 == 0 {
fmt.Fprintf(w, "\\n\\t\\t")
}
fmt.Fprintf(w, "0x%02x, ", b)
}
fmt.Fprintf(w, "\\n\\t},\\n")
}
fmt.Fprintf(w, "}\\n")
}
recordingConn
は、net.Conn
インターフェースを埋め込むことで、既存のネットワーク接続の機能を継承しつつ、Read
と Write
メソッドをオーバーライドして送受信されるデータを flows
スライスに記録します。WriteTo
メソッドは、記録されたバイト列をGoのバイトスライスリテラル形式(0x00, 0x01, ...
)で整形し、指定された io.Writer
に出力します。これにより、テストコードに直接貼り付けることができる形式でテストデータを生成できます。currentlyReading
フラグは、連続するRead/Write操作で同じフローにバイトを追加するか、新しいフローとして記録するかを制御するために使用されます。
TestRunClient
と TestRunServer
の変更
これらの関数は、もともと手動でTLSサーバー/クライアントを起動し、デバッグログを生成するためのユーティリティとして機能していました。このコミットでは、これらの関数が recordingConn
を使用するように変更され、GnuTLSのデバッグログの代わりに、Goの crypto/tls
パッケージ自身が生成するTLSバイト列を記録して出力する役割を担うようになりました。
例えば、TestRunClient
の変更点の一部は以下のようになります。
func TestRunClient(t *testing.T) {
if !*connect {
return
}
tcpConn, err := net.Dial("tcp", "127.0.0.1:10443")
if err != nil {
t.Fatal(err)
}
record := &recordingConn{
Conn: tcpConn,
}
config := GetTestConfig()
conn := Client(record, config) // recordingConn を使用してTLSクライアントを初期化
if err := conn.Handshake(); err != nil {
t.Fatalf("error from TLS handshake: %s", err)
}
conn.Write([]byte("hello\\n"))
conn.Close()
record.WriteTo(os.Stdout) // 記録されたバイト列を標準出力に出力
}
この変更により、開発者は go test -test.run "TestRunClient" -connect
のようにコマンドを実行するだけで、GoのTLS実装が生成する実際のハンドシェイクバイト列を直接取得できるようになりました。これにより、テストデータの生成プロセスが大幅に簡素化され、外部ツールへの依存がなくなりました。
parse-gnutls-cli-debug-log.py
の削除
このPythonスクリプトは、GnuTLSのデバッグログをパースしてGoのテストデータ形式に変換する役割を担っていました。recordingConn
の導入により、このスクリプトは不要となり、削除されました。これは、テストインフラストラクチャの簡素化と、外部依存性の排除というコミットの目的を明確に示しています。
関連リンク
- Go言語の
crypto/tls
パッケージのドキュメント: https://pkg.go.dev/crypto/tls - TLSプロトコルに関するRFC: RFC 5246 (TLS 1.2) など
参考にした情報源リンク
- GnuTLS 公式サイト: https://www.gnutls.org/
- TLSハンドシェイクの概要 (Mozilla Developer Network): https://developer.mozilla.org/ja/docs/Web/Security/Transport_Layer_Security
- 前方秘匿性 (Forward Secrecy) について: https://ja.wikipedia.org/wiki/%E5%89%8D%E6%96%B9%E7%A7%98%E5%8C%BF%E6%80%A7
- Go言語のテストについて: https://go.dev/doc/code#testing
- Go言語の
net.Conn
インターフェース: https://pkg.go.dev/net#Conn