[インデックス 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のエラーハンドリングの概念)