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

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

このコミットは、Go言語の標準ライブラリである crypto/tls パッケージにおけるエラーメッセージの改善を目的としています。TLSハンドシェイクやレコード処理中に発生する様々なエラーに対して、より詳細で分かりやすいメッセージを提供するように変更が加えられています。これにより、TLS接続の問題をデバッグする際の効率が大幅に向上します。

コミット

commit 6b29f7bfbe9ca985ce2419285f0b56b5428e1ffe
Author: Adam Langley <agl@golang.org>
Date:   Wed Feb 12 11:20:01 2014 -0500

    crypto/tls: better error messages.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/60580046

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

https://github.com/golang/go/commit/6b29f7bfbe9ca985ce2419285f0b56b5428e1ffe

元コミット内容

crypto/tls: better error messages.

このコミットは、crypto/tls パッケージ内のエラーメッセージを改善します。

変更の背景

TLS (Transport Layer Security) は、インターネット上での安全な通信を確立するための重要なプロトコルです。しかし、TLSハンドシェイクは複雑なプロセスであり、様々な要因で失敗する可能性があります。従来の crypto/tls パッケージでは、エラーが発生した際に返されるメッセージが汎用的で、問題の根本原因を特定するのが困難な場合がありました。例えば、「unexpected message」や「handshake failure」といったメッセージだけでは、何が、なぜ、どのように問題を引き起こしたのかを把握するのが難しいという課題がありました。

このコミットの背景には、開発者がTLS接続の問題をより迅速かつ正確に診断できるように、エラーメッセージの質を向上させるという明確な意図があります。具体的には、エラー発生時のコンテキスト(期待される値と実際の値の不一致など)をメッセージに含めることで、デバッグの労力を削減し、開発者の生産性を高めることを目指しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。

  • TLS (Transport Layer Security): インターネット上でクライアントとサーバー間の安全な通信を確立するための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、データの機密性、完全性、認証を提供します。
  • TLSハンドシェイク: クライアントとサーバーが安全な通信を開始する前に実行する一連のメッセージ交換プロセスです。このプロセス中に、プロトコルバージョン、暗号スイート、証明書、鍵交換パラメータなどがネゴシエートされます。ハンドシェイクが成功すると、暗号化された通信が開始されます。
    • ClientHello: クライアントがサーバーに送信する最初のメッセージで、サポートするTLSバージョン、暗号スイート、圧縮方式などを通知します。
    • ServerHello: サーバーがClientHelloに応答するメッセージで、選択したTLSバージョン、暗号スイート、圧縮方式などを通知します。
    • Certificate: サーバーが自身の公開鍵証明書をクライアントに送信するメッセージです。
    • ClientKeyExchange / ServerKeyExchange: 鍵交換のための情報が含まれるメッセージです。
    • ChangeCipherSpec: 以降の通信が暗号化されることを示すメッセージです。
    • Finished: ハンドシェイクの完了と、ハンドシェイクメッセージのハッシュ値が含まれるメッセージで、ハンドシェイクの整合性を検証します。
  • TLS Alert Protocol: TLSプロトコルの一部で、通信中に発生したエラーや警告を相手に通知するために使用されます。アラートは「警告 (warning)」または「致命的 (fatal)」のいずれかであり、致命的なアラートが送信されると、TLS接続は即座に終了します。
    • alertUnexpectedMessage: 予期しないメッセージを受信した際に送信されるアラートです。
    • alertProtocolVersion: サポートされていないプロトコルバージョンを受信した際に送信されるアラートです。
    • alertHandshakeFailure: ハンドシェイクが何らかの理由で失敗した際に送信される汎用的なアラートです。
    • alertInternalError: 内部エラーが発生した際に送信されるアラートです。
  • Go言語のエラーハンドリング: Go言語では、関数は通常、戻り値と error インターフェースの2つを返します。errornil でない場合、エラーが発生したことを示します。fmt.Errorf 関数は、フォーマットされた文字列から新しいエラーを作成するために使用されます。これにより、動的な情報を含むエラーメッセージを生成できます。
  • fmt.Errorferrors.New の違い:
    • errors.New("message"): 静的な文字列からエラーを作成します。メッセージは常に同じです。
    • fmt.Errorf("format string %v", value): フォーマット文字列と引数を使用してエラーメッセージを作成します。これにより、エラー発生時の具体的な値やコンテキストをメッセージに含めることができます。

技術的詳細

このコミットの主要な技術的変更は、crypto/tls パッケージ内でエラーを返す箇所において、より詳細な情報を含む fmt.Errorf を使用するように修正された点です。これにより、エラーメッセージが単なる汎用的な説明ではなく、問題の具体的な原因や関連するデータを示すようになりました。

具体的な変更点は以下の通りです。

  1. unexpectedMessageError ヘルパー関数の導入:

    • src/pkg/crypto/tls/common.gounexpectedMessageError(wanted, got interface{}) error という新しいヘルパー関数が追加されました。
    • この関数は、TLSハンドシェイク中に予期しないメッセージタイプを受信した場合に、一貫性のあるエラーメッセージ(例: tls: received unexpected handshake message of type %T when waiting for %T)を生成するために使用されます。これにより、期待されるメッセージタイプと実際に受信したメッセージタイプが明確に示され、デバッグが容易になります。
  2. fmt.Errorf を用いた詳細なエラーメッセージの生成:

    • src/pkg/crypto/tls/conn.gosrc/pkg/crypto/tls/handshake_client.gosrc/pkg/crypto/tls/handshake_server.gosrc/pkg/crypto/tls/key_agreement.go の各ファイルで、エラーを返す多くの箇所が errors.New から fmt.Errorf に変更されました。
    • 例えば、プロトコルバージョンの不一致の場合、以前は汎用的なエラーが返されていましたが、変更後は「tls: received record with version %x when expecting version %x」のように、受信したバージョンと期待されるバージョンがメッセージに含まれるようになりました。
    • 同様に、サポートされていない暗号スイート、証明書の解析失敗、公開鍵のタイプ不一致など、様々なエラーケースで具体的な情報がエラーメッセージに埋め込まれるようになりました。
  3. エラーメッセージへの「tls:」プレフィックスの追加:

    • 多くの新しいエラーメッセージには「tls:」というプレフィックスが追加されています。これにより、エラーが crypto/tls パッケージから発生したものであることが一目で分かり、ログの解析やエラーの分類に役立ちます。
  4. テストコードの適応:

    • src/pkg/crypto/tls/handshake_server_test.go では、エラーメッセージがより詳細になったことに伴い、テストの検証方法が変更されました。
    • 以前はエラーオブジェクトの厳密な比較を行っていましたが、今後は strings.Contains(err.Error(), expectedSubStr) を使用して、エラーメッセージが特定のサブ文字列を含むかどうかをチェックするようになりました。これにより、エラーメッセージの具体的な内容がテストで検証されるようになり、エラーメッセージの品質が維持されます。

これらの変更により、TLS接続のデバッグ時に得られる情報量が格段に増え、開発者は問題の特定と解決に要する時間を大幅に短縮できるようになります。

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

このコミットにおける主要なコード変更は、src/pkg/crypto/tls/common.go でのヘルパー関数の追加と、src/pkg/crypto/tls/conn.gosrc/pkg/crypto/tls/handshake_client.gosrc/pkg/crypto/tls/handshake_server.gosrc/pkg/crypto/tls/key_agreement.go におけるエラーメッセージの変更です。

src/pkg/crypto/tls/common.go

--- a/src/pkg/crypto/tls/common.go
+++ b/src/pkg/crypto/tls/common.go
@@ -9,6 +9,7 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/x509"
+	"fmt"
 	"io"
 	"math/big"
 	"strings"
@@ -540,3 +541,7 @@ func initDefaultCipherSuites() {
 		varDefaultCipherSuites[i] = suite.id
 	}
 }
+
+func unexpectedMessageError(wanted, got interface{}) error {
+	return fmt.Errorf("tls: received unexpected handshake message of type %T when waiting for %T", got, wanted)
+}

src/pkg/crypto/tls/conn.go (抜粋)

--- a/src/pkg/crypto/tls/conn.go
+++ b/src/pkg/crypto/tls/conn.go
@@ -12,6 +12,7 @@ import (
 	"crypto/subtle"
 	"crypto/x509"
 	"errors"
+	"fmt"
 	"io"
 	"net"
 	"sync"
@@ -518,14 +519,17 @@ func (c *Conn) readRecord(want recordType) error {
 	// else application data.  (We don't support renegotiation.)
 	switch want {
 	default:
-		return c.sendAlert(alertInternalError)
+		c.sendAlert(alertInternalError)
+		return errors.New("tls: unknown record type requested")
 	case recordTypeHandshake, recordTypeChangeCipherSpec:
 		if c.handshakeComplete {
-			return c.sendAlert(alertInternalError)
+			c.sendAlert(alertInternalError)
+			return errors.New("tls: handshake or ChangeCipherSpec requested after handshake complete")
 		}
 	case recordTypeApplicationData:
 		if !c.handshakeComplete {
-			return c.sendAlert(alertInternalError)
+			c.sendAlert(alertInternalError)
+			return errors.New("tls: application data record requested before handshake complete")
 		}
 	}
 
@@ -562,10 +566,12 @@ Again:
 	vers := uint16(b.data[1])<<8 | uint16(b.data[2])
 	n := int(b.data[3])<<8 | int(b.data[4])
 	if c.haveVers && vers != c.vers {
-		return c.sendAlert(alertProtocolVersion)
+		c.sendAlert(alertProtocolVersion)
+		return fmt.Errorf("tls: received record with version %x when expecting version %x", vers, c.vers)
 	}
 	if n > maxCiphertext {
-		return c.sendAlert(alertRecordOverflow)
+		c.sendAlert(alertRecordOverflow)
+		return fmt.Errorf("tls: oversized record received with length %d", n)
 	}
 	if !c.haveVers {
 		// First message, be extra suspicious:
@@ -577,7 +583,8 @@ Again:
 		// well under a kilobyte.  If the length is >= 12 kB,
 		// it's probably not real.
 		if (typ != recordTypeAlert && typ != want) || vers >= 0x1000 || n >= 0x3000 {
-			return c.sendAlert(alertUnexpectedMessage)
+			c.sendAlert(alertUnexpectedMessage)
+			return fmt.Errorf("tls: first record does not look like a TLS handshake")
 		}
 	}
 	if err := b.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
@@ -990,10 +997,10 @@ func (c *Conn) VerifyHostname(host string) error {
 	c.handshakeMutex.Lock()
 	defer c.handshakeMutex.Unlock()
 	if !c.isClient {
-		return errors.New("VerifyHostname called on TLS server connection")
+		return errors.New("tls: VerifyHostname called on TLS server connection")
 	}
 	if !c.handshakeComplete {
-		return errors.New("TLS handshake has not yet been performed")
+		return errors.New("tls: handshake has not yet been performed")
 	}
 	return c.peerCertificates[0].VerifyHostname(host)
 }

src/pkg/crypto/tls/handshake_client.go (抜粋)

--- a/src/pkg/crypto/tls/handshake_client.go
+++ b/src/pkg/crypto/tls/handshake_client.go
@@ -12,6 +12,7 @@ import (
 	"crypto/x509"
 	"encoding/asn1"
 	"errors"
+	"fmt"
 	"io"
 	"net"
 	"strconv"
@@ -126,20 +127,23 @@ NextCipherSuite:
 	}
 	serverHello, ok := msg.(*serverHelloMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(serverHello, msg)
 	}
 
 	vers, ok := c.config.mutualVersion(serverHello.vers)
 	if !ok || vers < VersionTLS10 {
 		// TLS 1.0 is the minimum version supported as a client.
-		return c.sendAlert(alertProtocolVersion)
+		c.sendAlert(alertProtocolVersion)
+		return fmt.Errorf("tls: server selected unsupported protocol version %x", serverHello.vers)
 	}
 	c.vers = vers
 	c.haveVers = true
 
 	suite := mutualCipherSuite(c.config.cipherSuites(), serverHello.cipherSuite)
 	if suite == nil {
-		return c.sendAlert(alertHandshakeFailure)
+		c.sendAlert(alertHandshakeFailure)
+		return fmt.Errorf("tls: server selected an unsupported cipher suite")
 	}
 
 	hs := &clientHandshakeState{
@@ -209,7 +213,8 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 	}
 	certMsg, ok := msg.(*certificateMsg)
 	if !ok || len(certMsg.certificates) == 0 {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(certMsg, msg)
 	}
 	hs.finishedHash.Write(certMsg.marshal())
 
@@ -218,7 +223,7 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 		cert, err := x509.ParseCertificate(asn1Data)
 		if err != nil {
 			c.sendAlert(alertBadCertificate)
-			return errors.New("failed to parse certificate from server: " + err.Error())
+			return errors.New("tls: failed to parse certificate from server: " + err.Error())
 		}
 		certs[i] = cert
 	}
@@ -248,7 +253,8 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 	case *rsa.PublicKey, *ecdsa.PublicKey:
 		break
 	default:
-		return c.sendAlert(alertUnsupportedCertificate)
+		c.sendAlert(alertUnsupportedCertificate)
+		return fmt.Errorf("tls: server's certificate contains an unsupported type of public key: %T", certs[0].PublicKey)
 	}
 
 	c.peerCertificates = certs
@@ -260,7 +266,8 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 		}
 		cs, ok := msg.(*certificateStatusMsg)
 		if !ok {
-			return c.sendAlert(alertUnexpectedMessage)
+			c.sendAlert(alertUnexpectedMessage)
+			return unexpectedMessageError(cs, msg)
 		}
 		hs.finishedHash.Write(cs.marshal())
 
@@ -371,7 +378,8 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 
 	shd, ok := msg.(*serverHelloDoneMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(shd, msg)
 	}
 	hs.finishedHash.Write(shd.marshal())
 
@@ -421,7 +429,8 @@ func (hs *clientHandshakeState) doFullHandshake() error {
 		err = errors.New("unknown private key type")
 	}
 	if err != nil {
-		return c.sendAlert(alertInternalError)
+		c.sendAlert(alertInternalError)
+		return errors.New("tls: failed to sign handshake with client certificate: " + err.Error())
 	}
 	certVerify.signature = signed
 
@@ -466,12 +475,13 @@ func (hs *clientHandshakeState) processServerHello() (bool, error) {
 	c := hs.c
 
 	if hs.serverHello.compressionMethod != compressionNone {
-		return false, c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return false, errors.New("tls: server selected unsupported compression format")
 	}
 
 	if !hs.hello.nextProtoNeg && hs.serverHello.nextProtoNeg {
 		c.sendAlert(alertHandshakeFailure)
-		return false, errors.New("server advertised unrequested NPN")
+		return false, errors.New("server advertised unrequested NPN extension")
 	}
 
 	if hs.serverResumedSession() {
@@ -497,13 +507,15 @@ func (hs *clientHandshakeState) readFinished() error {
 	}
 	serverFinished, ok := msg.(*finishedMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(serverFinished, msg)
 	}
 
 	verify := hs.finishedHash.serverSum(hs.masterSecret)
 	if len(verify) != len(serverFinished.verifyData) ||
 		subtle.ConstantTimeCompare(verify, serverFinished.verifyData) != 1 {
-		return c.sendAlert(alertHandshakeFailure)
+		c.sendAlert(alertHandshakeFailure)
+		return errors.New("tls: server's Finished message was incorrect")
 	}
 	hs.finishedHash.Write(serverFinished.marshal())
 	return nil
@@ -521,7 +533,8 @@ func (hs *clientHandshakeState) readSessionTicket() error {
 	}
 	sessionTicketMsg, ok := msg.(*newSessionTicketMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(sessionTicketMsg, msg)
 	}
 	hs.finishedHash.Write(sessionTicketMsg.marshal())

src/pkg/crypto/tls/handshake_server.go (抜粋)

--- a/src/pkg/crypto/tls/handshake_server.go
+++ b/src/pkg/crypto/tls/handshake_server.go
@@ -12,6 +12,7 @@ import (
 	"crypto/x509"
 	"encoding/asn1"
 	"errors"
+	"fmt"
 	"io"
 )
 
@@ -100,11 +101,13 @@ func (hs *serverHandshakeState) readClientHello() (isResume bool, err error) {
 	var ok bool
 	hs.clientHello, ok = msg.(*clientHelloMsg)
 	if !ok {
-		return false, c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return false, unexpectedMessageError(hs.clientHello, msg)
 	}
 	c.vers, ok = config.mutualVersion(hs.clientHello.vers)
 	if !ok {
-		return false, c.sendAlert(alertProtocolVersion)
+		c.sendAlert(alertProtocolVersion)
+		return false, fmt.Errorf("tls: client offered an unsupported, maximum protocol version of %x", hs.clientHello.vers)
 	}
 	c.haveVers = true
 
@@ -142,14 +145,16 @@ Curves:
 	}
 
 	if !foundCompression {
-		return false, c.sendAlert(alertHandshakeFailure)
+		c.sendAlert(alertHandshakeFailure)
+		return false, errors.New("tls: client does not support uncompressed connections")
 	}
 
 	hs.hello.vers = c.vers
 	hs.hello.random = make([]byte, 32)
 	_, err = io.ReadFull(config.rand(), hs.hello.random)
 	if err != nil {
-		return false, c.sendAlert(alertInternalError)
+		c.sendAlert(alertInternalError)
+		return false, err
 	}
 	hs.hello.secureRenegotiation = hs.clientHello.secureRenegotiation
 	hs.hello.compressionMethod = compressionNone
@@ -166,7 +171,8 @@ Curves:
 	}
 
 	if len(config.Certificates) == 0 {
-		return false, c.sendAlert(alertInternalError)
+		c.sendAlert(alertInternalError)
+		return false, errors.New("tls: no certificates configured")
 	}
 	hs.cert = &config.Certificates[0]
 	if len(hs.clientHello.serverName) > 0 {
@@ -195,7 +201,8 @@ Curves:
 	}
 
 	if hs.suite == nil {
-		return false, c.sendAlert(alertHandshakeFailure)
+		c.sendAlert(alertHandshakeFailure)
+		return false, errors.New("tls: no cipher suite supported by both client and server")
 	}
 
 	return false, nil
@@ -345,7 +352,8 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 	// certificate message, even if it's empty.
 	if config.ClientAuth >= RequestClientCert {
 		if certMsg, ok = msg.(*certificateMsg); !ok {
-			return c.sendAlert(alertHandshakeFailure)
+			c.sendAlert(alertUnexpectedMessage)
+			return unexpectedMessageError(certMsg, msg)
 		}
 		hs.finishedHash.Write(certMsg.marshal())
 
@@ -372,7 +380,8 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 	// Get client key exchange
 	ckx, ok := msg.(*clientKeyExchangeMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(ckx, msg)
 	}
 	hs.finishedHash.Write(ckx.marshal())
 
@@ -389,7 +399,8 @@ func (hs *serverHandshakeState) doFullHandshake() error {
 		}
 		certVerify, ok := msg.(*certificateVerifyMsg)
 		if !ok {
-			return c.sendAlert(alertUnexpectedMessage)
+			c.sendAlert(alertUnexpectedMessage)
+			return unexpectedMessageError(certVerify, msg)
 		}
 
 		switch key := pub.(type) {
@@ -469,7 +480,8 @@ func (hs *serverHandshakeState) readFinished() error {
 		}
 		nextProto, ok := msg.(*nextProtoMsg)
 		if !ok {
-			return c.sendAlert(alertUnexpectedMessage)
+			c.sendAlert(alertUnexpectedMessage)
+			return unexpectedMessageError(nextProto, msg)
 		}
 		hs.finishedHash.Write(nextProto.marshal())
 		c.clientProtocol = nextProto.proto
@@ -481,13 +493,15 @@ func (hs *serverHandshakeState) readFinished() error {
 	}
 	clientFinished, ok := msg.(*finishedMsg)
 	if !ok {
-		return c.sendAlert(alertUnexpectedMessage)
+		c.sendAlert(alertUnexpectedMessage)
+		return unexpectedMessageError(clientFinished, msg)
 	}
 
 	verify := hs.finishedHash.clientSum(hs.masterSecret)
 	if len(verify) != len(clientFinished.verifyData) ||
 		subtle.ConstantTimeCompare(verify, clientFinished.verifyData) != 1 {
-		return c.sendAlert(alertHandshakeFailure)
+		c.sendAlert(alertHandshakeFailure)
+		return errors.New("tls: client's Finished message is incorrect")
 	}
 
 	hs.finishedHash.Write(clientFinished.marshal())
@@ -590,7 +604,8 @@ func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (c
 		case *ecdsa.PublicKey, *rsa.PublicKey:
 			pub = key
 		default:
-			return nil, c.sendAlert(alertUnsupportedCertificate)
+			c.sendAlert(alertUnsupportedCertificate)
+			return nil, fmt.Errorf("tls: client's certificate contains an unsupported public key of type %T", certs[0].PublicKey)
 		}
 		c.peerCertificates = certs
 		return pub, nil

src/pkg/crypto/tls/key_agreement.go (抜粋)

--- a/src/pkg/crypto/tls/key_agreement.go
+++ b/src/pkg/crypto/tls/key_agreement.go
@@ -19,6 +19,9 @@ import (
 	"math/big"
 )
 
+var errClientKeyExchange = errors.New("tls: invalid ClientKeyExchange message")
+var errServerKeyExchange = errors.New("tls: invalid ServerKeyExchange message")
+
 // rsaKeyAgreement implements the standard TLS key agreement where the client
 // encrypts the pre-master secret to the server's public key.
 type rsaKeyAgreement struct{}
@@ -35,14 +38,14 @@ func (ka rsaKeyAgreement) processClientKeyExchange(config *Config, cert *Certifi
 	}
 
 	if len(ckx.ciphertext) < 2 {
-		return nil, errors.New("bad ClientKeyExchange")
+		return nil, errClientKeyExchange
 	}
 
 	ciphertext := ckx.ciphertext
 	if version != VersionSSL30 {
 		ciphertextLen := int(ckx.ciphertext[0])<<8 | int(ckx.ciphertext[1])
 		if ciphertextLen != len(ckx.ciphertext)-2 {
-			return nil, errors.New("bad ClientKeyExchange")
+			return nil, errClientKeyExchange
 		}
 		ciphertext = ckx.ciphertext[2:]
 	}
@@ -61,7 +64,7 @@ func (ka rsaKeyAgreement) processClientKeyExchange(config *Config, cert *Certifi
 }
 
 func (ka rsaKeyAgreement) processServerKeyExchange(config *Config, clientHello *clientHelloMsg, serverHello *serverHelloMsg, cert *x509.Certificate, skx *serverKeyExchangeMsg) error {
-	return errors.New("unexpected ServerKeyExchange")
+	return errors.New("tls: unexpected ServerKeyExchange")
 }
 
 func (ka rsaKeyAgreement) generateClientKeyExchange(config *Config, clientHello *clientHelloMsg, cert *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error) {
@@ -271,11 +274,11 @@ Curve:
 
 func (ka *ecdheKeyAgreement) processClientKeyExchange(config *Config, cert *Certificate, ckx *clientKeyExchangeMsg, version uint16) ([]byte, error) {
 	if len(ckx.ciphertext) == 0 || int(ckx.ciphertext[0]) != len(ckx.ciphertext)-1 {
-		return nil, errors.New("bad ClientKeyExchange")
+		return nil, errClientKeyExchange
 	}
 	x, y := elliptic.Unmarshal(ka.curve, ckx.ciphertext[1:])
 	if x == nil {
-		return nil, errors.New("bad ClientKeyExchange")
+		return nil, errClientKeyExchange
 	}
 	x, _ = ka.curve.ScalarMult(x, y, ka.privateKey)
 	preMasterSecret := make([]byte, (ka.curve.Params().BitSize+7)>>3)
@@ -285,8 +288,6 @@ func (ka *ecdheKeyAgreement) processClientKeyExchange(config *Config, cert *Cert
 	return preMasterSecret, nil
 }
 
-var errServerKeyExchange = errors.New("invalid ServerKeyExchange")
-
 func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHello *clientHelloMsg, serverHello *serverHelloMsg, cert *x509.Certificate, skx *serverKeyExchangeMsg) error {
 	if len(skx.key) < 4 {
 		return errServerKeyExchange

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
@@ -20,6 +20,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 	"testing"
 	"time"
 )
@@ -53,7 +54,7 @@ func init() {
 	testConfig.BuildNameToCertificate()
 }
 
-func testClientHelloFailure(t *testing.T, m handshakeMessage, expected error) {
+func testClientHelloFailure(t *testing.T, m handshakeMessage, expectedSubStr string) {
 	// Create in-memory network connection,
 	// send message to server.  Should return
 	// expected error.
@@ -68,20 +69,20 @@ func testClientHelloFailure(t *testing.T, m handshakeMessage, expected error) {
 	}()
 	err := Server(s, testConfig).Handshake()
 	s.Close()
-	if e, ok := err.(*net.OpError); !ok || e.Err != expected {
-		t.Errorf("Got error: %s; expected: %s", err, expected)
+	if err == nil || !strings.Contains(err.Error(), expectedSubStr) {
+		t.Errorf("Got error: %s; expected to match substring '%s'", err, expectedSubStr)
 	}
 }
 
 func TestSimpleError(t *testing.T) {
-	testClientHelloFailure(t, &serverHelloDoneMsg{}, alertUnexpectedMessage)
+	testClientHelloFailure(t, &serverHelloDoneMsg{}, "unexpected handshake message")
 }
 
 var badProtocolVersions = []uint16{0x0000, 0x0005, 0x0100, 0x0105, 0x0200, 0x0205}
 
 func TestRejectBadProtocolVersion(t *testing.T) {
 	for _, v := range badProtocolVersions {
-		testClientHelloFailure(t, &clientHelloMsg{vers: v}, alertProtocolVersion)
+		testClientHelloFailure(t, &clientHelloMsg{vers: v}, "unsupported, maximum protocol version")
 	}
 }
 
@@ -91,7 +92,7 @@ func TestNoSuiteOverlap(t *testing.T) {
 		cipherSuites:       []uint16{0xff00},
 		compressionMethods: []uint8{0},
 	}
-	testClientHelloFailure(t, clientHello, alertHandshakeFailure)
+	testClientHelloFailure(t, clientHello, "no cipher suite supported by both client and server")
 }
 
 func TestNoCompressionOverlap(t *testing.T) {
@@ -100,7 +101,7 @@ func TestNoCompressionOverlap(t *testing.T) {
 		cipherSuites:       []uint16{TLS_RSA_WITH_RC4_128_SHA},
 		compressionMethods: []uint8{0xff},
 	}
-	testClientHelloFailure(t, clientHello, alertHandshakeFailure)
+	testClientHelloFailure(t, clientHello, "client does not support uncompressed connections")
 }
 
 func TestTLS12OnlyCipherSuites(t *testing.T) {

コアとなるコードの解説

このコミットの核心は、Go言語の crypto/tls パッケージが生成するエラーメッセージの質を向上させることにあります。

  1. unexpectedMessageError 関数の役割:

    • common.go に追加された unexpectedMessageError 関数は、TLSハンドシェイクのステートマシンにおいて、現在の状態では予期されないメッセージを受信した場合に呼び出されます。
    • この関数は、fmt.Errorf を使用して、期待していたメッセージの型 (wanted) と実際に受信したメッセージの型 (got) をエラーメッセージに含めます。これにより、開発者は「なぜこのエラーが発生したのか」を即座に理解できます。例えば、「tls: received unexpected handshake message of type *tls.serverHelloDoneMsg when waiting for *tls.certificateMsg」のようなメッセージは、ハンドシェイクのどの段階で、どのようなメッセージの不一致があったのかを明確に示します。
  2. conn.go におけるエラーメッセージの具体化:

    • conn.go はTLSレコード層の処理を担当します。このファイルでは、readRecord 関数内で発生する様々なエラー(例: 不明なレコードタイプ、ハンドシェイク完了後のハンドシェイク要求、ハンドシェイク前のアプリケーションデータ要求、プロトコルバージョンの不一致、オーバーサイズレコードなど)に対して、より具体的なエラーメッセージが返されるようになりました。
    • 特に、プロトコルバージョンの不一致 (vers != c.vers) の際には、fmt.Errorf("tls: received record with version %x when expecting version %x", vers, c.vers) のように、受信したバージョンと期待されるバージョンが16進数で表示され、デバッグの際に非常に役立ちます。
    • VerifyHostname 関数も、より明確なエラーメッセージを返すように変更されています。
  3. handshake_client.go および handshake_server.go におけるエラーメッセージの具体化:

    • これらのファイルは、それぞれTLSクライアントとサーバーのハンドシェイクロジックを実装しています。
    • 多くの箇所で、unexpectedMessageError 関数が利用され、予期しないハンドシェイクメッセージに対するエラーが詳細化されました。
    • また、サポートされていないプロトコルバージョン、サポートされていない暗号スイート、証明書の解析失敗、公開鍵のタイプ不一致など、ハンドシェイク中に発生しうる様々なエラーに対して、fmt.Errorf を用いて具体的な情報(例: サーバーが選択したプロトコルバージョン、証明書の公開鍵タイプなど)を含むエラーメッセージが生成されるようになりました。これにより、ハンドシェイク失敗の原因を特定する手間が大幅に削減されます。
  4. key_agreement.go におけるエラーメッセージの統一:

    • key_agreement.go では、鍵交換メッセージの処理に関連するエラーメッセージが errClientKeyExchangeerrServerKeyExchange という変数にまとめられ、一貫性のある「tls: invalid ClientKeyExchange message」や「tls: unexpected ServerKeyExchange」といったメッセージが返されるようになりました。これにより、エラーメッセージの統一性が保たれます。
  5. テストコードの変更 (handshake_server_test.go):

    • エラーメッセージがより詳細になったため、テストコードもそれに合わせて変更されました。以前はエラーオブジェクトの厳密な比較を行っていましたが、これはエラーメッセージの具体的な内容が変わるとテストが失敗する原因となります。
    • 新しいテストでは、testClientHelloFailure 関数が expectedSubStr パラメータを受け取り、strings.Contains(err.Error(), expectedSubStr) を使用して、エラーメッセージが特定のサブ文字列を含むかどうかを検証します。これにより、エラーメッセージの具体的な内容が変更されても、その意図が正しく伝わっている限りテストが失敗しないようになり、テストの堅牢性が向上します。

これらの変更は、Go言語の crypto/tls パッケージの堅牢性と使いやすさを向上させる上で重要な役割を果たします。開発者は、より具体的で診断しやすいエラーメッセージを受け取ることで、TLS関連の問題をより効率的に解決できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • TLSプロトコルのRFC仕様
  • Go言語のソースコード (特に crypto/tls パッケージ)
  • Go言語のエラーハンドリングに関する一般的な知識