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

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

このコミットは、Go言語の標準ライブラリである crypto/tls パッケージに対する変更です。crypto/tls パッケージは、Transport Layer Security (TLS) プロトコル(旧称 Secure Sockets Layer (SSL))の実装を提供し、ネットワーク通信の暗号化と認証を可能にします。このパッケージは、Goアプリケーションが安全なクライアントおよびサーバーを構築するために不可欠な機能を提供します。

今回の変更は、TLSセッションチケット再開のサポートを追加するもので、これによりTLSハンドシェイクの効率が大幅に向上します。

コミット

commit 65c7dc4ace97520bc02b8986cb2a825ff70ab7e1
Author: Adam Langley <agl@golang.org>
Date:   Mon Sep 24 16:52:43 2012 -0400

    crypto/tls: support session ticket resumption.
    
    Session resumption saves a round trip and removes the need to perform
    the public-key operations of a TLS handshake when both the client and
    server support it (which is true of Firefox and Chrome, at least).
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6555051

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

https://github.com/golang/go/commit/65c7dc4ace97520bc02b8986cb2a825ff70ab7e1

元コミット内容

crypto/tls: support session ticket resumption.

Session resumption saves a round trip and removes the need to perform
the public-key operations of a TLS handshake when both the client and
server support it (which is true of Firefox and Chrome, at least).

R=golang-dev, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6555051

変更の背景

TLSハンドシェイクは、安全な通信チャネルを確立するために必要なプロセスですが、特に公開鍵暗号操作を含むフルハンドシェイクは計算コストが高く、ネットワークのラウンドトリップ時間(RTT)を増加させます。これは、ウェブサイトの読み込み速度やAPIの応答性など、アプリケーションのパフォーマンスに直接影響します。

セッション再開(Session Resumption)は、以前に確立されたTLSセッションの情報を再利用することで、このオーバーヘッドを削減するためのメカニズムです。これにより、クライアントとサーバーは、新しいセッションごとに完全なハンドシェイクを実行する代わりに、より短縮されたハンドシェイクでセッションを再開できます。

このコミットは、特に「セッションチケット再開 (Session Ticket Resumption)」のサポートを crypto/tls パッケージに追加することを目的としています。セッションチケットは、サーバーがセッション状態を内部的に保存する必要がある従来のセッションID再開とは異なり、暗号化されたセッション状態をクライアントに発行し、クライアントがそれを次回の接続時に提示することでセッションを再開する仕組みです。これにより、サーバー側の状態管理の負担が軽減され、スケーラビリティが向上します。

FirefoxやChromeなどの主要なブラウザがセッションチケット再開をサポートしていることから、この機能の追加はGo言語で開発されたサーバーアプリケーションのパフォーマンスと互換性を向上させる上で重要でした。

前提知識の解説

TLSハンドシェイクの基本

TLSハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に、互いを認証し、暗号化パラメータをネゴシエートするプロセスです。一般的なフルハンドシェイクの主要なステップは以下の通りです。

  1. ClientHello: クライアントがサポートするTLSバージョン、暗号スイート、圧縮方式、セッションID、ランダムなバイト列などをサーバーに送信します。
  2. ServerHello: サーバーがClientHelloから最適なTLSバージョン、暗号スイート、圧縮方式、新しいセッションID、サーバーのランダムなバイト列を選択してクライアントに返します。
  3. Certificate: サーバーが自身の公開鍵証明書をクライアントに送信します。クライアントはこれを使用してサーバーの身元を検証します。
  4. ServerKeyExchange (オプション): サーバーが鍵交換に必要な追加情報(例: Diffie-Hellmanパラメータ)を送信します。
  5. CertificateRequest (オプション): サーバーがクライアント認証を要求する場合、クライアント証明書を要求します。
  6. ServerHelloDone: サーバーがハンドシェイクメッセージの送信を完了したことを示します。
  7. ClientKeyExchange: クライアントが鍵交換情報をサーバーに送信します。これには、プリマスターシークレット(PreMasterSecret)が含まれ、これはセッションのマスターシークレットを導出するために使用されます。
  8. CertificateVerify (オプション): クライアントが証明書を送信した場合、自身の証明書が正当であることを証明するために、ハンドシェイクメッセージのハッシュに署名したものを送信します。
  9. ChangeCipherSpec: クライアントが、これ以降の通信がネゴシエートされた暗号スイートと鍵で暗号化されることをサーバーに通知します。
  10. Finished: クライアントが、これまでのハンドシェイクメッセージのハッシュから導出された検証データを送信します。これは、ハンドシェイクが改ざんされていないことを確認するためのものです。
  11. ChangeCipherSpec: サーバーが、これ以降の通信が暗号化されることをクライアントに通知します。
  12. Finished: サーバーが、これまでのハンドシェイクメッセージのハッシュから導出された検証データを送信します。

TLSセッション再開のメカニズム

TLSセッション再開には主に2つのメカニズムがあります。

  1. セッションID再開 (Session ID Resumption):

    • 最初のハンドシェイク中に、サーバーはセッションIDを生成し、ClientHelloとServerHelloで交換します。
    • サーバーは、このセッションIDに関連付けられたセッション状態(マスターシークレット、暗号スイートなど)を内部的にキャッシュします。
    • クライアントが同じサーバーに再接続する際、ClientHelloで以前のセッションIDを提示します。
    • サーバーがそのセッションIDに対応するキャッシュされた状態を見つけると、フルハンドシェイクではなく、より短いハンドシェイク(ClientHello, ServerHello, ChangeCipherSpec, Finished)でセッションを再開できます。
    • 課題: サーバーは多数のセッション状態を管理する必要があり、メモリ消費や分散環境での同期が課題となることがあります。
  2. セッションチケット再開 (Session Ticket Resumption - RFC 5077):

    • このメカニズムは、サーバー側の状態管理の負担を軽減するために導入されました。
    • 最初のハンドシェイクの終わりに、サーバーはセッション状態を暗号化し、それを「セッションチケット」としてクライアントに発行します。このチケットは NewSessionTicket メッセージとして送信されます。
    • クライアントは、このセッションチケットを安全に保存します。
    • クライアントが同じサーバーに再接続する際、ClientHelloの拡張フィールド (session_ticket 拡張) にこのセッションチケットを含めて送信します。
    • サーバーは、受け取ったセッションチケットを復号し、その中に含まれるセッション状態を検証します。検証が成功すれば、フルハンドシェイクなしでセッションを再開できます。
    • 利点: サーバーはセッション状態をキャッシュする必要がなく、ステートレスにセッション再開を処理できます。これにより、サーバーのスケーラビリティが向上します。

PRF (Pseudo-Random Function)

PRF(擬似乱数関数)は、TLSにおいて、マスターシークレットや鍵ブロック(MAC鍵、暗号鍵、IVなど)を導出するために使用される重要な関数です。PRFは、シード値(通常はクライアントとサーバーのランダム値の組み合わせ)と秘密(プリマスターシークレットまたはマスターシークレット)から、必要な長さの擬似乱数データを生成します。これにより、鍵導出プロセスに予測不可能性とセキュリティがもたらされます。

AES-CTRモードとHMAC-SHA256

セッションチケットの暗号化と認証には、以下の暗号プリミティブが使用されます。

  • AES (Advanced Encryption Standard): 共通鍵暗号方式の一つで、セッション状態の暗号化に使用されます。
  • CTR (Counter) モード: ブロック暗号の動作モードの一つで、ストリーム暗号のように機能します。各ブロックを暗号化する際にカウンター値をインクリメントし、それを鍵と組み合わせてXOR演算を行います。これにより、並列処理が可能になり、データのランダムアクセスが容易になります。
  • HMAC (Keyed-Hash Message Authentication Code): メッセージ認証コードの一種で、データの完全性と認証を提供します。秘密鍵とハッシュ関数(ここではSHA256)を組み合わせてメッセージのMACを生成し、受信側でMACを再計算して比較することで、データが改ざんされていないこと、および送信者が秘密鍵を知っていることを確認します。
  • SHA256 (Secure Hash Algorithm 256): 暗号学的ハッシュ関数の一つで、HMACの基盤として使用されます。

セッションチケットは、AES-CTRで暗号化され、HMAC-SHA256で認証されることで、機密性、完全性、認証が保証されます。

技術的詳細

このコミットは、Goの crypto/tls パッケージにTLSセッションチケット再開の機能を追加するために、複数のファイルにわたる広範な変更を導入しています。

セッションチケットの構造 (sessionState)

新しく追加された src/pkg/crypto/tls/ticket.go ファイルには、sessionState という構造体が定義されています。これは、セッションチケットにシリアライズされて保存されるセッション情報をカプセル化します。

type sessionState struct {
	vers         uint16   // TLSバージョン
	cipherSuite  uint16   // 使用された暗号スイート
	masterSecret []byte   // マスターシークレット
	certificates [][]byte // クライアント証明書チェーン (オプション)
}
  • vers: TLSプロトコルのバージョン(例: TLS 1.2は 0x0303)。
  • cipherSuite: セッションで使用された暗号スイートのID。
  • masterSecret: TLSハンドシェイク中に導出されたマスターシークレット。これがセッション再開の鍵となります。
  • certificates: クライアント認証が行われた場合、クライアントの証明書チェーンがここに保存されます。これにより、再開されたセッションでもクライアントの身元を再確認できます。

この sessionState 構造体には、バイト列へのシリアライズ (marshal) とバイト列からのデシリアライズ (unmarshal) メソッドが実装されており、セッションチケットとしての保存と読み込みを可能にしています。

セッションチケットの暗号化と認証

セッションチケットのセキュリティは、その暗号化と認証に依存します。ticket.go には encryptTicketdecryptTicket 関数が実装されています。

  • Config.SessionTicketKey [32]byte: Config 構造体(common.go に追加)に SessionTicketKey フィールドが追加されました。これは32バイトの鍵で、セッションチケットの暗号化と認証に使用されます。

    • 最初の16バイトはAES-CTRモードの暗号化鍵として使用されます。
    • 次の16バイトはHMAC-SHA256の認証鍵として使用されます。
    • この鍵は、サーバーが初めてハンドシェイクを行う際にランダムに生成されます。複数のサーバーが同じホストの接続を終端する場合、同じ SessionTicketKey を共有する必要があります。鍵が漏洩すると、過去および将来のTLS接続が危殆化する可能性があるため、厳重に管理する必要があります。
  • encryptTicket(state *sessionState) ([]byte, error):

    1. sessionState をバイト列にシリアライズします。
    2. AES-CTRモードでシリアライズされたデータを暗号化します。初期化ベクトル (IV) はランダムに生成され、暗号化されたデータにプレフィックスとして付加されます。
    3. 暗号化されたデータ全体(IV + 暗号文)に対してHMAC-SHA256を計算し、そのMACをデータの末尾に付加します。
    4. 結果として得られるバイト列がセッションチケットとしてクライアントに送信されます。
  • decryptTicket(encrypted []byte) (*sessionState, bool):

    1. 受け取ったセッションチケットからIV、暗号文、MACを分離します。
    2. HMAC-SHA256を再計算し、チケットに含まれるMACと比較して認証します。MACが一致しない場合、チケットは改ざんされているか、不正な鍵で生成されたものであり、復号は失敗します。
    3. 認証が成功した場合、AES-CTRモードで暗号文を復号し、元の sessionState のバイト列を取得します。
    4. バイト列を sessionState 構造体にデシリアライズします。

ハンドシェイクフローの変更

セッションチケット再開をサポートするために、TLSハンドシェイクのメッセージとサーバー側のロジックが大幅に変更されました。

  • clientHelloMsg の変更 (handshake_messages.go):

    • ticketSupported bool: クライアントがセッションチケット拡張をサポートしているかを示すフラグ。
    • sessionTicket []uint8: クライアントがサーバーに提示するセッションチケットのバイト列。
  • serverHelloMsg の変更 (handshake_messages.go):

    • ticketSupported bool: サーバーがセッションチケット拡張をサポートしているかを示すフラグ。
  • newSessionTicketMsg の導入 (handshake_messages.go):

    • 新しいハンドシェイクメッセージタイプ typeNewSessionTicket (値 4) が追加されました。
    • このメッセージは、サーバーがクライアントに新しいセッションチケットを発行するために使用されます。
    • ticket []byte: サーバーが暗号化して生成したセッションチケットのバイト列。
  • サーバー側のハンドシェイク状態管理 (serverHandshakeState in handshake_server.go):

    • サーバーハンドシェイクの進行中の状態を保持するための新しい構造体 serverHandshakeState が導入されました。これにより、ハンドシェイクロジックが整理され、セッション再開の分岐が容易になります。
  • ハンドシェイク処理の分岐 (serverHandshake in handshake_server.go):

    • serverHandshake 関数は、まずクライアントから ClientHello を読み込みます。
    • readClientHello 関数内で、クライアントがセッションチケットを提示しているか、およびそのチケットが有効であるかを checkForResumption 関数で確認します。
    • checkForResumptiontrue を返した場合(セッション再開が可能と判断された場合)、doResumeHandshake が呼び出され、短縮されたハンドシェイクが実行されます。
    • false を返した場合(セッション再開ができない、またはクライアントがチケットを提示していない場合)、doFullHandshake が呼び出され、通常のフルハンドシェイクが実行されます。
    • ハンドシェイクの最後に、フルハンドシェイクの場合は sendSessionTicket が呼び出され、新しいセッションチケットがクライアントに発行されます。

鍵導出の分離 (prf.go)

  • 以前は keysFromPreMasterSecret という単一の関数がプリマスターシークレットからマスターシークレットと鍵ブロック(MAC鍵、暗号鍵、IV)の両方を導出していました。
  • このコミットでは、この関数が masterFromPreMasterSecretkeysFromMasterSecret の2つに分割されました。
    • masterFromPreMasterSecret: プリマスターシークレットとランダム値からマスターシークレットのみを導出します。
    • keysFromMasterSecret: マスターシークレットとランダム値から鍵ブロック(MAC鍵、暗号鍵、IV)を導出します。
  • この分離は、セッション再開の文脈で特に重要です。セッション再開時には、マスターシークレットはセッションチケットから直接取得されるため、プリマスターシークレットからの導出は不要になります。しかし、鍵ブロックはマスターシークレットから常に導出する必要があります。この分割により、コードの再利用性とロジックの明確性が向上します。

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

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

  1. src/pkg/crypto/tls/ticket.go (新規追加):

    • sessionState 構造体の定義と、その marshal() および unmarshal() メソッド。
    • encryptTicket() および decryptTicket() 関数。これらはセッションチケットの暗号化、認証、復号のロジックを実装しています。
  2. src/pkg/crypto/tls/handshake_server.go:

    • serverHandshakeState 構造体の導入。
    • serverHandshake() 関数の大幅なリファクタリングと、セッション再開ロジックの追加。
    • readClientHello(): クライアントHelloメッセージの読み込みと、セッション再開の可能性の初期評価。
    • checkForResumption(): セッションチケットの復号と検証を行い、セッション再開が可能かどうかを判断。
    • doResumeHandshake(): セッション再開時の短縮ハンドシェイク処理。
    • doFullHandshake(): フルハンドシェイク処理。
    • establishKeys(): マスターシークレットから鍵ブロックを導出する処理。
    • sendSessionTicket(): 新しいセッションチケットをクライアントに発行する処理。
    • processCertsFromClient(): クライアント証明書の処理と検証。
    • tryCipherSuite(): サーバーがサポートする暗号スイートの選択ロジック。
  3. src/pkg/crypto/tls/common.go:

    • typeNewSessionTicket (ハンドシェイクメッセージタイプ) および extensionSessionTicket (TLS拡張タイプ) 定数の追加。
    • ConnectionState 構造体に DidResume bool フィールドを追加し、セッションが再開されたかどうかを示す。
    • Config 構造体に SessionTicketsDisabled bool (セッションチケットの無効化オプション) および SessionTicketKey [32]byte (セッションチケットの暗号化鍵) フィールドを追加。
  4. src/pkg/crypto/tls/handshake_messages.go:

    • clientHelloMsg および serverHelloMsg 構造体にセッションチケット関連のフィールド (ticketSupported, sessionTicket) を追加。
    • newSessionTicketMsg 構造体の新規追加と、その marshal() および unmarshal() メソッド。
  5. src/pkg/crypto/tls/prf.go:

    • keysFromPreMasterSecret 関数を masterFromPreMasterSecretkeysFromMasterSecret に分割。

コアとなるコードの解説

src/pkg/crypto/tls/ticket.go

このファイルは、セッションチケット再開機能の心臓部です。

  • sessionState 構造体:

    type sessionState struct {
    	vers         uint16
    	cipherSuite  uint16
    	masterSecret []byte
    	certificates [][]byte
    }
    

    この構造体は、セッション再開に必要なすべての情報を保持します。marshal() メソッドは、この構造体の内容をバイト列に変換し、unmarshal() メソッドはその逆を行います。これらのメソッドは、セッションチケットの内部表現を定義します。

  • encryptTicket 関数:

    func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) {
    	serialized := state.marshal()
    	encrypted := make([]byte, aes.BlockSize+len(serialized)+sha256.Size)
    	iv := encrypted[:aes.BlockSize]
    	macBytes := encrypted[len(encrypted)-sha256.Size:]
    
    	// IVをランダムに生成
    	if _, err := io.ReadFull(c.config.rand(), iv); err != nil {
    		return nil, err
    	}
    	// AES-CTRで暗号化
    	block, err := aes.NewCipher(c.config.SessionTicketKey[:16]) // 鍵の最初の16バイトを使用
    	if err != nil {
    		return nil, errors.New("tls: failed to create cipher while encrypting ticket: " + err.Error())
    	}
    	cipher.NewCTR(block, iv).XORKeyStream(encrypted[aes.BlockSize:], serialized)
    
    	// HMAC-SHA256で認証
    	mac := hmac.New(sha256.New, c.config.SessionTicketKey[16:32]) // 鍵の次の16バイトを使用
    	mac.Write(encrypted[:len(encrypted)-sha256.Size])
    	mac.Sum(macBytes[:0])
    
    	return encrypted, nil
    }
    

    この関数は、sessionState を受け取り、それをAES-CTRで暗号化し、HMAC-SHA256で認証タグを付加して、セッションチケットとしてクライアントに送信可能なバイト列を生成します。鍵は Config.SessionTicketKey から取得されます。

  • decryptTicket 関数:

    func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) {
    	// ... (長さチェック、IV/MAC/暗号文の分離) ...
    
    	// HMAC-SHA256で認証
    	mac := hmac.New(sha256.New, c.config.SessionTicketKey[16:32])
    	mac.Write(encrypted[:len(encrypted)-sha256.Size])
    	expected := mac.Sum(nil)
    
    	if subtle.ConstantTimeCompare(macBytes, expected) != 1 {
    		return nil, false // MACが一致しない場合は認証失敗
    	}
    
    	// AES-CTRで復号
    	block, err := aes.NewCipher(c.config.SessionTicketKey[:16])
    	if err != nil {
    		return nil, false
    	}
    	ciphertext := encrypted[aes.BlockSize : len(encrypted)-sha256.Size]
    	plaintext := ciphertext // CTRモードでは暗号化と復号が同じXOR操作
    	cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext)
    
    	state := new(sessionState)
    	ok := state.unmarshal(plaintext) // 復号されたバイト列をsessionStateにデシリアライズ
    	return state, ok
    }
    

    この関数は、クライアントから受け取ったセッションチケットを復号し、認証します。まずMACを検証し、次にAES-CTRで暗号文を復号して sessionState を再構築します。認証に失敗した場合や復号/デシリアライズに失敗した場合は、セッション再開は行われません。

src/pkg/crypto/tls/handshake_server.go

このファイルは、サーバー側のハンドシェイクロジックを管理し、セッション再開のフローを制御します。

  • serverHandshake 関数: この関数は、サーバーハンドシェイクのエントリポイントです。

    func (c *Conn) serverHandshake() error {
    	// ... (SessionTicketKeyの初期化) ...
    
    	hs := serverHandshakeState{c: c}
    	isResume, err := hs.readClientHello() // ClientHelloを読み込み、再開の可能性を判断
    	if err != nil {
    		return err
    	}
    
    	if isResume {
    		// セッション再開の場合
    		if err := hs.doResumeHandshake(); err != nil {
    			return err
    		}
    		if err := hs.establishKeys(); err != nil {
    			return err
    		}
    		if err := hs.sendFinished(); err != nil { // サーバーFinishedを送信
    			return err
    		}
    		if err := hs.readFinished(); err != nil { // クライアントFinishedを読み込み
    			return err
    		}
    		c.didResume = true // セッションが再開されたことを記録
    	} else {
    		// フルハンドシェイクの場合
    		if err := hs.doFullHandshake(); err != nil {
    			return err
    		}
    		if err := hs.establishKeys(); err != nil {
    			return err
    		}
    		if err := hs.readFinished(); err != nil {
    			return err
    		}
    		if err := hs.sendSessionTicket(); err != nil { // 新しいセッションチケットを送信
    			return err
    		}
    		if err := hs.sendFinished(); err != nil {
    			return err
    		}
    	}
    	c.handshakeComplete = true
    	return nil
    }
    

    このコードは、readClientHello の結果に基づいて、セッション再開 (doResumeHandshake) とフルハンドシェイク (doFullHandshake) のどちらのパスに進むかを明確に分岐させています。セッション再開時には、公開鍵操作をスキップし、ChangeCipherSpecFinished メッセージの交換のみでハンドシェイクを完了します。フルハンドシェイク時には、通常のハンドシェイクステップに加えて、新しいセッションチケットをクライアントに発行します。

  • checkForResumption 関数:

    func (hs *serverHandshakeState) checkForResumption() bool {
    	c := hs.c
    	var ok bool
    	// クライアントが提示したセッションチケットを復号
    	if hs.sessionState, ok = c.decryptTicket(hs.clientHello.sessionTicket); !ok {
    		return false // 復号失敗または認証失敗
    	}
    
    	// 復号されたセッション状態のバージョン、暗号スイート、クライアント証明書の要件などを検証
    	// ... (詳細な検証ロジック) ...
    
    	return true // 検証成功
    }
    

    この関数は、クライアントが ClientHello で送信したセッションチケットを decryptTicket を使って復号し、その内容が有効であるか(バージョン、暗号スイート、クライアント証明書の要件などが一致するか)を厳密に検証します。検証に失敗した場合、セッション再開は行われず、フルハンドシェイクにフォールバックします。

  • sendSessionTicket 関数:

    func (hs *serverHandshakeState) sendSessionTicket() error {
    	if !hs.hello.ticketSupported {
    		return nil // サーバーがチケットをサポートしていない場合、何もしない
    	}
    
    	c := hs.c
    	m := new(newSessionTicketMsg)
    
    	state := sessionState{
    		vers:         c.vers,
    		cipherSuite:  hs.suite.id,
    		masterSecret: hs.masterSecret,
    		certificates: hs.certsFromClient, // クライアント証明書があれば含める
    	}
    	m.ticket, err = c.encryptTicket(&state) // 新しいセッションチケットを暗号化
    	if err != nil {
    		return err
    	}
    
    	hs.finishedHash.Write(m.marshal())
    	c.writeRecord(recordTypeHandshake, m.marshal()) // NewSessionTicketメッセージを送信
    
    	return nil
    }
    

    フルハンドシェイクが成功した後、この関数は現在のセッション状態から新しい sessionState を構築し、encryptTicket を使用してそれを暗号化します。そして、NewSessionTicket メッセージとしてクライアントに送信します。これにより、クライアントは将来の接続でこのチケットを使用してセッションを再開できるようになります。

関連リンク

参考にした情報源リンク