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

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

このコミットは、Go言語の crypto/tls パッケージにおけるNPN (Next Protocol Negotiation) 拡張のパース処理に関するバグ修正です。具体的には、ServerHello メッセージ内でNPN拡張が最後の拡張ではない場合に、正しくパースできない問題を解決しています。

コミット

commit 7e90f7b4abac5fda50cbd1c41f14e8f63def0923
Author: Adam Langley <agl@golang.org>
Date:   Tue Oct 9 13:25:47 2012 -0400

    crypto/tls: fix NPN extension parsing.
    
    I typoed the code and tried to parse all the way to the end of the
    message. Therefore it fails when NPN is not the last extension in the
    ServerHello.
    
    Fixes #4088.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6637052

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

https://github.com/golang/go/commit/7e90f7b4abac5fda50cbd1c41f14e8f63def0923

元コミット内容

crypto/tls: fix NPN extension parsing.

このコミットは、NPN拡張のパース処理におけるタイプミスを修正するものです。元のコードでは、メッセージの最後までパースしようとしていたため、NPN拡張が ServerHello メッセージの最後の拡張ではない場合にパースが失敗していました。

変更の背景

この変更は、Go言語の crypto/tls パッケージにおけるNPN (Next Protocol Negotiation) 拡張の処理に関するバグを修正するために行われました。コミットメッセージによると、開発者がコードを記述する際にタイプミスを犯し、NPN拡張のデータをパースする際に、その拡張の範囲を超えてメッセージの最後まで読み込もうとしていました。

TLS (Transport Layer Security) の ServerHello メッセージには、複数の拡張 (extensions) が含まれることがあります。これらの拡張はそれぞれ特定の長さを持っており、パース時にはその拡張のデータ範囲内でのみ処理を行う必要があります。しかし、元の実装ではNPN拡張のデータ処理において、その拡張の実際の長さではなく、メッセージ全体の残りの部分を対象としてしまっていたため、NPN拡張の後に別の拡張が続く場合に、誤ったデータを読み込み、パースエラーを引き起こしていました。

この問題は、GoのIssueトラッカーで #4088 として報告されており、このコミットはその問題を解決するために作成されました。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウザとウェブサーバー間のHTTPS通信などで広く利用されています。TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行われる一連のメッセージ交換です。

TLSハンドシェイク

TLSハンドシェイクの主要なステップは以下の通りです。

  1. ClientHello: クライアントがサーバーに接続を要求し、サポートするTLSバージョン、暗号スイート、圧縮方式、および拡張などを通知します。
  2. ServerHello: サーバーがクライアントの要求に応答し、選択したTLSバージョン、暗号スイート、圧縮方式、およびサーバーがサポートする拡張などを通知します。
  3. Certificate: サーバーが自身のデジタル証明書をクライアントに送信します。
  4. ServerKeyExchange (オプション): 鍵交換に必要な追加情報(例: Diffie-Hellmanパラメータ)を送信します。
  5. CertificateRequest (オプション): サーバーがクライアント証明書を要求します。
  6. ServerHelloDone: サーバーがハンドシェイクメッセージの送信を完了したことを通知します。
  7. ClientKeyExchange: クライアントが鍵交換に必要な情報(例: プリマスターシークレット)を送信します。
  8. CertificateVerify (オプション): クライアントが自身の証明書を検証します。
  9. ChangeCipherSpec: クライアントがこれ以降の通信を暗号化することを示します。
  10. Finished: クライアントがハンドシェイクの完了を通知し、ハンドシェイクメッセージのハッシュを送信します。
  11. ChangeCipherSpec: サーバーがこれ以降の通信を暗号化することを示します。
  12. Finished: サーバーがハンドシェイクの完了を通知し、ハンドシェイクメッセージのハッシュを送信します。

このコミットで問題となっているのは、ServerHello メッセージ内の「拡張 (extensions)」のパースです。

TLS拡張 (Extensions)

TLS拡張は、TLSプロトコルに新しい機能や情報伝達のメカニズムを追加するための仕組みです。ClientHello および ServerHello メッセージ内で使用され、クライアントとサーバーが特定の機能をサポートしているかどうかをネゴシエートしたり、追加の情報を交換したりするために使われます。各拡張はタイプと長さ、そしてデータで構成されます。

NPN (Next Protocol Negotiation)

NPNは、TLSハンドシェイク中にアプリケーション層プロトコル(例: HTTP/1.1、SPDY、HTTP/2の前身)をネゴシエートするためのTLS拡張です。クライアントとサーバーが、TLS接続確立後にどのアプリケーションプロトコルを使用するかを合意するために利用されました。NPNは後にALPN (Application-Layer Protocol Negotiation) に置き換えられましたが、当時は広く使用されていました。

NPN拡張のデータは、サーバーがサポートするプロトコル名のリストを含んでいます。各プロトコル名は、その長さを示す1バイトのプレフィックスと、それに続くプロトコル名のバイト列で構成されます。

技術的詳細

このコミットの核心は、serverHelloMsg 構造体の unmarshal メソッドにおけるNPN拡張のパースロジックの修正です。

TLSの拡張は、type (2バイト) と length (2バイト) の後に data が続く形式でエンコードされます。unmarshal メソッドは、受信したバイト列からこれらの拡張を読み取り、それぞれの拡張タイプに基づいて処理を行います。

元のコードでは、NPN拡張 (extensionNextProtoNeg) を処理する際に、拡張のデータ部分を d := data としていました。ここで data は、現在の拡張の開始位置からメッセージの最後までを指すスライスでした。このため、NPN拡張の実際の長さ length を考慮せずに、for len(d) > 0 ループで d の最後まで読み込もうとしていました。

NPN拡張のデータフォーマットは、プロトコル名のリストであり、各プロトコル名は length (1バイト) と protocol_name (lengthバイト) のペアで構成されます。元のコードでは、この内部のプロトコル名のパースループ内で d = d[1:]d = d[l:] のようにスライスを更新していましたが、これはあくまでNPN拡張のデータ内部での移動です。しかし、外側の d := data がNPN拡張の実際のデータ範囲に限定されていなかったため、NPN拡張の後に別のTLS拡張が続く場合、NPNのパースロジックが誤って後続の拡張のデータをNPNのデータの一部として解釈しようとしていました。

修正では、d := data[:length] と変更されています。これにより、d はNPN拡張の実際のデータ部分のみを指すようになります。この変更によって、NPN拡張のパースは、その拡張に割り当てられた正確なバイト数に限定され、後続の拡張のデータに影響を与えることがなくなりました。

また、m.nextProtos = append(m.nextProtos, string(d[0:l]))m.nextProtos = append(m.nextProtos, string(d[:l])) に変更されています。これは、Goのスライス操作における慣用的な表現への変更であり、機能的な違いはほとんどありませんが、より簡潔で読みやすいコードになっています。d[0:l]d[:l] と同じ意味です。

さらに、clientHelloMsgserverHelloMsgunmarshal メソッドの冒頭で、ticketSupportedsessionTicket のフィールドが初期化されるようになりました。これは、パース処理の前にこれらのフィールドが確実にゼロ値にリセットされるようにするための防御的なプログラミングです。特に、ticketSupportedserverHelloMsgunmarshal メソッドでも初期化されています。

テストファイル handshake_messages_test.go では、clientHelloMsgserverHelloMsgGenerate メソッドに、ticketSupportedsessionTicketocspStapling のランダムな設定が追加されています。これにより、これらのフィールドが設定された場合のパース処理もテストされるようになり、テストカバレッジが向上しています。

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

src/pkg/crypto/tls/handshake_messages.go

--- a/src/pkg/crypto/tls/handshake_messages.go
+++ b/src/pkg/crypto/tls/handshake_messages.go
@@ -247,6 +247,8 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
 	m.nextProtoNeg = false
 	m.serverName = ""
 	m.ocspStapling = false
+	m.ticketSupported = false
+	m.sessionTicket = nil
 
 	if len(data) == 0 {
 		// ClientHello is optionally followed by extension data
@@ -478,6 +480,7 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool {
 	m.nextProtoNeg = false
 	m.nextProtos = nil
 	m.ocspStapling = false
+	m.ticketSupported = false
 
 	if len(data) == 0 {
 		// ServerHello is optionally followed by extension data
@@ -507,14 +510,14 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool {
 		switch extension {
 		case extensionNextProtoNeg:
 			m.nextProtoNeg = true
-			d := data
+			d := data[:length]
 			for len(d) > 0 {
 				l := int(d[0])
 				d = d[1:]
 				if l == 0 || l > len(d) {
 					return false
 				}
-				m.nextProtos = append(m.nextProtos, string(d[0:l]))
+				m.nextProtos = append(m.nextProtos, string(d[:l]))
 				d = d[l:]
 			}
 		case extensionStatusRequest:

src/pkg/crypto/tls/handshake_messages_test.go

--- a/src/pkg/crypto/tls/handshake_messages_test.go
+++ b/src/pkg/crypto/tls/handshake_messages_test.go
@@ -129,6 +129,12 @@ func (*clientHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value {\
 	for i := range m.supportedCurves {
 		m.supportedCurves[i] = uint16(rand.Intn(30000))
 	}\n+\tif rand.Intn(10) > 5 {\n+\t\tm.ticketSupported = true\n+\t\tif rand.Intn(10) > 5 {\n+\t\t\tm.sessionTicket = randomBytes(rand.Intn(300), rand)\n+\t\t}\n+\t}\n \n 	return reflect.ValueOf(m)\n }\n@@ -151,6 +157,13 @@ func (*serverHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value {\
 		}\n 	}\n \n+\tif rand.Intn(10) > 5 {\n+\t\tm.ocspStapling = true\n+\t}\n+\tif rand.Intn(10) > 5 {\n+\t\tm.ticketSupported = true\n+\t}\n+\n 	return reflect.ValueOf(m)\n }\n \n```

## コアとなるコードの解説

### `src/pkg/crypto/tls/handshake_messages.go` の変更点

1.  **`clientHelloMsg.unmarshal` および `serverHelloMsg.unmarshal` の初期化**:
    `clientHelloMsg` の `unmarshal` メソッドに `m.ticketSupported = false` と `m.sessionTicket = nil` が追加されました。
    `serverHelloMsg` の `unmarshal` メソッドに `m.ticketSupported = false` が追加されました。
    これらの変更は、メッセージをパースする前に、関連するフィールドが確実にデフォルト値(ゼロ値)にリセットされるようにするためのものです。これにより、以前のパース処理の残骸が新しいパース処理に影響を与えることを防ぎ、コードの堅牢性を高めます。

2.  **NPN拡張パースの修正**:
    `serverHelloMsg.unmarshal` メソッド内の `case extensionNextProtoNeg:` ブロックが修正されました。
    -   **`d := data` から `d := data[:length]` への変更**:
        これがこのコミットの最も重要な修正点です。元のコードでは、NPN拡張のデータ部分をパースするために `d := data` としていました。ここで `data` は、現在の拡張の開始位置から `ServerHello` メッセージの最後までを含むスライスでした。このため、NPN拡張の実際の長さ `length` を無視して、メッセージの最後までパースしようとしていました。
        修正後の `d := data[:length]` は、`data` スライスを `length` で指定されたNPN拡張の実際のデータ長に限定します。これにより、NPN拡張のパースロジックは、その拡張自身のデータ範囲内でのみ動作するようになり、NPN拡張の後に続く他のTLS拡張のデータを誤って読み込むことがなくなりました。これにより、NPN拡張が `ServerHello` メッセージの最後の拡張でなくても正しくパースできるようになります。
    -   **`string(d[0:l])` から `string(d[:l])` への変更**:
        これはGo言語のスライス操作におけるスタイルの変更です。`d[0:l]` と `d[:l]` はどちらもスライスの先頭から `l` バイト目までを意味し、機能的には同じです。しかし、`d[:l]` の方がより簡潔でGoの慣習に沿った記述です。

### `src/pkg/crypto/tls/handshake_messages_test.go` の変更点

1.  **`clientHelloMsg.Generate` の変更**:
    `clientHelloMsg` のテストデータ生成ロジックに、`ticketSupported` と `sessionTicket` フィールドをランダムに設定する処理が追加されました。これにより、セッションチケット拡張が有効な場合の `ClientHello` メッセージの生成とパースがテストされるようになります。

2.  **`serverHelloMsg.Generate` の変更**:
    `serverHelloMsg` のテストデータ生成ロジックに、`ocspStapling` と `ticketSupported` フィールドをランダムに設定する処理が追加されました。これにより、OCSPステープリング拡張やセッションチケット拡張が有効な場合の `ServerHello` メッセージの生成とパースがテストされるようになり、特にNPN拡張の後にこれらの拡張が続くケースもカバーされることで、今回のバグ修正の有効性が確認できるようになります。

これらのテストの追加は、バグ修正が正しく機能することを確認し、将来的な回帰を防ぐための重要なステップです。

## 関連リンク

*   Go Issue #4088: [https://code.google.com/p/go/issues/detail?id=4088](https://code.google.com/p/go/issues/detail?id=4088) (元のGoプロジェクトのIssueトラッカーへのリンクですが、現在はGitHubに移行している可能性があります)
*   Go CL 6637052: [https://golang.org/cl/6637052](https://golang.org/cl/6637052) (Goのコードレビューシステムへのリンク)

## 参考にした情報源リンク

*   TLS (Transport Layer Security) - Wikipedia: [https://ja.wikipedia.org/wiki/Transport_Layer_Security](https://ja.wikipedia.org/wiki/Transport_Layer_Security)
*   Next Protocol Negotiation (NPN) - Wikipedia: [https://en.wikipedia.org/wiki/Next_Protocol_Negotiation](https://en.wikipedia.org/wiki/Next_Protocol_Negotiation)
*   Application-Layer Protocol Negotiation (ALPN) - Wikipedia: [https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation)
*   RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: [https://datatracker.ietf.org/doc/html/rfc5246](https://datatracker.ietf.org/doc/html/rfc5246) (TLSプロトコルの詳細な仕様)
*   RFC 7301 - Transport Layer Security (TLS) Application-Layer Protocol Negotiation (ALPN) Extension: [https://datatracker.ietf.org/doc/html/rfc7301](https://datatracker.ietf.org/doc/html/rfc7301) (ALPNに関するRFC)
*   Go言語の公式ドキュメント (crypto/tlsパッケージ): [https://pkg.go.dev/crypto/tls](https://pkg.go.dev/crypto/tls) (Go言語のTLSパッケージに関する公式ドキュメント)
*   Go Slices: usage and internals: [https://go.dev/blog/slices](https://go.dev/blog/slices) (Goのスライスに関する公式ブログ記事)