[インデックス 13457] ファイルの概要
このコミットは、Go言語の標準ライブラリであるcrypto/rsa
パッケージ内のpkcs1v15.go
ファイルに対する変更です。このファイルは、RSA暗号におけるPKCS#1 v1.5パディングスキームの実装を含んでいます。具体的には、RSAの暗号化(EncryptPKCS1v15
)と署名(SignPKCS1v15
)の処理において、生成されるバイト列の長さを調整するための修正が行われています。
コミット
commit 93ea79ee7ed859287e6adc51ab04028e972403e1
Author: Adam Langley <agl@golang.org>
Date: Wed Jul 11 12:47:12 2012 -0400
crypto/rsa: left-pad PKCS#1 v1.5 outputs.
OpenSSL requires that RSA signatures be exactly the same byte-length
as the modulus. Currently it'll reject ~1/256 of our signatures: those
that end up a byte shorter.
Fixes #3796.
R=golang-dev, edsrzf, r
CC=golang-dev
https://golang.org/cl/6352093
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/93ea79ee7ed859287e6adc51ab04028e972403e1
元コミット内容
crypto/rsa: left-pad PKCS#1 v1.5 outputs.
OpenSSL requires that RSA signatures be exactly the same byte-length
as the modulus. Currently it'll reject ~1/256 of our signatures: those
that end up a byte shorter.
Fixes #3796.
R=golang-dev, edsrzf, r
CC=golang-dev
https://golang.org/cl/6352093
変更の背景
この変更の背景には、Go言語のcrypto/rsa
パッケージが生成するRSA署名が、一部のケースでOpenSSLによって拒否されるという互換性の問題がありました。
RSA署名や暗号化の処理では、結果として得られる数値(big.Int
型で表現される)をバイト列に変換します。このバイト列の長さは、使用されるRSA鍵のモジュラス(n
)のバイト長と厳密に一致する必要があります。PKCS#1 v1.5パディングスキームでは、メッセージを特定の構造(EM: Encoded Message)に変換し、そのEMをRSA処理の入力とします。EMの構造は、通常、モジュラス長と同じバイト長になるように設計されています。
しかし、Go言語のmath/big
パッケージにあるbig.Int.Bytes()
メソッドは、数値の先頭にあるゼロバイトを自動的に省略する特性があります。例えば、0x00FF
という値は[0xFF]
として表現されることがあります。PKCS#1 v1.5のEM構造では、特に署名の場合、先頭に0x00
バイトが付加されることが期待されます(例: 0x00 || 0x01 || PS || 0x00 || H
)。もし、RSA処理の結果が、この先頭の0x00
バイトを含めても、big.Int.Bytes()
によってその0x00
が省略されてしまうと、最終的な出力バイト列が期待されるモジュラス長よりも1バイト短くなってしまうケースが発生します。
OpenSSLのような厳格な暗号ライブラリは、RSA署名のバイト長がモジュラス長と完全に一致することを要求します。そのため、Goのcrypto/rsa
パッケージが生成した署名が1バイト短い場合、OpenSSLはそれを無効な署名として拒否していました。コミットメッセージにある「~1/256 of our signatures」という記述は、big.Int.Bytes()
が先頭のゼロを省略するかどうかが、数値の最上位バイトに依存するため、約256回に1回程度の頻度でこの問題が発生していたことを示唆しています。
この問題は、GoのIssue #3796として報告され、このコミットによって解決されました。
前提知識の解説
PKCS#1 v1.5 Padding Scheme
PKCS#1 v1.5は、RSA暗号化および署名のためのパディングスキームを定義する標準です。これにより、RSAのセキュリティが向上し、特定の攻撃を防ぐことができます。
暗号化 (Encryption) におけるEM構造:
PKCS#1 v1.5のRSAES-PKCS1-V1_5スキームでは、暗号化されるメッセージM
は、以下の形式のエンコードされたメッセージEM
に変換されます。
EM = 0x00 || 0x02 || PS || 0x00 || M
0x00
: 常に先頭に付加されるバイト。0x02
: パディングタイプを示すバイト。暗号化では0x02
が使用されます。PS
(Padding String): 少なくとも8バイトのランダムな非ゼロバイト列。0x00
: メッセージM
の開始を示す区切りバイト。M
: 暗号化される実際のメッセージ。
このEM
の全長は、RSA鍵のモジュラス長(バイト単位)と一致するように調整されます。
署名 (Signature) におけるEM構造:
PKCS#1 v1.5のRSASSA-PKCS1-V1_5スキームでは、署名されるハッシュ値H
は、以下の形式のエンコードされたメッセージEM
に変換されます。
EM = 0x00 || 0x01 || PS || 0x00 || T
0x00
: 常に先頭に付加されるバイト。0x01
: パディングタイプを示すバイト。署名では0x01
が使用されます。PS
(Padding String): 全て0xFF
バイトで埋められたバイト列。0x00
:T
の開始を示す区切りバイト。T
(DigestInfo): ハッシュアルゴリズムの識別子とハッシュ値を含む構造体。
同様に、このEM
の全長もモジュラス長と一致するように調整されます。
RSA署名とモジュラス長
RSA署名は、メッセージのハッシュ値を秘密鍵で暗号化することで生成されます。この「暗号化」された結果は、通常、RSA鍵のモジュラスn
と同じバイト長を持つ必要があります。例えば、2048ビットのRSA鍵であれば、モジュラス長は256バイト(2048/8)になります。生成された署名がこの長さに満たない場合、検証側(特にOpenSSLのような厳格な実装)はそれを不正な署名として扱います。
Goのmath/big
パッケージとbig.Int.Bytes()
Go言語のmath/big
パッケージは、任意精度の整数を扱うための機能を提供します。big.Int
型は、非常に大きな数値を扱う際に使用されます。
big.Int.Bytes()
メソッドは、big.Int
の値をビッグエンディアンのバイト列として返します。このメソッドの重要な特性は、結果のバイト列から先頭のゼロバイトを省略する(トリムする)ことです。
例:
big.NewInt(0x1234).Bytes()
は[]byte{0x12, 0x34}
を返します。big.NewInt(0x001234).Bytes()
も[]byte{0x12, 0x34}
を返します。big.NewInt(0x00).Bytes()
は[]byte{}
を返します。
この挙動は、数値の表現としては正しいですが、固定長を要求される暗号プロトコルにおいては問題となることがあります。特に、PKCS#1 v1.5のEM構造のように、先頭に0x00
バイトが必須であるにもかかわらず、big.Int
の計算結果がたまたまその0x00
バイトのみで構成される(つまり、数値が非常に小さい)場合、Bytes()
メソッドがその0x00
を省略してしまい、結果としてバイト列が1バイト短くなってしまうのです。
OpenSSL
OpenSSLは、SSL/TLSプロトコルや暗号化機能を提供するオープンソースのツールキットです。多くのアプリケーションやシステムで利用されており、暗号処理の標準的な実装として広く認識されています。OpenSSLは、セキュリティ上の理由から、暗号プロトコルの仕様に対して非常に厳格なチェックを行います。この厳格さが、Goのcrypto/rsa
パッケージが生成する署名のバイト長がわずかに異なる場合に、互換性の問題を引き起こしていました。
技術的詳細
このコミットの技術的な核心は、Goのmath/big.Int.Bytes()
メソッドの挙動と、PKCS#1 v1.5パディングスキーム、そしてOpenSSLの厳格な要件との間の不整合を解消することにあります。
RSA暗号化および署名処理では、最終的にbig.Int
型の数値c
(暗号化結果)またはs
(署名結果)が得られます。この数値は、PKCS#1 v1.5のEM構造を整数として解釈したものです。このEM構造は、常にモジュラス長と同じバイト長を持つように設計されています。しかし、c.Bytes()
やs.Bytes()
を直接呼び出すと、big.Int.Bytes()
の特性により、先頭のゼロバイトが省略される可能性があります。
具体的には、EM構造の先頭には常に0x00
バイトが付加されます。例えば、2048ビットのRSA鍵(モジュラス長256バイト)の場合、EMは256バイトのバイト列になります。この256バイトのバイト列をbig.Int
として解釈し、再度バイト列に戻す際に、もしそのbig.Int
の値が2^(2048-8)
(つまり、先頭の8ビットが全てゼロ)よりも小さい場合、big.Int.Bytes()
は先頭の0x00
バイトを省略して255バイトのバイト列を返してしまうことがあります。
このコミットは、この問題を解決するために、EncryptPKCS1v15
関数とSignPKCS1v15
関数の両方で、big.Int.Bytes()
の出力に対して明示的に「左パディング」(先頭にゼロを追加して固定長にする)を行うように変更しました。
新しいヘルパー関数copyWithLeftPad
が導入され、big.Int.Bytes()
が返したバイト列を、期待されるモジュラス長(em
スライスの長さ)に合わせて先頭にゼロを埋める処理を行います。これにより、生成される暗号文や署名が常に正しいバイト長を持つことが保証され、OpenSSLを含む他のシステムとの互換性が確保されます。
コアとなるコードの変更箇所
変更はsrc/pkg/crypto/rsa/pkcs1v15.go
ファイルに集中しています。
EncryptPKCS1v15
関数
--- a/src/pkg/crypto/rsa/pkcs1v15.go
+++ b/src/pkg/crypto/rsa/pkcs1v15.go
@@ -25,10 +25,10 @@ func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) (out []byte, er
return
}
- // EM = 0x02 || PS || 0x00 || M
- em := make([]byte, k-1)
- em[0] = 2
- ps, mm := em[1:len(em)-len(msg)-1], em[len(em)-len(msg):]
+ // EM = 0x00 || 0x02 || PS || 0x00 || M
+ em := make([]byte, k)
+ em[1] = 2
+ ps, mm := em[2:len(em)-len(msg)-1], em[len(em)-len(msg):]
err = nonZeroRandomBytes(ps, rand)
if err != nil {
return
@@ -38,7 +38,9 @@ func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) (out []byte, er
m := new(big.Int).SetBytes(em)
c := encrypt(new(big.Int), pub, m)
- out = c.Bytes()
+
+ copyWithLeftPad(em, c.Bytes())
+ out = em
return
}
- コメントが
// EM = 0x02 || PS || 0x00 || M
から// EM = 0x00 || 0x02 || PS || 0x00 || M
に変更され、EM構造の先頭に0x00
バイトが明示されました。 em
スライスの作成サイズがk-1
からk
に変更され、1バイト分大きくなりました。k
はモジュラスのバイト長です。em[0] = 2
がem[1] = 2
に変更され、0x02
バイトの位置が1つ右にシフトしました。これは、em[0]
が0x00
のために予約されたためです。ps
とmm
スライスの開始インデックスが1
から2
に変更され、同様にシフトされました。out = c.Bytes()
の代わりに、新しく追加されたcopyWithLeftPad(em, c.Bytes())
が呼び出され、その結果がout = em
として代入されるようになりました。これにより、c.Bytes()
の出力がem
の長さに合わせてパディングされます。
SignPKCS1v15
関数
--- a/src/pkg/crypto/rsa/pkcs1v15.go
+++ b/src/pkg/crypto/rsa/pkcs1v15.go
@@ -185,9 +187,12 @@ func SignPKCS1v15(rand io.Reader, priv *PrivateKey, hash crypto.Hash, hashed []b
m := new(big.Int).SetBytes(em)
c, err := decrypt(rand, priv, m)
- if err == nil {
- s = c.Bytes()
+ if err != nil {
+ return
}
+
+ copyWithLeftPad(em, c.Bytes())
+ s = em
return
}
s = c.Bytes()
の代わりに、copyWithLeftPad(em, c.Bytes())
が呼び出され、その結果がs = em
として代入されるようになりました。これにより、署名結果がem
の長さに合わせてパディングされます。
新しいヘルパー関数 copyWithLeftPad
--- a/src/pkg/crypto/rsa/pkcs1v15.go
+++ b/src/pkg/crypto/rsa/pkcs1v15.go
@@ -241,3 +246,13 @@ func pkcs1v15HashInfo(hash crypto.Hash, inLen int) (hashLen int, prefix []byte,
}\n \treturn\n }\n+\n+// copyWithLeftPad copies src to the end of dest, padding with zero bytes as\n+// needed.\n+func copyWithLeftPad(dest, src []byte) {\n+\tnumPaddingBytes := len(dest) - len(src)\n+\tfor i := 0; i < numPaddingBytes; i++ {\n+\t\tdest[i] = 0\n+\t}\n+\tcopy(dest[numPaddingBytes:], src)\n+}\n```
- `copyWithLeftPad`という新しい関数が追加されました。
- この関数は、`dest`(宛先スライス)と`src`(ソーススライス)の2つのバイトスライスを引数に取ります。
- `numPaddingBytes`は、`dest`の長さと`src`の長さの差を計算し、必要なパディングバイト数を決定します。
- 最初の`for`ループで、`dest`の先頭から`numPaddingBytes`の数だけ`0`バイトで埋めます。
- `copy(dest[numPaddingBytes:], src)`で、`src`の内容を`dest`のパディングバイトの直後からコピーします。
## コアとなるコードの解説
このコミットの核心は、`copyWithLeftPad`関数の導入と、それが`EncryptPKCS1v15`および`SignPKCS1v15`関数でどのように利用されているかにあります。
以前の実装では、RSAの暗号化または復号(署名生成)の結果として得られた`big.Int`型の数値`c`を、直接`c.Bytes()`でバイト列に変換し、それを出力としていました。しかし、前述の通り、`big.Int.Bytes()`は先頭のゼロを省略するため、結果のバイト列がモジュラス長よりも短くなる可能性がありました。
新しい実装では、まず`em`という名前のバイトスライスを、期待されるモジュラス長`k`と同じサイズで作成します。この`em`スライスは、PKCS#1 v1.5のEM構造の最終的なバイト表現を保持するためのバッファとして機能します。
`EncryptPKCS1v15`では、`em`スライスの`em[0]`を`0x00`として扱い、`em[1]`に`0x02`を設定することで、EM構造の先頭バイトを明示的に制御します。同様に、`SignPKCS1v15`でも、`em`スライスが最終的な署名結果のバッファとして使用されます。
そして、RSA処理の結果である`c.Bytes()`(これは先頭のゼロが省略されている可能性がある)を、`copyWithLeftPad(em, c.Bytes())`に渡します。
`copyWithLeftPad`関数は、`em`スライスを宛先とし、`c.Bytes()`の出力をソースとします。この関数は、`em`の長さと`c.Bytes()`の長さの差を計算し、その差分だけ`em`の先頭をゼロで埋めます。その後、`c.Bytes()`の内容を、ゼロパディングの直後から`em`にコピーします。
これにより、`em`スライスは常にモジュラス長と同じバイト長を持ち、かつ、`c.Bytes()`の実際の値が正しく格納され、必要に応じて先頭がゼロでパディングされた状態になります。最終的に、この`em`スライスが`out`(暗号化結果)または`s`(署名結果)として返されることで、OpenSSLが要求する厳密なバイト長要件が満たされるようになります。
この修正は、Goの`crypto/rsa`パッケージが生成するRSA署名および暗号文の互換性を大幅に向上させ、他の暗号ライブラリとの相互運用性を確保するために不可欠なものでした。
## 関連リンク
- **Go Issue #3796**: [https://code.google.com/p/go/issues/detail?id=3796](https://code.google.com/p/go/issues/detail?id=3796) (現在はGitHubに移行しているため、直接のリンクは機能しない可能性がありますが、当時のIssue番号です)
- **Gerrit Change-Id**: `https://golang.org/cl/6352093` (GoプロジェクトのGerritコードレビューシステムへのリンク)
## 参考にした情報源リンク
- **RFC 3447: PKCS #1: RSA Cryptography Specifications Version 2.1**: [https://datatracker.ietf.org/doc/html/rfc3447](https://datatracker.ietf.org/doc/html/rfc3447) (PKCS#1 v1.5パディングスキームの詳細な仕様)
- **Go言語 `math/big` パッケージ ドキュメント**: [https://pkg.go.dev/math/big](https://pkg.go.dev/math/big) (`big.Int.Bytes()`の挙動に関する情報)
- **OpenSSL ドキュメント**: [https://www.openssl.org/docs/](https://www.openssl.org/docs/) (OpenSSLのRSA実装に関する一般的な情報)
- **Go言語 `crypto/rsa` パッケージ ドキュメント**: [https://pkg.go.dev/crypto/rsa](https://pkg.go.dev/crypto/rsa) (GoのRSA実装に関する情報)