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

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

このコミットは、Go言語の標準ライブラリ crypto/aes パッケージにおける、アセンブリ実装のAES暗号化/復号処理におけるオーバーランのバグを修正するものです。具体的には、入力または出力バッファがAESのブロックサイズ(16バイト)に満たない場合に発生する可能性のある問題を解決し、より堅牢なエラーハンドリングを導入しています。

コミット

commit 3e8ed96c63145f5164dbc96b294220e1e6050b5d
Author: Russ Cox <rsc@golang.org>
Date:   Fri May 9 15:40:55 2014 -0400

    crypto/aes: fix overrun in assembly encrypt/decrypt
    
    Fixes #7928.
    
    LGTM=bradfitz
    R=golang-codereviews
    CC=agl, bradfitz, golang-codereviews
    https://golang.org/cl/91320043

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

https://github.com/golang/go/commit/3e8ed96c63145f5164dbc96b294220e1e6050b5d

元コミット内容

crypto/aes: fix overrun in assembly encrypt/decrypt

Fixes #7928.

LGTM=bradfitz
R=golang-codereviews
CC=agl, bradfitz, golang-codereviews
https://golang.org/cl/91320043

変更の背景

このコミットは、Go言語の crypto/aes パッケージにおいて、AES暗号化および復号処理のアセンブリ実装に潜在していたバグを修正するために行われました。具体的には、入力または出力として提供されるバイトスライス(バッファ)の長さが、AESのブロックサイズ(16バイト)よりも短い場合に、アセンブリコードが予期せぬ動作(オーバーラン)を引き起こす可能性がありました。

暗号化処理において、バッファのオーバーランは非常に危険な脆弱性につながります。これは、本来アクセスすべきでないメモリ領域に読み書きが行われることを意味し、データの破損、情報漏洩、あるいは悪意のあるコード実行の可能性を生じさせます。この問題は、GoのIssue #7928として報告されており、このコミットはその問題を解決することを目的としています。

この修正の背景には、暗号ライブラリが常に堅牢であり、予期せぬ入力に対しても安全に振る舞うべきであるというセキュリティ上の厳格な要件があります。特に、アセンブリで書かれたパフォーマンス重視のコードは、Goのランタイムが提供するメモリ安全性の保証の一部をバイパスする可能性があるため、入力の検証がより一層重要になります。

前提知識の解説

AES (Advanced Encryption Standard)

AESは、現代において最も広く利用されている対称鍵ブロック暗号アルゴリズムです。ブロック暗号とは、データを固定長のブロック単位で暗号化・復号する暗号方式を指します。AESの場合、ブロックサイズは常に128ビット(16バイト)です。つまり、AESのコアとなる暗号化・復号関数は、常に16バイトの入力ブロックを受け取り、16バイトの出力ブロックを生成します。

AESは、鍵長によってAES-128、AES-192、AES-256の3つのバリエーションがあり、それぞれ128ビット、192ビット、256ビットの鍵を使用します。Goの crypto/aes パッケージは、これらの鍵長に対応しています。

ブロック暗号の運用モード

ブロック暗号は、単体で使われることは少なく、通常はCBC(Cipher Block Chaining)、CTR(Counter)、GCM(Galois/Counter Mode)などの「運用モード」と組み合わせて使用されます。これらのモードは、複数のブロックをどのように処理するか、あるいはストリーム暗号のように振る舞うかを定義します。しかし、どの運用モードを使用するにしても、その基盤となるのは、固定長のブロックを処理するAESのコア関数です。したがって、コア関数が常に正しいブロックサイズで動作することが極めて重要です。

Go言語における panic

Go言語の panic は、回復不能なエラーが発生した際にプログラムの実行を停止させるメカニズムです。これは、C++の例外やJavaのRuntimeExceptionに似ていますが、Goでは通常、エラーは多値戻り値(error型)で処理されます。panicは、プログラマが予期しない、または回復が不可能と判断されるような状況(例: 配列の範囲外アクセス、nilポインタ参照など)で使用されます。

暗号ライブラリにおいて、不正な入力サイズが提供された場合に panic を使用することは、その入力がライブラリの前提条件を満たしておらず、安全な処理を継続できないことを明確に示し、開発者に問題の修正を促すための有効な手段です。これにより、不正なデータが処理され、潜在的なセキュリティ脆弱性につながることを防ぎます。

Go言語におけるアセンブリコードの利用

Go言語は通常、Goで書かれたコードをコンパイルして実行しますが、パフォーマンスが非常に重要な部分(特に暗号化処理やシステムコールなど)では、特定のアセンブリ言語で書かれたコードを利用することがあります。これは、CPUの特定の命令セット(例: IntelのAES-NI命令)を直接利用することで、Goで書かれた同等のコードよりもはるかに高速な処理を実現するためです。

crypto/aes パッケージも、AES-NI命令が利用可能なCPUではアセンブリコード(cipher_amd64.s など)を使用し、そうでない場合はGoで書かれたフォールバック実装(cipher_go.go)を使用するように設計されています。アセンブリコードは低レベルなメモリ操作を行うため、Goの型システムやランタイムが提供する安全性のチェックの一部が適用されません。そのため、アセンブリコードに渡す前の入力検証が特に重要になります。

技術的詳細

このコミットが修正する「オーバーラン」の問題は、crypto/aes パッケージの Encrypt および Decrypt メソッドが、入力または出力のバイトスライスがAESのブロックサイズ(BlockSize、つまり16バイト)よりも短い場合に、アセンブリ実装が期待するバッファサイズを満たさず、メモリの境界を越えてアクセスしてしまう可能性があったことに起因します。

AESのブロック暗号は、常に16バイトのブロックを処理します。したがって、EncryptDecrypt のような関数は、入力と出力の両方が少なくとも16バイトの長さを持つことを前提としています。しかし、以前の実装では、この前提条件がアセンブリコードに渡される前に十分に検証されていませんでした。

この修正では、cipher.go 内の Encrypt および Decrypt メソッドに、以下の厳格な入力検証が追加されました。

  1. 入力バッファの長さチェック: len(src) < BlockSize
  2. 出力バッファの長さチェック: len(dst) < BlockSize

これらのチェックにより、src(入力)または dst(出力)のバイトスライスが16バイト未満の場合、即座に panic が発生します。panic メッセージは、それぞれ "crypto/aes: input not full block" および "crypto/aes: output not full block" となります。

この変更の技術的な意義は以下の通りです。

  • メモリ安全性の向上: 不正なサイズのバッファがアセンブリコードに渡されることを防ぎ、メモリのオーバーランやアンダーランといった危険な挙動を根本的に排除します。これにより、暗号処理の堅牢性とセキュリティが大幅に向上します。
  • 「Fail-Fast」原則の適用: 問題のある入力が検出された場合、処理を続行して潜在的な破損や脆弱性を引き起こすのではなく、即座に panic することで問題を早期に発見し、開発者に修正を促します。これは、特にセキュリティが重要なライブラリにおいて推奨される設計原則です。
  • APIの明確化: Encrypt および Decrypt メソッドが常にフルブロックの入出力を期待するというAPIの契約が、コードレベルで明確に強制されるようになりました。これにより、ライブラリの誤用を防ぎます。
  • テストの強化: aes_test.go に追加された TestShortBlocks は、この種の入力エラーを意図的に引き起こし、panic が正しく発生することを確認するものです。これにより、将来的に同様の回帰バグが発生するのを防ぐための安全網が提供されます。

cipher_asm.go の変更は、単にGoのフォーマットツールによって追加された改行であり、機能的な変更はありません。これは、アセンブリコード自体が修正されたわけではなく、Goコード側でアセンブリコードを呼び出す前に安全性を確保する層が追加されたことを意味します。

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

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

  1. src/pkg/crypto/aes/aes_test.go
  2. src/pkg/crypto/aes/cipher.go

src/pkg/crypto/aes/aes_test.go の変更

--- a/src/pkg/crypto/aes/aes_test.go
+++ b/src/pkg/crypto/aes/aes_test.go
@@ -354,6 +354,34 @@ func TestCipherDecrypt(t *testing.T) {
 	}\n}\n \n+// Test short input/output.\n+// Assembly used to not notice.\n+// See issue 7928.\n+func TestShortBlocks(t *testing.T) {\n+\tbytes := func(n int) []byte { return make([]byte, n) }\n+\n+\tc, _ := NewCipher(bytes(16))\n+\n+\tmustPanic(t, \"crypto/aes: input not full block\", func() { c.Encrypt(bytes(1), bytes(1)) })\n+\tmustPanic(t, \"crypto/aes: input not full block\", func() { c.Decrypt(bytes(1), bytes(1)) })\n+\tmustPanic(t, \"crypto/aes: input not full block\", func() { c.Encrypt(bytes(100), bytes(1)) })\n+\tmustPanic(t, \"crypto/aes: input not full block\", func() { c.Decrypt(bytes(100), bytes(1)) })\n+\tmustPanic(t, \"crypto/aes: output not full block\", func() { c.Encrypt(bytes(1), bytes(100)) })\n+\tmustPanic(t, \"crypto/aes: output not full block\", func() { c.Decrypt(bytes(1), bytes(100)) })\n+}\n+\n+func mustPanic(t *testing.T, msg string, f func()) {\n+\tdefer func() {\n+\t\terr := recover()\n+\t\tif err == nil {\n+\t\t\tt.Errorf(\"function did not panic, wanted %q\", msg)\n+\t\t} else if err != msg {\n+\t\t\tt.Errorf(\"got panic %v, wanted %q\", err, msg)\n+\t\t}\n+\t}()\n+\tf()\n+}\n+\n func BenchmarkEncrypt(b *testing.B) {
  • TestShortBlocks 関数が追加されました。このテストは、NewCipher でAES暗号器を初期化した後、Encrypt および Decrypt メソッドに対して、意図的にブロックサイズに満たない入力または出力バッファを渡しています。
  • mustPanic ヘルパー関数が追加されました。この関数は、指定された関数 f が特定のパニックメッセージ msg でパニックするかどうかをテストします。これにより、TestShortBlocks で期待されるパニックが正しく発生するかを確認できます。

src/pkg/crypto/aes/cipher.go の変更

--- a/src/pkg/crypto/aes/cipher.go
+++ b/src/pkg/crypto/aes/cipher.go
@@ -46,9 +46,21 @@ func NewCipher(key []byte) (cipher.Block, error) {
 func (c *aesCipher) BlockSize() int { return BlockSize }
 
 func (c *aesCipher) Encrypt(dst, src []byte) {
+\tif len(src) < BlockSize {
+\t\tpanic(\"crypto/aes: input not full block\")
+\t}\n+\tif len(dst) < BlockSize {
+\t\tpanic(\"crypto/aes: output not full block\")
+\t}\n \tencryptBlock(c.enc, dst, src)
 }\n 
 func (c *aesCipher) Decrypt(dst, src []byte) {
+\tif len(src) < BlockSize {
+\t\tpanic(\"crypto/aes: input not full block\")
+\t}\n+\tif len(dst) < BlockSize {
+\t\tpanic(\"crypto/aes: output not full block\")
+\t}\n \tdecryptBlock(c.dec, dst, src)
 }\n```

*   `aesCipher` 型の `Encrypt` メソッドと `Decrypt` メソッドの冒頭に、入力 `src` と出力 `dst` のバイトスライスの長さが `BlockSize` (16バイト) 未満であるかをチェックする `if` 文が追加されました。
*   長さが不足している場合、それぞれ適切なパニックメッセージと共に `panic` が呼び出されます。

### `src/pkg/crypto/aes/cipher_asm.go` の変更

```diff
--- a/src/pkg/crypto/aes/cipher_asm.go
+++ b/src/pkg/crypto/aes/cipher_asm.go
@@ -21,6 +21,7 @@ func encryptBlock(xk []uint32, dst, src []byte) {
 	\tencryptBlockGo(xk, dst, src)\n \t}\n }\n+\n func decryptBlock(xk []uint32, dst, src []byte) {
 \tif useAsm {
 \t\tdecryptBlockAsm(len(xk)/4-1, &xk[0], &dst[0], &src[0])
@@ -28,6 +29,7 @@ func decryptBlock(xk []uint32, dst, src []byte) {
 \t\tdecryptBlockGo(xk, dst, src)\n \t}\n }\n+\n func expandKey(key []byte, enc, dec []uint32) {
 \tif useAsm {
 \t\trounds := 10
  • このファイルには機能的な変更はなく、単にGoのフォーマットツールによって追加された改行のみです。これは、アセンブリコード自体ではなく、それを呼び出すGoのラッパー関数で入力検証が行われるようになったことを示しています。

コアとなるコードの解説

このコミットの核心は、crypto/aes パッケージの Encrypt および Decrypt メソッドに導入された、厳格な入力/出力バッファサイズの検証です。

aesCipher 構造体は、AES暗号化/復号の具体的な実装(アセンブリまたはGo)をラップしています。EncryptDecrypt は、それぞれ encryptBlockdecryptBlock という内部関数を呼び出しますが、これらの内部関数は、入力と出力が常にAESのブロックサイズ(16バイト)であることを前提としています。

以前の実装では、この前提条件が呼び出し元によって保証されるか、あるいは内部関数が暗黙的に処理することを期待していました。しかし、アセンブリコードのような低レベルな実装では、このような暗黙の期待はメモリの境界外アクセス(オーバーラン)という形で脆弱性につながる可能性があります。例えば、アセンブリコードが常に16バイトを読み書きするように設計されている場合、それよりも短いバッファが渡されると、隣接するメモリ領域にアクセスしてしまい、予期せぬ動作やセキュリティ上の問題を引き起こす可能性があります。

今回の修正では、EncryptDecrypt の各メソッドの先頭で、len(src) < BlockSize および len(dst) < BlockSize というチェックが追加されました。ここで BlockSizecrypto/aes パッケージで定義されている定数で、AESのブロックサイズである16バイトを指します。

このチェックにより、もし入力または出力のバッファが16バイト未満であれば、即座に panic が発生し、プログラムの実行が停止します。これにより、不正なサイズのバッファが低レベルな暗号化ルーチンに渡されることを防ぎ、メモリのオーバーランを防ぎます。

panic を使用する理由は、crypto/aes パッケージのAPI設計にあります。EncryptDecryptcipher.Block インターフェースの一部であり、このインターフェースのメソッドはエラーを返しません。したがって、不正な入力に対しては panic を発生させるのがGoの慣習に沿った方法となります。これにより、ライブラリの利用者は、常にフルブロックのデータを扱う必要があることを明確に理解できます。

また、aes_test.go に追加された TestShortBlocks は、この新しい挙動を検証するための重要なテストです。このテストは、意図的に不正なサイズのバッファを Encrypt および Decrypt に渡し、期待通りに panic が発生することを確認します。これにより、この修正が正しく機能していること、そして将来の変更によってこの重要な安全チェックが誤って削除されないことを保証します。

この修正は、Goの暗号ライブラリの堅牢性とセキュリティを向上させる上で非常に重要であり、暗号処理における入力検証の重要性を改めて示しています。

関連リンク

  • Go Issue #7928 (このコミットで修正された問題):
    • 直接的なIssueページは見つかりませんでしたが、コミットメッセージに明記されています。GoのIssueトラッカーの変更や、クローズされたIssueの検索性によるものと考えられます。
  • Go CL 91320043 (このコミットの変更リスト):

参考にした情報源リンク