Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18095] ファイルの概要

このコミットは、Go言語の標準ライブラリ crypto/tls パッケージにおける参照テストの仕組みを大幅に改善するものです。具体的には、テストデータがGoのソースコード内にリテラルとして埋め込まれていた従来の方式から、testdata/ ディレクトリに外部ファイルとして保存する方式へと変更し、テストの更新を容易にすることを目的としています。

コミット

commit 6f149492bf939d30de3d02049939768041b73aba
Author: Adam Langley <agl@golang.org>
Date:   Fri Dec 20 11:37:05 2013 -0500

    crypto/tls: rework reference tests.
    
    The practice of storing reference connections for testing has worked
    reasonably well, but the large blocks of literal data in the .go files
    is ugly and updating the tests is a real problem because their number
    has grown.
    
    This CL changes the way that reference tests work. It's now possible to
    automatically update the tests and the test data is now stored in
    testdata/. This should make it easier to implement changes that affect
    all connections, like implementing the renegotiation extension.
    
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/42060044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6f149492bf939d30de3d02049939768041b73aba

元コミット内容

crypto/tls: rework reference tests.

このコミットの目的は、crypto/tls パッケージの参照テストを再構築することです。テストのために参照接続を保存するこれまでの方法は機能していましたが、Goファイル内に大量のリテラルデータが直接記述されており、テストの数が増えるにつれて更新が困難になっていました。

この変更により、参照テストの動作方法が変わり、テストを自動的に更新できるようになります。また、テストデータは testdata/ ディレクトリに保存されるようになりました。これにより、再ネゴシエーション拡張の実装など、すべての接続に影響する変更を実装する作業が容易になるはずです。

変更の背景

Go言語の crypto/tls パッケージは、TLS (Transport Layer Security) プロトコルを実装しており、セキュアな通信を提供するために非常に重要なコンポーネントです。TLSの実装は複雑であり、様々なプロトコルバージョン、暗号スイート、拡張機能、そしてエラーケースに対応する必要があります。そのため、堅牢なテストスイートが不可欠です。

このコミットが作成された2013年当時、crypto/tls の参照テストは、実際のTLSハンドシェイクやデータ交換のバイナリデータをGoのソースコード内にバイトスライス([]byte)のリテラルとして直接埋め込む形式を採用していました。これは、テスト対象のTLS実装が特定の参照実装(例えばOpenSSL)と互換性があることを確認するために有効な手法でした。しかし、コミットメッセージが指摘するように、このアプローチにはいくつかの問題がありました。

  1. コードの可読性と保守性: 大量のバイナリデータがGoファイル内に直接記述されているため、コードが非常に読みにくく、テストの意図を理解するのが困難でした。
  2. テスト更新の困難さ: TLSプロトコルは進化し、新しい機能(例: 再ネゴシエーション拡張)や修正が頻繁に導入されます。既存のテストデータがコード内にハードコードされていると、プロトコルの変更や新しいテストケースの追加のたびに、手動でバイナリデータを更新し、Goファイルに埋め込み直す必要がありました。これは非常に手間がかかり、エラーを引き起こしやすい作業でした。特に、すべての接続に影響するような広範な変更の場合、多数のテストケースを一度に更新する必要があり、その負担は甚大でした。
  3. テストデータの管理: テストデータがコードと一体化しているため、テストデータ自体のバージョン管理や再利用がしにくいという問題もありました。

これらの課題を解決し、crypto/tls パッケージのテストインフラをより柔軟で保守しやすいものにするために、参照テストの根本的な見直しが必要とされました。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上での通信を暗号化し、認証を行うためのプロトコルです。ウェブブラウジング(HTTPS)、電子メール(SMTPS)、VPNなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間で安全なチャネルを確立するために、以下の主要なフェーズを経ます。

  1. ハンドシェイクプロトコル: クライアントとサーバーが互いに認証し、暗号化アルゴリズムや共有鍵をネゴシエートするフェーズです。このフェーズで交換されるメッセージは、TLS通信のセキュリティの基盤となります。
  2. レコードプロトコル: ハンドシェイクで確立された安全なチャネル上で、アプリケーションデータを暗号化して送受信するフェーズです。

TLSハンドシェイク

TLSハンドシェイクは、以下のような一連のメッセージ交換で構成されます(簡略化された流れ)。

  • ClientHello: クライアントがサポートするTLSバージョン、暗号スイート、圧縮方式、ランダムなバイト列などをサーバーに通知します。
  • ServerHello: サーバーがClientHelloの情報に基づいて、使用するTLSバージョン、選択した暗号スイート、ランダムなバイト列などをクライアントに通知します。
  • Certificate: サーバーが自身の公開鍵証明書をクライアントに送信します。クライアントはこれを用いてサーバーの身元を確認します。
  • ServerKeyExchange: サーバーが鍵交換に必要な追加情報(例: Diffie-Hellmanパラメータ)を送信します。
  • ServerHelloDone: サーバーがハンドシェイクメッセージの送信を完了したことを示します。
  • ClientKeyExchange: クライアントが鍵交換に必要な情報(例: プリマスターシークレット)をサーバーに送信します。
  • CertificateVerify (オプション): クライアント証明書認証を行う場合、クライアントが自身の証明書を提示し、その証明書に対応する秘密鍵を所有していることを証明するために、ハンドシェイクメッセージのハッシュに署名して送信します。
  • ChangeCipherSpec: 以降の通信がネゴシエートされた暗号スイートと鍵で暗号化されることを通知します。
  • Finished: ハンドシェイクの完了と、これまでのハンドシェイクメッセージの整合性を検証するためのメッセージ認証コード(MAC)を送信します。

参照テスト (Reference Tests)

参照テストとは、特定のプロトコル実装(この場合、Goの crypto/tls)が、別の信頼できる「参照」実装(例えばOpenSSLやGnuTLS)と互換性があることを検証するためのテスト手法です。これは、プロトコルの仕様が複雑で、実装間の相互運用性が重要となる場合に特に有効です。

従来の参照テストでは、GoのTLSクライアントまたはサーバーが、OpenSSLなどの外部参照実装と実際に通信を行い、その際に交換される生のバイナリデータ(TLSレコードやハンドシェイクメッセージ)をキャプチャしていました。このキャプチャされたデータが「参照データ」としてGoのテストコード内にハードコードされ、テスト実行時にはGoのTLS実装がこの参照データと一致する出力を生成するか、または参照データを受け取って正しく処理できるかを検証していました。

testdata/ ディレクトリ

Goのテストでは、テストに関連する補助的なデータファイルを testdata/ という名前のディレクトリに配置することが慣習となっています。go test コマンドは、このディレクトリ内のファイルをテスト実行時に自動的に認識し、テストコードから相対パスでアクセスできるようにします。これにより、テストデータとテストコードを分離し、テストの管理を容易にすることができます。

再ネゴシエーション拡張 (TLS Renegotiation Extension)

TLSの再ネゴシエーションとは、確立されたTLSセッション中に、新しい暗号パラメータや認証情報をネゴシエートし直すプロセスです。例えば、セッション中にクライアント認証を追加したり、より強力な暗号スイートに切り替えたりする場合に利用されます。しかし、再ネゴシエーションは過去にセキュリティ上の脆弱性(Renegotiation Attack)が発見された経緯があり、TLS 1.2以降では安全な再ネゴシエーションのための拡張(RFC 5746)が導入されています。このコミットの背景には、Goの crypto/tls がこの再ネゴシエーション拡張を正しく実装する必要があり、そのためのテストを容易にするという意図も含まれています。

技術的詳細

このコミットの技術的な核心は、TLSハンドシェイクの参照テストのデータ管理と実行フローを根本的に変更した点にあります。

変更前のアプローチ

変更前は、handshake_client_test.gohandshake_server_test.go のようなテストファイル内に、rsaRC4ClientScript のようなバイトスライスの配列が直接定義されていました。これらのバイトスライスは、TLSハンドシェイク中にクライアントとサーバー間で送受信される生のTLSレコード(ヘッダーとペイロードを含む)を表していました。

例えば、rsaRC4ClientScript は、RSA-RC4暗号スイートを使用したクライアントハンドシェイクのシーケンスを、送受信されるバイト列のペアとして保持していました。テスト関数は、このスクリプトを順に実行し、GoのTLS実装が生成する出力が期待されるバイト列と一致するか、またはGoのTLS実装が受信したバイト列を正しく解釈できるかを検証していました。

この方式の最大の問題は、テストデータがGoのコードと密結合しているため、データの更新が非常に困難であったことです。TLSプロトコルのわずかな変更でも、これらのバイトスライス全体を再生成し、手動でコピー&ペーストする必要がありました。

変更後のアプローチ

このコミットでは、以下の主要な変更が導入されました。

  1. テストデータの外部化:

    • TLSハンドシェイクの参照データは、src/pkg/crypto/tls/testdata/ ディレクトリ内の個別のファイルに移動されました。ファイル名は Client-TLSv10-RSA-RC4 のように、テストのタイプとプロトコル、暗号スイートを示す形式になりました。
    • これらのファイルには、従来のバイトスライスと同様に、送受信されるTLSレコードの生データが格納されます。ただし、Goのコードとしてではなく、純粋なバイナリデータとして保存されます。
    • parseTestData 関数が導入され、これらの外部ファイルを読み込み、バイトスライスの配列としてパースする役割を担います。
  2. テスト実行フレームワークの導入:

    • clientTest および serverTest という構造体が導入されました。これらは、テスト名、参照サーバーを実行するためのコマンド(例: OpenSSLの s_server)、TLS設定 (Config)、証明書、秘密鍵などのテストメタデータをカプセル化します。
    • run メソッドがこれらのテスト構造体に追加され、テストの実行ロジックを抽象化します。
    • connFromCommand 関数が導入され、OpenSSLなどの外部参照実装を子プロセスとして起動し、Goのテストコードと参照実装間のネットワーク接続を確立します。これにより、実際のTLS通信をシミュレートできます。
  3. 自動更新機能 (-update フラグ):

    • go test コマンドに -update フラグが追加されました。このフラグが指定されると、テストは参照実装(OpenSSLなど)と実際に通信を行い、その通信で交換された生のTLSレコードをキャプチャし、対応する testdata/ ファイルに自動的に書き込みます。
    • これにより、TLSプロトコルの変更や新しいテストケースの追加があった場合でも、手動でバイナリデータを編集する代わりに、-update フラグを付けてテストを実行するだけで、テストデータを簡単に更新できるようになりました。これは、特に「再ネゴシエーション拡張」のような複雑なプロトコル機能のテストにおいて、開発者の負担を大幅に軽減します。
  4. テストの抽象化:

    • runClientTestForVersion, runClientTestTLS10, runClientTestTLS11, runClientTestTLS12 といったヘルパー関数が導入され、異なるTLSバージョンや暗号スイートに対するテストを簡潔に記述できるようになりました。これにより、テストコードの重複が削減され、可読性が向上しました。

変更によるメリット

  • 保守性の向上: テストデータがコードから分離され、外部ファイルとして管理されるようになったため、テストデータの更新や追加が格段に容易になりました。
  • 自動化: -update フラグにより、テストデータの自動生成・更新が可能になり、開発者の手作業によるエラーのリスクが減少しました。
  • 可読性の向上: 大量のバイナリリテラルがコードから削除されたため、テストコード自体の可読性が向上しました。
  • 柔軟性: 参照実装との実際の通信をシミュレートするフレームワークが導入されたことで、より現実的なシナリオでのテストが可能になり、将来的なプロトコル変更への対応も容易になりました。

コアとなるコードの変更箇所

このコミットにおける主要な変更は、以下のファイルに集中しています。

  1. src/pkg/crypto/tls/handshake_client_test.go:

    • 従来のバイトスライスリテラル (rsaRC4ClientScript, ecdheRSAAESClientScript など) が削除されました。
    • clientTest 構造体、connFromCommand 関数、run メソッド、およびバージョンごとのヘルパー関数 (runClientTestTLS10 など) が追加されました。
    • テストケース (TestHandshakeClientRSARC4 など) が、新しいフレームワークを使用するように書き換えられました。
  2. src/pkg/crypto/tls/handshake_server_test.go:

    • クライアントテストと同様に、サーバー側の参照テストからもバイトスライスリテラルが削除され、新しいフレームワーク (serverTest 構造体など) を使用するように変更されました。
  3. src/pkg/crypto/tls/handshake_test.go:

    • recordingConn 構造体(ネットワーク接続の送受信データを記録するためのラッパー)や parseTestData 関数(testdata/ ファイルをパースするためのヘルパー)など、新しいテストフレームワークの共通ユーティリティが追加されました。
    • update フラグの定義もここに含まれます。
  4. src/pkg/crypto/tls/testdata/:

    • このディレクトリが新規作成され、TLSハンドシェイクの参照データが個別のファイルとして多数追加されました。これらのファイルは、従来のGoコード内のバイトスライスリテラルに対応するバイナリデータを含んでいます。

コアとなるコードの解説

handshake_client_test.go の変更点

// 変更前:
// var rsaRC4ClientScript = [][]byte{...} // 大量のバイトスライスリテラル
// func TestHandshakeClientRSARC4(t *testing.T) {
//     var config = *testConfig
//     config.CipherSuites = []uint16{TLS_RSA_WITH_RC4_128_SHA}
//     testClientScript(t, "RSA-RC4", rsaRC4ClientScript, &config)
// }

// 変更後:
type clientTest struct {
	name string
	command []string
	config *Config
	cert []byte
	key interface{}
}

// connFromCommand は参照サーバープロセスを起動し、接続を確立する
func (test *clientTest) connFromCommand() (conn *recordingConn, child *exec.Cmd, stdin blockingSource, err error) {
	// OpenSSLなどの外部コマンドを起動し、証明書と秘密鍵を一時ファイルに書き込む
	// ネットワーク接続を確立し、recordingConnでラップして返す
}

// run はクライアントテストを実行する主要なメソッド
func (test *clientTest) run(t *testing.T, write bool) {
	var clientConn, serverConn net.Conn
	var recordingConn *recordingConn
	var childProcess *exec.Cmd
	var stdin blockingSource

	if write { // -update フラグが指定されている場合
		// 参照サーバーを起動し、実際の通信を記録する
		recordingConn, childProcess, stdin, err = test.connFromCommand()
		clientConn = recordingConn
	} else { // 通常のテスト実行の場合
		// testdata/ から参照データを読み込み、net.Pipe を使って通信をシミュレート
		clientConn, serverConn = net.Pipe()
		flows, err := test.loadData() // testdata/ からデータを読み込む
		// flows を使って serverConn 側で送受信をシミュレート
	}

	client := Client(clientConn, config) // GoのTLSクライアントを初期化

	// クライアントハンドシェイクを実行し、データを送受信
	go func() {
		client.Write([]byte("hello\n"))
		client.Close()
		clientConn.Close()
		doneChan <- true
	}()

	// シミュレーションまたは実際の通信の完了を待つ

	if write {
		// 記録された通信データを testdata/ ファイルに書き込む
		recordingConn.WriteTo(out)
	}
}

// runClientTestForVersion は特定のTLSバージョンでクライアントテストを実行するヘルパー
func runClientTestForVersion(t *testing.T, template *clientTest, prefix, option string) {
	test := *template
	test.name = prefix + test.name
	test.command = append([]string(nil), test.command...)
	test.command = append(test.command, option) // OpenSSLコマンドにバージョンオプションを追加
	test.run(t, *update) // -update フラグの状態に応じてテストを実行
}

// TestHandshakeClientRSARC4 は新しいフレームワークを使用
func TestHandshakeClientRSARC4(t *testing.T) {
	test := &clientTest{
		name:    "RSA-RC4",
		command: []string{"openssl", "s_server", "-cipher", "RC4-SHA"},
	}
	runClientTestTLS10(t, test)
	runClientTestTLS11(t, test)
	runClientTestTLS12(t, test)
}

handshake_test.go の変更点

// recordingConn は net.Conn のラッパーで、送受信されたバイト列を記録する
type recordingConn struct {
	net.Conn
	flows [][]byte // 送受信されたバイト列のシーケンス
}

func (r *recordingConn) Read(b []byte) (n int, err error) {
	n, err = r.Conn.Read(b)
	if n > 0 {
		r.flows = append(r.flows, b[:n]) // 受信データを記録
	}
	return
}

func (r *recordingConn) Write(b []byte) (n int, err error) {
	n, err = r.Conn.Write(b)
	if n > 0 {
		r.flows = append(r.flows, b[:n]) // 送信データを記録
	}
	return
}

// WriteTo は記録されたフローを io.Writer に書き出す
func (r *recordingConn) WriteTo(w io.Writer) (n int64, err error) {
	// flows の内容を特定のフォーマットでファイルに書き出すロジック
}

// parseTestData は testdata/ ファイルからバイト列のフローを読み込む
func parseTestData(r io.Reader) ([][]byte, error) {
	// ファイルの内容をパースして [][]byte を返すロジック
}

var update = flag.Bool("update", false, "update the reference tests")

これらの変更により、テストの構造が劇的に改善されました。特に、clientTest 構造体と run メソッド、そして recordingConn の導入は、テストの柔軟性と自動化を可能にする上で中心的な役割を果たしています。-update フラグは、開発者がテストデータを手動で管理する手間を省き、TLS実装の変更に迅速に対応できるようにするための重要な機能です。

関連リンク

参考にした情報源リンク