[インデックス 17988] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/cipher
パッケージにおける Cipher Feedback (CFB) モードの実装に関するバグ修正です。具体的には、XORKeyStream
メソッド内のループ条件の不備と、入力 (src
) と出力 (dst
) のバイトスライスが同じメモリ領域を指す(エイリアシングする)場合の安全性に関する問題が修正されています。
コミット
commit 1d546005af72e186c319722ccd293b64246488e3
Author: Adam Langley <agl@golang.org>
Date: Sun Dec 15 12:55:59 2013 -0500
crypto/cipher: fix CFB mode.
a073d65e6f8c had a couple of bugs in the CFB mode that I missed in code review:
1) The loop condition wasn't updated from the old version.
2) It wasn't safe when src and dst aliased.
Fixes #6950.
R=golang-dev, hanwen
CC=golang-dev
https://golang.org/cl/42110043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d546005af72e186c319722ccd293b64246488e3
元コミット内容
このコミットは、以前のコミット a073d65e6f8c
で導入された crypto/cipher
パッケージの CFB モードにおける2つのバグを修正するものです。
変更の背景
Go言語の crypto/cipher
パッケージは、暗号化アルゴリズムとそのモードを提供します。CFB (Cipher Feedback) モードは、ブロック暗号をストリーム暗号のように扱うためのモードの一つであり、ネットワーク通信など、データがブロック単位ではなくストリームとして扱われる場合に特に有用です。
このコミットの背景には、以前のコミット a073d65e6f8c
で導入された CFB モードの実装に、以下の2つの重要なバグが存在したことがあります。
- ループ条件の不備:
XORKeyStream
メソッド内のデータ処理ループが、スライスが更新されるロジックに合わせて適切に更新されていなかったため、一部のデータが処理されない、または不正な処理が行われる可能性がありました。 - エイリアシングの安全性欠如:
src
(入力) とdst
(出力) のバイトスライスが同じメモリ領域を指す(エイリアシングする)場合に、暗号化または復号化の処理が正しく行われないというセキュリティ上の問題がありました。これは、src
の内容がdst
への書き込みによって途中で変更されてしまい、その後の処理で誤ったsrc
の値が参照されることで発生します。
これらのバグは、GoのIssue #6950 として報告されており、このコミットはその問題を解決するために作成されました。暗号ライブラリにおけるこのようなバグは、データの破損だけでなく、セキュリティ上の脆弱性にもつながるため、迅速な修正が求められました。
前提知識の解説
1. ブロック暗号とストリーム暗号
- ブロック暗号: データを固定長のブロック(例: 128ビット)に分割し、ブロックごとに暗号化・復号化を行う暗号方式です(例: AES)。
- ストリーム暗号: データをビットまたはバイト単位で暗号化・復号化を行う暗号方式です。鍵ストリームと呼ばれる擬似乱数を生成し、平文とXOR演算することで暗号文を生成します。
2. 暗号利用モード (Modes of Operation)
ブロック暗号を繰り返し適用して、任意の長さのデータを暗号化・復号化するための方法です。様々なモードがあり、それぞれ異なる特性とセキュリティ上の考慮事項があります。
3. CFB (Cipher Feedback) モード
CFBモードは、ブロック暗号をストリーム暗号のように動作させるためのモードです。
- 特徴:
- ブロック暗号の暗号化関数のみを使用します。
- 前の暗号文ブロック(または初期ベクトル IV)をブロック暗号に入力し、その出力の一部を鍵ストリームとして使用します。
- 鍵ストリームと平文(または暗号文)をXORすることで、暗号文(または平文)を生成します。
- 自己同期性を持つことができます(一部のモードでは、ビットエラーが発生しても、数ブロック後には同期が回復します)。
- 暗号化の仕組み:
- 初期ベクトル (IV) をブロック暗号に入力し、出力を生成します。
- その出力の一部を鍵ストリームとして使用し、現在の平文ブロックとXOR演算して暗号文ブロックを生成します。
- 生成された暗号文ブロックを次のブロックの入力として使用します。
- 復号化の仕組み:
- 暗号化時と同じ方法で鍵ストリームを生成します(前の暗号文ブロックをブロック暗号に入力します)。
- 生成された鍵ストリームと現在の暗号文ブロックをXOR演算して平文ブロックを復元します。
4. エイリアシング (Aliasing)
プログラミングにおいて、複数の変数や参照が同じメモリ上のデータ(オブジェクトや配列など)を指している状態を指します。特に、入力バッファと出力バッファが同じメモリ領域を共有している場合、出力への書き込みが入力の読み取りに影響を与え、予期せぬ動作やバグを引き起こす可能性があります。暗号処理では、このようなエイリアシングがセキュリティ上の脆弱性につながることもあります。
5. Go言語のスライス (Slice)
Go言語のスライスは、配列の一部を参照する動的なビューです。src
と dst
がスライスである場合、それらが同じ基底配列を共有している可能性があります。
6. XORKeyStream
インターフェース
Goの crypto/cipher
パッケージでは、ストリーム暗号は XORKeyStream(dst, src []byte)
メソッドを持つインターフェースを実装することが一般的です。このメソッドは、src
からデータを読み込み、鍵ストリームとXOR演算した結果を dst
に書き込みます。
技術的詳細
このコミットは、crypto/cipher/cfb.go
内の cfb
構造体の XORKeyStream
メソッドに焦点を当てています。
1. ループ条件の修正
元のコードでは、for i := 0; i < len(src); i++
というループ条件が使用されていました。しかし、ループ内で src
と dst
スライスは dst = dst[n:]
および src = src[n:]
のように、処理されたバイト数 n
に応じて先頭が切り詰められて(スライスが再設定されて)いました。このため、len(src)
はループの各イテレーションで変化しますが、i
は常にインクリメントされるため、ループが正しく終了しない、または一部のデータが処理されない可能性がありました。
修正後のコードでは、ループ条件が for len(src) > 0
に変更されています。これにより、src
スライスにデータが残っている限りループが継続され、src
と dst
が適切に切り詰められることで、すべてのデータが確実に処理されるようになります。これは、ストリーム処理において非常に一般的なパターンです。
2. エイリアシング問題の解決
CFBモードでは、次のブロックの鍵ストリームを生成するために、暗号化の場合は生成された暗号文を、復号化の場合は入力された暗号文を内部状態 (x.next
) にコピーする必要があります。
元のコードでは、xorBytes
の呼び出しと x.next
への copy
の順序が、エイリアシングのシナリオで問題を引き起こしていました。
-
元のコードの課題:
// ... if x.decrypt { copy(x.next[x.outUsed:], src[:n]) // 復号化: src (暗号文) をコピー } else { copy(x.next[x.outUsed:], dst[:n]) // 暗号化: dst (平文) をコピー } n := xorBytes(dst, src, x.out[x.outUsed:]) // dst に結果を書き込む // ...
暗号化の場合 (
!x.decrypt
)、copy(x.next[x.outUsed:], dst[:n])
がxorBytes
の前に実行されていました。この時点でのdst
はまだ平文を含んでいます。しかし、CFB暗号化では、次のブロックの入力として生成された暗号文が必要になります。xorBytes
が実行されるとdst
は暗号文に上書きされますが、その前に平文がx.next
にコピーされてしまうため、内部状態が不正になり、結果として誤った鍵ストリームが生成されていました。 -
修正後のコード:
// ... if x.decrypt { // 復号化: src (入力された暗号文) を x.next にコピー。 // src は xorBytes の後でスライスされるため、ここでは全体をコピー。 copy(x.next[x.outUsed:], src) } // xorBytes を先に実行し、dst に正しい暗号文/平文を書き込む n := xorBytes(dst, src, x.out[x.outUsed:]) if !x.decrypt { // 暗号化: dst (生成された暗号文) を x.next にコピー。 // xorBytes の後に実行されるため、dst は既に暗号文になっている。 copy(x.next[x.outUsed:], dst) } // ...
この修正では、
xorBytes
の呼び出しを常にcopy
操作の前に移動させました。- 復号化の場合 (
x.decrypt
):x.next
には入力された暗号文 (src
) が必要です。src
はxorBytes
の後でスライスされるため、xorBytes
の前にsrc
をコピーしても問題ありません。 - 暗号化の場合 (
!x.decrypt
):x.next
には生成された暗号文 (dst
) が必要です。xorBytes
が先に実行されることでdst
に正しい暗号文が書き込まれ、その後にcopy(x.next[x.outUsed:], dst)
が実行されることで、正しい暗号文がx.next
にコピーされるようになります。
- 復号化の場合 (
この変更により、src
と dst
がエイリアシングしている場合でも、x.next
が常に正しい値で更新されることが保証され、CFBモードの安全性が向上しました。
3. テストケースの追加
cfb_test.go
では、エイリアシングのシナリオを明示的にテストするために、plaintext
の長さを増やし、XORKeyStream
の呼び出しで src
と dst
に同じスライスを渡すように変更されました。
// 暗号化
copy(ciphertext, plaintext) // plaintext を ciphertext にコピー
cfb.XORKeyStream(ciphertext, ciphertext) // src と dst が同じスライス
// 復号化
copy(plaintextCopy, ciphertext) // ciphertext を plaintextCopy にコピー
cfbdec.XORKeyStream(plaintextCopy, plaintextCopy) // src と dst が同じスライス
これにより、XORKeyStream
がエイリアシング条件下で正しく動作するかどうかが検証されるようになりました。
コアとなるコードの変更箇所
src/pkg/crypto/cipher/cfb.go
--- a/src/pkg/crypto/cipher/cfb.go
+++ b/src/pkg/crypto/cipher/cfb.go
@@ -16,21 +16,22 @@ type cfb struct {
}
func (x *cfb) XORKeyStream(dst, src []byte) {
- for i := 0; i < len(src); i++ {
+ for len(src) > 0 {
if x.outUsed == len(x.out) {
x.b.Encrypt(x.out, x.next)
x.outUsed = 0
}
- n := xorBytes(dst, src, x.out[x.outUsed:])
if x.decrypt {
// We can precompute a larger segment of the
// keystream on decryption. This will allow
// larger batches for xor, and we should be
// able to match CTR/OFB performance.
- copy(x.next[x.outUsed:], src[:n])
- } else {
- copy(x.next[x.outUsed:], dst[:n])
+ copy(x.next[x.outUsed:], src)
+ }
+ n := xorBytes(dst, src, x.out[x.outUsed:])
+ if !x.decrypt {
+ copy(x.next[x.outUsed:], dst)
}
dst = dst[n:]
src = src[n:]
src/pkg/crypto/cipher/cfb_test.go
--- a/src/pkg/crypto/cipher/cfb_test.go
+++ b/src/pkg/crypto/cipher/cfb_test.go
@@ -19,16 +19,18 @@ func TestCFB(t *testing.T) {
return
}
- plaintext := []byte("this is the plaintext")
+ plaintext := []byte("this is the plaintext. this is the plaintext.")
iv := make([]byte, block.BlockSize())
rand.Reader.Read(iv)
cfb := cipher.NewCFBEncrypter(block, iv)
ciphertext := make([]byte, len(plaintext))
- cfb.XORKeyStream(ciphertext, plaintext)
+ copy(ciphertext, plaintext)
+ cfb.XORKeyStream(ciphertext, ciphertext)
cfbdec := cipher.NewCFBDecrypter(block, iv)
plaintextCopy := make([]byte, len(plaintext))
- cfbdec.XORKeyStream(plaintextCopy, ciphertext)
+ copy(plaintextCopy, ciphertext)
+ cfbdec.XORKeyStream(plaintextCopy, plaintextCopy)
if !bytes.Equal(plaintextCopy, plaintext) {
t.Errorf("got: %x, want: %x", plaintextCopy, plaintext)
コアとなるコードの解説
cfb.go
の変更点
-
ループ条件の変更:
for i := 0; i < len(src); i++ {
からfor len(src) > 0 {
へ変更。- これにより、
src
スライスが処理されるたびに先頭が切り詰められる(src = src[n:]
)という動作と同期し、残りのデータがなくなるまでループが正確に繰り返されるようになりました。
-
xorBytes
とcopy
の順序変更と条件分岐の修正:- 元のコードでは、
xorBytes
の前にx.next
へのcopy
が行われていました。 - 修正後、
xorBytes
が常に先に実行されるようになりました。 - 復号化 (
x.decrypt
が true の場合):copy(x.next[x.outUsed:], src)
: 入力された暗号文 (src
) をx.next
にコピーします。復号化では、次の鍵ストリーム生成のために前の暗号文が必要なので、これは正しい動作です。src
はxorBytes
の後でスライスされるため、xorBytes
の前にコピーしても問題ありません。
- 暗号化 (
!x.decrypt
が true の場合):copy(x.next[x.outUsed:], dst)
:xorBytes
の実行後、dst
には平文と鍵ストリームのXOR結果である暗号文が格納されます。この暗号文をx.next
にコピーすることで、次のブロックの鍵ストリーム生成のための正しい入力が提供されます。エイリアシングのシナリオでは、この順序が非常に重要です。
- 元のコードでは、
cfb_test.go
の変更点
-
plaintext
の長さの増加:"this is the plaintext"
から"this is the plaintext. this is the plaintext."
へ変更。- これにより、より長いデータに対するテストが可能になり、特に複数ブロックにわたる処理の正確性を検証しやすくなります。
-
エイリアシングテストの追加:
- 暗号化と復号化の両方で、
XORKeyStream
のsrc
とdst
引数に同じスライスを渡すように変更されました。 copy(ciphertext, plaintext)
とcfb.XORKeyStream(ciphertext, ciphertext)
:plaintext
の内容をciphertext
にコピーした後、ciphertext
を入力と出力の両方に使用して暗号化を実行します。copy(plaintextCopy, ciphertext)
とcfbdec.XORKeyStream(plaintextCopy, plaintextCopy)
: 同様に、復号化でも同じスライスを使用します。- これらの変更により、
XORKeyStream
メソッドが入力と出力のバッファが重複している(エイリアシングしている)場合でも、正しく動作することが保証されるようになりました。これは、多くの暗号ライブラリで考慮されるべき重要なエッジケースです。
- 暗号化と復号化の両方で、
これらの変更は、CFBモードの実装の堅牢性と安全性を大幅に向上させ、暗号処理における一般的な落とし穴(ループ処理の不備やエイリアシング)を効果的に修正しています。
関連リンク
- Go Issue #6950: https://github.com/golang/go/issues/6950
- Go Code Review 42110043: https://golang.org/cl/42110043
参考にした情報源リンク
- Cipher Feedback (CFB) mode: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)
- Go
crypto/cipher
package documentation: https://pkg.go.dev/crypto/cipher - Go Slices: usage and internals: https://go.dev/blog/slices