[インデックス 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ハンドシェイクは、クライアントとサーバーが安全な通信を開始する前に、互いを認証し、暗号化パラメータをネゴシエートするプロセスです。一般的なフルハンドシェイクの主要なステップは以下の通りです。
- ClientHello: クライアントがサポートするTLSバージョン、暗号スイート、圧縮方式、セッションID、ランダムなバイト列などをサーバーに送信します。
- ServerHello: サーバーがClientHelloから最適なTLSバージョン、暗号スイート、圧縮方式、新しいセッションID、サーバーのランダムなバイト列を選択してクライアントに返します。
- Certificate: サーバーが自身の公開鍵証明書をクライアントに送信します。クライアントはこれを使用してサーバーの身元を検証します。
- ServerKeyExchange (オプション): サーバーが鍵交換に必要な追加情報(例: Diffie-Hellmanパラメータ)を送信します。
- CertificateRequest (オプション): サーバーがクライアント認証を要求する場合、クライアント証明書を要求します。
- ServerHelloDone: サーバーがハンドシェイクメッセージの送信を完了したことを示します。
- ClientKeyExchange: クライアントが鍵交換情報をサーバーに送信します。これには、プリマスターシークレット(PreMasterSecret)が含まれ、これはセッションのマスターシークレットを導出するために使用されます。
- CertificateVerify (オプション): クライアントが証明書を送信した場合、自身の証明書が正当であることを証明するために、ハンドシェイクメッセージのハッシュに署名したものを送信します。
- ChangeCipherSpec: クライアントが、これ以降の通信がネゴシエートされた暗号スイートと鍵で暗号化されることをサーバーに通知します。
- Finished: クライアントが、これまでのハンドシェイクメッセージのハッシュから導出された検証データを送信します。これは、ハンドシェイクが改ざんされていないことを確認するためのものです。
- ChangeCipherSpec: サーバーが、これ以降の通信が暗号化されることをクライアントに通知します。
- Finished: サーバーが、これまでのハンドシェイクメッセージのハッシュから導出された検証データを送信します。
TLSセッション再開のメカニズム
TLSセッション再開には主に2つのメカニズムがあります。
-
セッションID再開 (Session ID Resumption):
- 最初のハンドシェイク中に、サーバーはセッションIDを生成し、ClientHelloとServerHelloで交換します。
- サーバーは、このセッションIDに関連付けられたセッション状態(マスターシークレット、暗号スイートなど)を内部的にキャッシュします。
- クライアントが同じサーバーに再接続する際、ClientHelloで以前のセッションIDを提示します。
- サーバーがそのセッションIDに対応するキャッシュされた状態を見つけると、フルハンドシェイクではなく、より短いハンドシェイク(ClientHello, ServerHello, ChangeCipherSpec, Finished)でセッションを再開できます。
- 課題: サーバーは多数のセッション状態を管理する必要があり、メモリ消費や分散環境での同期が課題となることがあります。
-
セッションチケット再開 (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
には encryptTicket
と decryptTicket
関数が実装されています。
-
Config.SessionTicketKey [32]byte
:Config
構造体(common.go
に追加)にSessionTicketKey
フィールドが追加されました。これは32バイトの鍵で、セッションチケットの暗号化と認証に使用されます。- 最初の16バイトはAES-CTRモードの暗号化鍵として使用されます。
- 次の16バイトはHMAC-SHA256の認証鍵として使用されます。
- この鍵は、サーバーが初めてハンドシェイクを行う際にランダムに生成されます。複数のサーバーが同じホストの接続を終端する場合、同じ
SessionTicketKey
を共有する必要があります。鍵が漏洩すると、過去および将来のTLS接続が危殆化する可能性があるため、厳重に管理する必要があります。
-
encryptTicket(state *sessionState) ([]byte, error)
:sessionState
をバイト列にシリアライズします。- AES-CTRモードでシリアライズされたデータを暗号化します。初期化ベクトル (IV) はランダムに生成され、暗号化されたデータにプレフィックスとして付加されます。
- 暗号化されたデータ全体(IV + 暗号文)に対してHMAC-SHA256を計算し、そのMACをデータの末尾に付加します。
- 結果として得られるバイト列がセッションチケットとしてクライアントに送信されます。
-
decryptTicket(encrypted []byte) (*sessionState, bool)
:- 受け取ったセッションチケットからIV、暗号文、MACを分離します。
- HMAC-SHA256を再計算し、チケットに含まれるMACと比較して認証します。MACが一致しない場合、チケットは改ざんされているか、不正な鍵で生成されたものであり、復号は失敗します。
- 認証が成功した場合、AES-CTRモードで暗号文を復号し、元の
sessionState
のバイト列を取得します。 - バイト列を
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
inhandshake_server.go
):- サーバーハンドシェイクの進行中の状態を保持するための新しい構造体
serverHandshakeState
が導入されました。これにより、ハンドシェイクロジックが整理され、セッション再開の分岐が容易になります。
- サーバーハンドシェイクの進行中の状態を保持するための新しい構造体
-
ハンドシェイク処理の分岐 (
serverHandshake
inhandshake_server.go
):serverHandshake
関数は、まずクライアントからClientHello
を読み込みます。readClientHello
関数内で、クライアントがセッションチケットを提示しているか、およびそのチケットが有効であるかをcheckForResumption
関数で確認します。checkForResumption
がtrue
を返した場合(セッション再開が可能と判断された場合)、doResumeHandshake
が呼び出され、短縮されたハンドシェイクが実行されます。false
を返した場合(セッション再開ができない、またはクライアントがチケットを提示していない場合)、doFullHandshake
が呼び出され、通常のフルハンドシェイクが実行されます。- ハンドシェイクの最後に、フルハンドシェイクの場合は
sendSessionTicket
が呼び出され、新しいセッションチケットがクライアントに発行されます。
鍵導出の分離 (prf.go
)
- 以前は
keysFromPreMasterSecret
という単一の関数がプリマスターシークレットからマスターシークレットと鍵ブロック(MAC鍵、暗号鍵、IV)の両方を導出していました。 - このコミットでは、この関数が
masterFromPreMasterSecret
とkeysFromMasterSecret
の2つに分割されました。masterFromPreMasterSecret
: プリマスターシークレットとランダム値からマスターシークレットのみを導出します。keysFromMasterSecret
: マスターシークレットとランダム値から鍵ブロック(MAC鍵、暗号鍵、IV)を導出します。
- この分離は、セッション再開の文脈で特に重要です。セッション再開時には、マスターシークレットはセッションチケットから直接取得されるため、プリマスターシークレットからの導出は不要になります。しかし、鍵ブロックはマスターシークレットから常に導出する必要があります。この分割により、コードの再利用性とロジックの明確性が向上します。
コアとなるコードの変更箇所
このコミットにおける主要な変更箇所は以下のファイルに集中しています。
-
src/pkg/crypto/tls/ticket.go
(新規追加):sessionState
構造体の定義と、そのmarshal()
およびunmarshal()
メソッド。encryptTicket()
およびdecryptTicket()
関数。これらはセッションチケットの暗号化、認証、復号のロジックを実装しています。
-
src/pkg/crypto/tls/handshake_server.go
:serverHandshakeState
構造体の導入。serverHandshake()
関数の大幅なリファクタリングと、セッション再開ロジックの追加。readClientHello()
: クライアントHelloメッセージの読み込みと、セッション再開の可能性の初期評価。checkForResumption()
: セッションチケットの復号と検証を行い、セッション再開が可能かどうかを判断。doResumeHandshake()
: セッション再開時の短縮ハンドシェイク処理。doFullHandshake()
: フルハンドシェイク処理。establishKeys()
: マスターシークレットから鍵ブロックを導出する処理。sendSessionTicket()
: 新しいセッションチケットをクライアントに発行する処理。processCertsFromClient()
: クライアント証明書の処理と検証。tryCipherSuite()
: サーバーがサポートする暗号スイートの選択ロジック。
-
src/pkg/crypto/tls/common.go
:typeNewSessionTicket
(ハンドシェイクメッセージタイプ) およびextensionSessionTicket
(TLS拡張タイプ) 定数の追加。ConnectionState
構造体にDidResume bool
フィールドを追加し、セッションが再開されたかどうかを示す。Config
構造体にSessionTicketsDisabled bool
(セッションチケットの無効化オプション) およびSessionTicketKey [32]byte
(セッションチケットの暗号化鍵) フィールドを追加。
-
src/pkg/crypto/tls/handshake_messages.go
:clientHelloMsg
およびserverHelloMsg
構造体にセッションチケット関連のフィールド (ticketSupported
,sessionTicket
) を追加。newSessionTicketMsg
構造体の新規追加と、そのmarshal()
およびunmarshal()
メソッド。
-
src/pkg/crypto/tls/prf.go
:keysFromPreMasterSecret
関数をmasterFromPreMasterSecret
とkeysFromMasterSecret
に分割。
コアとなるコードの解説
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
) のどちらのパスに進むかを明確に分岐させています。セッション再開時には、公開鍵操作をスキップし、ChangeCipherSpec
とFinished
メッセージの交換のみでハンドシェイクを完了します。フルハンドシェイク時には、通常のハンドシェイクステップに加えて、新しいセッションチケットをクライアントに発行します。 -
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
メッセージとしてクライアントに送信します。これにより、クライアントは将来の接続でこのチケットを使用してセッションを再開できるようになります。
関連リンク
- RFC 5077 - Transport Layer Security (TLS) Session Resumption without Server-Side State: https://datatracker.ietf.org/doc/html/rfc5077
参考にした情報源リンク
- TLSハンドシェイクの基本:
- TLSセッション再開:
- AES-CTRモード:
- HMAC:
- Go言語
crypto/tls
パッケージのドキュメント: - Go言語
crypto/aes
パッケージのドキュメント: - Go言語
crypto/cipher
パッケージのドキュメント: - Go言語
crypto/hmac
パッケージのドキュメント: - Go言語
crypto/sha256
パッケージのドキュメント: