[インデックス 14339] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/x509
パッケージ内の pem_decrypt.go
ファイルと、そのテストファイルである pem_decrypt_test.go
に関連する変更を含んでいます。
pem_decrypt.go
は、PEM (Privacy-Enhanced Mail) 形式で暗号化されたブロック(通常は秘密鍵など)を復号化するためのロジックを実装しています。特に、RFC 1423で定義されているパディングスキームと、DES、Triple DES、AESなどの一般的なブロック暗号に対応しています。
pem_decrypt_test.go
は、pem_decrypt.go
で実装された復号化機能が正しく動作するかを検証するための単体テストを含んでいます。このコミットでは、既存のテストに加えて、修正されたロジックを検証するための新しいテストケースが追加されています。
コミット
commit 768ba46cc1bbff1c8f49f395d551ffb75b0b6bdf
Author: Roger Peppe <rogpeppe@gmail.com>
Date: Wed Nov 7 15:16:34 2012 +0000
crypto/x509: fix DecryptPEMBlock
The current implement can fail when the
block size is not a multiple of 8 bytes.
This CL makes it work, and also checks that the
data is in fact a multiple of the block size.
R=agl, agl
CC=golang-dev
https://golang.org/cl/6827058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/768ba46cc1bbff1c8f49f395d551ffb75b0b6bdf
元コミット内容
crypto/x509: fix DecryptPEMBlock
現在の実装は、ブロックサイズが8バイトの倍数でない場合に失敗する可能性があります。 この変更はそれを機能させ、データが実際にブロックサイズの倍数であることもチェックします。
変更の背景
このコミットの背景には、crypto/x509
パッケージの DecryptPEMBlock
関数が、特定の条件下でPEM形式で暗号化されたデータを正しく復号化できないというバグが存在していました。具体的には、復号化対象のデータブロックのサイズが、使用されているブロック暗号のブロックサイズ(例えばAESの場合は16バイト)の倍数ではない場合に、パディングの検証ロジックが誤動作し、復号化が失敗するという問題でした。
元の実装では、パディングの検証がDES暗号(ブロックサイズ8バイト)を前提としたハードコードされた値(最大8バイトのパディング)に依存していました。このため、AESなど8バイト以外のブロックサイズを持つ暗号が使用された場合、復号化されたデータのパディングが正しく処理されず、「不正なパディング」または「パスワードが間違っている」という誤ったエラーが返される可能性がありました。
このコミットは、このパディング検証の不備を修正し、より汎用的な方法でブロックサイズに応じたパディングチェックを行うことで、異なるブロックサイズを持つ暗号でも DecryptPEMBlock
が堅牢に動作するように改善することを目的としています。
前提知識の解説
PEM (Privacy-Enhanced Mail)
PEMは、公開鍵証明書、秘密鍵、CSR (Certificate Signing Request) などの暗号化データをASCIIテキスト形式で表現するためのエンコーディング形式です。通常、-----BEGIN ...-----
と -----END ...-----
というヘッダとフッタで囲まれたBase64エンコードされたデータで構成されます。秘密鍵などの機密性の高いデータは、PEMブロック内でパスワードによって暗号化されることがあります。
X.509
X.509は、公開鍵証明書のフォーマットを定義するITU-Tの標準規格です。SSL/TLS、VPN、コード署名など、様々なセキュリティプロトコルで利用されています。Go言語の crypto/x509
パッケージは、X.509証明書の解析、生成、検証などの機能を提供します。
DEK-Info (Data Encryption Key Info)
PEMブロックがパスワードで暗号化されている場合、その暗号化に使用されたアルゴリズムと初期化ベクトル (IV) の情報が DEK-Info
ヘッダとしてPEMブロック内に含まれることがあります。例えば、DEK-Info: AES-128-CBC,74611ABC2571AF11B1BF9B69E62C89E7
のように記述され、AES-128-CBC
が暗号化アルゴリズム、その後の16進数文字列がIVを示します。
ブロック暗号 (Block Cipher)
ブロック暗号は、データを固定長のブロック単位で暗号化・復号化する対称鍵暗号方式です。DES (Data Encryption Standard)、Triple DES (3DES)、AES (Advanced Encryption Standard) などが代表的です。各ブロック暗号には固有の「ブロックサイズ」があり、例えばDESは8バイト、AESは16バイトです。
CBC (Cipher Block Chaining) モード
CBCは、ブロック暗号の運用モードの一つです。各平文ブロックは、暗号化される前に前の暗号文ブロックとXORされます。これにより、同じ平文ブロックが複数回出現しても、異なる暗号文ブロックが生成され、パターン攻撃に対する耐性が向上します。CBCモードでは、最初のブロックを暗号化するために「初期化ベクトル (IV)」が必要です。
パディング (Padding)
ブロック暗号では、平文の長さがブロックサイズの厳密な倍数である必要があります。もし平文の長さがブロックサイズの倍数でない場合、最後のブロックをブロックサイズに合わせるために、追加のバイト(パディング)が挿入されます。復号化後、このパディングは除去される必要があります。
RFC 1423 パディングスキーム
RFC 1423で定義されているパディングスキームは、PKCS#5/PKCS#7パディングに似ています。このスキームでは、パディングバイトの値がパディングの長さを示します。 例えば、ブロックサイズが8バイトの場合:
- 平文がブロックサイズの倍数で、パディングが不要な場合でも、常に1ブロック分のパディングが追加されます(値は8)。
- 平文の最後のブロックが5バイトで、あと3バイト必要なら、
[... 0x03 0x03 0x03]
のように3バイトの0x03
が追加されます。 - 復号化後、最後のバイトを読み取り、その値がパディングの長さであると判断し、その長さ分のバイトをデータから除去します。 このパディングスキームの重要な特性は、パディングの長さが1バイトからブロックサイズまでの範囲であることです。また、パディングバイトはすべて同じ値である必要があります。
技術的詳細
DecryptPEMBlock
関数は、PEM形式で暗号化されたデータブロックとパスワードを受け取り、復号化された平文データを返します。この関数は、以下の主要なステップで動作します。
- DEK-Infoの解析: PEMブロックのヘッダから
DEK-Info
を抽出し、暗号化アルゴリズム(例:AES-128-CBC
)と初期化ベクトル (IV) を取得します。 - 鍵導出: パスワードとIVの一部(ソルトとして使用)から、暗号化に使用する鍵を導出します。
- ブロック暗号の初期化: 取得したアルゴリズムと導出した鍵、IVを使用して、適切なブロック暗号(DES、AESなど)とCBCデクリプターを初期化します。
- データ復号化: 暗号化されたPEMブロックのバイト列を、初期化したデクリプターで復号化します。
- パディング除去: 復号化されたデータから、RFC 1423スキームに従ってパディングを除去します。
このコミット以前のバグは、ステップ5のパディング除去ロジックにありました。具体的には、パディングの検証において、パディングの最大長が常に8バイトであると仮定していました。これはDES暗号のブロックサイズ(8バイト)には合致しますが、AES暗号(ブロックサイズ16バイト)のような異なるブロックサイズを持つ暗号では問題となります。
修正の核心は、パディングの最大長をハードコードされた 8
ではなく、実際に使用されているブロック暗号の BlockSize()
メソッドから動的に取得した値を使用するように変更した点です。これにより、パディング検証ロジックが汎用化され、DES、AESなど、異なるブロックサイズを持つすべてのサポート対象暗号に対して正しく機能するようになりました。
さらに、復号化されたデータの長さがブロックサイズの倍数であるかどうかのチェックも追加されました。これは、パディングが正しく適用されていることを保証するための重要な健全性チェックであり、不正なデータや攻撃に対する堅牢性を高めます。もし復号化されたデータの長さがブロックサイズの倍数でない場合、それはパディングが破損しているか、データ自体が不正であることを意味するため、「無効なパディング」エラーを返します。
コアとなるコードの変更箇所
src/pkg/crypto/x509/pem_decrypt.go
の変更が主要です。
-
rfc1423Algos
マップの移動:--- a/src/pkg/crypto/x509/pem_decrypt.go +++ b/src/pkg/crypto/x509/pem_decrypt.go @@ -25,6 +25,16 @@ type rfc1423Algo struct { keySize int }\n +// rfc1423Algos is a mapping of encryption algorithm to an rfc1423Algo that can +// create block ciphers for that mode. +var rfc1423Algos = map[string]rfc1423Algo{ + "DES-CBC": {des.NewCipher, 8}, + "DES-EDE3-CBC": {des.NewTripleDESCipher, 24}, + "AES-128-CBC": {aes.NewCipher, 16}, + "AES-192-CBC": {aes.NewCipher, 24}, + "AES-256-CBC": {aes.NewCipher, 32}, +}\n+ // deriveKey uses a key derivation function to stretch the password into a key // with the number of bits our cipher requires. This algorithm was derived from // the OpenSSL source. @@ -45,16 +55,6 @@ func (c rfc1423Algo) deriveKey(password, salt []byte) []byte { return out }\n -// rfc1423Algos is a mapping of encryption algorithm to an rfc1423Algo that can -// create block ciphers for that mode. -// var rfc1423Algos = map[string]rfc1423Algo{ -// "DES-CBC": {des.NewCipher, 8}, -// "DES-EDE3-CBC": {des.NewTripleDESCipher, 24}, -// "AES-128-CBC": {aes.NewCipher, 16}, -// "AES-192-CBC": {aes.NewCipher, 24}, -// "AES-256-CBC": {aes.NewCipher, 32}, -// }\n- // IsEncryptedPEMBlock returns if the PEM block is password encrypted. func IsEncryptedPEMBlock(b *pem.Block) bool { _, ok := b.Headers["DEK-Info"]
rfc1423Algos
マップの定義がファイルの少し上の位置に移動されました。これは機能的な変更ではなく、コードの整理(リファクタリング)です。 -
rfc1423Algos
ルックアップとエラーチェックの順序変更:--- a/src/pkg/crypto/x509/pem_decrypt.go +++ b/src/pkg/crypto/x509/pem_decrypt.go @@ -81,6 +81,10 @@ func DecryptPEMBlock(b *pem.Block, password []byte) ([]byte, error) { }\n mode, hexIV := dek[:idx], dek[idx+1:] + ciph, ok := rfc1423Algos[mode] + if !ok { + return nil, errors.New("x509: unknown encryption mode") + }\n iv, err := hex.DecodeString(hexIV) if err != nil { return nil, err @@ -89,11 +93,6 @@ func DecryptPEMBlock(b *pem.Block, password []byte) ([]byte, error) { return nil, errors.New("x509: not enough bytes in IV") }\n - ciph, ok := rfc1423Algos[mode] - if !ok { - return nil, errors.New("x509: unknown encryption mode") - }\n- // Based on the OpenSSL implementation. The salt is the first 8 bytes // of the initialization vector. key := ciph.deriveKey(password, iv[:8])
rfc1423Algos
マップから暗号情報を取得する処理と、それが成功したかどうかのチェックが、IV (Initialization Vector) のデコード処理よりも前に行われるようになりました。これにより、未知の暗号モードが指定された場合に、より早くエラーを検出できるようになります。 -
パディング検証ロジックの修正:
--- a/src/pkg/crypto/x509/pem_decrypt.go +++ b/src/pkg/crypto/x509/pem_decrypt.go @@ -107,27 +106,27 @@ func DecryptPEMBlock(b *pem.Block, password []byte) ([]byte, error) { dec.CryptBlocks(data, b.Bytes) // Blocks are padded using a scheme where the last n bytes of padding are all - // equal to n. It can pad from 1 to 8 bytes inclusive. See RFC 1423. + // equal to n. It can pad from 1 to blocksize bytes inclusive. See RFC 1423. // For example:\n //\t[x y z 2 2] //\t[x y 7 7 7 7 7 7 7] // If we detect a bad padding, we assume it is an invalid password. dlen := len(data) - if dlen == 0 {\n+ blockSize := block.BlockSize() + if dlen == 0 || dlen%blockSize != 0 {\n return nil, errors.New("x509: invalid padding") }\n- last := data[dlen-1] - if dlen < int(last) {\n+ last := int(data[dlen-1]) + if dlen < last {\n return nil, IncorrectPasswordError }\n- if last == 0 || last > 8 {\n+ if last == 0 || last > blockSize {\n return nil, IncorrectPasswordError }\n- for _, val := range data[dlen-int(last):] {\n- \tif val != last {\n+ for _, val := range data[dlen-last:] {\n+ \tif int(val) != last {\n \treturn nil, IncorrectPasswordError }\n }\n-\n- return data[:dlen-int(last)], nil + return data[:dlen-last], nil }
これが最も重要な変更点です。
blockSize := block.BlockSize()
を導入し、使用されているブロック暗号の実際のブロックサイズを取得するようにしました。- 復号化されたデータの長さ
dlen
が0
であるか、またはblockSize
の倍数でない場合にx509: invalid padding
エラーを返すチェックdlen%blockSize != 0
を追加しました。 - パディングの長さ
last
が0
であるか、またはblockSize
を超える場合にIncorrectPasswordError
を返すように、last > 8
をlast > blockSize
に変更しました。これにより、パディングの最大長が動的に決定されるようになりました。 last
変数の型変換がint(last)
からint(data[dlen-1])
に変更され、より明確になりました。- パディングバイトの検証ループ
for _, val := range data[dlen-last:]
内でも、int(val)
とlast
の比較が行われるようになりました。
src/pkg/crypto/x509/pem_decrypt_test.go
の変更:
AES-128-CBC
で暗号化された新しいPEMブロックのテストケースが追加されました。これは、AESが16バイトのブロックサイズを持つため、上記のパディング検証の修正が正しく機能するかを確認するためのものです。
コアとなるコードの解説
このコミットの核心は、DecryptPEMBlock
関数内のパディング除去ロジックの汎用化と堅牢化です。
以前の実装では、RFC 1423パディングの検証において、パディングの最大長が固定値 8
であると仮定していました。これは、DES暗号(ブロックサイズ8バイト)を使用する場合には問題ありませんでしたが、AES暗号(ブロックサイズ16バイト)のように異なるブロックサイズを持つ暗号が使用された場合、この仮定が崩れ、パディングの検証が失敗していました。例えば、AESで暗号化されたデータが16バイトのパディングを持つ場合、元のコードでは last > 8
のチェックで不正なパディングと判断されてしまっていました。
新しいコードでは、まず blockSize := block.BlockSize()
を呼び出すことで、現在使用されているブロック暗号の実際のブロックサイズを動的に取得します。これにより、パディングの検証ロジックが特定のブロックサイズに依存しなくなります。
次に、復号化されたデータ data
の長さ dlen
が 0
であるか、または blockSize
の倍数でない場合にエラーを返す dlen%blockSize != 0
というチェックが追加されました。これは、ブロック暗号の性質上、復号化されたデータは常にブロックサイズの倍数であるべきであるという前提に基づいています。このチェックにより、データが破損しているか、不正な形式である場合に早期に検出できるようになります。
さらに、パディングの長さ last
が 0
であるか、または blockSize
を超える場合に IncorrectPasswordError
を返すように、条件が last > 8
から last > blockSize
に変更されました。これにより、パディングの最大長がブロック暗号の実際のブロックサイズに合わせられるため、AESのような16バイトブロックの暗号でもパディングが正しく検証されるようになります。
これらの変更により、DecryptPEMBlock
関数は、異なるブロックサイズを持つ様々なブロック暗号で暗号化されたPEMブロックに対して、より正確で堅牢な復号化とパディング除去を実行できるようになりました。追加されたテストケースは、この修正がAES-128-CBCのようなシナリオで正しく機能することを保証します。
関連リンク
- Go言語
crypto/x509
パッケージのドキュメント: https://pkg.go.dev/crypto/x509 - RFC 1423: Privacy Enhancements for Internet Electronic Mail: Part III: Algorithms, Modes, and Identifiers: https://datatracker.ietf.org/doc/html/rfc1423
参考にした情報源リンク
- RFC 1423 のパディングスキームに関する情報
- Go言語の
crypto/cipher
パッケージのBlock
インターフェースに関する情報 - Go言語の
crypto/des
およびcrypto/aes
パッケージに関する情報 - OpenSSLのPEM暗号化に関する一般的な情報