[インデックス 12734] ファイルの概要
このコミットは、Go言語のcrypto/tls
パッケージにおける、TLSハンドシェイク時の楕円曲線暗号(ECC)ベースの暗号スイート選択に関するバグ修正です。具体的には、クライアントとサーバー間で共通の楕円曲線が合意されていないにもかかわらず、ECC暗号スイートが選択されてしまう可能性があり、その結果、nilポインタ参照によるパニックが発生する問題を解決します。
コミット
commit 1d8ec87135d109aebbac5631bda9c2af37f5d593
Author: Adam Langley <agl@golang.org>
Date: Fri Mar 23 10:48:51 2012 -0400
crypto/tls: don't select ECC ciphersuites with no mutual curve.
The existing code that tried to prevent ECC ciphersuites from being
selected when there were no mutual curves still left |suite| set.
This lead to a panic on a nil pointer when there were no acceptable
ciphersuites at all.
Thanks to George Kadianakis for pointing it out.
R=golang-dev, r, bradfitz
CC=golang-dev
https://golang.org/cl/5857043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d8ec87135d109aebbac5631bda9c2af37f5d593
元コミット内容
crypto/tls: don't select ECC ciphersuites with no mutual curve.
既存のコードは、共通の曲線がない場合にECC暗号スイートが選択されるのを防ごうとしていましたが、suite
変数が設定されたままになっていました。これにより、許容可能な暗号スイートが全くない場合に、nilポインタ参照によるパニックが発生しました。
George Kadianakis氏に感謝します。
変更の背景
TLS(Transport Layer Security)ハンドシェイクの過程で、クライアントとサーバーは通信に使用する暗号スイート(Cipher Suite)を合意します。暗号スイートは、鍵交換アルゴリズム、認証アルゴリズム、バルク暗号化アルゴリズム、メッセージ認証コード(MAC)アルゴリズムの組み合わせを定義します。
このコミットの背景にある問題は、特に楕円曲線暗号(ECC)を使用する暗号スイートに関連しています。ECCベースの暗号スイートを選択する際には、クライアントとサーバーの両方がサポートする共通の楕円曲線(Named Curve)が存在する必要があります。
以前のcrypto/tls
の実装では、共通の楕円曲線がない場合にECC暗号スイートが選択されないようにするロジックが存在していました。しかし、このロジックには不備があり、たとえ共通の曲線が見つからなくても、suite
という変数が以前に選択された(しかし実際には使用できない)暗号スイートの情報を保持したままになってしまうことがありました。
その結果、もしクライアントが提示した暗号スイートの中に、サーバーがサポートする共通の楕円曲線を持つECC暗号スイートが一つもなかった場合、最終的に有効な暗号スイートが何も選択されない状態になります。この時、suite
変数がnilのまま、または無効な値を指したまま後続の処理に進むと、nilポインタ参照が発生し、プログラムがパニック(クラッシュ)するという重大なバグがありました。
このバグは、特に特定のクライアント設定やネットワーク環境下で、TLS接続の確立に失敗し、サーバーアプリケーションが予期せず終了する原因となっていました。George Kadianakis氏によってこの問題が指摘され、修正の必要性が認識されました。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号プロトコルです。ウェブブラウザとサーバー間のHTTPS通信などで広く利用されています。TLSは、通信のプライバシー、データの完全性、および認証を提供します。
TLSハンドシェイク
TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に、互いの身元を確認し、暗号化アルゴリズムや鍵を合意するためのプロセスです。主なステップは以下の通りです。
- ClientHello: クライアントがサポートするTLSバージョン、暗号スイートのリスト、圧縮方式、拡張(例:サポートする楕円曲線)などをサーバーに送信します。
- ServerHello: サーバーがClientHelloを受け取り、クライアントが提示したリストの中から、自身がサポートする最適なTLSバージョン、暗号スイート、圧縮方式を選択し、クライアントに返します。
- Certificate: サーバーが自身のデジタル証明書をクライアントに送信し、身元を証明します。
- ServerKeyExchange (ECCの場合): サーバーが鍵交換に必要な公開鍵情報などを送信します。ECCの場合、使用する楕円曲線や公開鍵の座標などが含まれます。
- ClientKeyExchange: クライアントが鍵交換に必要な情報をサーバーに送信します。
- ChangeCipherSpec: 以降の通信が暗号化されることを通知します。
- Finished: ハンドシェイクの完了を通知し、これまでのハンドシェイクメッセージのハッシュを送信して、メッセージが改ざんされていないことを確認します。
暗号スイート (Cipher Suite)
暗号スイートは、TLS通信で使用される一連のアルゴリズムの組み合わせを定義します。例えば、TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
という暗号スイートは以下を示します。
TLS
: TLSプロトコルを使用。ECDHE
: 鍵交換に楕円曲線ディフィー・ヘルマン鍵交換(Ephemeral Diffie-Hellman using Elliptic Curves)を使用。RSA
: サーバー認証にRSAアルゴリズムを使用。AES_128_GCM
: バルク暗号化にAES(Advanced Encryption Standard)の128ビット鍵長、GCM(Galois/Counter Mode)モードを使用。SHA256
: メッセージ認証コード(MAC)にSHA-256ハッシュ関数を使用。
ECC (Elliptic Curve Cryptography)
楕円曲線暗号(ECC)は、公開鍵暗号の一種で、従来のRSAなどに比べて短い鍵長で同等のセキュリティ強度を提供できるため、モバイルデバイスやリソースが限られた環境で広く利用されています。ECCは、特定の数学的な楕円曲線上の点の演算に基づいています。
楕円曲線 (Elliptic Curve)
ECCでは、特定の数学的な楕円曲線が使用されます。これらの曲線は、標準化されたもの(例:NIST P-256, P-384, P-521など)がいくつか存在し、クライアントとサーバーは共通してサポートする曲線を選択する必要があります。TLSハンドシェイクのClientHelloメッセージの拡張フィールド(Supported Elliptic Curves Extension)で、クライアントがサポートする楕円曲線のリストをサーバーに提示します。
nil
ポインタ参照
プログラミングにおいて、nil
(またはnull
)は「何もない」ことを示す特殊な値です。ポインタがnil
を指しているにもかかわらず、そのポインタが指す先のデータにアクセスしようとすると、プログラムは「nilポインタ参照」エラーを起こし、通常はクラッシュ(パニック)します。
技術的詳細
このコミットは、Go言語のcrypto/tls
パッケージ内のサーバーサイドのハンドシェイクロジック、特にhandshake_server.go
とkey_agreement.go
の2つのファイルに影響を与えます。
問題の核心は、handshake_server.go
内のFindCipherSuite
ラベルが付いたループにありました。このループは、クライアントが提示した暗号スイートのリスト(clientHello.cipherSuites
)と、サーバーが設定でサポートする暗号スイートのリスト(config.cipherSuites()
)を比較し、共通の暗号スイートを見つけようとします。
以前の実装では、共通のid
を持つ暗号スイートが見つかると、suite
変数にその暗号スイートの情報を設定していました。しかし、その後に続くチェックで、その暗号スイートがECCベースであり、かつクライアントとサーバー間で共通の楕円曲線が合意されていない場合(suite.elliptic && !ellipticOk
)、その暗号スイートはスキップされるべきでした。
問題は、このスキップ処理が行われた際に、suite
変数が以前に設定された(しかし実際には使用できない)値を保持したままになってしまう点にありました。もし、クライアントが提示したすべての暗号スイートが、このellipticOk
チェックで不合格になった場合、最終的にFindCipherSuite
ループを抜けた時点でsuite
変数は、有効な暗号スイートを指していないにもかかわらず、nilではない状態(または無効な状態)で残ってしまう可能性がありました。
そして、後続の処理で、この無効なsuite
変数(特にECC関連のフィールド)にアクセスしようとすると、nilポインタ参照が発生し、パニックを引き起こしていました。
このコミットの修正は、このロジックを改善し、suite
変数が実際に使用可能な暗号スイートのみを指すようにすることで、nilポインタパニックを防ぎます。具体的には、suite
変数を直接設定する前に、一時的なcandidate
変数を使用し、すべてのチェック(特にellipticOk
のチェック)を通過した場合にのみ、suite
変数に値を代入するように変更されています。これにより、有効な暗号スイートが見つからなかった場合には、suite
変数が確実にnilのままとなり、後続の処理で適切にエラーハンドリングされるようになります。
また、key_agreement.go
では、共通の楕円曲線が全く見つからなかった場合に、明示的にエラーを返すように変更されています。これにより、nilポインタパニックではなく、より適切なエラーメッセージが返されるようになります。
コアとなるコードの変更箇所
src/pkg/crypto/tls/handshake_server.go
--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -60,21 +60,23 @@ FindCipherSuite:
for _, id := range clientHello.cipherSuites {
for _, supported := range config.cipherSuites() {
if id == supported {
- suite = nil
+ var candidate *cipherSuite
+
for _, s := range cipherSuites {
if s.id == id {
- suite = s
+ candidate = s
break
}
}
- if suite == nil {
+ if candidate == nil {
continue
}
// Don't select a ciphersuite which we can't
// support for this client.
- if suite.elliptic && !ellipticOk {
+ if candidate.elliptic && !ellipticOk {
continue
}
+ suite = candidate
break FindCipherSuite
}
}
src/pkg/crypto/tls/key_agreement.go
--- a/src/pkg/crypto/tls/key_agreement.go
+++ b/src/pkg/crypto/tls/key_agreement.go
@@ -130,6 +130,10 @@ Curve:
}
}\n
+\tif curveid == 0 {
+\t\treturn nil, errors.New("tls: no supported elliptic curves offered")
+\t}\n
+\n var x, y *big.Int
var err error
ka.privateKey, x, y, err = elliptic.GenerateKey(ka.curve, config.rand())
コアとなるコードの解説
src/pkg/crypto/tls/handshake_server.go
の変更
suite = nil
の削除とvar candidate *cipherSuite
の導入:- 変更前は、共通の
id
を持つ暗号スイートが見つかると、まずsuite
変数をnil
にリセットし、その後suite = s
で実際の暗号スイートオブジェクトを代入していました。しかし、このsuite = nil
は冗長であり、問題の根本原因ではありませんでした。 - 重要な変更は、
suite
変数を直接操作する代わりに、一時的なcandidate
(候補)変数*cipherSuite
を導入したことです。
- 変更前は、共通の
suite = s
からcandidate = s
へ:cipherSuites
リストからid
が一致する暗号スイートが見つかった場合、それを直接suite
に代入するのではなく、candidate
に代入するように変更されました。
if suite == nil
からif candidate == nil
へ:candidate
がnil
である場合のチェックに修正されました。これは、cipherSuites
リスト内にクライアントが提示したid
を持つ暗号スイートが見つからなかった場合を処理します。この場合、ループの次のイテレーションに進みます。
if suite.elliptic && !ellipticOk
からif candidate.elliptic && !ellipticOk
へ:- ECC暗号スイートであり、かつ共通の楕円曲線が合意されていない場合(
!ellipticOk
)のチェックも、suite
ではなくcandidate
に対して行われるようになりました。この条件が真の場合、そのcandidate
はスキップされ、ループの次のイテレーションに進みます。
- ECC暗号スイートであり、かつ共通の楕円曲線が合意されていない場合(
suite = candidate
の追加:- これが最も重要な変更点です。
candidate
がすべてのチェック(nil
でないこと、ellipticOk
であること)を通過した場合にのみ、そのcandidate
の値が最終的なsuite
変数に代入されます。 - この変更により、もし有効な暗号スイートが一つも見つからなかった場合、
suite
変数は初期値のnil
のままFindCipherSuite
ループを抜けることが保証されます。これにより、後続の処理でsuite
がnil
であることを適切にチェックし、パニックを回避できるようになります。
- これが最も重要な変更点です。
src/pkg/crypto/tls/key_agreement.go
の変更
- 共通の楕円曲線が見つからない場合のエラーハンドリングの追加:
curveid == 0
という条件は、クライアントとサーバー間で共通の楕円曲線が全く見つからなかったことを示します。- 変更前は、この状況で
elliptic.GenerateKey
がnil
のcurve
引数で呼び出され、パニックを引き起こす可能性がありました。 - 変更後は、
curveid == 0
の場合に、明示的にnil
とerrors.New("tls: no supported elliptic curves offered")
を返します。これにより、より明確なエラーメッセージが提供され、パニックが回避されます。
これらの変更により、TLSハンドシェイク中にECC暗号スイートが選択される際に、共通の楕円曲線が存在しない場合に発生していたnilポインタパニックが解消され、より堅牢なエラーハンドリングが実現されました。
関連リンク
参考にした情報源リンク
- Transport Layer Security (TLS) - Wikipedia
- Cipher suite - Wikipedia
- Elliptic-curve cryptography - Wikipedia
- Go's crypto/tls package documentation (当時のバージョンに基づく)
- TLS Handshake Explained (一般的なTLSハンドシェイクの解説)
- Nil pointer dereference - Wikipedia
- Go言語におけるエラーハンドリング (一般的なGoのエラーハンドリングの概念)