[インデックス 10527] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/tls
パッケージにおける、TLS (Transport Layer Security) ハンドシェイク時の暗号スイートの処理に関する重要な修正です。具体的には、Goのマップ(map
)のイテレーション順序が保証されないという特性に依存していた部分を修正し、暗号スイートの選択と提示の順序が非決定論的にならないように変更しています。これにより、TLSハンドシェイクの堅牢性と予測可能性が向上します。
コミット
commit 1eb7ca924b184d06706cee78cf56d022ebb1fe5a
Author: Adam Langley <agl@golang.org>
Date: Mon Nov 28 15:34:16 2011 -0500
crypto/tls: don't rely on map iteration order.
Previously we were using the map iteration order to set the order of
the cipher suites in the ClientHello.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5440048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1eb7ca924b184d06706cee78cf56d022ebb1fe5a
元コミット内容
このコミットの元々の内容は、Goの crypto/tls
パッケージにおいて、クライアントがサーバーに提示する暗号スイートのリスト(ClientHelloメッセージの一部)の順序が、Goのマップのイテレーション順序に依存していたという問題点を指摘し、それを修正するものです。
変更の背景
TLSハンドシェイクにおいて、クライアントはサポートする暗号スイートのリストを優先順位付きでサーバーに提示します。サーバーはこのリストの中から、自身がサポートし、かつクライアントが提示した中で最も優先度の高い暗号スイートを選択します。この「優先順位」は、セキュリティ強度やパフォーマンスの観点から非常に重要です。
Go言語の map
型は、要素の挿入順序やイテレーション順序を保証しません。つまり、同じマップに対して同じキーと値を追加しても、イテレーションするたびに異なる順序で要素が返される可能性があります。
このコミット以前の crypto/tls
パッケージでは、暗号スイートの定義を map[uint16]*cipherSuite
という形式で保持していました。そして、ClientHelloメッセージで暗号スイートのリストを構築する際に、このマップをイテレーションして順序を決定していました。しかし、Goのマップの非決定論的なイテレーション順序の特性により、クライアントがサーバーに提示する暗号スイートの順序が実行ごとに変わってしまう可能性がありました。
これは、TLSハンドシェイクの予測可能性を損ない、潜在的にセキュリティ上の問題(例えば、より弱い暗号スイートが意図せず優先されてしまうなど)を引き起こす可能性がありました。このコミットは、この非決定論的な挙動を排除し、暗号スイートの提示順序を明確に制御できるようにするために行われました。
前提知識の解説
TLS/SSL ハンドシェイク
TLS (Transport Layer Security) は、インターネット上でデータを安全にやり取りするための暗号化プロトコルです。かつてはSSL (Secure Sockets Layer) と呼ばれていました。TLS接続を確立する最初のステップが「ハンドシェイク」です。
ハンドシェイクの主要なステップは以下の通りです。
- ClientHello: クライアントがサーバーに接続を要求し、自身がサポートするTLSバージョン、暗号スイートのリスト(優先順位付き)、圧縮方式、ランダムなバイト列などを送信します。
- ServerHello: サーバーはClientHelloを受け取り、クライアントが提示した情報の中から、自身がサポートする最適なTLSバージョン、暗号スイート、圧縮方式、ランダムなバイト列などを選択してクライアントに返します。
- 証明書交換: サーバーは自身のデジタル証明書をクライアントに送信し、自身の身元を証明します。
- 鍵交換: クライアントとサーバーは、共通の秘密鍵を安全に生成するための鍵交換アルゴリズムを実行します。
- Finishedメッセージ: 両者がハンドシェイクの完了と、以降の通信が暗号化されることを確認するメッセージを交換します。
暗号スイート (Cipher Suite)
暗号スイートは、TLS接続で使用される一連の暗号アルゴリズムの組み合わせを定義するものです。通常、以下の要素を含みます。
- 鍵交換アルゴリズム (Key Exchange Algorithm): クライアントとサーバーが共通の秘密鍵を安全に確立する方法(例: RSA, Diffie-Hellman, ECDHE)。
- 認証アルゴリズム (Authentication Algorithm): サーバーの身元をクライアントが検証する方法(例: RSA, DSA, ECDSA)。
- 対称暗号アルゴリズム (Symmetric Encryption Algorithm): 実際のデータ暗号化に使用されるアルゴリズム(例: AES, 3DES, RC4)。
- ハッシュ関数 (Hash Function) / MACアルゴリズム (Message Authentication Code): メッセージの完全性と認証を保証するためのアルゴリズム(例: SHA-1, SHA-256)。
例えば、TLS_RSA_WITH_AES_128_CBC_SHA
という暗号スイートは、RSAによる鍵交換と認証、AES-128-CBCによるデータ暗号化、SHAによるMACを使用することを示します。
Go言語のマップのイテレーション順序
Go言語の組み込み型である map
は、キーと値のペアを格納するハッシュテーブルです。Goの仕様では、マップをイテレーションする際の順序は保証されていません。これは、マップの実装がパフォーマンス最適化のために内部的に要素の順序を動的に変更する可能性があるためです。
この非決定論的な挙動は、通常のデータ処理では問題になりませんが、順序が意味を持つ場合(例えば、プロトコルメッセージのフィールド順序や、セキュリティ関連の優先順位など)には、明示的に順序を制御する必要があります。
技術的詳細
このコミットの技術的詳細は、Goのマップの非決定論的なイテレーション順序がTLSハンドシェイクのClientHelloメッセージにおける暗号スイートの提示順序に影響を与えていた問題を、データ構造の変更によって解決した点にあります。
-
データ構造の変更:
- 以前は、
cipherSuites
という変数がmap[uint16]*cipherSuite
型で定義されていました。uint16
は暗号スイートのIDを表し、*cipherSuite
はその詳細な定義を持つ構造体へのポインタです。 - このコミットでは、
cipherSuites
を[]*cipherSuite
(暗号スイート構造体へのポインタのスライス) 型に変更しました。スライスは要素の順序を保証するため、この変更により暗号スイートの定義順序が固定されます。 cipherSuite
構造体自体にもid uint16
フィールドが追加され、各スイートが自身のIDを持つようになりました。これにより、スライス内の各要素が自身のIDを保持し、検索時に利用できるようになります。
- 以前は、
-
暗号スイート検索ロジックの変更:
mutualCipherSuite
関数は、クライアントとサーバー間で共通の暗号スイートを見つけるために使用されます。以前はマップから直接IDで検索していましたが、スライスに変更されたため、スライスを線形探索してIDが一致するスイートを見つけるように変更されました。- サーバー側のハンドシェイク処理 (
handshake_server.go
) においても、クライアントが提示した暗号スイートIDと、サーバーがサポートする暗号スイートを比較する際に、cipherSuites
スライスをイテレーションして適切なスイートを見つけるロジックに修正されました。
-
ClientHelloメッセージ生成への影響:
initDefaultCipherSuites
関数は、デフォルトの暗号スイートリストを初期化するために使用されます。この関数も、cipherSuites
マップをイテレーションする代わりに、cipherSuites
スライスをイテレーションして、その順序でデフォルトリストを構築するように変更されました。- これにより、ClientHelloメッセージで送信される暗号スイートの順序が、
cipherSuites
スライスで定義された順序と一致するようになり、非決定論的な挙動が排除されました。
この変更は、TLSプロトコルにおける暗号スイートの順序の重要性を認識し、Go言語の特性(マップの非決定論的な順序)を考慮した堅牢な実装への改善と言えます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/pkg/crypto/tls/cipher_suites.go
ファイルにおける cipherSuites
変数の定義変更と、それに伴う関連関数の修正です。
src/pkg/crypto/tls/cipher_suites.go
--- a/src/pkg/crypto/tls/cipher_suites.go
+++ b/src/pkg/crypto/tls/cipher_suites.go
@@ -37,6 +37,7 @@ type keyAgreement interface {
// A cipherSuite is a specific combination of key agreement, cipher and MAC
// function. All cipher suites currently assume RSA key agreement.
type cipherSuite struct {
+ id uint16 // 新しく追加されたフィールド
// the lengths, in bytes, of the key material needed for each component.
keyLen int
macLen int
@@ -50,13 +51,13 @@ type cipherSuite struct {
mac func(version uint16, macKey []byte) macFunction
}
-var cipherSuites = map[uint16]*cipherSuite{ // 変更前: マップ
- TLS_RSA_WITH_RC4_128_SHA: &cipherSuite{16, 20, 0, rsaKA, false, cipherRC4, macSHA1},
- TLS_RSA_WITH_3DES_EDE_CBC_SHA: &cipherSuite{24, 20, 8, rsaKA, false, cipher3DES, macSHA1},
- TLS_RSA_WITH_AES_128_CBC_SHA: &cipherSuite{16, 20, 16, rsaKA, false, cipherAES, macSHA1},
- TLS_ECDHE_RSA_WITH_RC4_128_SHA: &cipherSuite{16, 20, 0, ecdheRSAKA, true, cipherRC4, macSHA1},
- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: &cipherSuite{24, 20, 8, ecdheRSAKA, true, cipher3DES, macSHA1},
- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: &cipherSuite{16, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1},
+var cipherSuites = []*cipherSuite{ // 変更後: スライス
+ &cipherSuite{TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, false, cipherRC4, macSHA1},
+ &cipherSuite{TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, false, cipher3DES, macSHA1},
+ &cipherSuite{TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, false, cipherAES, macSHA1},
+ &cipherSuite{TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, true, cipherRC4, macSHA1},
+ &cipherSuite{TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, true, cipher3DES, macSHA1},
+ &cipherSuite{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1},
}
func cipherRC4(key, iv []byte, isRead bool) interface{} {
@@ -159,15 +160,20 @@ func ecdheRSAKA() keyAgreement {
return new(ecdheRSAKeyAgreement)
}
-// mutualCipherSuite returns a cipherSuite and its id given a list of supported
+// mutualCipherSuite returns a cipherSuite given a list of supported
// ciphersuites and the id requested by the peer.
-func mutualCipherSuite(have []uint16, want uint16) (suite *cipherSuite, id uint16) { // 変更前: 戻り値にidが含まれる
+func mutualCipherSuite(have []uint16, want uint16) *cipherSuite { // 変更後: 戻り値からidが削除
for _, id := range have {
if id == want {
- return cipherSuites[id], id // 変更前: マップから直接取得
+ for _, suite := range cipherSuites { // 変更後: スライスを線形探索
+ if suite.id == want {
+ return suite
+ }
+ }
+ return nil
}
}
- return
+ return nil
}
// A list of the possible cipher suite ids. Taken from
コアとなるコードの解説
cipherSuite
構造体の変更
cipherSuite
構造体に id uint16
フィールドが追加されました。これにより、各暗号スイートの定義が自身のIDを内部に持つようになり、スライスとして管理する際に個々のスイートを識別できるようになります。
cipherSuites
変数の型変更
最も重要な変更は、cipherSuites
変数の型が map[uint16]*cipherSuite
から []*cipherSuite
へと変更された点です。
- 変更前 (
map[uint16]*cipherSuite
): 暗号スイートのIDをキーとして、その定義を値として持つマップでした。マップのイテレーション順序は保証されないため、このマップをイテレーションして暗号スイートのリストを生成すると、その順序が非決定論的になっていました。 - 変更後 (
[]*cipherSuite
): 暗号スイートの定義をポインタのスライスとして保持します。スライスは要素の順序を保証するため、このスライスをイテレーションすることで、定義された順序で暗号スイートのリストを取得できるようになります。これにより、ClientHelloメッセージで提示される暗号スイートの順序が固定され、予測可能になります。
mutualCipherSuite
関数の変更
この関数は、クライアントとサーバーが共通してサポートする暗号スイートを見つけるために使用されます。
- 変更前:
cipherSuites[id]
のように、マップから直接IDを使って暗号スイートを取得していました。戻り値も(suite *cipherSuite, id uint16)
のように、スイートとIDの両方を返していました。 - 変更後:
cipherSuites
がスライスになったため、マップのように直接IDでアクセスすることはできません。そのため、for _, suite := range cipherSuites
のようにスライスを線形探索し、suite.id == want
でIDが一致するスイートを見つけるように変更されました。また、suite
オブジェクト自体がIDを持つようになったため、戻り値からid
が削除され、*cipherSuite
のみを返すようになりました。
これらの変更により、crypto/tls
パッケージはGoのマップの非決定論的なイテレーション順序に依存することなく、TLSハンドシェイクにおける暗号スイートの順序を確実に制御できるようになりました。
関連リンク
- Go言語の
crypto/tls
パッケージのドキュメント (当時のバージョンに近いもの): 2011年当時のGoのドキュメントは現在のものとは異なる可能性がありますが、基本的なAPIは参考になります。 - TLSプロトコルに関するRFC (例: RFC 5246 - TLS 1.2):
参考にした情報源リンク
- Go言語の
map
型に関する公式ドキュメントやブログ記事(イテレーション順序の非保証について言及されているもの)。 - TLSプロトコルに関する一般的な情報源(MDN Web Docs, Wikipediaなど)。
- Go Gerrit (golang.org/cl) のコミットページ: https://golang.org/cl/5440048 (コミットメッセージに記載されているリンク)
- GitHubのコミットページ: https://github.com/golang/go/commit/1eb7ca924b184d06706cee78cf56d022ebb1fe5a