[インデックス 19659] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/rsa
パッケージにおけるセキュリティ上の脆弱性を修正するものです。具体的には、RSA PKCS#1 v1.5パディングスキームを用いた復号処理において、短いセッションキーが与えられた場合に発生する境界外アクセス(out-of-bounds access)のバグを修正しています。この修正は、src/pkg/crypto/rsa/pkcs1v15.go
、src/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
を返していました。しかし、サイドチャネル攻撃を防ぐためには、復号が成功したかどうかにかかわらず、常に同じメモリパターンで処理を進める必要があります。
修正後、decryptPKCS1v15
は valid int, em []byte, index int, err error
という新しい戻り値のシグネチャを持つようになりました。
em []byte
: これは、パディングを含む復号された完全なエンコード済みメッセージ(em
)です。復号が成功したかどうかにかかわらず、常にこの完全なスライスが返されます。これにより、メモリアクセスパターンが一定に保たれます。index int
:valid
が1(成功)の場合、このindex
は元のメッセージがem
内のどこから始まるかを示します。valid
が0(失敗)の場合、index
の値は意味を持ちませんが、定数時間操作のために計算は行われます。
この変更により、DecryptPKCS1v15
や DecryptPKCS1v15SessionKey
の呼び出し元は、valid
フラグと index
を利用して、復号されたメッセージを安全に抽出できるようになりました。特に、index = subtle.ConstantTimeSelect(valid, index+1, 0)
の行は、valid
の値に応じて index
を選択的に設定することで、定数時間性を維持しながらメッセージの開始位置を決定しています。
DecryptPKCS1v15SessionKey
関数の変更
この関数は、RSAで暗号化されたセッションキーを復号し、指定された key
バッファにコピーする役割を担っています。元の実装では、decryptPKCS1v15
から返された msg
の長さが key
の長さと一致するかを ConstantTimeEq
でチェックし、その後 ConstantTimeCopy
で msg
を key
にコピーしていました。
問題は、decryptPKCS1v15
が返す msg
が、パディングの構造が不正な場合に短いスライスになる可能性があったことです。この短いスライスを ConstantTimeCopy
に渡すと、ConstantTimeCopy
内部で境界外アクセスが発生する可能性がありました。
修正後、DecryptPKCS1v15SessionKey
は decryptPKCS1v15
から返される完全な 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
関数には、引数として渡されるスライス x
と y
の長さが異なる場合に 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
スライスを返し、DecryptPKCS1v15SessionKey
が em
スライスから安全な方法でメッセージ部分を抽出して 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)-index
はem
スライス内の実際のメッセージの長さを表します。これを期待されるセッションキーの長さ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
の追加
新しいテストケース TestShortSessionKey
が pkcs1v15_test.go
に追加されました。このテストは、短いセッションキー(ここでは1バイトのメッセージを暗号化したもの)を DecryptPKCS1v15SessionKey
に渡した場合に、境界外アクセスが発生しないことを確認します。また、復号が失敗した場合に key
バッファが変更されないことも検証しています。これは、定数時間操作の重要な側面であり、復号の成否にかかわらず、秘密情報が漏洩しないことを保証するためです。
これらの変更により、Goの crypto/rsa
パッケージは、短いセッションキーが与えられた場合の境界外アクセスという脆弱性から保護され、より堅牢な定数時間暗号操作が保証されるようになりました。
関連リンク
- Go言語の
crypto
パッケージのドキュメント: https://pkg.go.dev/crypto/rsa - Go言語の
crypto/subtle
パッケージのドキュメント: https://pkg.go.dev/crypto/subtle - PKCS #1 v2.2: RSA Cryptography Standard (RFC 8017): https://datatracker.ietf.org/doc/html/rfc8017 (PKCS#1 v1.5はこれの古いバージョンで定義されていますが、このRFCが最新の参照点です)
参考にした情報源リンク
- 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"