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

[インデックス 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は、主に以下の機能を提供します。

  1. 認証: 通信相手が主張するエンティティであることを確認します。通常、サーバーはデジタル証明書を提示し、クライアントはその証明書を検証することでサーバーの身元を確認します。
  2. 機密性: 通信内容が第三者に傍受されても解読されないように暗号化します。
  3. 完全性: 通信内容が転送中に改ざんされていないことを保証します。

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 が直接渡されていたためです。

このコミットでは、この問題を解決するために以下の主要な変更が行われました。

  1. 選択された証明書の伝播:

    • handshake_server.go 内で、SNIに基づいて config.getCertificateForName(clientHello.serverName) を呼び出して適切な証明書 (cert) を取得した後、この cert 変数がハンドシェイクの後半の処理に渡されるように修正されました。
    • 特に、keyAgreement.generateServerKeyExchangekeyAgreement.processClientKeyExchange の関数シグネチャが変更され、選択された *Certificate オブジェクトを引数として受け取るようになりました。これにより、鍵交換の処理が、デフォルトの証明書ではなく、SNIによって選択された特定の証明書に関連付けられた秘密鍵を使用できるようになります。
  2. OCSP Stapling の修正:

    • OCSP (Online Certificate Status Protocol) Stapling は、証明書の失効情報をサーバーが直接提供する仕組みです。変更前は、OCSP Stapling の応答も config.Certificates[0].OCSPStaple から取得されていました。
    • このコミットにより、SNIで選択された certOCSPStaple が使用されるように修正され、OCSP Stapling も選択された証明書に正しく対応するようになりました。
  3. テストケースの追加:

    • handshake_server_test.goTestHandshakeServerSNI という新しいテストケースが追加されました。このテストは、クライアントが "snitest.com" というSNI拡張を送信し、サーバーがそれに対応する証明書を正しく選択し、ハンドシェイクを完了できることを検証します。
    • テスト設定 (testConfig) には、デフォルトの証明書に加えて、"snitest.com" 用の別の証明書 (testSNICertificate) が追加され、BuildNameToCertificate() が呼び出されてSNIマッピングが構築されます。
    • selectCertificateBySNIScript という新しいバイト列が定義され、SNI拡張を含む ClientHello メッセージのシミュレーションと、それに対するサーバーの ServerHelloCertificateServerKeyExchange などの応答が記述されています。

これらの変更により、crypto/tls パッケージはSNIを正しく処理し、複数の証明書を持つサーバーが、クライアントが要求するホスト名に基づいて適切な証明書と秘密鍵のペアを使用できるようになりました。これにより、TLSハンドシェイクの信頼性とセキュリティが向上します。

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

このコミットにおける主要なコード変更は、以下のファイルに集中しています。

  1. 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 オブジェクトが渡されるようになりました。
  2. src/pkg/crypto/tls/handshake_server.go:

    • SNIに基づいて選択された証明書 (cert) を保持する変数が導入されました。
    • 以前は config.Certificates[0] が直接参照されていた箇所が、この cert 変数を使用するように変更されました。
    • 特に、certificateMsg の構築、ocspStapling の設定、そして keyAgreement.generateServerKeyExchange および keyAgreement.processClientKeyExchange の呼び出しにおいて、cert が引数として渡されるようになりました。
  3. src/pkg/crypto/tls/key_agreement.go:

    • rsaKeyAgreement および ecdheRSAKeyAgreementgenerateServerKeyExchangeprocessClientKeyExchange メソッドのシグネチャが cipher_suites.go の変更に合わせて更新されました。
    • これらのメソッド内で、秘密鍵の取得元が config.Certificates[0].PrivateKey から、引数として渡された cert.PrivateKey に変更されました。これにより、SNIで選択された証明書に対応する秘密鍵が使用されるようになります。
  4. 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 変数に適切な証明書が格納され、その後のハンドシェイクメッセージ(certificateMsgcertificateStatusMsg)の構築や鍵交換処理 (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.DecryptPKCS1v15SessionKeyrsa.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ハンドシェイクをシミュレートし、サーバーが正しい証明書を選択し、それに対応する秘密鍵で鍵交換を完了できることを確認します。これにより、将来の回帰を防ぐことができます。

関連リンク

参考にした情報源リンク