Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 ディレクティブと同様の目的を持ちます。SSLHonorCipherOrderOn に設定されている場合、Apacheはクライアントが提示する暗号スイートの順序ではなく、サーバーの設定ファイルで定義された暗号スイートの順序を優先して選択します。これにより、サーバー管理者は、より安全な暗号スイートを優先的に使用するように強制し、既知の脆弱性を持つ暗号スイートの使用を避けることができます。

このコミットは、GoのTLSサーバーにも同様の制御メカニズムを導入し、サーバー管理者がセキュリティポリシーをより厳密に適用できるようにすることを目的としています。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネット上でデータを安全にやり取りするための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間の通信を暗号化し、データの完全性を保証し、通信相手の認証を行うことで、盗聴、改ざん、なりすましを防ぎます。

TLSハンドシェイク

TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に実行する一連のステップです。このプロセス中に、両者はプロトコルバージョン、暗号スイート、認証情報などをネゴシエートします。

ハンドシェイクの主要なステップは以下の通りです。

  1. ClientHello: クライアントがサーバーに接続を要求し、サポートするTLSバージョン、暗号スイートのリスト(優先順位付き)、圧縮方式、ランダムなバイト列などを送信します。
  2. ServerHello: サーバーはClientHelloを受け取り、クライアントが提示したリストの中から、自身がサポートする最適なTLSバージョンと暗号スイートを選択し、自身のランダムなバイト列と共にクライアントに返します。
  3. Certificate: サーバーは自身のデジタル証明書をクライアントに送信し、自身の身元を証明します。
  4. ServerKeyExchange (オプション): 鍵交換アルゴリズムによっては、サーバーが追加の鍵交換情報を送信します。
  5. ServerHelloDone: サーバーはハンドシェイクの初期フェーズが完了したことを示します。
  6. ClientKeyExchange: クライアントはサーバーの公開鍵を使用して、セッション鍵の生成に必要な情報を暗号化してサーバーに送信します。
  7. ChangeCipherSpec: クライアントは、これ以降の通信が暗号化されることをサーバーに通知します。
  8. Finished: クライアントは、ハンドシェイクのこれまでのメッセージのハッシュを暗号化して送信し、ハンドシェイクが成功したことを確認します。
  9. ChangeCipherSpec: サーバーも同様に、これ以降の通信が暗号化されることをクライアントに通知します。
  10. 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つの主要なアプローチがあります。

  1. クライアントの優先順位を尊重する (Client Order Preference): サーバーはクライアントが提示したリストの順序に従い、自身がサポートする最初の暗号スイートを選択します。多くのTLS実装のデフォルトの挙動です。
  2. サーバーの優先順位を強制する (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

	// ... 既存のフィールド ...
}
  • PreferServerCipherSuitesbool 型で、true に設定された場合、サーバーは自身の CipherSuites フィールドで定義された順序を優先して暗号スイートを選択します。デフォルトは false で、この場合、クライアントの優先順位が尊重されます。

src/pkg/crypto/tls/handshake_server.go の変更

サーバーハンドシェイクの状態を管理する serverHandshakeState 構造体に関連するロジックが変更されました。特に、hs.clientHello.cipherSuitesc.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: サーバーが暗号スイートを選択する際に、その順序を尊重するリストです。PreferServerCipherSuitestrue ならサーバーの CipherSuitesfalse ならクライアントの clientHello.cipherSuites が設定されます。
  • supportedList: 選択された暗号スイートが、もう一方のピアによってもサポートされているかを確認するためのリストです。PreferServerCipherSuitestrue ならクライアントの clientHello.cipherSuitesfalse ならサーバーの 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 が追加されました。

このテストでは、以下のシナリオを検証しています。

  1. デフォルトの挙動(クライアント優先): PreferServerCipherSuitesfalse の場合、サーバーがクライアントの優先順位に従って暗号スイートを選択することを確認します。
    • サーバーの CipherSuites: [RC4, AES128, ECDHE_RC4]
    • クライアントの CipherSuites: [AES128, RC4]
    • 期待される結果: AES128 (クライアントのリストで最初にサーバーがサポートするもの)
  2. サーバー優先の挙動: PreferServerCipherSuitestrue の場合、サーバーが自身の優先順位に従って暗号スイートを選択することを確認します。
    • サーバーの 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 を初期化する際にこのフィールドを設定することで、セキュリティポリシーを適用できるようになります。

src/pkg/crypto/tls/handshake_server.go の変更点

  • 暗号スイート選択ロジックの変更:
    • 以前は hs.clientHello.cipherSuites を直接ループして、サーバーがサポートする最初の暗号スイートを探していました。
    • 変更後は、PreferServerCipherSuites の値に基づいて preferenceListsupportedList の2つのリストを動的に構築します。
      • PreferServerCipherSuitestrue の場合:
        • preferenceList はサーバーの c.config.cipherSuites() となり、サーバーが定義した順序で暗号スイートを評価します。
        • supportedList はクライアントの hs.clientHello.cipherSuites となり、選択された暗号スイートがクライアントによってもサポートされているかを確認します。
      • PreferServerCipherSuitesfalse の場合(デフォルトの挙動):
        • 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 フィールドが暗号スイートの選択にどのように影響するかを具体的に検証します。
    • まず、デフォルトの挙動(クライアント優先)をテストし、次に PreferServerCipherSuitestrue に設定してサーバー優先の挙動をテストします。
    • これにより、新機能が期待通りに動作し、サーバーが自身の暗号スイートの優先順位を強制できることが確認されます。

これらの変更により、GoのTLSサーバーは、より柔軟な暗号スイート選択ポリシーをサポートし、セキュリティ要件に応じてサーバー側の優先順位を適用できるようになりました。

関連リンク

参考にした情報源リンク