[インデックス 13061] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/tls パッケージにおける、TLSハンドシェイクメッセージの一つである certificateMsg のデコード処理におけるバグ修正です。具体的には、証明書の長さを表す certLen フィールドのデコードが、その長さが 2^16-1 (65535) バイトを超える場合に誤っていた問題を修正しています。これにより、大きな証明書を含むTLS通信が正しく確立できない可能性がありました。
コミット
commit 99142f55370650fcdf1ecc742a5c3c26fdd1200f
Author: Michael Gehring <mg@ebfe.org>
Date: Mon May 14 12:26:29 2012 -0400
crypto/tls: fix decoding of certLen in certificateMsg.unmarshal
certLen was decoded incorrectly if length > 2^16-1.
R=golang-dev, agl
CC=golang-dev
https://golang.org/cl/6197077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/99142f55370650fcdf1ecc742a5c3c26fdd1200f
元コミット内容
このコミットは、crypto/tls パッケージ内の certificateMsg 構造体の unmarshal メソッドにおける certLen (証明書の長さ) のデコード処理の誤りを修正するものです。具体的には、証明書の長さが 2^16-1 (65535) バイトよりも大きい場合に、certLen の値が正しくデコードされないという問題がありました。
変更の背景
TLS (Transport Layer Security) プロトコルにおいて、クライアントまたはサーバーは Certificate メッセージを送信し、自身の公開鍵証明書(および証明書チェーン)を相手に提示します。このメッセージのフォーマットはRFC 5246 (TLS 1.2) で定義されており、各証明書は3バイトの長さフィールドに続いて実際の証明書データが配置されます。
このコミットが修正する問題は、Go言語の crypto/tls ライブラリが、この3バイトの長さフィールドを誤って解釈していたことに起因します。具体的には、3バイトの長さフィールドを uint32 型に変換する際に、最上位バイトのビットシフトが誤っていたため、2^16-1 (65535) を超える長さの証明書が正しく処理できませんでした。
このバグが存在すると、以下のような問題が発生する可能性がありました。
- TLSハンドシェイクの失敗: 長い証明書を使用しているサーバーまたはクライアントとのTLS接続が確立できない。
- サービス停止: 特定の証明書サイズに依存するシステムで、TLS通信が機能しなくなる。
- セキュリティリスク: 誤った長さの解釈により、パースエラーやデータ処理の不整合が発生し、潜在的な脆弱性につながる可能性。
この修正は、TLS通信の堅牢性と互換性を確保するために不可欠でした。
前提知識の解説
TLS (Transport Layer Security) プロトコル
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う一連のメッセージ交換です。
TLSハンドシェイクにおける Certificate メッセージ
TLSハンドシェイクの過程で、サーバー(またはクライアント認証を行う場合はクライアントも)は Certificate メッセージを送信し、自身の公開鍵証明書を相手に提示します。このメッセージには、通常、エンドエンティティ証明書とその証明書を発行したCA(認証局)の証明書など、複数の証明書が含まれることがあります。
Certificate メッセージの構造は以下のようになっています(TLS 1.2のRFC 5246より抜粋):
struct {
opaque ASN.1Cert<1..2^24-1>;
} Certificate;
struct {
Certificate certificate_list<1..2^24-1>;
} CertificateList;
ここで重要なのは、ASN.1Cert の長さが 1..2^24-1 の範囲で、3バイトの長さフィールドで表現される点です。certificate_list 全体の長さも同様に3バイトで表現されます。
バイトオーダーとビットシフト
コンピュータのメモリ上では、データはバイトの並びとして格納されます。複数のバイトで構成される数値(例えば、3バイトの長さフィールド)を扱う際には、バイトオーダー(エンディアン)が重要になります。TLSプロトコルでは、ネットワークバイトオーダー(ビッグエンディアン)が一般的に使用されます。これは、最上位バイトが最初に、最下位バイトが最後に配置される形式です。
ビットシフト演算子 (<<) は、バイナリ表現のビットを左または右に移動させるために使用されます。左シフト x << n は、x の値を 2^n 倍することに相当します。例えば、d[0] << 16 は、d[0] の値を16ビット左にシフトし、実質的に d[0] * 2^16 を計算します。これは、3バイトの数値の最上位バイトを正しい位置に配置するために使用されます。
Go言語の uint32 型
uint32 はGo言語の符号なし32ビット整数型です。これは0から 2^32-1 までの値を保持できます。TLSの3バイトの長さフィールドは最大 2^24-1 の値を表現できるため、uint32 はその値を格納するのに十分な大きさです。
技術的詳細
このバグは、certificateMsg.unmarshal メソッド内で、証明書の長さを表す certLen 変数を計算する際のビットシフト演算の誤りにありました。
TLSプロトコルでは、証明書の長さは3バイト(24ビット)で表現されます。例えば、データストリームから d[0], d[1], d[2] という3バイトを読み込んだ場合、これらを結合して24ビットの長さを表現するには、以下のように計算するのが正しい方法です(ビッグエンディアンの場合):
length = (d[0] << 16) | (d[1] << 8) | d[2]
d[0]は最上位バイトなので、16ビット左にシフトして2^16の位に配置します。d[1]は中央のバイトなので、8ビット左にシフトして2^8の位に配置します。d[2]は最下位バイトなので、シフトは不要です。
しかし、バグのあるコードでは、d[0] を 24 ビット左にシフトしていました。
certLen := uint32(d[0])<<24 | uint32(d[1])<<8 | uint32(d[2]) (修正前)
この <<24 というシフトは、d[0] を32ビット整数の最上位バイト(第4バイト)の位置に配置しようとするものです。しかし、certLen は3バイト(24ビット)の長さを表すため、d[0] は24ビット数値の最上位バイト(第3バイト)の位置に配置されるべきでした。
結果として、d[0] の値が uint32 の最上位8ビットに移動してしまい、本来の24ビットの長さフィールドの範囲外に押し出されていました。これにより、certLen の計算結果が常に0になるか、あるいは非常に小さな値になる可能性がありました。
特に、certLen が 2^16-1 (65535) を超える場合、つまり d[0] が0以外の値を持つ場合に問題が顕在化しました。d[0] が0の場合(長さが65535以下の場合)、d[0]<<24 は0になるため、d[1]<<8 | d[2] の部分のみが有効となり、結果的に2バイトの長さとして正しく解釈されていました。しかし、d[0] が1以上になると、d[0]<<24 が非ゼロとなり、本来の長さとは全く異なる値が certLen に設定されてしまい、結果として len(d) < 3+certLen のチェックで false が返されるか、あるいは不正なメモリ領域を読み込もうとするなどの問題が発生していました。
修正は、d[0] のシフト量を 24 から 16 に変更することで、d[0] が24ビット数値の最上位バイトとして正しく解釈されるようにしました。
certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2]) (修正後)
これにより、3バイトの長さフィールドが正しく uint32 型の変数 certLen にデコードされるようになり、2^16-1 を超える長さの証明書も適切に処理できるようになりました。
コアとなるコードの変更箇所
変更は src/pkg/crypto/tls/handshake_messages.go ファイルの2箇所です。
--- a/src/pkg/crypto/tls/handshake_messages.go
+++ b/src/pkg/crypto/tls/handshake_messages.go
@@ -563,7 +563,7 @@ func (m *certificateMsg) unmarshal(data []byte) bool {
if len(d) < 4 {
return false
}
- certLen := uint32(d[0])<<24 | uint32(d[1])<<8 | uint32(d[2])
+ certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2])
if uint32(len(d)) < 3+certLen {
return false
}
@@ -575,7 +575,7 @@ func (m *certificateMsg) unmarshal(data []byte) bool {
m.certificates = make([][]byte, numCerts)
d = data[7:]
for i := 0; i < numCerts; i++ {
- certLen := uint32(d[0])<<24 | uint32(d[1])<<8 | uint32(d[2])
+ certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2])
m.certificates[i] = d[3 : 3+certLen]
d = d[3+certLen:]
}
コアとなるコードの解説
handshake_messages.go ファイル内の certificateMsg 構造体の unmarshal メソッドは、受信したTLSハンドシェイクメッセージのバイト列をGoのデータ構造に変換(デコード)する役割を担っています。
このメソッド内には、証明書リスト全体の長さ、およびリスト内の個々の証明書の長さをデコードする部分があります。どちらの箇所でも、証明書の長さは3バイトで表現されており、これを uint32 型の certLen 変数に変換していました。
修正前のコード:
certLen := uint32(d[0])<<24 | uint32(d[1])<<8 | uint32(d[2])
この行は、d スライスから読み込んだ最初の3バイト (d[0], d[1], d[2]) を結合して certLen を計算しています。
uint32(d[0])<<24:d[0]は最上位バイト(MSB)として扱われるべきですが、24ビット左にシフトすることで、uint32の32ビット空間の最上位8ビット(バイト3)に配置されていました。これは、24ビットの長さフィールドの文脈では誤りです。24ビットの数値の最上位バイトは、16ビット左にシフトされるべきでした。uint32(d[1])<<8:d[1]は中央のバイトとして、8ビット左にシフトされ、正しい位置に配置されていました。uint32(d[2]):d[2]は最下位バイト(LSB)として、シフトされずに正しい位置に配置されていました。
| (ビットOR) 演算子によって、これら3つの部分が結合され、最終的な certLen の値が生成されます。
修正後のコード:
certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2])
この修正では、d[0] のシフト量が 24 から 16 に変更されました。
uint32(d[0])<<16:d[0]を16ビット左にシフトすることで、uint32の32ビット空間において、24ビットの長さフィールドの最上位バイト(バイト2)として正しく配置されます。これにより、d[0],d[1],d[2]の3バイトが、それぞれ2^16,2^8,2^0の位に正しく対応し、ビッグエンディアン形式で24ビットの長さが正確に計算されるようになります。
この変更により、certLen が 2^16-1 (65535) を超える値(つまり、d[0] が0以外の値を持つ場合)でも正しくデコードされるようになり、TLSハンドシェイクにおける証明書の処理が堅牢になりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/99142f55370650fcdf1ecc742a5c3c26fdd1200f
- Go CL (Changelist) 6197077: https://golang.org/cl/6197077
参考にした情報源リンク
- RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: https://datatracker.ietf.org/doc/html/rfc5246 (特に 7.4.2. Certificate Message のセクション)
- Go言語のビット演算に関するドキュメントやチュートリアル (一般的な知識として)
- TLSプロトコルに関する一般的な解説 (一般的な知識として)
- ビッグエンディアンとリトルエンディアンに関する解説 (一般的な知識として)
- Go言語の
crypto/tlsパッケージのソースコード (一般的な知識として)