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

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

このコミットは、Go言語の標準ライブラリ crypto/rsa パッケージにおけるセキュリティ上の脆弱性を修正するものです。具体的には、RSA PKCS#1 v1.5パディングスキームを用いた復号処理において、短いセッションキーが与えられた場合に発生する境界外アクセス(out-of-bounds access)のバグを修正しています。この修正は、src/pkg/crypto/rsa/pkcs1v15.gosrc/pkg/crypto/rsa/pkcs1v15_test.go、および src/pkg/crypto/subtle/constant_time.go の3つのファイルにわたる変更を含んでいます。

pkcs1v15.go は、PKCS#1 v1.5パディングスキームを使用したRSA暗号の復号および署名検証ロジックを実装しています。 pkcs1v15_test.go は、pkcs1v15.go の機能に対するテストケースを含んでおり、今回の修正に合わせて新しいテストが追加されています。 constant_time.go は、サイドチャネル攻撃を防ぐための定数時間(constant-time)操作を提供するユーティリティ関数群を含んでいます。

コミット

commit 372f399e00693b1d49bc1243feb66f2c9bf0dd5c
Author: Adam Langley <agl@golang.org>
Date:   Wed Jul 2 15:28:57 2014 -0700

    crypto/rsa: fix out-of-bound access with short session keys.
    
    Thanks to Cedric Staub for noting that a short session key would lead
    to an out-of-bounds access when conditionally copying the too short
    buffer over the random session key.
    
    LGTM=davidben, bradfitz
    R=davidben, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/102670044

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

https://github.com/golang/go/commit/372f399e00693b1d49bc1243feb66f2c9bf0dd5c

元コミット内容

crypto/rsa: fix out-of-bound access with short session keys.

Thanks to Cedric Staub for noting that a short session key would lead
to an out-of-bounds access when conditionally copying the too short
buffer over the random session key.

LGTM=davidben, bradfitz
R=davidben, bradfitz
CC=golang-codereviews
https://golang.org/cl/102670044

変更の背景

この変更は、Cedric Staub氏によって報告された crypto/rsa パッケージの脆弱性に対応するものです。具体的には、DecryptPKCS1v15SessionKey 関数が、暗号化されたセッションキーを復号する際に、意図的に短いセッションキーが与えられた場合に境界外アクセスを引き起こす可能性がありました。

暗号システムにおける境界外アクセスは、単なるプログラムのクラッシュ以上の深刻な問題を引き起こす可能性があります。特に、秘密鍵やセッションキーなどの機密情報を扱う文脈では、このようなバグはサイドチャネル攻撃の温床となり得ます。攻撃者は、プログラムの異常終了、メモリの内容、または処理時間のわずかな変動などを観察することで、秘密情報を推測しようと試みる可能性があります。

このバグは、復号されたデータが期待されるセッションキーの長さよりも短い場合に、その短いバッファをランダムなセッションキーに条件付きでコピーする際に発生していました。定数時間操作を意図して実装されたコードパスでこのような境界外アクセスが発生することは、定数時間性の保証を破り、潜在的な情報漏洩のリスクを高めるため、早急な修正が必要とされました。

前提知識の解説

RSA暗号

RSAは、公開鍵暗号方式の一つで、現代のセキュリティ通信において広く利用されています。公開鍵と秘密鍵のペアを使用し、公開鍵で暗号化されたデータは対応する秘密鍵でのみ復号でき、秘密鍵で署名されたデータは公開鍵で検証できます。その安全性は、大きな合成数の素因数分解の困難性に基づいています。

PKCS#1 v1.5パディング

RSA暗号は、そのままでは平文を直接暗号化するのではなく、パディングスキームと組み合わせて使用されます。PKCS#1 v1.5は、RSA暗号で最も広く使われているパディングスキームの一つです。 暗号化の場合、平文は特定のフォーマット(0x00 || 0x02 || PS || 0x00 || M)に従ってパディングされます。ここで PS はランダムな非ゼロバイトの列(パディング文字列)、M はメッセージです。このパディングは、メッセージの構造を隠蔽し、選択平文攻撃(Chosen-plaintext attack)などの攻撃を防ぐ役割があります。 復号時には、このパディングフォーマットが検証され、メッセージが抽出されます。

サイドチャネル攻撃と定数時間処理 (Constant-time operations)

サイドチャネル攻撃とは、暗号アルゴリズムの実装から漏洩する物理的な情報(処理時間、消費電力、電磁波放射、キャッシュヒット/ミスなど)を分析することで、秘密鍵などの機密情報を推測しようとする攻撃手法です。 例えば、秘密鍵のビット値によって処理時間がわずかに異なる場合、攻撃者はその時間差を測定することで秘密鍵のビットを特定できる可能性があります。

これを防ぐために、「定数時間処理(Constant-time operations)」という設計原則が重要になります。定数時間処理とは、入力データ(特に秘密情報)の値によらず、常に同じ時間で実行され、同じメモリアクセスパターンを持つようにコードを記述することです。これにより、サイドチャネルからの情報漏洩を防ぎ、暗号システムの安全性を高めます。

Go言語の crypto/subtle パッケージは、このような定数時間操作を安全に実装するためのプリミティブを提供します。例えば、ConstantTimeEq は2つの値が等しいかどうかを定数時間で比較し、ConstantTimeCopy は条件付きでデータをコピーする際に定数時間性を保証します。

セッションキー

セッションキーは、共通鍵暗号システムで一時的に使用される対称鍵です。通常、RSAなどの公開鍵暗号を用いて安全に交換され、その後の通信は高速な共通鍵暗号で行われます。セッションキーは、そのセッションの間だけ有効であり、セッション終了後には破棄されます。

技術的詳細

今回の修正は、主に crypto/rsa パッケージ内の DecryptPKCS1v15 および DecryptPKCS1v15SessionKey 関数、そして crypto/subtle パッケージ内の ConstantTimeCopy 関数に焦点を当てています。

decryptPKCS1v15 関数の変更

元の decryptPKCS1v15 関数は、復号の成功を示す valid フラグと、復号されたメッセージ msg を返していました。しかし、サイドチャネル攻撃を防ぐためには、復号が成功したかどうかにかかわらず、常に同じメモリパターンで処理を進める必要があります。

修正後、decryptPKCS1v15valid int, em []byte, index int, err error という新しい戻り値のシグネチャを持つようになりました。

  • em []byte: これは、パディングを含む復号された完全なエンコード済みメッセージ(em)です。復号が成功したかどうかにかかわらず、常にこの完全なスライスが返されます。これにより、メモリアクセスパターンが一定に保たれます。
  • index int: valid が1(成功)の場合、この index は元のメッセージが em 内のどこから始まるかを示します。valid が0(失敗)の場合、index の値は意味を持ちませんが、定数時間操作のために計算は行われます。

この変更により、DecryptPKCS1v15DecryptPKCS1v15SessionKey の呼び出し元は、valid フラグと index を利用して、復号されたメッセージを安全に抽出できるようになりました。特に、index = subtle.ConstantTimeSelect(valid, index+1, 0) の行は、valid の値に応じて index を選択的に設定することで、定数時間性を維持しながらメッセージの開始位置を決定しています。

DecryptPKCS1v15SessionKey 関数の変更

この関数は、RSAで暗号化されたセッションキーを復号し、指定された key バッファにコピーする役割を担っています。元の実装では、decryptPKCS1v15 から返された msg の長さが key の長さと一致するかを ConstantTimeEq でチェックし、その後 ConstantTimeCopymsgkey にコピーしていました。

問題は、decryptPKCS1v15 が返す msg が、パディングの構造が不正な場合に短いスライスになる可能性があったことです。この短いスライスを ConstantTimeCopy に渡すと、ConstantTimeCopy 内部で境界外アクセスが発生する可能性がありました。

修正後、DecryptPKCS1v15SessionKeydecryptPKCS1v15 から返される完全な em スライスを使用するようになりました。 valid &= subtle.ConstantTimeEq(int32(len(em)-index), int32(len(key))) の行では、em の有効なメッセージ部分の長さ(len(em)-index)と期待されるセッションキーの長さ(len(key))を比較しています。 そして、subtle.ConstantTimeCopy(valid, key, em[len(em)-len(key):]) の行で、em の末尾から len(key) バイトを安全にコピーしています。これにより、ConstantTimeCopy に渡されるソーススライスが常に適切な長さを持つことが保証され、境界外アクセスが防止されます。

ConstantTimeCopy 関数の変更

src/pkg/crypto/subtle/constant_time.go にある ConstantTimeCopy 関数には、引数として渡されるスライス xy の長さが異なる場合に panic を発生させるチェックが追加されました。

func ConstantTimeCopy(v int, x, y []byte) {
	if len(x) != len(y) {
		panic("subtle: slices have different lengths")
	}
	// ... (既存のロジック)
}

この変更は、ConstantTimeCopy の契約を明確にし、誤った使用を防ぐための防御的なプログラミングです。ConstantTimeCopy は、ソースとデスティネーションのスライスが同じ長さであることを前提として動作するため、このチェックはランタイムエラーを早期に検出し、より深刻なセキュリティ問題(例えば、境界外アクセス)に発展するのを防ぎます。

短いセッションキーが問題を引き起こした理由

元の実装では、decryptPKCS1v15 がパディングの構造が不正であると判断した場合、msg スライスが期待される長さよりも短くなる可能性がありました。特に、PKCS#1 v1.5パディングの 0x00 区切り文字が見つからない場合、msg は空のスライスになることもありました。 DecryptPKCS1v15SessionKey は、この短い msg スライスを ConstantTimeCopy のソースとして使用していました。ConstantTimeCopy は、ソースとデスティネーションの長さが同じであることを暗黙的に期待してループを回すため、ソーススライスがデスティネーションスライス(key)よりも短い場合、ConstantTimeCopy 内部で key バッファへの境界外書き込みが発生する可能性がありました。この境界外書き込みは、key バッファの直後のメモリ領域を破壊し、プログラムのクラッシュや、より悪質な場合にはメモリの内容を操作される可能性がありました。

今回の修正は、decryptPKCS1v15 が常に完全な em スライスを返し、DecryptPKCS1v15SessionKeyem スライスから安全な方法でメッセージ部分を抽出して ConstantTimeCopy に渡すことで、この問題を根本的に解決しています。

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

src/pkg/crypto/rsa/pkcs1v15.go

--- a/src/pkg/crypto/rsa/pkcs1v15.go
+++ b/src/pkg/crypto/rsa/pkcs1v15.go
@@ -53,11 +53,14 @@ func DecryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (out [\
 	if err := checkPub(&priv.PublicKey); err != nil {
 		return nil, err
 	}
-	valid, out, err := decryptPKCS1v15(rand, priv, ciphertext)
-	if err == nil && valid == 0 {
-		err = ErrDecryption
+	valid, out, index, err := decryptPKCS1v15(rand, priv, ciphertext)
+	if err != nil {
+		return
 	}
-
+	if valid == 0 {
+		return nil, ErrDecryption
+	}
+	out = out[index:]
 	return
 }
 
@@ -80,21 +83,32 @@ func DecryptPKCS1v15SessionKey(rand io.Reader, priv *PrivateKey, ciphertext []by\
 	k := (priv.N.BitLen() + 7) / 8
 	if k-(len(key)+3+8) < 0 {
-		err = ErrDecryption
-		return
+		return ErrDecryption
 	}
 
-	valid, msg, err := decryptPKCS1v15(rand, priv, ciphertext)
+	valid, em, index, err := decryptPKCS1v15(rand, priv, ciphertext)
 	if err != nil {
 		return
 	}
 
-	valid &= subtle.ConstantTimeEq(int32(len(msg)), int32(len(key)))\
-	subtle.ConstantTimeCopy(valid, key, msg)
+	if len(em) != k {
+		// This should be impossible because decryptPKCS1v15 always
+		// returns the full slice.
+		return ErrDecryption
+	}
+
+	valid &= subtle.ConstantTimeEq(int32(len(em)-index), int32(len(key)))
+	subtle.ConstantTimeCopy(valid, key, em[len(em)-len(key):])
 	return
 }
 
-func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid int, msg []byte, err error) {
+// decryptPKCS1v15 decrypts ciphertext using priv and blinds the operation if
+// rand is not nil. It returns one or zero in valid that indicates whether the
+// plaintext was correctly structured. In either case, the plaintext is
+// returned in em so that it may be read independently of whether it was valid
+// in order to maintain constant memory access patterns. If the plaintext was
+// valid then index contains the index of the original message in em.
+func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid int, em []byte, index int, err error) {
 	k := (priv.N.BitLen() + 7) / 8
 	if k < 11 {
 		err = ErrDecryption
@@ -107,7 +121,7 @@ func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid\
 		return
 	}
 
-	em := leftPad(m.Bytes(), k)
+	em = leftPad(m.Bytes(), k)
 	firstByteIsZero := subtle.ConstantTimeByteEq(em[0], 0)
 	secondByteIsTwo := subtle.ConstantTimeByteEq(em[1], 2)
 
@@ -115,8 +129,7 @@ func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid\
 	// octets, followed by a 0, followed by the message.
 	//   lookingForIndex: 1 iff we are still looking for the zero.
 	//   index: the offset of the first zero byte.
-	var lookingForIndex, index int
-	lookingForIndex = 1
+	lookingForIndex := 1
 
 	for i := 2; i < len(em); i++ {
 		equals0 := subtle.ConstantTimeByteEq(em[i], 0)
@@ -129,8 +142,8 @@ func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid\
 	validPS := subtle.ConstantTimeLessOrEq(2+8, index)
 
 	valid = firstByteIsZero & secondByteIsTwo & (^lookingForIndex & 1) & validPS
-	msg = em[index+1:]
-	return
+	index = subtle.ConstantTimeSelect(valid, index+1, 0)
+	return valid, em, index, nil
 }
 
 // nonZeroRandomBytes fills the given slice with non-zero random octets.

src/pkg/crypto/rsa/pkcs1v15_test.go

--- a/src/pkg/crypto/rsa/pkcs1v15_test.go
+++ b/src/pkg/crypto/rsa/pkcs1v15_test.go
@@ -227,6 +227,26 @@ func TestUnpaddedSignature(t *testing.T) {
 	}\
 }\
 
+func TestShortSessionKey(t *testing.T) {
+	// This tests that attempting to decrypt a session key where the
+	// ciphertext is too small doesn't run outside the array bounds.
+	ciphertext, err := EncryptPKCS1v15(rand.Reader, &rsaPrivateKey.PublicKey, []byte{1})
+	if err != nil {
+		t.Fatalf("Failed to encrypt short message: %s", err)
+	}
+
+	var key [32]byte
+	if err := DecryptPKCS1v15SessionKey(nil, rsaPrivateKey, ciphertext, key[:]); err != nil {
+		t.Fatalf("Failed to decrypt short message: %s", err)
+	}
+
+	for _, v := range key {
+		if v != 0 {
+			t.Fatal("key was modified when ciphertext was invalid")
+		}
+	}
+}
+
 // In order to generate new test vectors you'll need the PEM form of this key:
 // -----BEGIN RSA PRIVATE KEY-----
 // MIIBOgIBAJBALKZD0nEffqM1ACuak0bijtqE2QrI/KLADv7l3kK3ppMyCuLKoF0

src/pkg/crypto/subtle/constant_time.go

--- a/src/pkg/crypto/subtle/constant_time.go
+++ b/src/pkg/crypto/subtle/constant_time.go
@@ -49,9 +49,14 @@ func ConstantTimeEq(x, y int32) int {
 	return int(z & 1)
 }\
 
-// ConstantTimeCopy copies the contents of y into x iff v == 1. If v == 0, x is left unchanged.\
-// Its behavior is undefined if v takes any other value.\
+// ConstantTimeCopy copies the contents of y into x (a slice of equal length)\
+// if v == 1. If v == 0, x is left unchanged. Its behavior is undefined if v\
+// takes any other value.\
 func ConstantTimeCopy(v int, x, y []byte) {
+\tif len(x) != len(y) {
+\t\tpanic("subtle: slices have different lengths")
+\t}\
+\
 	xmask := byte(v - 1)\
 	ymask := byte(^(v - 1))\
 	for i := 0; i < len(x); i++ {

コアとなるコードの解説

decryptPKCS1v15 関数のシグネチャと戻り値の変更

  • 変更前: func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid int, msg []byte, err error)
  • 変更後: func decryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) (valid int, em []byte, index int, err error)

最も重要な変更は、msg []byte の代わりに em []byte, index int が返されるようになった点です。

  • em は、復号された完全なエンコード済みメッセージ(パディングを含む)を表します。復号が成功したかどうかにかかわらず、常にこの完全なスライスが返されることで、メモリアクセスパターンが一定に保たれ、サイドチャネル攻撃のリスクが低減されます。
  • index は、em スライス内で実際のメッセージが始まるオフセットを示します。これにより、呼び出し元は em[index:] のようにしてメッセージを安全に抽出できます。

DecryptPKCS1v15 および DecryptPKCS1v15SessionKey での index の利用

decryptPKCS1v15 から返された index を利用して、メッセージの抽出方法が変更されました。

  • DecryptPKCS1v15 内:

    	valid, out, index, err := decryptPKCS1v15(rand, priv, ciphertext)
    	// ...
    	out = out[index:]
    

    復号された out スライスから、index を使って実際のメッセージ部分を切り出しています。これにより、パディング部分が安全に除去されます。

  • DecryptPKCS1v15SessionKey 内:

    	valid, em, index, err := decryptPKCS1v15(rand, priv, ciphertext)
    	// ...
    	valid &= subtle.ConstantTimeEq(int32(len(em)-index), int32(len(key)))
    	subtle.ConstantTimeCopy(valid, key, em[len(em)-len(key):])
    

    ここで、len(em)-indexem スライス内の実際のメッセージの長さを表します。これを期待されるセッションキーの長さ len(key) と比較し、valid フラグを更新します。 そして、subtle.ConstantTimeCopy に渡すソーススライスを em[len(em)-len(key):] とすることで、常に key と同じ長さのソーススライスが提供されるようにしています。これにより、ConstantTimeCopy 内部での境界外アクセスが防止されます。

ConstantTimeCopy の長さチェック追加

func ConstantTimeCopy(v int, x, y []byte) {
	if len(x) != len(y) {
		panic("subtle: slices have different lengths")
	}
	// ...
}

ConstantTimeCopy 関数に、コピー元 (y) とコピー先 (x) のスライスの長さが異なる場合に panic を発生させるチェックが追加されました。これは、ConstantTimeCopy が設計上、同じ長さのスライス間で動作することを前提としているため、誤った使用を防ぐための防御的な措置です。これにより、開発者が誤って異なる長さのスライスを渡した場合に、早期に問題を検出し、より深刻なランタイムエラーやセキュリティ脆弱性(今回の境界外アクセスのような)に発展するのを防ぎます。

TestShortSessionKey の追加

新しいテストケース TestShortSessionKeypkcs1v15_test.go に追加されました。このテストは、短いセッションキー(ここでは1バイトのメッセージを暗号化したもの)を DecryptPKCS1v15SessionKey に渡した場合に、境界外アクセスが発生しないことを確認します。また、復号が失敗した場合に key バッファが変更されないことも検証しています。これは、定数時間操作の重要な側面であり、復号の成否にかかわらず、秘密情報が漏洩しないことを保証するためです。

これらの変更により、Goの crypto/rsa パッケージは、短いセッションキーが与えられた場合の境界外アクセスという脆弱性から保護され、より堅牢な定数時間暗号操作が保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴: https://github.com/golang/go/commits/master
  • Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/102670044 はGerritの変更リストへのリンクです)
  • サイドチャネル攻撃に関する一般的な情報源 (例: Wikipedia, OWASPなど)
  • 定数時間プログラミングに関する情報源 (例: 暗号ライブラリのドキュメント、セキュリティブログなど)
  • RSA暗号とPKCS#1 v1.5パディングに関する一般的な暗号学の教科書やオンラインリソース。
  • Cedric Staub氏のセキュリティ研究に関する情報 (公開されている場合)
  • Go言語の crypto/rsa および crypto/subtle パッケージのソースコード。
  • Go言語の公式ブログやセキュリティアドバイザリ (該当する場合)。
  • Google検索: "Go crypto/rsa out of bounds", "PKCS#1 v1.5 padding", "constant-time cryptography", "side-channel attacks"