[インデックス 18631] ファイルの概要
このコミットは、Go言語の crypto/tls
パッケージにおける楕円曲線ディフィー・ヘルマン鍵共有 (ECDHE) ハンドシェイク時の曲線選択ロジックの変更に関するものです。具体的には、サーバーが自身の曲線設定を優先するように変更され、デフォルトではP-256曲線が優先されるようになりました。
コミット
commit db99a8faa89cdd10435de16a7230fd0ce8e47139
Author: Adam Langley <agl@golang.org>
Date: Mon Feb 24 17:57:51 2014 -0500
crypto/tls: pick ECDHE curves based on server preference.
Currently an ECDHE handshake uses the client's curve preference. This
generally means that we use P-521. However, P-521's strength is
mismatched with the rest of the cipher suite in most cases and we have
a fast, constant-time implementation of P-256.
With this change, Go servers will use P-256 where the client supports
it although that can be overridden in the Config.
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/66060043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/db99a8faa89cdd10435de16a7230fd0ce8e47139
元コミット内容
crypto/tls: pick ECDHE curves based on server preference.
Currently an ECDHE handshake uses the client's curve preference. This generally means that we use P-521. However, P-521's strength is mismatched with the rest of the cipher suite in most cases and we have a fast, constant-time implementation of P-256.
With this change, Go servers will use P-256 where the client supports it although that can be overridden in the Config.
変更の背景
TLS (Transport Layer Security) ハンドシェイクにおいて、ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) 鍵共有方式が使用される際、これまでGoの crypto/tls
パッケージはクライアントが提示する楕円曲線の優先順位に従って曲線を選択していました。この挙動により、多くの場合、P-521曲線が選択される傾向にありました。
しかし、P-521曲線は非常に高いセキュリティ強度を持つ一方で、TLSハンドシェイクで同時に使用される他の暗号スイート(例:署名アルゴリズムやハッシュ関数)の強度と比べて過剰であることが多く、全体的なセキュリティバランスが取れていないという問題がありました。また、P-521曲線の計算はP-256曲線に比べて計算コストが高く、特にサーバー側での処理性能に影響を与える可能性がありました。
これに対し、P-256曲線は、多くの一般的なセキュリティ要件を満たしつつ、Goのランタイムにおいて高速かつ定数時間(constant-time)で実装されていました。定数時間実装は、サイドチャネル攻撃(タイミング攻撃など)に対する耐性を高める上で非常に重要です。
このような背景から、GoのTLSサーバーがより効率的でバランスの取れたセキュリティを提供できるよう、クライアントの優先順位ではなく、サーバーが自身の優先順位に基づいてECDHE曲線を選択するよう変更する必要がありました。これにより、P-256のような最適化された曲線が優先的に使用されることが期待されました。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号プロトコルです。ウェブブラウザとサーバー間のHTTPS通信などで広く利用されています。TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために、互いの能力を交換し、鍵を共有し、認証を行う一連のプロセスです。
ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)
ECDHEは、TLSハンドシェイクで用いられる鍵共有アルゴリズムの一つです。楕円曲線暗号 (ECC) をベースにしており、Diffie-Hellman鍵共有の特性である「前方秘匿性 (Forward Secrecy)」を提供します。前方秘匿性とは、セッション鍵が漏洩しても、過去の通信内容が解読されないという特性です。これは、セフェメラル(一時的)な鍵ペアを各ハンドシェイクごとに生成することで実現されます。
楕円曲線 (Elliptic Curve)
楕円曲線暗号 (ECC) は、公開鍵暗号の一種で、有限体上の楕円曲線の数学的特性を利用しています。RSAなどの他の公開鍵暗号方式に比べて、同等のセキュリティ強度をより短い鍵長で実現できるため、計算リソースが限られた環境(モバイルデバイスなど)や、より高速な処理が求められる場面で有利です。
TLSで一般的に使用される標準的な楕円曲線には、NIST (National Institute of Standards and Technology) が推奨する曲線群があります。このコミットで言及されているP-256、P-384、P-521は、それぞれ異なるビット長のセキュリティ強度を持つNIST曲線です。
- P-256 (secp256r1): 256ビットの素数体上の曲線。広く採用されており、高い性能と十分なセキュリティ強度を提供します。
- P-384 (secp384r1): 384ビットの素数体上の曲線。P-256よりも高いセキュリティ強度を持ちます。
- P-521 (secp521r1): 521ビットの素数体上の曲線。NIST曲線の中で最も高いセキュリティ強度を持ちますが、計算コストも最も高くなります。
クライアントの曲線優先順位とサーバーの曲線優先順位
TLSハンドシェイクの ClientHello
メッセージには、クライアントがサポートする楕円曲線とその優先順位が含まれます。従来のGoのTLS実装では、サーバーはこのクライアントの優先順位に従って曲線を選択していました。
しかし、サーバー側にも独自の曲線選択の優先順位を持つことが望ましい場合があります。例えば、特定の曲線がハードウェアアクセラレーションに対応している、あるいは定数時間実装されているなど、性能やセキュリティの観点からサーバーが優先したい曲線が存在する場合です。このコミットは、このサーバー側の優先順位を導入することを目的としています。
定数時間実装 (Constant-Time Implementation)
暗号アルゴリズムの実装において、処理時間が入力データや秘密情報に依存しないようにすることを「定数時間実装」と呼びます。もし処理時間が秘密情報(例:秘密鍵)に依存する場合、攻撃者は処理時間のわずかな差を測定することで、秘密情報を推測する「タイミング攻撃」などのサイドチャネル攻撃を行う可能性があります。定数時間実装は、このような攻撃に対する重要な防御策となります。P-256の定数時間実装は、この曲線の採用を促進する大きな要因です。
技術的詳細
このコミットの主要な変更点は、TLSハンドシェイクにおけるECDHE鍵共有の際に、サーバーがクライアントの提示する曲線リストの中から、自身の Config
で設定された優先順位に基づいて曲線を選択するようになったことです。
具体的には、以下の変更が行われました。
-
CurveID
型の導入: 楕円曲線のIDを表すために、uint16
のエイリアスとしてCurveID
という新しい型がsrc/pkg/crypto/tls/common.go
に導入されました。これにより、コードの可読性と型安全性が向上しました。既存のcurveP256
,curveP384
,curveP521
定数もCurveP256
,CurveP384
,CurveP521
に変更され、CurveID
型を持つようになりました。 -
Config.CurvePreferences
フィールドの追加:Config
構造体(TLS接続の設定を保持する)にCurvePreferences []CurveID
という新しいフィールドが追加されました。これにより、GoのTLSサーバーは、使用するECDHE曲線の優先順位を明示的に設定できるようになりました。このフィールドが空の場合、デフォルトの優先順位 (CurveP256
,CurveP384
,CurveP521
) が使用されます。 -
サーバー側の曲線選択ロジックの変更:
src/pkg/crypto/tls/handshake_server.go
のreadClientHello
関数内で、クライアントがサポートする曲線の中から、サーバーのconfig.curvePreferences()
に基づいて最適な曲線を選択するロジックが追加されました。以前は、クライアントが提示した最初のサポートされる曲線が選択されていました。src/pkg/crypto/tls/key_agreement.go
のecdheKeyAgreement.generateServerKeyExchange
関数が変更され、サーバーのConfig
に設定されたCurvePreferences
を考慮してECDHE曲線を選択するようになりました。具体的には、サーバーの優先する曲線リスト (preferredCurves
) を順に走査し、その曲線がクライアントのClientHello
でサポートされている場合に、その曲線が選択されます。これにより、サーバーは自身の性能やセキュリティ要件に合った曲線を優先的に使用できるようになります。
-
クライアント側の変更:
src/pkg/crypto/tls/handshake_client.go
では、ClientHello
メッセージでクライアントが提示するサポートされる曲線リストが、c.config.curvePreferences()
から取得されるようになりました。これにより、クライアント側も設定に基づいてサポートする曲線を制御できるようになります。 -
テストデータの更新: 多数の
testdata
ファイルが更新され、新しい曲線選択ロジックが反映されたハンドシェイクメッセージのダンプが含まれるようになりました。これは、変更がTLSプロトコルの挙動に正しく影響を与えていることを確認するために重要です。
この変更により、GoのTLSサーバーは、クライアントの意図にかかわらず、P-256のような高速で定数時間実装が可能な曲線を優先的に使用できるようになり、全体的な性能とサイドチャネル攻撃に対する耐性が向上しました。
コアとなるコードの変更箇所
src/pkg/crypto/tls/common.go
--- a/src/pkg/crypto/tls/common.go
+++ b/src/pkg/crypto/tls/common.go
@@ -82,12 +82,14 @@ const (
scsvRenegotiation uint16 = 0x00ff
)
-// TLS Elliptic Curves
+// CurveID is the type of a TLS identifier for an elliptic curve. See
// http://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8
+type CurveID uint16
+
const (
- curveP256 uint16 = 23
- curveP384 uint16 = 24
- curveP521 uint16 = 25
+ CurveP256 CurveID = 23
+ CurveP384 CurveID = 24
+ CurveP521 CurveID = 25
)
// TLS Elliptic Curve Point Formats
@@ -290,6 +292,11 @@ type Config struct {
// which is currently TLS 1.2.
MaxVersion uint16
+ // CurvePreferences contains the elliptic curves that will be used in
+ // an ECDHE handshake, in preference order. If empty, the default will
+ // be used.
+ CurvePreferences []CurveID
+
serverInitOnce sync.Once // guards calling (*Config).serverInit
}
@@ -348,6 +355,15 @@ func (c *Config) maxVersion() uint16 {
return c.MaxVersion
}
+var defaultCurvePreferences = []CurveID{CurveP256, CurveP384, CurveP521}
+
+func (c *Config) curvePreferences() []CurveID {
+ if c == nil || len(c.CurvePreferences) == 0 {
+ return defaultCurvePreferences
+ }
+ return c.CurvePreferences
+}
+
// mutualVersion returns the protocol version to use given the advertised
// version of the peer.
func (c *Config) mutualVersion(vers uint16) (uint16, bool) {
src/pkg/crypto/tls/handshake_server.go
--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -117,12 +117,14 @@ func (hs *serverHandshakeState) readClientHello() (isResume bool, err error) {
hs.hello = new(serverHelloMsg)
supportedCurve := false
+ preferredCurves := config.curvePreferences()
Curves:
for _, curve := range hs.clientHello.supportedCurves {
- switch curve {
- case curveP256, curveP384, curveP521:
- supportedCurve = true
- break Curves
+ for _, supported := range preferredCurves {
+ if supported == curve {
+ supportedCurve = true
+ break Curves
+ }
}
}
src/pkg/crypto/tls/key_agreement.go
--- a/src/pkg/crypto/tls/key_agreement.go
+++ b/src/pkg/crypto/tls/key_agreement.go
@@ -163,6 +163,20 @@ func pickTLS12HashForSignature(sigType uint8, clientSignatureAndHashes []signatu
return 0, errors.New("tls: client doesn't support any common hash functions")
}
+func curveForCurveID(id CurveID) (elliptic.Curve, bool) {
+ switch id {
+ case CurveP256:
+ return elliptic.P256(), true
+ case CurveP384:
+ return elliptic.P384(), true
+ case CurveP521:
+ return elliptic.P521(), true
+ default:
+ return nil, false
+ }
+
+}
+
// ecdheRSAKeyAgreement implements a TLS key agreement where the server
// generates a ephemeral EC public/private key pair and signs it. The
// pre-master secret is then calculated using ECDH. The signature may
@@ -176,23 +190,16 @@ type ecdheKeyAgreement struct {
}\n \n func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) {
-\tvar curveid uint16
-\n-Curve:\n-\tfor _, c := range clientHello.supportedCurves {\n-\t\tswitch c {\n-\t\tcase curveP256:\n-\t\t\tka.curve = elliptic.P256()\n-\t\t\tcurveid = c\n-\t\t\tbreak Curve\n-\t\tcase curveP384:\n-\t\t\tka.curve = elliptic.P384()\n-\t\t\tcurveid = c\n-\t\t\tbreak Curve\n-\t\tcase curveP521:\n-\t\t\tka.curve = elliptic.P521()\n-\t\t\tcurveid = c\n-\t\t\tbreak Curve\n+\tvar curveid CurveID
+\tpreferredCurves := config.curvePreferences()
+\n+NextCandidate:
+\tfor _, candidate := range preferredCurves {
+\t\tfor _, c := range clientHello.supportedCurves {
+\t\t\tif candidate == c {
+\t\t\t\tcurveid = c
+\t\t\t\tbreak NextCandidate
+\t\t\t}
+\t\t}
\t}\n \n@@ -200,6 +207,11 @@ Curve:\n \t\treturn nil, errors.New(\"tls: no supported elliptic curves offered\")\n \t}\n \n+\tvar ok bool\n+\tif ka.curve, ok = curveForCurveID(curveid); !ok {\n+\t\treturn nil, errors.New(\"tls: preferredCurves includes unsupported curve\")\n+\t}\n+\n \tvar x, y *big.Int\n \tvar err error\n \tka.privateKey, x, y, err = elliptic.GenerateKey(ka.curve, config.rand())\n@@ -293,19 +305,13 @@ func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHell
\t\treturn errServerKeyExchange
\t}\n \tif skx.key[0] != 3 { // named curve
-\t\treturn errors.New(\"server selected unsupported curve\")
+\t\treturn errors.New(\"tls: server selected unsupported curve\")
\t}\n-\tcurveid := uint16(skx.key[1])<<8 | uint16(skx.key[2])
+\tcurveid := CurveID(skx.key[1])<<8 | CurveID(skx.key[2])
\n-\tswitch curveid {\n-\tcase curveP256:\n-\t\tka.curve = elliptic.P256()\n-\tcase curveP384:\n-\t\tka.curve = elliptic.P384()\n-\tcase curveP521:\n-\t\tka.curve = elliptic.P521()\n-\tdefault:\n-\t\treturn errors.New(\"server selected unsupported curve\")
+\tvar ok bool
+\tif ka.curve, ok = curveForCurveID(curveid); !ok {\n+\t\treturn errors.New(\"tls: server selected unsupported curve\")
\t}\n \n \tpublicLen := int(skx.key[3])
コアとなるコードの解説
src/pkg/crypto/tls/common.go
の変更
CurveID
型の導入: 楕円曲線の識別子をより明確にするために、uint16
のエイリアスとしてCurveID
が定義されました。これにより、コードの意図がより明確になり、誤った型の値が渡されるのを防ぐことができます。- 定数名の変更:
curveP256
,curveP384
,curveP521
がCurveP256
,CurveP384
,CurveP521
に変更され、新しいCurveID
型を持つようになりました。これはGoの命名規則 (CamelCase
for exported identifiers) にも合致しています。 Config.CurvePreferences
フィールドの追加:Config
構造体にCurvePreferences []CurveID
が追加されました。これは、TLSサーバーがECDHEハンドシェイクで使用する楕円曲線の優先順位を、開発者が明示的に設定できるようにするためのものです。このリストの順序が、サーバーが曲線を試す順序になります。defaultCurvePreferences
変数とConfig.curvePreferences()
メソッドの追加:defaultCurvePreferences
は、CurvePreferences
が設定されていない場合にデフォルトで使用される曲線の優先順位を定義しています。デフォルトではP-256
,P-384
,P-521
の順になっています。これは、P-256が高速で定数時間実装されているため、これを優先するというコミットの意図を反映しています。Config.curvePreferences()
メソッドは、Config
がnilであるか、CurvePreferences
が空の場合にdefaultCurvePreferences
を返し、それ以外の場合は設定されたCurvePreferences
を返します。これにより、常に有効な曲線リストが使用されることが保証されます。
src/pkg/crypto/tls/handshake_server.go
の変更
readClientHello
関数の変更:- サーバーがクライアントから受け取った
ClientHello
メッセージのsupportedCurves
リストを処理する際に、config.curvePreferences()
で定義されたサーバーの優先順位が考慮されるようになりました。 Curves:
ラベル付きのループ内で、クライアントがサポートする曲線 (hs.clientHello.supportedCurves
) を走査し、その各曲線がサーバーの優先する曲線リスト (preferredCurves
) のいずれかと一致するかどうかを確認します。- 以前は
switch
文でハードコードされたP-256, P-384, P-521のいずれかであるかを確認していましたが、この変更により、サーバーのConfig
に基づいて柔軟に優先順位を決定できるようになりました。 - 最初に一致した曲線が見つかった時点で
supportedCurve
をtrue
に設定し、ループを抜けます。これにより、サーバーは自身の優先順位に従って、クライアントもサポートする最も優先度の高い曲線を選択します。
- サーバーがクライアントから受け取った
src/pkg/crypto/tls/key_agreement.go
の変更
curveForCurveID
関数の追加:CurveID
を受け取り、対応するelliptic.Curve
オブジェクトを返すヘルパー関数が追加されました。これにより、CurveID
から実際の楕円曲線オブジェクトへのマッピングが一元化され、コードの重複が削減されました。ecdheKeyAgreement.generateServerKeyExchange
関数の変更:- サーバーが鍵交換メッセージを生成する際に、
clientHello.supportedCurves
とconfig.curvePreferences()
の両方を考慮してcurveid
を選択するようになりました。 NextCandidate:
ラベル付きの新しいループが導入され、サーバーの優先する曲線リスト (preferredCurves
) を順に走査します。各優先曲線について、クライアントがその曲線をサポートしているか (clientHello.supportedCurves
に含まれているか) を確認します。- 最初にクライアントとサーバーの両方がサポートする曲線が見つかった場合、その曲線が
curveid
として選択され、ループが中断されます。これにより、サーバーの優先順位が尊重されます。 - 選択された
curveid
に対応するelliptic.Curve
オブジェクトをcurveForCurveID
を使用して取得し、ka.curve
に設定します。
- サーバーが鍵交換メッセージを生成する際に、
ecdheKeyAgreement.processServerKeyExchange
関数の変更:- クライアントがサーバーから受け取った鍵交換メッセージを処理する際に、サーバーが選択した曲線ID (
curveid
) をCurveID
型として読み取るようになりました。 - この
curveid
に対応するelliptic.Curve
オブジェクトをcurveForCurveID
を使用して取得し、ka.curve
に設定します。これにより、クライアントはサーバーが選択した曲線を正しく解釈できるようになります。
- クライアントがサーバーから受け取った鍵交換メッセージを処理する際に、サーバーが選択した曲線ID (
これらの変更により、GoのTLSサーバーは、ECDHE鍵共有において、クライアントの提示する曲線リストの中から、自身の設定に基づいて最適な曲線(デフォルトではP-256)を能動的に選択できるようになりました。これにより、性能とセキュリティ特性のバランスが改善され、特に定数時間実装の恩恵を最大限に活用できるようになります。
関連リンク
- TLS (Transport Layer Security) - Wikipedia
- 楕円曲線ディフィー・ヘルマン鍵共有 - Wikipedia
- 楕円曲線暗号 - Wikipedia
- NIST (National Institute of Standards and Technology)
- IANA TLS Parameters - Elliptic Curve Registries (TLSで利用される楕円曲線の公式レジストリ)
参考にした情報源リンク
- golang/go commit db99a8faa89cdd10435de16a7230fd0ce8e47139 on GitHub
- Go CL 66060043 (Gerrit Code Review)
- Go issue 6999: crypto/tls: pick ECDHE curves based on server preference
- Constant-time programming - Wikipedia
- Forward secrecy - Wikipedia
- NIST curves (secp256r1, secp384r1, secp521r1)
- TLS Handshake Explained (Cloudflare)
- What is ECDHE? (SSL.com)