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

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

このコミットは、Go言語の crypto/tls パッケージにおいて、クライアントサイドでのTLSセッション再開(Session Resumption)のサポートを追加するものです。これにより、クライアントは以前に確立したTLSセッションの情報を再利用し、その後のハンドシェイクプロセスを高速化できるようになります。

コミット

Author: Gautham Thambidorai gautham.dorai@gmail.com Date: Wed Jan 22 18:24:03 2014 -0500

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

https://github.com/golang/go/commit/988ffc0fe2dac1072b8c0f22458b8c04295174ff

元コミット内容

crypto/tls: Client side support for TLS session resumption.

Adam (agl@) had already done an initial review of this CL in a branch.

Added ClientSessionState to Config which now allows clients to keep state
required to resume a TLS session with a server. A client handshake will try
and use the SessionTicket/MasterSecret in this cached state if the server
acknowledged resumption.

We also added support to cache ClientSessionState object in Config that will
be looked up by server remote address during the handshake.

R=golang-codereviews, agl, rsc, agl, agl, bradfitz, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/15680043

変更の背景

TLS(Transport Layer Security)は、インターネット上での安全な通信を確立するためのプロトコルです。TLSハンドシェイクは、クライアントとサーバー間で暗号化パラメータをネゴシエートし、セキュアな接続を確立するプロセスであり、計算コストが高い処理です。特に、多数の接続を頻繁に確立するアプリケーションでは、このハンドシェイクのオーバーヘッドがパフォーマンスのボトルネックとなることがあります。

TLSセッション再開は、このオーバーヘッドを削減するためのメカニズムです。一度確立されたセッションの情報をキャッシュし、同じクライアントとサーバーが再度接続する際に、完全なハンドシェイクを省略してセッションを迅速に再開できるようにします。これにより、接続確立にかかる時間とリソース消費が大幅に削減され、特にモバイル環境や高負荷なサービスにおいてユーザーエクスペリエンスとサーバー効率が向上します。

このコミット以前のGoの crypto/tls パッケージは、クライアントサイドでのセッション再開を直接サポートしていませんでした。そのため、クライアントがサーバーとの間でセッションを再開しようとしても、常に完全なハンドシェイクを実行する必要がありました。この変更は、GoのTLS実装のパフォーマンスと効率を向上させるために不可欠な機能追加でした。

前提知識の解説

TLS (Transport Layer Security)

TLSは、インターネットなどのコンピュータネットワーク上でデータを安全に交換するための暗号化プロトコルです。TLSは、通信のプライバシー(盗聴防止)、完全性(改ざん防止)、および認証(通信相手のなりすまし防止)を提供します。TLSは、HTTP(HTTPS)、FTP、SMTPなど、多くのアプリケーション層プロトコルで使用されています。

TLSハンドシェイク

TLSハンドシェイクは、クライアントとサーバーがセキュアな通信を開始する前に行う一連のメッセージ交換です。このプロセスでは、以下のことが行われます。

  1. Helloメッセージの交換: クライアントとサーバーが互いにサポートするTLSバージョン、暗号スイート、圧縮方式などを通知し、ランダムな値を交換します。
  2. サーバー認証: サーバーが自身のデジタル証明書をクライアントに提示し、クライアントはそれを検証してサーバーの身元を確認します。
  3. 鍵交換: クライアントとサーバーが、セッション中に使用する共通の秘密鍵(マスターシークレット)を安全に生成するための情報を交換します。このプロセスは、Diffie-Hellman鍵交換などのアルゴリズムを使用して行われます。
  4. 暗号化パラメータの確立: マスターシークレットから、実際のデータ暗号化に使用されるセッション鍵が導出されます。
  5. Finishedメッセージ: ハンドシェイクの完了と、これまでのメッセージが改ざんされていないことを確認するためのメッセージを交換します。

TLSセッション再開 (TLS Session Resumption)

TLSセッション再開は、TLSハンドシェイクのオーバーヘッドを削減するためのメカニズムです。主に以下の2つの方法があります。

  1. セッションID (Session ID): TLS 1.2以前で広く使われていた方法です。サーバーはセッションIDを生成し、クライアントに送信します。クライアントはセッションIDと関連するセッション情報をキャッシュします。次回接続時にクライアントがこのセッションIDを提示し、サーバーがそのIDに対応するセッション情報を保持していれば、完全なハンドシェイクを省略してセッションを再開できます。
  2. セッションチケット (Session Ticket / RFC 5077): TLS 1.2以降で導入された、よりセキュアでスケーラブルな方法です。サーバーはセッション情報を暗号化して「セッションチケット」としてクライアントに送信します。クライアントはこのチケットをキャッシュし、次回接続時にサーバーに提示します。サーバーはチケットを復号してセッション情報を再構築し、セッションを再開します。サーバー側でセッション状態を保持する必要がないため、サーバーのスケーラビリティが向上します。

このコミットは、主にセッションチケットベースのセッション再開に焦点を当てています。

マスターシークレット (Master Secret)

TLSハンドシェイク中にクライアントとサーバー間で合意される48バイトの秘密鍵です。このマスターシークレットから、実際のデータ暗号化に使用されるセッション鍵(対称鍵)が導出されます。セッション再開時には、このマスターシークレットを再利用することで、鍵交換プロセスを省略できます。

LRUキャッシュ (Least Recently Used Cache)

LRUキャッシュは、キャッシュアルゴリズムの一種で、最も長い間使用されていない(Least Recently Used)データをキャッシュから削除し、新しいデータを格納する方式です。これにより、キャッシュの容量が限られている場合でも、最も頻繁にアクセスされるデータがキャッシュに残りやすくなります。このコミットでは、クライアントがセッション情報を効率的に管理するためにLRUキャッシュが導入されています。

技術的詳細

このコミットは、Goの crypto/tls パッケージにクライアントサイドのTLSセッション再開機能を追加するために、以下の主要な変更を導入しています。

  1. ClientSessionState 構造体の導入:

    • sessionTicket: サーバーから受け取った暗号化されたセッションチケット。
    • vers: ネゴシエートされたTLSバージョン。
    • cipherSuite: ネゴシエートされた暗号スイート。
    • masterSecret: フルハンドシェイクで生成されたマスターシークレット。
    • serverCertificates: サーバーが提示した証明書チェーン。 この構造体は、セッション再開に必要なすべての情報をカプセル化します。
  2. ClientSessionCache インターフェースの定義:

    • Get(sessionKey string) (session *ClientSessionState, ok bool): 指定されたキーに関連付けられた ClientSessionState を取得します。
    • Put(sessionKey string, cs *ClientSessionState): 指定されたキーと ClientSessionState をキャッシュに追加します。 このインターフェースにより、様々なキャッシュ実装をプラグインできるようになります。
  3. Config 構造体への ClientSessionCache フィールドの追加:

    • ClientSessionCache ClientSessionCache: クライアントがセッション再開のために ClientSessionState エントリをキャッシュするために使用するキャッシュ実装を設定します。これにより、ユーザーは独自のキャッシュメカニズムを提供できます。
  4. LRUキャッシュ実装 lruSessionCache の提供:

    • NewLRUClientSessionCache(capacity int) ClientSessionCache: LRU(Least Recently Used)戦略を使用する ClientSessionCache のデフォルト実装を提供します。これは container/list パッケージを使用して実装されており、指定された容量を超えると最も古いエントリを削除します。
  5. clientHandshake 関数の大幅な変更:

    • セッション再開の試行: ハンドシェイク開始時に、Config.ClientSessionCache が設定されている場合、クライアントはリモートアドレスに基づいてキャッシュから既存のセッション情報を検索します。
    • ClientHello メッセージの調整: キャッシュされたセッション情報が見つかり、それが有効な場合、ClientHello メッセージに sessionTicket とランダムな sessionId を含めて、サーバーにセッション再開を要求します。
    • ハンドシェイクフローの分岐: サーバーがセッション再開を受け入れた場合(ServerHellosessionId がクライアントの sessionId と一致する場合)、ハンドシェイクは短縮されたパスをたどります。このパスでは、鍵交換が省略され、キャッシュされたマスターシークレットが使用されます。
    • NewSessionTicket メッセージの処理: サーバーが NewSessionTicket メッセージを送信した場合、クライアントはそのチケットと関連するセッション情報を抽出し、ClientSessionState オブジェクトを作成してキャッシュに保存します。
    • clientHandshakeState 構造体の導入: ハンドシェイクの状態を管理するためのヘルパーステート構造体 clientHandshakeState が導入され、コードの可読性と保守性が向上しています。
  6. テストの追加:

    • TestClientResumption: クライアントサイドのセッション再開機能が正しく動作するかを検証するテストが追加されました。これには、セッション再開の成功、無効なセッションチケットキーの場合のフォールバック、異なる暗号スイートの場合の動作などが含まれます。
    • TestLRUClientSessionCache: LRUキャッシュの実装が正しく動作するかを検証するテストが追加されました。

これらの変更により、GoのTLSクライアントは、セッション再開をサポートするサーバーとの間で、より効率的で高速な接続確立が可能になりました。

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

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

  • src/pkg/crypto/tls/common.go:

    • ClientSessionState 構造体と ClientSessionCache インターフェースが定義されました。
    • Config 構造体に ClientSessionCache フィールドが追加されました。
    • LRUキャッシュのデフォルト実装である lruSessionCache 構造体と、そのコンストラクタ NewLRUClientSessionCache、および Put, Get メソッドが追加されました。
    • container/list パッケージがインポートされました。
  • src/pkg/crypto/tls/conn.go:

    • readHandshake 関数で typeNewSessionTicket メッセージを処理するためのケースが追加されました。
  • src/pkg/crypto/tls/handshake_client.go:

    • clientHandshakeState 構造体が導入され、クライアントハンドシェイクのロジックがこの構造体のメソッドに分割されました。
    • clientHandshake 関数内で、セッションキャッシュからのセッション情報の取得、ClientHello メッセージへのセッションチケットとセッションIDの追加、およびセッション再開の有無に応じたハンドシェイクフローの分岐ロジックが実装されました。
    • processServerHello, readFinished, readSessionTicket, sendFinished, establishKeys, doFullHandshake, serverResumedSession, clientSessionCacheKey といった新しいヘルパー関数/メソッドが追加され、ハンドシェイクの各ステップがよりモジュール化されました。
  • src/pkg/crypto/tls/handshake_client_test.go:

    • TestClientResumption 関数が追加され、クライアントサイドのセッション再開機能のテストケースが記述されました。
    • TestLRUClientSessionCache 関数が追加され、LRUキャッシュの実装が正しく動作するかを検証するテストケースが記述されました。
  • src/pkg/crypto/tls/handshake_server_test.go:

    • testHandshake 関数に done チャネルが追加され、クライアントのハンドシェイク完了を待機するようになりました。これはテストの安定性向上のための変更です。
  • src/pkg/go/build/deps_test.go:

    • crypto/tls パッケージの依存関係に container/list が追加されました。

コアとなるコードの解説

src/pkg/crypto/tls/common.go

このファイルでは、TLSセッション再開の基盤となるデータ構造とインターフェースが定義されています。

// ClientSessionState contains the state needed by clients to resume TLS
// sessions.
type ClientSessionState struct {
	sessionTicket      []uint8             // Encrypted ticket used for session resumption with server
	vers               uint16              // SSL/TLS version negotiated for the session
	cipherSuite        uint16              // Ciphersuite negotiated for the session
	masterSecret       []byte              // MasterSecret generated by client on a full handshake
	serverCertificates []*x509.Certificate // Certificate chain presented by the server
}

// ClientSessionCache is a cache of ClientSessionState objects that can be used
// by a client to resume a TLS session with a given server. ClientSessionCache
// implementations should expect to be called concurrently from different
// goroutines.
type ClientSessionCache interface {
	// Get searches for a ClientSessionState associated with the given key.
	// On return, ok is true if one was found.
	Get(sessionKey string) (session *ClientSessionState, ok bool)

	// Put adds the ClientSessionState to the cache with the given key.
	Put(sessionKey string, cs *ClientSessionState)
}

// A Config structure is used to configure a TLS client or server. After one
// has been passed to a TLS function it must not be modified.
type Config struct {
    // ... 既存のフィールド ...
	// SessionCache is a cache of ClientSessionState entries for TLS session
	// resumption.
	ClientSessionCache ClientSessionCache
    // ... 既存のフィールド ...
}

// lruSessionCache is a ClientSessionCache implementation that uses an LRU
// caching strategy.
type lruSessionCache struct {
	sync.Mutex

	m        map[string]*list.Element
	q        *list.List
	capacity int
}

// NewLRUClientSessionCache returns a ClientSessionCache with the given
// capacity that uses an LRU strategy. If capacity is < 1, a default capacity
// is used instead.
func NewLRUClientSessionCache(capacity int) ClientSessionCache {
	const defaultSessionCacheCapacity = 64

	if capacity < 1 {
		capacity = defaultSessionCacheCapacity
	}
	return &lruSessionCache{
		m:        make(map[string]*list.Element),
		q:        list.New(),
		capacity: capacity,
	}
}

// Put adds the provided (sessionKey, cs) pair to the cache.
func (c *lruSessionCache) Put(sessionKey string, cs *ClientSessionState) {
	c.Lock()
	defer c.Unlock()

	if elem, ok := c.m[sessionKey]; ok {
		entry := elem.Value.(*lruSessionCacheEntry)
		entry.state = cs
		c.q.MoveToFront(elem) // LRU: Move to front on update
		return
	}

	if c.q.Len() < c.capacity {
		entry := &lruSessionCacheEntry{sessionKey, cs}
		c.m[sessionKey] = c.q.PushFront(entry) // Add to front
		return
	}

	// Cache is full, evict LRU item
	elem := c.q.Back()
	entry := elem.Value.(*lruSessionCacheEntry)
	delete(c.m, entry.sessionKey) // Remove from map
	entry.sessionKey = sessionKey // Reuse entry for new item
	entry.state = cs
	c.q.MoveToFront(elem) // Move to front
	c.m[sessionKey] = elem // Update map with new key
}

// Get returns the ClientSessionState value associated with a given key. It
// returns (nil, false) if no value is found.
func (c *lruSessionCache) Get(sessionKey string) (*ClientSessionState, bool) {
	c.Lock()
	defer c.Unlock()

	if elem, ok := c.m[sessionKey]; ok {
		c.q.MoveToFront(elem) // LRU: Move to front on access
		return elem.Value.(*lruSessionCacheEntry).state, true
	}
	return nil, false
}

ClientSessionState は、セッション再開に必要な情報を保持します。ClientSessionCache は、この ClientSessionState をキャッシュするためのインターフェースを定義し、Config 構造体にそのインスタンスを設定できるようにします。lruSessionCache は、ClientSessionCache インターフェースの具体的な実装であり、container/list を使用してLRU(Least Recently Used)アルゴリズムに基づいたキャッシュを提供します。Put メソッドは、新しいエントリを追加したり、既存のエントリを更新したりする際に、LRUの原則に従って要素をリストの先頭に移動させます。キャッシュが満杯の場合、最も古い(リストの末尾にある)エントリが削除されます。Get メソッドは、要素がアクセスされた際にリストの先頭に移動させ、その要素が最近使用されたことを示します。

src/pkg/crypto/tls/handshake_client.go

このファイルには、クライアントハンドシェイクのロジックが実装されており、セッション再開の主要な処理が追加されています。

type clientHandshakeState struct {
	c            *Conn
	serverHello  *serverHelloMsg
	hello        *clientHelloMsg
	suite        *cipherSuite
	finishedHash finishedHash
	masterSecret []byte
	session      *ClientSessionState // Cached session state for resumption
}

func (c *Conn) clientHandshake() error {
    // ... 既存の初期化処理 ...

	var session *ClientSessionState
	var cacheKey string
	sessionCache := c.config.ClientSessionCache
	if c.config.SessionTicketsDisabled {
		sessionCache = nil
	}

	if sessionCache != nil {
		hello.ticketSupported = true // Indicate support for session tickets

		// Try to resume a previously negotiated TLS session, if available.
		cacheKey = clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
		candidateSession, ok := sessionCache.Get(cacheKey)
		if ok {
			// Check that the ciphersuite/version used for the previous session are still valid.
			cipherSuiteOk := false
			for _, id := range hello.cipherSuites {
				if id == candidateSession.cipherSuite {
					cipherSuiteOk = true
					break
				}
			}

			versOk := candidateSession.vers >= c.config.minVersion() &&
				candidateSession.vers <= c.config.maxVersion()
			if versOk && cipherSuiteOk {
				session = candidateSession
			}
		}
	}

	if session != nil {
		hello.sessionTicket = session.sessionTicket
		// A random session ID is used to detect when the
		// server accepted the ticket and is resuming a session
		// (see RFC 5077).
		hello.sessionId = make([]byte, 16)
		if _, err := io.ReadFull(c.config.rand(), hello.sessionId); err != nil {
			c.sendAlert(alertInternalError)
			return errors.New("tls: short read from Rand: " + err.Error())
		}
	}

	c.writeRecord(recordTypeHandshake, hello.marshal())

    // ... サーバーからの応答の読み取り ...

	hs := &clientHandshakeState{
		c:            c,
		serverHello:  serverHello,
		hello:        hello,
		suite:        suite,
		finishedHash: newFinishedHash(c.vers),
		session:      session, // Pass the candidate session to the state
	}

	hs.finishedHash.Write(hs.hello.marshal())
	hs.finishedHash.Write(hs.serverHello.marshal())

	isResume, err := hs.processServerHello() // Determine if session is resumed
	if err != nil {
		return err
	}

	if isResume {
		// Shortened handshake path for session resumption
		if err := hs.establishKeys(); err != nil {
			return err
		}
		if err := hs.readSessionTicket(); err != nil { // Read NewSessionTicket if sent
			return err
		}
		if err := hs.readFinished(); err != nil {
			return err
		}
		if err := hs.sendFinished(); err != nil {
			return err
		}
	} else {
		// Full handshake path
		if err := hs.doFullHandshake(); err != nil {
			return err
		}
		if err := hs.establishKeys(); err != nil {
			return err
		}
		if err := hs.sendFinished(); err != nil {
			return err
		}
		if err := hs.readSessionTicket(); err != nil { // Read NewSessionTicket if sent
			return err
		}
		if err := hs.readFinished(); err != nil {
			return err
		}
	}

	if sessionCache != nil && hs.session != nil && session != hs.session {
		// Cache the new or updated session state
		sessionCache.Put(cacheKey, hs.session)
	}

	c.didResume = isResume
	c.handshakeComplete = true
	c.cipherSuite = suite.id
	return nil
}

func (hs *clientHandshakeState) processServerHello() (bool, error) {
	c := hs.c

	// ... 圧縮方式とNPNのチェック ...

	if hs.serverResumedSession() {
		// Restore masterSecret and peerCerts from previous state
		hs.masterSecret = hs.session.masterSecret
		c.peerCertificates = hs.session.serverCertificates
		return true, nil // Session resumed
	}
	return false, nil // Full handshake
}

func (hs *clientHandshakeState) readSessionTicket() error {
	if !hs.serverHello.ticketSupported {
		return nil
	}

	c := hs.c
	msg, err := c.readHandshake()
	if err != nil {
		return err
	}
	sessionTicketMsg, ok := msg.(*newSessionTicketMsg)
	if !ok {
		return c.sendAlert(alertUnexpectedMessage)
	}
	hs.finishedHash.Write(sessionTicketMsg.marshal())

	hs.session = &ClientSessionState{
		sessionTicket:      sessionTicketMsg.ticket,
		vers:               c.vers,
		cipherSuite:        hs.suite.id,
		masterSecret:       hs.masterSecret,
		serverCertificates: c.peerCertificates,
	}

	return nil
}

func (hs *clientHandshakeState) sendFinished() error {
	c := hs.c

	c.writeRecord(recordTypeChangeCipherSpec, []byte{1}) // Send ChangeCipherSpec

	if hs.serverHello.nextProtoNeg {
		// ... NPN (Next Protocol Negotiation) 処理 ...
	}

	finished := new(finishedMsg)
	finished.verifyData = hs.finishedHash.clientSum(hs.masterSecret)
	hs.finishedHash.Write(finished.marshal())
	c.writeRecord(recordTypeHandshake, finished.marshal()) // Send Finished
	return nil
}

func clientSessionCacheKey(serverAddr net.Addr, config *Config) string {
	if len(config.ServerName) > 0 {
		return config.ServerName
	}
	return serverAddr.String()
}

clientHandshake 関数は、クライアントハンドシェイクの開始点です。まず、Config.ClientSessionCache が設定されているかを確認し、設定されていればキャッシュから既存のセッション情報を検索します。有効なセッションが見つかった場合、ClientHello メッセージにそのセッションチケットとランダムなセッションIDを含めてサーバーに送信します。

clientHandshakeState 構造体は、ハンドシェイクの進行中に必要なすべての状態を保持するためのヘルパーです。processServerHello メソッドは、サーバーがセッション再開を受け入れたかどうかを判断します(サーバーの ServerHello メッセージのセッションIDがクライアントが送信したものと一致するかどうかで判断)。

セッションが再開された場合、ハンドシェイクは短縮されたパスをたどります。このパスでは、鍵交換が省略され、キャッシュされたマスターシークレットが使用されます。readSessionTicket メソッドは、サーバーから送信される NewSessionTicket メッセージを処理し、新しいセッションチケットと関連する情報を抽出して ClientSessionState オブジェクトを作成し、キャッシュに保存します。

sendFinished メソッドは、ChangeCipherSpec メッセージと Finished メッセージを送信し、クライアント側のハンドシェイクを完了させます。

clientSessionCacheKey 関数は、サーバーのアドレスと Config.ServerName に基づいて、セッションキャッシュのキーを生成します。これにより、同じサーバーへの接続に対してセッション情報が適切にキャッシュされるようになります。

これらの変更により、GoのTLSクライアントは、セッション再開をサポートするサーバーとの間で、より効率的で高速な接続確立が可能になりました。

関連リンク

参考にした情報源リンク

  • 上記のGitHubコミットページ
  • Go言語の crypto/tls パッケージのソースコード
  • TLSセッション再開に関するRFC 5077
  • LRUキャッシュに関する一般的な情報