[インデックス 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ハンドシェイクの主要なステップは以下の通りです。
- ClientHello: クライアントがサーバーに接続を要求し、サポートするTLSバージョン、暗号スイート、圧縮方式、および拡張などを通知します。
- ServerHello: サーバーがクライアントの要求に応答し、選択したTLSバージョン、暗号スイート、圧縮方式、およびサーバーがサポートする拡張などを通知します。
- Certificate: サーバーが自身のデジタル証明書をクライアントに送信します。
- ServerKeyExchange (オプション): 鍵交換に必要な追加情報(例: Diffie-Hellmanパラメータ)を送信します。
- CertificateRequest (オプション): サーバーがクライアント証明書を要求します。
- ServerHelloDone: サーバーがハンドシェイクメッセージの送信を完了したことを通知します。
- ClientKeyExchange: クライアントが鍵交換に必要な情報(例: プリマスターシークレット)を送信します。
- CertificateVerify (オプション): クライアントが自身の証明書を検証します。
- ChangeCipherSpec: クライアントがこれ以降の通信を暗号化することを示します。
- Finished: クライアントがハンドシェイクの完了を通知し、ハンドシェイクメッセージのハッシュを送信します。
- ChangeCipherSpec: サーバーがこれ以降の通信を暗号化することを示します。
- 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]
と同じ意味です。
さらに、clientHelloMsg
と serverHelloMsg
の unmarshal
メソッドの冒頭で、ticketSupported
と sessionTicket
のフィールドが初期化されるようになりました。これは、パース処理の前にこれらのフィールドが確実にゼロ値にリセットされるようにするための防御的なプログラミングです。特に、ticketSupported
は serverHelloMsg
の unmarshal
メソッドでも初期化されています。
テストファイル handshake_messages_test.go
では、clientHelloMsg
と serverHelloMsg
の Generate
メソッドに、ticketSupported
と sessionTicket
、ocspStapling
のランダムな設定が追加されています。これにより、これらのフィールドが設定された場合のパース処理もテストされるようになり、テストカバレッジが向上しています。
コアとなるコードの変更箇所
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のスライスに関する公式ブログ記事)