[インデックス 17433] ファイルの概要
このコミットは、Go言語の crypto/tls
パッケージにAES-GCM (Galois/Counter Mode) 暗号スイートのサポートを追加するものです。TLS (Transport Layer Security) における暗号スイートの選択は、通信のセキュリティとパフォーマンスに直接影響します。この変更は、既存の暗号スイートが抱える既知の脆弱性や構造上の問題を解決し、より堅牢な暗号化メカニズムを提供することを目的としています。
コミット
commit 2fe9a5a3e826d8b2dc45652e1b5d1c23eeeb428b
Author: Adam Langley <agl@golang.org>
Date: Thu Aug 29 17:18:59 2013 -0400
crypto/tls: support AES-GCM.
AES-GCM is the only current TLS ciphersuite that doesn't have
cryptographic weaknesses (RC4), nor major construction issues (CBC mode
ciphers) and has some deployment (i.e. not-CCM).
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13249044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2fe9a5a3e826d8b2dc4565e1b5d1c23eeeb428b
元コミット内容
crypto/tls: support AES-GCM.
AES-GCM is the only current TLS ciphersuite that doesn't have
cryptographic weaknesses (RC4), nor major construction issues (CBC mode
ciphers) and has some deployment (i.e. not-CCM).
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13249044
変更の背景
このコミットの主な背景には、当時のTLS暗号スイートが抱えていたセキュリティ上の課題があります。
- RC4の脆弱性: RC4 (Rivest Cipher 4) は、かつて広く使われていたストリーム暗号ですが、複数の攻撃手法(特にRC4 Bar Mitzvah攻撃など)によって深刻な脆弱性が指摘されていました。これにより、RC4を使用するTLS接続は盗聴のリスクに晒される可能性がありました。
- CBCモード暗号の構造上の問題: CBC (Cipher Block Chaining) モードは、ブロック暗号の一般的な運用モードですが、TLSにおけるCBCモードの実装にはLucky Thirteen攻撃などのタイミング攻撃に対する脆弱性が存在しました。これはパディングオラクル攻撃の一種で、復号エラーのタイミング情報から平文を推測される可能性があります。
- より安全な代替の必要性: 上記の脆弱性に対処するため、より安全で堅牢な暗号化モードへの移行が求められていました。AES-GCMは、認証付き暗号 (Authenticated Encryption with Associated Data, AEAD) の一種であり、データの機密性(暗号化)と完全性・認証(MAC)を同時に提供するため、これらの問題を解決する有力な候補でした。
- CCMの限定的な普及: AES-CCM (Counter with CBC-MAC) もAEADの一種ですが、AES-GCMに比べて普及度が低く、相互運用性の観点からAES-GCMが優先されました。
このコミットは、Goの crypto/tls
ライブラリが、これらの既知の脆弱性を持つ暗号スイートから、より安全なAES-GCMへの移行をサポートすることで、TLS通信のセキュリティを向上させることを目的としています。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上での通信を暗号化し、認証するためのプロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、ハンドシェイクプロトコルとレコードプロトコルという2つの主要な層で構成されています。
- ハンドシェイクプロトコル: クライアントとサーバーが通信を開始する際に、互いの認証、暗号スイートのネゴシエーション、セッションキーの生成などを行います。
- レコードプロトコル: ハンドシェイクで確立されたセッションキーと暗号スイートを使用して、実際のアプリケーションデータを暗号化・復号化し、完全性を保護します。
暗号スイート (Cipher Suite)
暗号スイートは、TLS接続で使用される暗号化アルゴリズムの組み合わせを定義するものです。通常、以下の要素を含みます。
- 鍵交換アルゴリズム (Key Exchange Algorithm): サーバーとクライアントが共有秘密鍵を安全に確立する方法(例: RSA, ECDHE)。
- 認証アルゴリズム (Authentication Algorithm): サーバー(およびオプションでクライアント)の身元を検証する方法(例: RSA, ECDSA)。
- バルク暗号化アルゴリズム (Bulk Encryption Algorithm): 実際のデータを暗号化する方法(例: AES, RC4, 3DES)。
- メッセージ認証コード (MAC) アルゴリズム (Message Authentication Code Algorithm): データの完全性と認証を保証する方法(例: SHA1, SHA256)。
AES-GCM (Advanced Encryption Standard - Galois/Counter Mode)
AES-GCMは、AESブロック暗号をGCMモードで運用する認証付き暗号 (AEAD) です。
- AES (Advanced Encryption Standard): 広く採用されているブロック暗号標準です。
- GCM (Galois/Counter Mode): カウンターモード (CTR) とGMAC (Galois Message Authentication Code) を組み合わせた運用モードです。
- CTRモード: ブロック暗号をストリーム暗号のように動作させ、並列処理が可能です。
- GMAC: 認証タグを生成し、データの完全性と認証を提供します。
- AEAD (Authenticated Encryption with Associated Data): データの機密性(暗号化)と完全性・認証を同時に提供する暗号化モードです。これにより、暗号文の改ざんや偽造を検出できます。GCMは、暗号化と認証を単一のパスで処理できるため、効率的です。
RC4 (Rivest Cipher 4)
RC4は、ストリーム暗号の一種で、かつてTLSで広く使用されていました。しかし、鍵スケジューリングアルゴリズムの脆弱性や、特定の鍵ストリームの繰り返しパターンにより、統計的な偏りが生じ、攻撃者が平文を推測できる可能性が指摘されました。
CBC (Cipher Block Chaining) モード
CBCは、ブロック暗号の運用モードの一つで、前のブロックの暗号文が次のブロックの平文とXORされることで連鎖的に暗号化されます。TLSにおけるCBCモードの実装では、パディングの処理方法に起因するタイミング攻撃(Lucky Thirteen攻撃など)の脆弱性が発見されました。攻撃者は、復号エラーのタイミングの違いを観測することで、パディングの正当性を判断し、最終的に平文を推測することが可能になります。
ノンス (Nonce)
ノンス (Number used once) は、暗号化操作において一度だけ使用されるランダムまたは擬似ランダムな値です。GCMのようなAEADモードでは、各暗号化操作でユニークなノンスを使用することが極めて重要です。同じ鍵とノンスのペアで複数のメッセージを暗号化すると、セキュリティが著しく損なわれる可能性があります。TLSのAES-GCMでは、ノンスの一部としてシーケンス番号が使用されます。
技術的詳細
このコミットは、Goの crypto/tls
パッケージにAES-GCM暗号スイートを統合するために、以下の主要な変更を導入しています。
-
cipherSuite
構造体の拡張:src/pkg/crypto/tls/cipher_suites.go
内のcipherSuite
構造体に、AEAD暗号スイートをサポートするための新しいフィールドaead func(key, fixedNonce []byte) cipher.AEAD
が追加されました。これにより、従来のストリーム暗号 (cipher
) やブロック暗号 (mac
) とは異なるAEADインターフェースを扱うことができるようになります。- 既存の暗号スイート定義(RC4, CBCモード)ではこの
aead
フィールドはnil
に設定され、新しく追加されるAES-GCMスイートでのみ使用されます。
-
AES-GCM暗号スイートの追加:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
(0xc02f) とTLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
(0xc02b) の2つの新しいAES-GCM暗号スイートがcipherSuites
リストに追加されました。これらは、鍵長、MAC長、IV長が適切に設定され、aeadAESGCM
関数が指定されています。- これらの新しい暗号スイートは、セキュリティ上の推奨順序に従って、既存のRC4やCBCモードのスイートよりも上位に配置されています。
-
fixedNonceAEAD
構造体とaeadAESGCM
関数の導入:src/pkg/crypto/tls/cipher_suites.go
にfixedNonceAEAD
という新しい構造体が定義されました。これはcipher.AEAD
インターフェースをラップし、TLSのAES-GCMで必要とされる固定ノンス(implicit nonce)とシーケンス番号(explicit nonce)の組み合わせを処理します。- TLS 1.2のAES-GCMでは、ノンスは12バイトで構成され、そのうち4バイトが固定(implicit nonce)、残りの8バイトが明示的(explicit nonce)です。この明示的なノンスとしてTLSレコードのシーケンス番号が使用されます。
fixedNonceAEAD
は、この固定ノンス部分を保持し、シーケンス番号と組み合わせて完全なノンスを生成します。 aeadAESGCM
関数は、AES-GCMのcipher.AEAD
インターフェースを生成するファクトリ関数です。aes.NewCipher
とcipher.NewGCM
を使用してAES-GCMインスタンスを作成し、それをfixedNonceAEAD
でラップして返します。
-
conn.go
における暗号化/復号化ロジックの変更:src/pkg/crypto/tls/conn.go
のhalfConn.decrypt
およびhalfConn.encrypt
メソッドが、AEAD暗号スイートを処理できるように修正されました。- AEADの場合、
explicitIVLen
(明示的IVの長さ) が8バイトに設定されます。これは、TLSのAES-GCMでシーケンス番号がノンスとして使用される部分に対応します。 - AEADの復号化 (
decrypt
) では、ペイロードから明示的ノンス(シーケンス番号)を抽出し、追加認証データ (Additional Data) を構築してc.Open
を呼び出します。追加認証データは、シーケンス番号、レコードヘッダの一部、平文の長さから構成されます。 - AEADの暗号化 (
encrypt
) では、明示的ノンス(シーケンス番号)と追加認証データを構築し、c.Seal
を呼び出します。 - 従来のMAC計算とパディング処理は、AEADを使用しない暗号スイートの場合にのみ実行されるように条件分岐が追加されました。AEADはMACとパディングを内部で処理するため、別途MAC計算は不要です。
- シーケンス番号のインクリメント (
hc.incSeq()
) の位置が調整され、AEADと非AEADの両方で適切に処理されるようになりました。
-
ハンドシェイク処理の変更:
src/pkg/crypto/tls/handshake_client.go
とsrc/pkg/crypto/tls/handshake_server.go
において、クライアントとサーバーのハンドシェイク中に暗号スイートが選択された際、そのスイートがAEADであるかどうかに応じて、suite.cipher
とsuite.mac
を使用するか、またはsuite.aead
を使用するかのロジックが追加されました。
-
テストケースの追加:
src/pkg/crypto/tls/handshake_server_test.go
にTestAESGCM
という新しいテストケースが追加されました。このテストは、AES-GCM暗号スイートを使用したTLS 1.2接続が正しく確立され、データが暗号化・復号化されることを検証します。これには、クライアントとサーバー間の実際のTLSハンドシェイクとデータ交換をシミュレートするためのバイト列 (aesGCMServerScript
) が含まれています。
これらの変更により、Goの crypto/tls
パッケージは、よりセキュアなAES-GCM暗号スイートをサポートし、TLS通信のセキュリティと堅牢性を向上させました。特に、RC4やCBCモードの既知の脆弱性に対する対策が講じられています。
コアとなるコードの変更箇所
src/pkg/crypto/tls/cipher_suites.go
--- a/src/pkg/crypto/tls/cipher_suites.go
+++ b/src/pkg/crypto/tls/cipher_suites.go
@@ -49,22 +49,25 @@ type cipherSuite struct {
elliptic bool
cipher func(key, iv []byte, isRead bool) interface{}
mac func(version uint16, macKey []byte) macFunction
+ aead func(key, fixedNonce []byte) cipher.AEAD
}
var cipherSuites = []*cipherSuite{
// Ciphersuite order is chosen so that ECDHE comes before plain RSA
// and RC4 comes before AES (because of the Lucky13 attack).
- {TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, true, cipherRC4, macSHA1},
- {TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, true, cipherRC4, macSHA1},
- {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1},
- {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1},
- {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1},
- {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1},
- {TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, false, cipherRC4, macSHA1},
- {TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, false, cipherAES, macSHA1},
- {TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, false, cipherAES, macSHA1},
- {TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, true, cipher3DES, macSHA1},
- {TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, false, cipher3DES, macSHA1},
+ {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, true, nil, nil, aeadAESGCM},
+ {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, true, nil, nil, aeadAESGCM},
+ {TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, true, cipherRC4, macSHA1, nil},
+ {TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, true, cipherRC4, macSHA1, nil},
+ {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1, nil},
+ {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1, nil},
+ {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, true, cipherAES, macSHA1, nil},
+ {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, true, cipherAES, macSHA1, nil},
+ {TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, false, cipherRC4, macSHA1, nil},
+ {TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, false, cipherAES, macSHA1, nil},
+ {TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, false, cipherAES, macSHA1, nil},
+ {TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, true, cipher3DES, macSHA1, nil},
+ {TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, false, cipher3DES, macSHA1, nil},
}
func cipherRC4(key, iv []byte, isRead bool) interface{} {
@@ -106,6 +109,46 @@ type macFunction interface {
MAC(digestBuf, seq, header, data []byte) []byte
}
+// fixedNonceAEAD wraps an AEAD and prefixes a fixed portion of the nonce to
+// each call.
+type fixedNonceAEAD struct {
+ // sealNonce and openNonce are buffers where the larger nonce will be
+ // constructed. Since a seal and open operation may be running
+ // concurrently, there is a separate buffer for each.
+ sealNonce, openNonce []byte
+ aead cipher.AEAD
+}
+
+func (f *fixedNonceAEAD) NonceSize() int { return 8 }
+func (f *fixedNonceAEAD) Overhead() int { return f.aead.Overhead() }
+
+func (f *fixedNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte {
+ copy(f.sealNonce[len(f.sealNonce)-8:], nonce)
+ return f.aead.Seal(out, f.sealNonce, plaintext, additionalData)
+}
+
+func (f *fixedNonceAEAD) Open(out, nonce, plaintext, additionalData []byte) ([]byte, error) {
+ copy(f.openNonce[len(f.openNonce)-8:], nonce)
+ return f.aead.Open(out, f.openNonce, plaintext, additionalData)
+}
+
+func aeadAESGCM(key, fixedNonce []byte) cipher.AEAD {
+ aes, err := aes.NewCipher(key)
+ if err != nil {
+ panic(err)
+ }
+ aead, err := cipher.NewGCM(aes)
+ if err != nil {
+ panic(err)
+ }
+
+ nonce1, nonce2 := make([]byte, 12), make([]byte, 12)
+ copy(nonce1, fixedNonce)
+ copy(nonce2, fixedNonce)
+
+ return &fixedNonceAEAD{nonce1, nonce2, aead}
+}
+
// ssl30MAC implements the SSLv3 MAC function, as defined in
// www.mozilla.org/projects/security/pki/nss/ssl/draft302.txt section 5.2.3.1
type ssl30MAC struct {
@@ -197,15 +240,17 @@ func mutualCipherSuite(have []uint16, want uint16) *cipherSuite {
// A list of the possible cipher suite ids. Taken from
// http://www.iana.org/assignments/tls-parameters/tls-parameters.xml
const (
- TLS_RSA_WITH_RC4_128_SHA uint16 = 0x0005
- TLS_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0x000a
- TLS_RSA_WITH_AES_128_CBC_SHA uint16 = 0x002f
- TLS_RSA_WITH_AES_256_CBC_SHA uint16 = 0x0035
- TLS_ECDHE_ECDSA_WITH_RC4_128_SHA uint16 = 0xc007
- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA uint16 = 0xc009
- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA uint16 = 0xc00a
- TLS_ECDHE_RSA_WITH_RC4_128_SHA uint16 = 0xc011
- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0xc012
- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA uint16 = 0xc013
- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA uint16 = 0xc014
+ TLS_RSA_WITH_RC4_128_SHA uint16 = 0x0005
+ TLS_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0x000a
+ TLS_RSA_WITH_AES_128_CBC_SHA uint16 = 0x002f
+ TLS_RSA_WITH_AES_256_CBC_SHA uint16 = 0x0035
+ TLS_ECDHE_ECDSA_WITH_RC4_128_SHA uint16 = 0xc007
+ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA uint16 = 0xc009
+ TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA uint16 = 0xc00a
+ TLS_ECDHE_RSA_WITH_RC4_128_SHA uint16 = 0xc011
+ TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0xc012
+ TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA uint16 = 0xc013
+ TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA uint16 = 0xc014
+ TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02f
+ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02b
)
src/pkg/crypto/tls/conn.go
--- a/src/pkg/crypto/tls/conn.go
+++ b/src/pkg/crypto/tls/conn.go
@@ -146,6 +146,9 @@ func (hc *halfConn) changeCipherSpec() error {
hc.mac = hc.nextMac
hc.nextCipher = nil
hc.nextMac = nil
+ for i := range hc.seq {
+ hc.seq[i] = 0
+ }
return nil
}
@@ -255,6 +258,26 @@ func (hc *halfConn) decrypt(b *block) (ok bool, prefixLen int, alertValue alert)
switch c := hc.cipher.(type) {
case cipher.Stream:
c.XORKeyStream(payload, payload)
+ case cipher.AEAD:
+ explicitIVLen = 8
+ if len(payload) < explicitIVLen {
+ return false, 0, alertBadRecordMAC
+ }
+ nonce := payload[:8]
+ payload = payload[8:]
+
+ var additionalData [13]byte
+ copy(additionalData[:], hc.seq[:])
+ copy(additionalData[8:], b.data[:3])
+ n := len(payload) - c.Overhead()
+ additionalData[11] = byte(n >> 8)
+ additionalData[12] = byte(n)
+ var err error
+ payload, err = c.Open(payload[:0], nonce, payload, additionalData[:])
+ if err != nil {
+ return false, 0, alertBadRecordMAC
+ }
+ b.resize(recordHeaderLen + explicitIVLen + len(payload))
case cbcMode:
blockSize := c.BlockSize()
if hc.version >= VersionTLS11 {
@@ -305,13 +328,13 @@ func (hc *halfConn) decrypt(b *block) (ok bool, prefixLen int, alertValue alert)
b.resize(recordHeaderLen + explicitIVLen + n)
remoteMAC := payload[n:]
localMAC := hc.mac.MAC(hc.inDigestBuf, hc.seq[0:], b.data[:recordHeaderLen], payload[:n])
- hc.incSeq()
if subtle.ConstantTimeCompare(localMAC, remoteMAC) != 1 || paddingGood != 255 {
return false, 0, alertBadRecordMAC
}
hc.inDigestBuf = localMAC
}
+ hc.incSeq()
return true, recordHeaderLen + explicitIVLen, 0
}
@@ -338,7 +361,6 @@ func (hc *halfConn) encrypt(b *block, explicitIVLen int) (bool, alert) {
// mac
if hc.mac != nil {
mac := hc.mac.MAC(hc.outDigestBuf, hc.seq[0:], b.data[:recordHeaderLen], b.data[recordHeaderLen+explicitIVLen:])
- hc.incSeq()
n := len(b.data)
b.resize(n + len(mac))
@@ -353,6 +375,20 @@ func (hc *halfConn) encrypt(b *block, explicitIVLen int) (bool, alert) {
switch c := hc.cipher.(type) {
case cipher.Stream:
c.XORKeyStream(payload, payload)
+ case cipher.AEAD:
+ payloadLen := len(b.data) - recordHeaderLen - explicitIVLen
+ b.resize(len(b.data) + c.Overhead())
+ nonce := b.data[recordHeaderLen : recordHeaderLen+explicitIVLen]
+ payload := b.data[recordHeaderLen+explicitIVLen:]
+ payload = payload[:payloadLen]
+
+ var additionalData [13]byte
+ copy(additionalData[:], hc.seq[:])
+ copy(additionalData[8:], b.data[:3])
+ additionalData[11] = byte(payloadLen >> 8)
+ additionalData[12] = byte(payloadLen)
+
+ c.Seal(payload[:0], nonce, payload, additionalData[:])
case cbcMode:
blockSize := c.BlockSize()
if explicitIVLen > 0 {
@@ -372,6 +408,7 @@ func (hc *halfConn) encrypt(b *block, explicitIVLen int) (bool, alert) {
n := len(b.data) - recordHeaderLen
b.data[3] = byte(n >> 8)
b.data[4] = byte(n)
+ hc.incSeq()
return true, 0
}
@@ -660,6 +697,7 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err error) {
if m > maxPlaintext {
m = maxPlaintext
}
+ explicitIVLen := 0
+ explicitIVIsSeq := false
var cbc cbcMode
if c.out.version >= VersionTLS11 {
@@ -668,8 +706,18 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err error) {
explicitIVLen = cbc.BlockSize()
}
}
+ if explicitIVLen == 0 {
+ if _, ok := c.out.cipher.(cipher.AEAD); ok {
+ explicitIVLen = 8
+ // The AES-GCM construction in TLS has an
+ // explicit nonce so that the nonce can be
+ // random. However, the nonce is only 8 bytes
+ // which is too small for a secure, random
+ // nonce. Therefore we use the sequence number
+ // as the nonce.
+ explicitIVIsSeq = true
+ }
+ }
b.resize(recordHeaderLen + explicitIVLen + m)
b.data[0] = byte(typ)
vers := c.vers
@@ -682,8 +732,12 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err error) {
b.data[4] = byte(n)
if explicitIVLen > 0 {
explicitIV := b.data[recordHeaderLen : recordHeaderLen+explicitIVLen]
- if _, err = io.ReadFull(c.config.rand(), explicitIV); err != nil {
- break
+ if explicitIVIsSeq {
+ copy(explicitIV, c.out.seq[:])
+ } else {
+ if _, err = io.ReadFull(c.config.rand(), explicitIV); err != nil {
+ break
+ }
}
}
copy(b.data[recordHeaderLen+explicitIVLen:], data)
コアとなるコードの解説
cipher_suites.go
cipherSuite
構造体へのaead
フィールド追加:aead func(key, fixedNonce []byte) cipher.AEAD
が追加されたことで、AEAD暗号スイートを表現できるようになりました。cipher
とmac
は従来の暗号スイート用です。
cipherSuites
リストの更新:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
とTLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
が追加され、aeadAESGCM
関数が指定されています。これらのスイートではcipher
とmac
はnil
に設定されます。- 新しいAES-GCMスイートは、セキュリティ上の理由からリストの先頭に配置されています。これは、ハンドシェイク時にクライアントが提示する暗号スイートの優先順位に影響します。
fixedNonceAEAD
構造体:- TLSのAES-GCMでは、ノンスは12バイトですが、そのうち4バイトは固定値(implicit nonce)で、残りの8バイトはシーケンス番号(explicit nonce)です。
fixedNonceAEAD
は、この固定ノンス部分をsealNonce
とopenNonce
に保持し、Seal
およびOpen
メソッド内で、引数として渡される8バイトのシーケンス番号と組み合わせて完全な12バイトのノンスを構築します。これにより、Goの標準cipher.AEAD
インターフェースをTLSのGCM要件に適合させています。
aeadAESGCM
関数:- この関数は、AES鍵と固定ノンスを受け取り、
fixedNonceAEAD
のインスタンスを返します。 - 内部では
aes.NewCipher
でAESブロック暗号を、cipher.NewGCM
でGCMモードのAEADインスタンスを作成しています。
- この関数は、AES鍵と固定ノンスを受け取り、
conn.go
halfConn.decrypt
およびhalfConn.encrypt
の変更:switch c := hc.cipher.(type)
文にcase cipher.AEAD:
が追加され、AEAD暗号スイートの場合の処理が定義されました。- 復号化 (
decrypt
):explicitIVLen
が8に設定されます。これは、TLSのAES-GCMでシーケンス番号がノンスとして使用される部分です。- ペイロードから8バイトのノンス(シーケンス番号)を抽出します。
additionalData
(追加認証データ) を構築します。これは、シーケンス番号、レコードヘッダのタイプとバージョン、そして平文の長さから構成されます。このデータは暗号化されませんが、認証タグの計算に含まれるため、改ざんを検出できます。c.Open
を呼び出して復号化と認証を行います。エラーが発生した場合(認証失敗など)はalertBadRecordMAC
を返します。
- 暗号化 (
encrypt
):- 同様に
explicitIVLen
が8に設定されます。 additionalData
を構築し、c.Seal
を呼び出して暗号化と認証を行います。
- 同様に
- シーケンス番号のインクリメント:
hc.incSeq()
の呼び出し位置が、AEADと非AEADの両方で適切にシーケンス番号がインクリメントされるように調整されました。AEADではMAC計算が不要になったため、その後の共通の処理として移動されています。
Conn.writeRecord
の変更:explicitIVLen
の計算ロジックが変更され、AEAD暗号スイートの場合にexplicitIVLen
が8に設定されるようになりました。explicitIVIsSeq
という新しいフラグが導入され、AEADの場合にノンスとしてシーケンス番号を使用することを示します。explicitIV
の生成ロジックが変更され、explicitIVIsSeq
がtrue
の場合はシーケンス番号をコピーし、それ以外の場合は乱数を使用するように分岐しています。これは、TLSのAES-GCMの仕様に準拠するためです。
これらの変更により、GoのTLS実装は、AES-GCMの認証付き暗号の特性を適切に利用し、より安全な通信を提供できるようになりました。
関連リンク
- TLS 1.2 (RFC 5246): 特にセクション 6.2.3.3 (Authenticated Encryption with Associated Data (AEAD) Ciphers)
- AES-GCM (RFC 5288): TLSにおけるAES-GCMの利用について
- Go言語の
crypto/tls
パッケージ: - Go言語の
crypto/cipher
パッケージ:
参考にした情報源リンク
- Lucky Thirteen Attack:
- RC4 Bar Mitzvah Attack:
- Authenticated Encryption with Associated Data (AEAD):
- Galois/Counter Mode (GCM):
- Go CL 13249044:
- IANA TLS Parameters:
- Goのcrypto/tlsパッケージの歴史と進化:
- Goの公式ブログやセキュリティ関連の発表も参照すると、TLS実装の進化の背景がより深く理解できます。