[インデックス 12888] ファイルの概要
このコミットは、Go言語の crypto/tls
パッケージにおける重要なバグ修正です。具体的には、Server Name Indication (SNI) を使用して特定の証明書が選択された際に、その証明書に対応する秘密鍵ではなく、常にデフォルトの秘密鍵が使用されてしまう問題を解決します。これにより、SNIが有効な環境でのTLSハンドシェイクの失敗や、誤った証明書検証を防ぎます。
コミット
commit e6e8b72377a8235b0dca4bbe485800341c6880cf
Author: Adam Langley <agl@golang.org>
Date: Thu Apr 12 12:35:21 2012 -0400
crypto/tls: don't always use the default private key.
When SNI based certificate selection is enabled, we previously used
the default private key even if we selected a non-default certificate.
Fixes #3367.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5987058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e6e8b72377a8235b0dca4bbe485800341c6880cf
元コミット内容
crypto/tls: don't always use the default private key.
SNIベースの証明書選択が有効な場合、非デフォルトの証明書が選択されても、以前はデフォルトの秘密鍵が使用されていました。
Issue #3367 を修正します。
変更の背景
この変更の背景には、TLS (Transport Layer Security) プロトコルにおける Server Name Indication (SNI) の実装上の問題がありました。SNIは、単一のIPアドレスとポートで複数のTLS証明書をホストすることを可能にするTLSの拡張機能です。クライアントはTLSハンドシェイクの初期段階で、接続したいホスト名(サーバー名)をサーバーに通知します。これにより、サーバーはそのホスト名に対応する適切な証明書を選択してクライアントに提示できます。
しかし、Goの crypto/tls
パッケージの以前の実装では、SNIによって特定のホスト名に対応する証明書が正常に選択されたとしても、その証明書と対になるべき秘密鍵ではなく、設定されているデフォルトの秘密鍵を常に使用してしまうというバグが存在しました。
TLSハンドシェイクにおいて、サーバーは自身の証明書をクライアントに提示し、その証明書に対応する秘密鍵を用いて、クライアントから送られてきたプリマスターシークレット(鍵交換の材料)を復号したり、ハンドシェイクメッセージに署名したりする必要があります。秘密鍵が証明書と一致しない場合、クライアントはサーバーの身元を検証できず、ハンドシェイクは失敗します。
このバグは、特に複数のドメインを単一のTLSサーバーでホストしている環境(例えば、共有ホスティングサービスやCDNなど)において、深刻な問題を引き起こす可能性がありました。クライアントがSNIを使用して正しい証明書を要求しても、サーバーが誤った秘密鍵を使用するため、TLS接続が確立できないという事態が発生していました。
この問題は、GoのIssueトラッカーで #3367 として報告され、このコミットによって修正されました。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、主に以下の機能を提供します。
- 認証: 通信相手が主張するエンティティであることを確認します。通常、サーバーはデジタル証明書を提示し、クライアントはその証明書を検証することでサーバーの身元を確認します。
- 機密性: 通信内容が第三者に傍受されても解読されないように暗号化します。
- 完全性: 通信内容が転送中に改ざんされていないことを保証します。
TLS通信は「TLSハンドシェイク」と呼ばれる初期のネゴシエーションプロセスから始まります。このハンドシェイク中に、クライアントとサーバーは互いの能力を交換し、暗号スイート(使用する暗号アルゴリズムの組み合わせ)を合意し、鍵交換を行い、セッション鍵を確立します。
デジタル証明書と秘密鍵
TLSにおいて、サーバーの身元を証明するためにデジタル証明書が使用されます。デジタル証明書は、サーバーの公開鍵、サーバーの識別情報(ドメイン名など)、そして信頼できる認証局(CA)による署名が含まれています。クライアントはCAの公開鍵を使って証明書の署名を検証し、サーバーの身元を確認します。
秘密鍵は、公開鍵とペアになる鍵で、サーバーのみが保持します。TLSハンドシェイクの過程で、サーバーは秘密鍵を使用して、クライアントから送られてきた暗号化されたデータを復号したり、特定のメッセージにデジタル署名を行ったりします。秘密鍵は厳重に管理される必要があり、公開鍵と秘密鍵のペアが正しく機能することで、安全な通信が保証されます。
SNI (Server Name Indication)
SNIは、TLSプロトコルの拡張機能であり、単一のIPアドレスとポート番号で複数のウェブサイト(ドメイン)をホストするサーバーが、クライアントがどのドメインに接続しようとしているかを識別できるようにします。
SNIが導入される前は、サーバーはIPアドレスごとに1つのTLS証明書しか提供できませんでした。これは、TLSハンドシェイクが始まる前にクライアントがどのホスト名に接続しようとしているかを知る方法がなかったためです。しかし、SNIを使用すると、クライアントは ClientHello
メッセージの一部として、接続しようとしているサーバーのホスト名(例: www.example.com
)を送信します。サーバーはこの情報を受け取り、そのホスト名に対応する適切な証明書を選択してクライアントに提示することができます。
この機能は、特に仮想ホスティング環境において非常に重要です。SNIがなければ、各ドメインに専用のIPアドレスが必要となり、IPv4アドレスの枯渇問題やインフラコストの増大につながります。
鍵交換とプリマスターシークレット
TLSハンドシェイクの重要なステップの一つが鍵交換です。クライアントとサーバーは、このステップで「プリマスターシークレット」と呼ばれる共通の秘密情報を生成します。このプリマスターシークレットは、その後の通信を暗号化するための「マスターシークレット」を導出するための基になります。
RSA鍵交換の場合、クライアントはランダムなプリマスターシークレットを生成し、サーバーの公開鍵で暗号化してサーバーに送信します。サーバーは自身の秘密鍵でこの暗号化されたプリマスターシークレットを復号します。このプロセスが成功するためには、サーバーが提示した証明書に対応する正しい秘密鍵を使用することが不可欠です。もし誤った秘密鍵が使用された場合、サーバーはプリマスターシークレットを復号できず、ハンドシェイクは失敗します。
技術的詳細
このコミットの技術的詳細は、Goの crypto/tls
パッケージにおけるTLSサーバーハンドシェイクのロジック、特に証明書と秘密鍵の選択プロセスに焦点を当てています。
変更前は、handshake_server.go
内で、クライアントからSNI情報が提供された場合でも、config.Certificates[0]
(設定された証明書リストの最初の証明書、つまりデフォルトの証明書)の秘密鍵が常に使用されていました。これは、keyAgreement.generateServerKeyExchange
および keyAgreement.processClientKeyExchange
の呼び出しにおいて、config.Certificates[0].PrivateKey
が直接渡されていたためです。
このコミットでは、この問題を解決するために以下の主要な変更が行われました。
-
選択された証明書の伝播:
handshake_server.go
内で、SNIに基づいてconfig.getCertificateForName(clientHello.serverName)
を呼び出して適切な証明書 (cert
) を取得した後、このcert
変数がハンドシェイクの後半の処理に渡されるように修正されました。- 特に、
keyAgreement.generateServerKeyExchange
とkeyAgreement.processClientKeyExchange
の関数シグネチャが変更され、選択された*Certificate
オブジェクトを引数として受け取るようになりました。これにより、鍵交換の処理が、デフォルトの証明書ではなく、SNIによって選択された特定の証明書に関連付けられた秘密鍵を使用できるようになります。
-
OCSP Stapling の修正:
- OCSP (Online Certificate Status Protocol) Stapling は、証明書の失効情報をサーバーが直接提供する仕組みです。変更前は、OCSP Stapling の応答も
config.Certificates[0].OCSPStaple
から取得されていました。 - このコミットにより、SNIで選択された
cert
のOCSPStaple
が使用されるように修正され、OCSP Stapling も選択された証明書に正しく対応するようになりました。
- OCSP (Online Certificate Status Protocol) Stapling は、証明書の失効情報をサーバーが直接提供する仕組みです。変更前は、OCSP Stapling の応答も
-
テストケースの追加:
handshake_server_test.go
にTestHandshakeServerSNI
という新しいテストケースが追加されました。このテストは、クライアントが "snitest.com" というSNI拡張を送信し、サーバーがそれに対応する証明書を正しく選択し、ハンドシェイクを完了できることを検証します。- テスト設定 (
testConfig
) には、デフォルトの証明書に加えて、"snitest.com" 用の別の証明書 (testSNICertificate
) が追加され、BuildNameToCertificate()
が呼び出されてSNIマッピングが構築されます。 selectCertificateBySNIScript
という新しいバイト列が定義され、SNI拡張を含むClientHello
メッセージのシミュレーションと、それに対するサーバーのServerHello
、Certificate
、ServerKeyExchange
などの応答が記述されています。
これらの変更により、crypto/tls
パッケージはSNIを正しく処理し、複数の証明書を持つサーバーが、クライアントが要求するホスト名に基づいて適切な証明書と秘密鍵のペアを使用できるようになりました。これにより、TLSハンドシェイクの信頼性とセキュリティが向上します。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルに集中しています。
-
src/pkg/crypto/tls/cipher_suites.go
:keyAgreement
インターフェースのgenerateServerKeyExchange
およびprocessClientKeyExchange
メソッドのシグネチャが変更されました。- 変更前:
generateServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg)
- 変更後:
generateServerKeyExchange(*Config, *Certificate, *clientHelloMsg, *serverHelloMsg)
- 変更前:
processClientKeyExchange(*Config, *clientKeyExchangeMsg, uint16)
- 変更後:
processClientKeyExchange(*Config, *Certificate, *clientKeyExchangeMsg, uint16)
- これにより、鍵交換処理を行う際に、選択された
Certificate
オブジェクトが渡されるようになりました。
-
src/pkg/crypto/tls/handshake_server.go
:- SNIに基づいて選択された証明書 (
cert
) を保持する変数が導入されました。 - 以前は
config.Certificates[0]
が直接参照されていた箇所が、このcert
変数を使用するように変更されました。 - 特に、
certificateMsg
の構築、ocspStapling
の設定、そしてkeyAgreement.generateServerKeyExchange
およびkeyAgreement.processClientKeyExchange
の呼び出しにおいて、cert
が引数として渡されるようになりました。
- SNIに基づいて選択された証明書 (
-
src/pkg/crypto/tls/key_agreement.go
:rsaKeyAgreement
およびecdheRSAKeyAgreement
のgenerateServerKeyExchange
とprocessClientKeyExchange
メソッドのシグネチャがcipher_suites.go
の変更に合わせて更新されました。- これらのメソッド内で、秘密鍵の取得元が
config.Certificates[0].PrivateKey
から、引数として渡されたcert.PrivateKey
に変更されました。これにより、SNIで選択された証明書に対応する秘密鍵が使用されるようになります。
-
src/pkg/crypto/tls/handshake_server_test.go
:testConfig
にSNIテスト用の追加の証明書 (testSNICertificate
) が設定され、testConfig.BuildNameToCertificate()
が呼び出されるようになりました。TestHandshakeServerSNI
という新しいテスト関数が追加され、SNIベースの証明書選択が正しく機能するかを検証するスクリプト (selectCertificateBySNIScript
) が定義されました。- 既存のテストヘルパー関数
loadPEMCert
の引数名がclicert
からclientCertificate
に変更されました。
コアとなるコードの解説
cipher_suites.go
の変更
keyAgreement
インターフェースの変更は、TLSハンドシェイクにおける鍵交換の抽象化レイヤーに影響を与えます。generateServerKeyExchange
はサーバーがクライアントに送る鍵交換メッセージを生成し、processClientKeyExchange
はクライアントから受け取った鍵交換メッセージを処理してプリマスターシークレットを導出します。これらの関数が *Certificate
引数を受け取るようになったことで、鍵交換のロジックが、現在アクティブな(SNIによって選択された)証明書にアクセスできるようになり、その証明書に関連付けられた秘密鍵を確実に使用できるようになります。
handshake_server.go
の変更
このファイルはTLSサーバーハンドシェイクの主要なロジックを含んでいます。変更の核心は、SNI処理の後に cert
変数に適切な証明書が格納され、その後のハンドシェイクメッセージ(certificateMsg
、certificateStatusMsg
)の構築や鍵交換処理 (keyAgreement
メソッドの呼び出し) で、この cert
変数が一貫して使用されるようになった点です。
// 変更前:
// if len(clientHello.serverName) > 0 {
// c.serverName = clientHello.serverName
// certMsg.certificates = config.getCertificateForName(clientHello.serverName).Certificate
// } else {
// certMsg.certificates = config.Certificates[0].Certificate
// }
// 変更後:
cert := &config.Certificates[0] // デフォルトの証明書を初期値とする
if len(clientHello.serverName) > 0 {
c.serverName = clientHello.serverName
cert = config.getCertificateForName(clientHello.serverName) // SNIで選択された証明書に更新
}
// ...
// 鍵交換処理への引数として、選択された cert を渡す
skx, err := keyAgreement.generateServerKeyExchange(config, cert, clientHello, hello)
// ...
preMasterSecret, err := keyAgreement.processClientKeyExchange(config, cert, ckx, c.vers)
この変更により、SNIが提供された場合は config.getCertificateForName
で取得した証明書が、そうでない場合は config.Certificates[0]
が cert
変数に格納され、その後の処理で一貫して使用されるようになります。
key_agreement.go
の変更
このファイルには、RSAやECDHE-RSAなどの具体的な鍵交換アルゴリズムの実装が含まれています。ここで重要なのは、rsa.DecryptPKCS1v15SessionKey
や rsa.SignPKCS1v1v15
の呼び出しにおいて、秘密鍵の取得元が config.Certificates[0].PrivateKey
から cert.PrivateKey
に変更されたことです。
// rsaKeyAgreement.processClientKeyExchange 内の変更
// 変更前:
// err = rsa.DecryptPKCS1v15SessionKey(config.rand(), config.Certificates[0].PrivateKey.(*rsa.PrivateKey), ciphertext, preMasterSecret)
// 変更後:
err = rsa.DecryptPKCS1v15SessionKey(config.rand(), cert.PrivateKey.(*rsa.PrivateKey), ciphertext, preMasterSecret)
// ecdheRSAKeyAgreement.generateServerKeyExchange 内の変更
// 変更前:
// sig, err := rsa.SignPKCS1v15(config.rand(), config.Certificates[0].PrivateKey.(*rsa.PrivateKey), crypto.MD5SHA1, md5sha1)
// 変更後:
sig, err := rsa.SignPKCS1v15(config.rand(), cert.PrivateKey.(*rsa.PrivateKey), crypto.MD5SHA1, md5sha1)
この変更により、鍵交換の暗号操作が、SNIによって選択された証明書に紐付けられた正しい秘密鍵で行われることが保証されます。これが、このコミットの最も重要な修正点であり、TLSハンドシェイクの成功に直結します。
handshake_server_test.go
の変更
テストコードの追加は、この修正が意図通りに機能することを検証するために不可欠です。TestHandshakeServerSNI
は、実際のSNIハンドシェイクをシミュレートし、サーバーが正しい証明書を選択し、それに対応する秘密鍵で鍵交換を完了できることを確認します。これにより、将来の回帰を防ぐことができます。
関連リンク
- Go Issue 3367: https://github.com/golang/go/issues/3367
- Go CL 5987058: https://golang.org/cl/5987058
参考にした情報源リンク
- TLS (Transport Layer Security) - Wikipedia: https://ja.wikipedia.org/wiki/Transport_Layer_Security
- Server Name Indication (SNI) - Wikipedia: https://ja.wikipedia.org/wiki/Server_Name_Indication
- デジタル証明書 - Wikipedia: https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E8%A8%BC%E6%98%8E%E6%9B%B8
- 公開鍵暗号 - Wikipedia: https://ja.wikipedia.org/wiki/%E5%85%AC%E9%96%8B%E9%8D%B5%E6%9A%97%E5%8F%B7
- Online Certificate Status Protocol (OCSP) - Wikipedia: https://ja.wikipedia.org/wiki/Online_Certificate_Status_Protocol