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

[インデックス 17634] ファイルの概要

このコミットは、Go言語の標準ライブラリである crypto/tls パッケージ内の変更に関するものです。crypto/tls パッケージは、TLS (Transport Layer Security) プロトコルを実装しており、Goアプリケーションが安全な通信を行うための基盤を提供します。具体的には、サーバーとクライアント間のハンドシェイク、暗号スイートのネゴシエーション、データの暗号化と復号化などを担当します。

このコミットで変更された主要なファイルは以下の通りです。

  • src/pkg/crypto/tls/cipher_suites.go: TLSハンドシェイク中に利用可能な暗号スイートの定義と、それらの特性を管理するファイルです。
  • src/pkg/crypto/tls/handshake_server.go: TLSサーバー側のハンドシェイク処理ロジックを実装しているファイルです。クライアントからの接続要求を受け入れ、証明書の提示、暗号スイートの選択、鍵交換などを行います。
  • src/pkg/crypto/tls/handshake_server_test.go: handshake_server.go のテストコードが含まれており、サーバーハンドシェイクの様々なシナリオを検証します。

コミット

commit eef7035ec860a2a8eac39ed3f4c0cb2a384599a2
Author: Adam Langley <agl@golang.org>
Date:   Tue Sep 17 13:30:36 2013 -0400

    crypto/tls: don't select ECDSA ciphersuites with only an RSA certificate.
    
    47ec7a68b1a2 added support for ECDSA ciphersuites but didn't alter the
    cipher suite selection to take that into account. Thus Go servers could
    try and select an ECDSA cipher suite while only having an RSA
    certificate, leading to connection failures.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/13239053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/eef7035ec860a2a8eac39ed3f4c0cb2a384599a2

元コミット内容

crypto/tls: don't select ECDSA ciphersuites with only an RSA certificate.

47ec7a68b1a2 added support for ECDSA ciphersuites but didn't alter the
cipher suite selection to take that into account. Thus Go servers could
try and select an ECDSA cipher suite while only having an RSA
certificate, leading to connection failures.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/13239053

変更の背景

このコミットの背景には、Go言語の crypto/tls パッケージにおける暗号スイート選択ロジックの不整合がありました。以前のコミット 47ec7a68b1a2 でECDSA (Elliptic Curve Digital Signature Algorithm) ベースの暗号スイートがサポートに追加されました。しかし、この追加は、サーバーが利用可能な証明書の種類(RSAまたはECDSA)を考慮せずに暗号スイートを選択する既存のロジックを修正していませんでした。

その結果、GoサーバーがRSA証明書しか持っていないにもかかわらず、クライアントとのTLSハンドシェイク中にECDSA暗号スイートを選択しようとする問題が発生しました。TLSハンドシェイクでは、サーバーが提示する証明書の公開鍵のタイプ(RSAまたはECDSA)と、選択された暗号スイートが使用する署名アルゴリズムのタイプが一致している必要があります。この不一致が発生すると、ハンドシェイクが失敗し、クライアントとサーバー間の安全な接続が確立できなくなっていました。

このコミットは、この問題を解決し、サーバーが自身の証明書タイプに基づいて適切な暗号スイートのみを選択するように、暗号スイート選択ロジックを修正することを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のTLS/SSLおよび暗号技術に関する前提知識が必要です。

1. TLS/SSLハンドシェイクの概要

TLS (Transport Layer Security) は、インターネット上で安全な通信を行うためのプロトコルです。クライアントとサーバーが安全な接続を確立する際に「ハンドシェイク」と呼ばれる一連のプロセスを実行します。サーバー側のハンドシェイクの主要なステップは以下の通りです。

  • ClientHello: クライアントがサポートするTLSバージョン、暗号スイートのリスト、圧縮方式、拡張機能などをサーバーに送信します。
  • ServerHello: サーバーはClientHelloを受け取り、クライアントと合意したTLSバージョン、選択した暗号スイート、セッションIDなどをクライアントに返します。
  • Certificate: サーバーは自身の身元を証明するためにデジタル証明書をクライアントに提示します。この証明書にはサーバーの公開鍵が含まれています。
  • ServerKeyExchange (オプション): 鍵交換アルゴリズムによっては、サーバーが追加の鍵交換情報を送信します(例: ECDHEの場合)。
  • CertificateRequest (オプション): サーバーがクライアント認証を要求する場合、クライアント証明書を要求します。
  • ServerHelloDone: サーバーが自身のハンドシェイクメッセージの送信を完了したことを示します。
  • ClientKeyExchange: クライアントは、サーバーの公開鍵(証明書から取得)または鍵交換アルゴリズム(例: ECDHE)を使用して、セッション鍵の生成に必要な情報を暗号化してサーバーに送信します。
  • ChangeCipherSpec: クライアントとサーバーが、これ以降の通信をネゴシエートされた暗号スイートとセッション鍵で暗号化することを示します。
  • Finished: ハンドシェイクの完了を検証するためのメッセージを暗号化して送信します。

2. 暗号スイート (Cipher Suite)

暗号スイートは、TLS接続で使用される一連のアルゴリズムの組み合わせを定義します。通常、以下の要素を含みます。

  • 鍵交換アルゴリズム (Key Exchange Algorithm): クライアントとサーバーがセッション鍵を安全に共有する方法を決定します。
    • RSA: サーバーのRSA公開鍵を使用して、クライアントが生成したプリマスターシークレットを暗号化し、サーバーに送信します。サーバーは自身のRSA秘密鍵で復号します。Perfect Forward Secrecy (PFS) を提供しません。
    • ECDHE (Elliptic Curve Diffie-Hellman Ephemeral): 楕円曲線ディフィー・ヘルマン鍵交換の「一時的 (Ephemeral)」バージョンです。セッションごとに新しい鍵ペアを生成するため、PFSを提供します。
  • 署名アルゴリズム (Authentication/Signature Algorithm): サーバーが自身の身元を証明するためにデジタル署名を行うアルゴリズムです。サーバー証明書に含まれる公開鍵のタイプと一致する必要があります。
    • RSA: RSA公開鍵暗号方式に基づくデジタル署名。
    • ECDSA (Elliptic Curve Digital Signature Algorithm): 楕円曲線暗号に基づくデジタル署名。
  • バルク暗号化アルゴリズム (Bulk Encryption Algorithm): 実際のデータ転送に使用される対称鍵暗号アルゴリズム(例: AES, RC4, 3DES)。
  • メッセージ認証コード (MAC) アルゴリズム: メッセージの完全性と認証を保証するためのハッシュ関数(例: SHA1, SHA256)。

暗号スイートの命名規則は TLS_鍵交換_署名_暗号化_MAC のようになります。 例: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

  • 鍵交換: ECDHE
  • 署名: RSA (サーバー証明書がRSAである必要がある)
  • 暗号化: AES_128_GCM
  • MAC: SHA256

例: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256

  • 鍵交換: ECDHE
  • 署名: ECDSA (サーバー証明書がECDSAである必要がある)
  • 暗号化: AES_128_GCM
  • MAC: SHA256

3. RSA証明書とECDSA証明書

デジタル証明書は、公開鍵とそれに関連する所有者の情報を結びつけ、信頼された認証局 (CA) によって署名されたものです。証明書には公開鍵が含まれており、この公開鍵のタイプによって証明書の種類が区別されます。

  • RSA証明書: 公開鍵がRSAアルゴリズムに基づいています。この証明書を持つサーバーは、RSA鍵交換やRSA署名を使用する暗号スイートと互換性があります。
  • ECDSA証明書: 公開鍵が楕円曲線暗号 (ECC) に基づいています。この証明書を持つサーバーは、ECDSA署名を使用する暗号スイートと互換性があります。

サーバーが提示する証明書のタイプと、選択される暗号スイートの署名アルゴリズムは一致している必要があります。例えば、サーバーがRSA証明書しか持っていない場合、ECDSA署名を使用する暗号スイート(例: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)を選択することはできません。なぜなら、サーバーはその暗号スイートで要求されるECDSA署名を行うための秘密鍵を持っていないからです。

技術的詳細

このコミットは、Goの crypto/tls パッケージにおけるサーバー側の暗号スイート選択ロジックを強化し、サーバーが持つ証明書のタイプ(RSAまたはECDSA)と選択される暗号スイートの署名アルゴリズムが一致するように修正しています。

1. cipherSuite 構造体の変更とフラグの導入

以前の cipherSuite 構造体には elliptic bool フィールドがあり、その暗号スイートが楕円曲線ベースの鍵交換(ECDHE)を使用するかどうかを示していました。しかし、これは鍵交換の種類のみを考慮しており、署名アルゴリズムの種類(RSAまたはECDSA)を区別していませんでした。

このコミットでは、より汎用的な flags int フィールドが導入され、以下の新しい定数が定義されました。

const (
	// suiteECDH indicates that the cipher suite involves elliptic curve
	// Diffie-Hellman. This means that it should only be selected when the
	// client indicates that it supports ECC with a curve and point format
	// that we're happy with.
	suiteECDHE = 1 << iota
	// suiteECDSA indicates that the cipher suite involves an ECDSA
	// signature and therefore may only be selected when the server's
	// certificate is ECDSA. If this is not set then the cipher suite is
	// RSA based.
	suiteECDSA
)
  • suiteECDHE: 暗号スイートがECDHE鍵交換を使用することを示します。
  • suiteECDSA: 暗号スイートがECDSA署名を使用することを示します。このフラグが設定されていない場合、その暗号スイートはRSAベースの署名を使用すると見なされます。

これにより、各暗号スイートが持つ特性(鍵交換の種類と署名アルゴリズムの種類)をより正確に表現できるようになりました。cipherSuites 配列内の各暗号スイート定義も、この新しい flags フィールドを使用するように更新されています。例えば、TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 のようなECDSAベースの暗号スイートには suiteECDHE | suiteECDSA のフラグが設定されます。

2. サーバーハンドシェイク状態 (serverHandshakeState) の拡張

serverHandshakeState 構造体には、新たに ecdsaOk bool フィールドと cert *Certificate フィールドが追加されました。

type serverHandshakeState struct {
	// ...
	ellipticOk      bool
	ecdsaOk         bool // 追加
	sessionState    *sessionState
	finishedHash    finishedHash
	masterSecret    []byte
	certsFromClient [][]byte
	cert            *Certificate // 追加
}
  • cert: サーバーがクライアントに提示する証明書を保持します。以前はハンドシェイク中にローカル変数として扱われていましたが、状態として保持されるようになりました。
  • ecdsaOk: サーバーが提示する証明書がECDSA秘密鍵を持っているかどうかを示すブール値です。これは、サーバーがECDSA署名を行う能力があるかどうかを判断するために使用されます。

serverHandshake 関数内で、サーバーが使用する証明書 (hs.cert) が決定された後、その証明書の秘密鍵がECDSAタイプであるかどうかがチェックされ、hs.ecdsaOk が設定されます。

	// ...
	hs.cert = &config.Certificates[0]
	if len(hs.clientHello.serverName) > 0 {
		hs.cert = config.getCertificateForName(hs.clientHello.serverName)
	}

	_, hs.ecdsaOk = hs.cert.PrivateKey.(*ecdsa.PrivateKey) // ここでecdsaOkを設定
	// ...

3. 暗号スイート選択ロジックの修正 (tryCipherSuite)

最も重要な変更は、Conn 型の tryCipherSuite 関数のシグネチャと内部ロジックです。この関数は、クライアントが提示した暗号スイートのリストの中から、サーバーがサポートし、かつ現在の状況で利用可能な最適な暗号スイートを選択する役割を担います。

変更前:

func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk bool) *cipherSuite

変更後:

func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk, ecdsaOk bool) *cipherSuite

新しい ecdsaOk 引数が追加されました。

そして、この関数内の暗号スイート選択ロジックが以下のように修正されました。

			// Don't select a ciphersuite which we can't
			// support for this client.
			if (candidate.flags&suiteECDHE != 0) && !ellipticOk {
				continue
			}
			// 重要な変更点: 署名アルゴリズムと証明書タイプの整合性をチェック
			if (candidate.flags&suiteECDSA != 0) != ecdsaOk {
				continue
			}
			return candidate
  • if (candidate.flags&suiteECDHE != 0) && !ellipticOk: これは以前の candidate.elliptic && !ellipticOk と同等で、暗号スイートがECDHE鍵交換を要求するにもかかわらず、クライアントが楕円曲線暗号をサポートしていない場合にそのスイートをスキップします。
  • if (candidate.flags&suiteECDSA != 0) != ecdsaOk: これがこのコミットの核心的な変更です。
    • candidate.flags&suiteECDSA != 0: 選択しようとしている暗号スイートがECDSA署名を使用するかどうかをチェックします。
    • ecdsaOk: サーバーがECDSA証明書を持っているかどうかを示します。
    • !=: この論理は、「暗号スイートがECDSA署名を要求すること」と「サーバーがECDSA証明書を持っていること」が一致しない場合に true となります。つまり、以下のいずれかの状況で continue してこの暗号スイートをスキップします。
      • 暗号スイートがECDSA署名を要求する (suiteECDSA フラグあり) が、サーバーがECDSA証明書を持っていない (ecdsaOkfalse)。
      • 暗号スイートがECDSA署名を要求しない (suiteECDSA フラグなし、つまりRSAベース) が、サーバーがECDSA証明書を持っている (ecdsaOktrue)。

このロジックにより、サーバーは自身の証明書タイプ(RSAまたはECDSA)と互換性のある署名アルゴリズムを持つ暗号スイートのみを選択するようになります。これにより、RSA証明書しか持たないサーバーが誤ってECDSA暗号スイートを選択し、ハンドシェイクが失敗する問題を根本的に解決します。

4. doFullHandshake における hs.cert の利用統一

doFullHandshake 関数内で、OCSP Stapling、証明書メッセージの送信、鍵交換情報の生成など、サーバー証明書を参照する箇所が、ローカル変数 cert ではなく、hs.cert フィールドを使用するように統一されました。これにより、コードの一貫性が向上し、証明書情報がハンドシェイク状態の一部として適切に管理されるようになります。

5. テストケースの追加

src/pkg/crypto/tls/handshake_server_test.goTestCipherSuiteCertPreferance という新しいテストケースが追加されました。このテストは、サーバーがRSA証明書を持っている場合にRSA暗号スイートが選択され、ECDSA証明書を持っている場合にECDSA暗号スイートが選択されることを検証します。これにより、修正された暗号スイート選択ロジックが期待通りに機能することが保証されます。

コアとなるコードの変更箇所

src/pkg/crypto/tls/cipher_suites.go

--- a/src/pkg/crypto/tls/cipher_suites.go
+++ b/src/pkg/crypto/tls/cipher_suites.go
@@ -34,6 +34,19 @@ type keyAgreement interface {
 	generateClientKeyExchange(*Config, *clientHelloMsg, *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error)
 }
 
+const (
+	// suiteECDH indicates that the cipher suite involves elliptic curve
+	// Diffie-Hellman. This means that it should only be selected when the
+	// client indicates that it supports ECC with a curve and point format
+	// that we're happy with.
+	suiteECDHE = 1 << iota
+	// suiteECDSA indicates that the cipher suite involves an ECDSA
+	// signature and therefore may only be selected when the server's
+	// certificate is ECDSA. If this is not set then the cipher suite is
+	// RSA based.
+	suiteECDSA
+)
+
 // A cipherSuite is a specific combination of key agreement, cipher and MAC
 // function. All cipher suites currently assume RSA key agreement.
 type cipherSuite struct {
@@ -43,31 +56,29 @@ type cipherSuite struct {
 	macLen int
 	ivLen  int
 	ka     func(version uint16) keyAgreement
-	// If elliptic is set, a server will only consider this ciphersuite if
-	// the ClientHello indicated that the client supports an elliptic curve
-	// and point format that we can handle.
-	elliptic bool
-	cipher   func(key, iv []byte, isRead bool) interface{}
-	mac      func(version uint16, macKey []byte) macFunction
-	aead     func(key, fixedNonce []byte) cipher.AEAD
+	// flags is a bitmask of the suite* values, above.
+	flags  int
+	cipher func(key, iv []byte, isRead bool) interface{}
+	mac    func(version uint16, macKey []byte) macFunction
+	aead   func(key, fixedNonce []byte) cipher.AEAD
 }
 
 var cipherSuites = []*cipherSuite{
 	// Ciphersuite order is chosen so that ECDHE comes before plain RSA
 	// and RC4 comes before AES (because of the Lucky13 attack).
-	{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, true, nil, nil, aeadAESGCM},\
-	{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, true, nil, nil, aeadAESGCM},\
-	{TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, true, cipherRC4, macSHA1, nil},\
-	{TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, true, cipherRC4, macSHA1, nil},\
-	{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1, nil},\
-	{TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1, nil},\
-	{TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1, nil},\
-	{TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1, nil},\
-	{TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, false, cipherRC4, macSHA1, nil},\
-	{TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, false, cipherAES, macSHA1, nil},\
-	{TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, false, cipherAES, macSHA1, nil},\
-	{TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, true, cipher3DES, macSHA1, nil},\
-	{TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, false, cipher3DES, macSHA1, nil},\
+	{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, suiteECDHE, nil, nil, aeadAESGCM},\
+	{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, suiteECDHE | suiteECDSA, nil, nil, aeadAESGCM},\
+	{TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, suiteECDHE, cipherRC4, macSHA1, nil},\
+	{TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherRC4, macSHA1, nil},\
+	{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},\
+	{TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},\
+	{TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},\
+	{TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},\
+	{TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, 0, cipherRC4, macSHA1, nil},\
+	{TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},\
+	{TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},\
+	{TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, suiteECDHE, cipher3DES, macSHA1, nil},\
+	{TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, 0, cipher3DES, macSHA1, nil},\
 }
 
 func cipherRC4(key, iv []byte, isRead bool) interface{} {

src/pkg/crypto/tls/handshake_server.go

--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -23,10 +23,12 @@ type serverHandshakeState struct {
 	hello           *serverHelloMsg
 	suite           *cipherSuite
 	ellipticOk      bool
+	ecdsaOk         bool // 追加
 	sessionState    *sessionState
 	finishedHash    finishedHash
 	masterSecret    []byte
 	certsFromClient [][]byte
+	cert            *Certificate // 追加
 }
 
 // serverHandshake performs a TLS handshake as a server.
@@ -167,6 +169,16 @@ Curves:
 		hs.hello.nextProtos = config.NextProtos
 	}
 
+	if len(config.Certificates) == 0 {
+		return false, c.sendAlert(alertInternalError)
+	}
+	hs.cert = &config.Certificates[0]
+	if len(hs.clientHello.serverName) > 0 {
+		hs.cert = config.getCertificateForName(hs.clientHello.serverName)
+	}
+
+	_, hs.ecdsaOk = hs.cert.PrivateKey.(*ecdsa.PrivateKey) // ecdsaOk の設定
+
 	if hs.checkForResumption() {
 		return true, nil
 	}
@@ -181,7 +193,7 @@ Curves:
 	}
 
 	for _, id := range preferenceList {
-		if hs.suite = c.tryCipherSuite(id, supportedList, hs.ellipticOk); hs.suite != nil {
+		if hs.suite = c.tryCipherSuite(id, supportedList, hs.ellipticOk, hs.ecdsaOk); hs.suite != nil { // ecdsaOk を引数に追加
 			break
 		}
 	}
@@ -222,7 +234,7 @@ func (hs *serverHandshakeState) checkForResumption() bool {
 	}
 
 	// Check that we also support the ciphersuite from the session.
-	hs.suite = c.tryCipherSuite(hs.sessionState.cipherSuite, c.config.cipherSuites(), hs.ellipticOk)
+	hs.suite = c.tryCipherSuite(hs.sessionState.cipherSuite, c.config.cipherSuites(), hs.ellipticOk, hs.ecdsaOk) // ecdsaOk を引数に追加
 	if hs.suite == nil {
 		return false
 	}
@@ -264,15 +276,7 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 	config := hs.c.config
 	c := hs.c
 
-	if len(config.Certificates) == 0 {
-		return c.sendAlert(alertInternalError)
-	}
-	cert := &config.Certificates[0]
-	if len(hs.clientHello.serverName) > 0 {
-		cert = config.getCertificateForName(hs.clientHello.serverName)
-	}
-
-	if hs.clientHello.ocspStapling && len(cert.OCSPStaple) > 0 {
+	if hs.clientHello.ocspStapling && len(hs.cert.OCSPStaple) > 0 { // hs.cert を使用
 		hs.hello.ocspStapling = true
 	}
 
@@ -282,20 +286,20 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 	c.writeRecord(recordTypeHandshake, hs.hello.marshal())
 
 	certMsg := new(certificateMsg)
-	certMsg.certificates = cert.Certificate
+	certMsg.certificates = hs.cert.Certificate // hs.cert を使用
 	hs.finishedHash.Write(certMsg.marshal())
 	c.writeRecord(recordTypeHandshake, certMsg.marshal())
 
 	if hs.hello.ocspStapling {
 		certStatus := new(certificateStatusMsg)
 		certStatus.statusType = statusTypeOCSP
-		certStatus.response = cert.OCSPStaple
+		certStatus.response = hs.cert.OCSPStaple // hs.cert を使用
 		hs.finishedHash.Write(certStatus.marshal())
 		c.writeRecord(recordTypeHandshake, certStatus.marshal())
 	}
 
 	keyAgreement := hs.suite.ka(c.vers)
-	skx, err := keyAgreement.generateServerKeyExchange(config, cert, hs.clientHello, hs.hello)
+	skx, err := keyAgreement.generateServerKeyExchange(config, hs.cert, hs.clientHello, hs.hello) // hs.cert を使用
 	if err != nil {
 		c.sendAlert(alertHandshakeFailure)
 		return err
@@ -419,7 +423,7 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 		hs.finishedHash.Write(certVerify.marshal())
 	}
 
-	preMasterSecret, err := keyAgreement.processClientKeyExchange(config, cert, ckx, c.vers)
+	preMasterSecret, err := keyAgreement.processClientKeyExchange(config, hs.cert, ckx, c.vers) // hs.cert を使用
 	if err != nil {
 		c.sendAlert(alertHandshakeFailure)
 		return err
@@ -601,7 +605,7 @@ func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (c
 
 // tryCipherSuite returns a cipherSuite with the given id if that cipher suite
 // is acceptable to use.
-func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk bool) *cipherSuite {
+func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk, ecdsaOk bool) *cipherSuite { // ecdsaOk を引数に追加
 	for _, supported := range supportedCipherSuites {
 		if id == supported {
 			var candidate *cipherSuite
@@ -617,7 +621,10 @@ func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipti
 			}
 			// Don't select a ciphersuite which we can't
 			// support for this client.
-			if candidate.elliptic && !ellipticOk {
+			if (candidate.flags&suiteECDHE != 0) && !ellipticOk { // flags を使用
+				continue
+			}
+			if (candidate.flags&suiteECDSA != 0) != ecdsaOk { // 署名アルゴリズムと証明書タイプの整合性チェック
 				continue
 			}
 			return candidate

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
@@ -302,6 +302,28 @@ func TestClientAuthECDSA(t *testing.T) {
 	}\
 }\
 
+// TestCipherSuiteCertPreferance ensures that we select an RSA ciphersuite with
+// an RSA certificate and an ECDSA ciphersuite with an ECDSA certificate.
+func TestCipherSuiteCertPreferance(t *testing.T) {
+	var config = *testConfig
+	config.CipherSuites = []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}
+	config.MaxVersion = VersionTLS11
+	config.PreferServerCipherSuites = true
+	testServerScript(t, "CipherSuiteCertPreference", tls11ECDHEAESServerScript, &config, nil)
+
+	config = *testConfig
+	config.CipherSuites = []uint16{TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA}
+	config.Certificates = []Certificate{
+		Certificate{
+			Certificate: [][]byte{testECDSACertificate},
+			PrivateKey:  testECDSAPrivateKey,
+		},
+	}
+	config.BuildNameToCertificate()
+	config.PreferServerCipherSuites = true
+	testServerScript(t, "CipherSuiteCertPreference2", ecdheECDSAAESServerScript, &config, nil)
+}
+
 func TestTLS11Server(t *testing.T) {
 	var config = *testConfig
 	config.CipherSuites = []uint16{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}

コアとなるコードの解説

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

  • suiteECDHEsuiteECDSA 定数の導入:

    const (
    	suiteECDHE = 1 << iota
    	suiteECDSA
    )
    

    これらの定数は、暗号スイートがECDHE鍵交換を使用するか、またはECDSA署名を使用するかを示すビットマスクとして機能します。iota を使用することで、suiteECDHE1 (001b)、suiteECDSA2 (010b) となります。

  • cipherSuite 構造体の elliptic から flags への変更:

    type cipherSuite struct {
    	// ...
    	// elliptic bool // 削除
    	flags  int // 追加
    	// ...
    }
    

    これにより、暗号スイートの特性を単一のブール値ではなく、複数のビットフラグで表現できるようになり、より柔軟な管理が可能になりました。

  • cipherSuites 配列の更新:

    var cipherSuites = []*cipherSuite{
    	// ...
    	{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, suiteECDHE, nil, nil, aeadAESGCM},
    	{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, suiteECDHE | suiteECDSA, nil, nil, aeadAESGCM},
    	// ...
    	{TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, 0, cipherRC4, macSHA1, nil},
    	// ...
    }
    

    各暗号スイートの定義において、以前の elliptic フィールドの値が新しい flags フィールドに置き換えられました。例えば、TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 はECDHE鍵交換とECDSA署名の両方を使用するため、suiteECDHE | suiteECDSA のフラグが設定されています。RSAベースの暗号スイート(例: TLS_RSA_WITH_RC4_128_SHA)は、どちらのフラグも持たないため 0 が設定されています。

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

  • serverHandshakeState 構造体の拡張:

    type serverHandshakeState struct {
    	// ...
    	ellipticOk      bool
    	ecdsaOk         bool // 追加: サーバーがECDSA証明書を持っているか
    	// ...
    	cert            *Certificate // 追加: サーバーが提示する証明書
    }
    

    ecdsaOk は、サーバーがECDSA秘密鍵を持つ証明書をロードしている場合に true となります。cert は、ハンドシェイク中に使用されるサーバー証明書へのポインタです。

  • serverHandshake 関数での ecdsaOk の設定:

    	hs.cert = &config.Certificates[0]
    	if len(hs.clientHello.serverName) > 0 {
    		hs.cert = config.getCertificateForName(hs.clientHello.serverName)
    	}
    	_, hs.ecdsaOk = hs.cert.PrivateKey.(*ecdsa.PrivateKey)
    

    サーバーが提示する証明書 (hs.cert) の秘密鍵の型をチェックし、それが *ecdsa.PrivateKey であれば hs.ecdsaOktrue に設定します。これにより、サーバーがECDSA署名を行う能力があるかどうかがハンドシェイク状態に記録されます。

  • tryCipherSuite 関数のシグネチャ変更とロジックの修正:

    func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, ellipticOk, ecdsaOk bool) *cipherSuite {
    	// ...
    	if (candidate.flags&suiteECDHE != 0) && !ellipticOk {
    		continue
    	}
    	if (candidate.flags&suiteECDSA != 0) != ecdsaOk {
    		continue
    	}
    	return candidate
    }
    

    tryCipherSuite 関数は、ecdsaOk という新しい引数を受け取るようになりました。 内部の選択ロジックでは、まず suiteECDHE フラグと ellipticOk をチェックし、クライアントがECDHEをサポートしない場合はスキップします。 次に、if (candidate.flags&suiteECDSA != 0) != ecdsaOk の行が、このコミットの最も重要な部分です。

    • candidate.flags&suiteECDSA != 0: 候補の暗号スイートがECDSA署名を使用するかどうか。
    • ecdsaOk: サーバーがECDSA証明書を持っているかどうか。 この条件は、以下のいずれかの場合に true となり、その暗号スイートはスキップされます。
    1. 暗号スイートがECDSA署名を要求する (suiteECDSA フラグあり) が、サーバーはRSA証明書しか持っていない (ecdsaOkfalse)。
    2. 暗号スイートがRSA署名を使用する (suiteECDSA フラグなし) が、サーバーはECDSA証明書を持っている (ecdsaOktrue)。 このロジックにより、サーバーは自身の証明書タイプと一致する署名アルゴリズムを持つ暗号スイートのみを選択するようになります。
  • doFullHandshake 関数での hs.cert の利用:

    	// ...
    	if hs.clientHello.ocspStapling && len(hs.cert.OCSPStaple) > 0 { // cert -> hs.cert
    		hs.hello.ocspStapling = true
    	}
    	// ...
    	certMsg.certificates = hs.cert.Certificate // cert -> hs.cert
    	// ...
    	certStatus.response = hs.cert.OCSPStaple // cert -> hs.cert
    	// ...
    	skx, err := keyAgreement.generateServerKeyExchange(config, hs.cert, hs.clientHello, hs.hello) // cert -> hs.cert
    	// ...
    	preMasterSecret, err := keyAgreement.processClientKeyExchange(config, hs.cert, ckx, c.vers) // cert -> hs.cert
    	// ...
    

    doFullHandshake 内で、OCSP Stapling、証明書メッセージ、鍵交換など、サーバー証明書を参照するすべての箇所が、ローカル変数 cert ではなく、serverHandshakeState のフィールドである hs.cert を使用するように変更されました。これにより、証明書情報の一貫した管理が保証されます。

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

  • TestCipherSuiteCertPreferance テストケースの追加:
    func TestCipherSuiteCertPreferance(t *testing.T) {
    	// ... RSA証明書とECDHE_RSA/ECDHE_ECDSAスイートのテスト
    	testServerScript(t, "CipherSuiteCertPreference", tls11ECDHEAESServerScript, &config, nil)
    
    	// ... ECDSA証明書とECDHE_RSA/ECDHE_ECDSAスイートのテスト
    	config.Certificates = []Certificate{
    		Certificate{
    			Certificate: [][]byte{testECDSACertificate},
    			PrivateKey:  testECDSAPrivateKey,
    		},
    	}
    	// ...
    	testServerScript(t, "CipherSuiteCertPreference2", ecdheECDSAAESServerScript, &config, nil)
    }
    
    このテストは、サーバーが設定された証明書タイプ(RSAまたはECDSA)に基づいて、適切な暗号スイート(ECDHE_RSAまたはECDHE_ECDSA)が選択されることを検証します。これにより、修正されたロジックが正しく機能することが確認されます。

これらの変更により、GoのTLSサーバーは、自身の証明書タイプと互換性のない暗号スイートを誤って選択することがなくなり、TLSハンドシェイクの信頼性と成功率が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • TLS/SSLプロトコルに関する一般的な情報源(RFCなど)
  • デジタル証明書と公開鍵暗号に関する一般的な情報源
  • コミットメッセージと変更されたソースコード
  • Goのコードレビューシステム (Gerrit) の該当コミットページ: https://golang.org/cl/13239053 (コミットメッセージに記載)