[インデックス 14949] ファイルの概要
このコミットでは、Go言語の標準ライブラリである crypto/tls
パッケージにおいて、TLSサーバーがクライアントの提示する暗号スイートの順序ではなく、サーバー自身の暗号スイートの優先順位を強制できる機能が追加されました。具体的には以下のファイルが変更されています。
src/pkg/crypto/tls/common.go
:tls.Config
構造体に新しいフィールドが追加されました。src/pkg/crypto/tls/handshake_server.go
: サーバーサイドのハンドシェイクロジックが変更され、暗号スイートの選択にサーバーの優先順位を考慮するようになりました。src/pkg/crypto/tls/handshake_server_test.go
: 新しい機能の動作を検証するためのテストが追加されました。
コミット
crypto/tls: サーバーが自身の暗号スイートの優先順位を強制できるようにする。
以前は、GoのTLSサーバーは常にクライアントの優先順位を考慮して暗号スイートを選択していました。この変更により、サーバーの優先順位を使用するオプションが追加され、これは tls.Config.CipherSuites
を設定することで表現できます。
これはApacheの SSLHonorCipherOrder
ディレクティブを模倣したものです。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/793cbd5b81619e19eaae289ec8071e2016f85db9
元コミット内容
commit 793cbd5b81619e19eaae289ec8071e2016f85db9
Author: Adam Langley <agl@golang.org>
Date: Tue Jan 22 10:10:38 2013 -0500
crypto/tls: allow the server to enforce its ciphersuite preferences.
Previously, Go TLS servers always took the client's preferences into
account when selecting a ciphersuite. This change adds the option of
using the server's preferences, which can be expressed by setting
tls.Config.CipherSuites.
This mirrors Apache's SSLHonorCipherOrder directive.
R=golang-dev, nightlyone, bradfitz, ality
CC=golang-dev
https://golang.org/cl/7163043
変更の背景
この変更の背景には、TLSハンドシェイクにおける暗号スイートの選択メカニズムと、セキュリティ上のベストプラクティスがあります。
従来のGoのTLSサーバーでは、クライアントが提示する暗号スイートのリストを、クライアントが提示した順序で評価し、サーバーがサポートする最初の暗号スイートを選択していました。これは「クライアントの優先順位を尊重する」という一般的な挙動です。しかし、この挙動にはセキュリティ上の懸念が存在します。
例えば、クライアントが脆弱な暗号スイート(例: RC4など)をリストの先頭に提示し、サーバーもそれをサポートしている場合、サーバーはその脆弱な暗号スイートを選択してしまう可能性があります。これは、サーバーがより強力で安全な暗号スイートをサポートしているにもかかわらず、クライアントの優先順位によってセキュリティレベルが低下するリスクを意味します。
このような問題を回避するため、サーバー側で暗号スイートの選択順序を強制できる機能が求められていました。この機能は、Apache HTTP Serverにおける SSLHonorCipherOrder
ディレクティブと同様の目的を持ちます。SSLHonorCipherOrder
が On
に設定されている場合、Apacheはクライアントが提示する暗号スイートの順序ではなく、サーバーの設定ファイルで定義された暗号スイートの順序を優先して選択します。これにより、サーバー管理者は、より安全な暗号スイートを優先的に使用するように強制し、既知の脆弱性を持つ暗号スイートの使用を避けることができます。
このコミットは、GoのTLSサーバーにも同様の制御メカニズムを導入し、サーバー管理者がセキュリティポリシーをより厳密に適用できるようにすることを目的としています。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上でデータを安全にやり取りするための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間の通信を暗号化し、データの完全性を保証し、通信相手の認証を行うことで、盗聴、改ざん、なりすましを防ぎます。
TLSハンドシェイク
TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に実行する一連のステップです。このプロセス中に、両者はプロトコルバージョン、暗号スイート、認証情報などをネゴシエートします。
ハンドシェイクの主要なステップは以下の通りです。
- ClientHello: クライアントがサーバーに接続を要求し、サポートするTLSバージョン、暗号スイートのリスト(優先順位付き)、圧縮方式、ランダムなバイト列などを送信します。
- ServerHello: サーバーはClientHelloを受け取り、クライアントが提示したリストの中から、自身がサポートする最適なTLSバージョンと暗号スイートを選択し、自身のランダムなバイト列と共にクライアントに返します。
- Certificate: サーバーは自身のデジタル証明書をクライアントに送信し、自身の身元を証明します。
- ServerKeyExchange (オプション): 鍵交換アルゴリズムによっては、サーバーが追加の鍵交換情報を送信します。
- ServerHelloDone: サーバーはハンドシェイクの初期フェーズが完了したことを示します。
- ClientKeyExchange: クライアントはサーバーの公開鍵を使用して、セッション鍵の生成に必要な情報を暗号化してサーバーに送信します。
- ChangeCipherSpec: クライアントは、これ以降の通信が暗号化されることをサーバーに通知します。
- Finished: クライアントは、ハンドシェイクのこれまでのメッセージのハッシュを暗号化して送信し、ハンドシェイクが成功したことを確認します。
- ChangeCipherSpec: サーバーも同様に、これ以降の通信が暗号化されることをクライアントに通知します。
- Finished: サーバーもハンドシェイクのこれまでのメッセージのハッシュを暗号化して送信し、ハンドシェイクが成功したことを確認します。
この後、クライアントとサーバーは確立されたセッション鍵を使用して、アプリケーションデータを暗号化して通信します。
暗号スイート (Cipher Suite)
暗号スイートは、TLS通信で使用される一連のアルゴリズムの組み合わせを定義するものです。通常、以下の要素を含みます。
- 鍵交換アルゴリズム: クライアントとサーバーがセッション鍵を安全に共有する方法(例: RSA, Diffie-Hellman, ECDHE)。
- 認証アルゴリズム: サーバー(およびオプションでクライアント)の身元を検証する方法(例: RSA, DSA, ECDSA)。
- バルク暗号化アルゴリズム: 実際のアプリケーションデータを暗号化する方法(例: AES, 3DES, RC4)。
- MACアルゴリズム: メッセージの完全性を保証する方法(例: SHA-256, SHA-384)。
暗号スイートは通常、TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
のような形式で表現されます。
クライアントとサーバーの暗号スイート優先順位
TLSハンドシェイクにおいて、クライアントはサポートする暗号スイートのリストを優先順位付きでサーバーに提示します。サーバーは、このリストと自身がサポートする暗号スイートのリストを比較し、両者が共通してサポートする暗号スイートの中から一つを選択します。
この選択プロセスには2つの主要なアプローチがあります。
- クライアントの優先順位を尊重する (Client Order Preference): サーバーはクライアントが提示したリストの順序に従い、自身がサポートする最初の暗号スイートを選択します。多くのTLS実装のデフォルトの挙動です。
- サーバーの優先順位を強制する (Server Order Preference): サーバーはクライアントが提示したリストの中から、自身が設定した暗号スイートの優先順位に従って最適なものを選択します。これは、サーバー管理者がセキュリティポリシーを厳密に適用したい場合に有用です。
Apacheの SSLHonorCipherOrder
ディレクティブ
Apache HTTP Serverの mod_ssl
モジュールには、SSLHonorCipherOrder
というディレクティブがあります。
SSLHonorCipherOrder On
: サーバーは、クライアントが提示する暗号スイートの順序ではなく、サーバーのSSLCipherSuite
ディレクティブで定義された暗号スイートの順序を優先します。これにより、サーバー管理者は、より強力で安全な暗号スイートを優先的に使用するように強制できます。SSLHonorCipherOrder Off
(デフォルト): サーバーはクライアントが提示する暗号スイートの順序を尊重します。
このコミットは、Goの crypto/tls
パッケージに SSLHonorCipherOrder
と同様の機能を追加するものです。
技術的詳細
このコミットは、Goの crypto/tls
パッケージにおけるTLSサーバーの暗号スイート選択ロジックを拡張し、サーバーが自身の優先順位を強制できるようにします。
src/pkg/crypto/tls/common.go
の変更
tls.Config
構造体に新しいフィールド PreferServerCipherSuites
が追加されました。
type Config struct {
// ... 既存のフィールド ...
// CipherSuites is a list of supported cipher suites by the server,
// in order of preference. If CipherSuites is nil, TLS uses a list
// of suites supported by the implementation.
CipherSuites []uint16
// PreferServerCipherSuites controls whether the server selects the
// client's most preferred ciphersuite, or the server's most preferred
// ciphersuite. If true then the server's preference, as expressed in
// the order of elements in CipherSuites, is used.
PreferServerCipherSuites bool
// ... 既存のフィールド ...
}
PreferServerCipherSuites
はbool
型で、true
に設定された場合、サーバーは自身のCipherSuites
フィールドで定義された順序を優先して暗号スイートを選択します。デフォルトはfalse
で、この場合、クライアントの優先順位が尊重されます。
src/pkg/crypto/tls/handshake_server.go
の変更
サーバーハンドシェイクの状態を管理する serverHandshakeState
構造体に関連するロジックが変更されました。特に、hs.clientHello.cipherSuites
と c.config.cipherSuites()
のどちらを優先リストとして使用するかが、PreferServerCipherSuites
の値に基づいて動的に決定されるようになりました。
変更の核心は、暗号スイートの選択ループにあります。
// 変更前:
// for _, id := range hs.clientHello.cipherSuites {
// if hs.suite = c.tryCipherSuite(id, hs.ellipticOk); hs.suite != nil {
// break
// }
// }
// 変更後:
var preferenceList, supportedList []uint16
if c.config.PreferServerCipherSuites {
preferenceList = c.config.cipherSuites()
supportedList = hs.clientHello.cipherSuites
} else {
preferenceList = hs.clientHello.cipherSuites
supportedList = c.config.cipherSuites()
}
for _, id := range preferenceList {
if hs.suite = c.tryCipherSuite(id, supportedList, hs.ellipticOk); hs.suite != nil {
break
}
}
preferenceList
: サーバーが暗号スイートを選択する際に、その順序を尊重するリストです。PreferServerCipherSuites
がtrue
ならサーバーのCipherSuites
、false
ならクライアントのclientHello.cipherSuites
が設定されます。supportedList
: 選択された暗号スイートが、もう一方のピアによってもサポートされているかを確認するためのリストです。PreferServerCipherSuites
がtrue
ならクライアントのclientHello.cipherSuites
、false
ならサーバーのCipherSuites
が設定されます。
また、tryCipherSuite
関数のシグネチャも変更され、supportedCipherSuites
という新しい引数が追加されました。これにより、暗号スイートが実際にサポートされているかどうかのチェックがより柔軟に行えるようになりました。
// 変更前:
// func (c *Conn) tryCipherSuite(id uint16, ellipticOk bool) *cipherSuite {
// for _, supported := range c.config.cipherSuites() {
// 変更後:
func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk bool) *cipherSuite {
for _, supported := range supportedCipherSuites {
この変更により、tryCipherSuite
は、preferenceList
から取得した id
が、supportedList
の中に存在するかどうかを確認するようになりました。
src/pkg/crypto/tls/handshake_server_test.go
の変更
新しい PreferServerCipherSuites
機能の動作を検証するためのテストケース TestCipherSuitePreference
が追加されました。
このテストでは、以下のシナリオを検証しています。
- デフォルトの挙動(クライアント優先):
PreferServerCipherSuites
がfalse
の場合、サーバーがクライアントの優先順位に従って暗号スイートを選択することを確認します。- サーバーの
CipherSuites
:[RC4, AES128, ECDHE_RC4]
- クライアントの
CipherSuites
:[AES128, RC4]
- 期待される結果:
AES128
(クライアントのリストで最初にサーバーがサポートするもの)
- サーバーの
- サーバー優先の挙動:
PreferServerCipherSuites
がtrue
の場合、サーバーが自身の優先順位に従って暗号スイートを選択することを確認します。- サーバーの
CipherSuites
:[RC4, AES128, ECDHE_RC4]
- クライアントの
CipherSuites
:[AES128, RC4]
- 期待される結果:
RC4
(サーバーのリストで最初にクライアントがサポートするもの)
- サーバーの
これらのテストにより、新機能が意図通りに動作することが保証されます。
コアとなるコードの変更箇所
src/pkg/crypto/tls/common.go
--- a/src/pkg/crypto/tls/common.go
+++ b/src/pkg/crypto/tls/common.go
@@ -184,6 +184,12 @@ type Config struct {
// is nil, TLS uses a list of suites supported by the implementation.
CipherSuites []uint16
+ // PreferServerCipherSuites controls whether the server selects the
+ // client's most preferred ciphersuite, or the server's most preferred
+ // ciphersuite. If true then the server's preference, as expressed in
+ // the order of elements in CipherSuites, is used.
+ PreferServerCipherSuites bool
+
// SessionTicketsDisabled may be set to true to disable session ticket
// (resumption) support.
SessionTicketsDisabled bool
src/pkg/crypto/tls/handshake_server.go
--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -180,8 +180,17 @@ Curves:
return true, nil
}
- for _, id := range hs.clientHello.cipherSuites {
- if hs.suite = c.tryCipherSuite(id, hs.ellipticOk); hs.suite != nil {
+ var preferenceList, supportedList []uint16
+ if c.config.PreferServerCipherSuites {
+ preferenceList = c.config.cipherSuites()
+ supportedList = hs.clientHello.cipherSuites
+ } else {
+ preferenceList = hs.clientHello.cipherSuites
+ supportedList = c.config.cipherSuites()
+ }
+
+ for _, id := range preferenceList {
+ if hs.suite = c.tryCipherSuite(id, supportedList, hs.ellipticOk); hs.suite != nil {
break
}
}
@@ -222,7 +231,7 @@ func (hs *serverHandshakeState) checkForResumption() bool {
}
// Check that we also support the ciphersuite from the session.
- hs.suite = c.tryCipherSuite(hs.sessionState.cipherSuite, hs.ellipticOk)
+ hs.suite = c.tryCipherSuite(hs.sessionState.cipherSuite, c.config.cipherSuites(), hs.ellipticOk)
if hs.suite == nil {
return false
}
@@ -568,8 +577,8 @@ func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (*
// tryCipherSuite returns a cipherSuite with the given id if that cipher suite
// is acceptable to use.
-func (c *Conn) tryCipherSuite(id uint16, ellipticOk bool) *cipherSuite {
- for _, supported := range c.config.cipherSuites() {
+func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk bool) *cipherSuite {
+ for _, supported := range supportedCipherSuites {
if id == supported {
var candidate *cipherSuite
src/pkg/crypto/tls/handshake_server_test.go
--- a/src/pkg/crypto/tls/handshake_server_test.go
+++ b/src/pkg/crypto/tls/handshake_server_test.go
@@ -125,6 +125,50 @@ func TestClose(t *testing.T) {
}
}
+func testHandshake(clientConfig, serverConfig *Config) (state ConnectionState, err error) {
+ c, s := net.Pipe()
+ go func() {
+ cli := Client(c, clientConfig)
+ cli.Handshake()
+ c.Close()
+ }()
+ server := Server(s, serverConfig)
+ err = server.Handshake()
+ if err == nil {
+ state = server.ConnectionState()
+ }
+ s.Close()
+ return
+}
+
+func TestCipherSuitePreference(t *testing.T) {
+ serverConfig := &Config{
+ CipherSuites: []uint16{TLS_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_RC4_128_SHA},
+ Certificates: testConfig.Certificates,
+ }
+ clientConfig := &Config{
+ CipherSuites: []uint16{TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_RC4_128_SHA},
+ InsecureSkipVerify: true,
+ }
+ state, err := testHandshake(clientConfig, serverConfig)
+ if err != nil {
+ t.Fatalf("handshake failed: %s", err)
+ }
+ if state.CipherSuite != TLS_RSA_WITH_AES_128_CBC_SHA {
+ // By default the server should use the client's preference.
+ t.Fatalf("Client's preference was not used, got %x", state.CipherSuite)
+ }
+
+ serverConfig.PreferServerCipherSuites = true
+ state, err = testHandshake(clientConfig, serverConfig)
+ if err != nil {
+ t.Fatalf("handshake failed: %s", err)
+ }
+ if state.CipherSuite != TLS_RSA_WITH_RC4_128_SHA {
+ t.Fatalf("Server's preference was not used, got %x", state.CipherSuite)
+ }
+}
+
func testServerScript(t *testing.T, name string, serverScript [][]byte, config *Config, peers []*x509.Certificate) {
c, s := net.Pipe()
srv := Server(s, config)
コアとなるコードの解説
src/pkg/crypto/tls/common.go
の変更点
PreferServerCipherSuites bool
フィールドの追加:- このブール値は、TLSサーバーが暗号スイートを選択する際に、クライアントの優先順位を尊重するか(
false
、デフォルト)、サーバー自身の優先順位を強制するか(true
)を制御します。 - サーバー管理者は、
tls.Config
を初期化する際にこのフィールドを設定することで、セキュリティポリシーを適用できるようになります。
- このブール値は、TLSサーバーが暗号スイートを選択する際に、クライアントの優先順位を尊重するか(
src/pkg/crypto/tls/handshake_server.go
の変更点
- 暗号スイート選択ロジックの変更:
- 以前は
hs.clientHello.cipherSuites
を直接ループして、サーバーがサポートする最初の暗号スイートを探していました。 - 変更後は、
PreferServerCipherSuites
の値に基づいてpreferenceList
とsupportedList
の2つのリストを動的に構築します。PreferServerCipherSuites
がtrue
の場合:preferenceList
はサーバーのc.config.cipherSuites()
となり、サーバーが定義した順序で暗号スイートを評価します。supportedList
はクライアントのhs.clientHello.cipherSuites
となり、選択された暗号スイートがクライアントによってもサポートされているかを確認します。
PreferServerCipherSuites
がfalse
の場合(デフォルトの挙動):preferenceList
はクライアントのhs.clientHello.cipherSuites
となり、クライアントが提示した順序で暗号スイートを評価します。supportedList
はサーバーのc.config.cipherSuites()
となり、選択された暗号スイートがサーバーによってもサポートされているかを確認します。
- この変更により、
for
ループは常にpreferenceList
をイテレートし、tryCipherSuite
関数にsupportedList
を渡すことで、両者の共通の暗号スイートの中から、preferenceList
の順序で最初に一致するものを選択するようになりました。
- 以前は
tryCipherSuite
関数のシグネチャ変更:func (c *Conn) tryCipherSuite(id uint16, ellipticOk bool) *cipherSuite
からfunc (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk bool) *cipherSuite
へと変更されました。- 新しい引数
supportedCipherSuites
は、暗号スイートid
が実際にサポートされているかを確認するためのリストを提供します。これにより、tryCipherSuite
はより汎用的なチェック関数となり、クライアントとサーバーのどちらの優先順位を考慮する場合でも適切に機能するようになりました。
src/pkg/crypto/tls/handshake_server_test.go
の変更点
testHandshake
ヘルパー関数の追加:- クライアントとサーバーの
tls.Config
を受け取り、TLSハンドシェイクを実行し、結果のConnectionState
とエラーを返すユーティリティ関数です。テストケースの記述を簡素化します。
- クライアントとサーバーの
TestCipherSuitePreference
テストケースの追加:- このテストは、
PreferServerCipherSuites
フィールドが暗号スイートの選択にどのように影響するかを具体的に検証します。 - まず、デフォルトの挙動(クライアント優先)をテストし、次に
PreferServerCipherSuites
をtrue
に設定してサーバー優先の挙動をテストします。 - これにより、新機能が期待通りに動作し、サーバーが自身の暗号スイートの優先順位を強制できることが確認されます。
- このテストは、
これらの変更により、GoのTLSサーバーは、より柔軟な暗号スイート選択ポリシーをサポートし、セキュリティ要件に応じてサーバー側の優先順位を適用できるようになりました。
関連リンク
- Go CL 7163043: https://golang.org/cl/7163043
参考にした情報源リンク
- TLSハンドシェイクの概要: https://www.cloudflare.com/ja-jp/learning/ssl/what-happens-in-a-tls-handshake/
- Apache
SSLHonorCipherOrder
ディレクティブ: https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslhonorcipherorder - TLS Cipher Suites Explained: https://www.ssl.com/guide/tls-cipher-suites-explained/
- Go
crypto/tls
パッケージのドキュメント (当時のバージョン): 検索結果から当時のドキュメントを参照し、tls.Config
の変更点を確認しました。 - TLS/SSL Cipher Suite Order: https://www.globalsign.com/en/blog/tls-ssl-cipher-suite-order
- TLS Handshake: https://www.ibm.com/docs/en/ztpf/2020?topic=security-tls-handshake
- What is a Cipher Suite?: https://www.digicert.com/blog/what-is-a-cipher-suite