[インデックス 14022] ファイルの概要
このコミットは、Go言語の crypto/x509
パッケージに、パスワードで保護されたPEM形式の鍵を復号するための DecryptBlock
(後に DecryptPEMBlock
に変更) 関数を追加するものです。これにより、ユーザーはパスワードで暗号化された秘密鍵をGoアプリケーション内で安全にロードし、利用できるようになります。
コミット
commit 70ab57ea2dc9c4c5124204ca28dbbac41c94ecb0
Author: Jeff Wendling <jeff@spacemonkey.com>
Date: Thu Oct 4 15:42:57 2012 -0400
crypto/x509: add DecryptBlock function for loading password protected keys
Adds a DecryptBlock function which takes a password and a *pem.Block and
returns the decrypted DER bytes suitable for passing into other crypto/x509
functions.
R=golang-dev, agl, leterip
CC=golang-dev
https://golang.org/cl/6555052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/70ab57ea2dc9c4c5124204ca28dbbac41c94ecb0
元コミット内容
crypto/x509
パッケージに、パスワードで保護された鍵をロードするための DecryptBlock
関数を追加します。
この関数は、パスワードと *pem.Block
を受け取り、他の crypto/x509
関数に渡すのに適した復号化されたDERバイトを返します。
変更の背景
デジタル証明書や秘密鍵は、セキュリティを確保するためにしばしばパスワードで暗号化されて保存されます。特に、PEM (Privacy-Enhanced Mail) 形式は、鍵や証明書をテキスト形式で表現するための広く普及した標準です。しかし、Go言語の crypto/x509
パッケージには、これまでパスワードで保護されたPEM形式の鍵を直接復号する機能がありませんでした。
この機能がないと、Goアプリケーションでパスワード付きの秘密鍵を扱う場合、外部ツール(例: OpenSSLコマンドラインツール)で事前に復号しておくか、自前で復号ロジックを実装する必要がありました。これは開発者にとって不便であり、セキュリティ上のリスクも伴う可能性がありました。
このコミットは、このギャップを埋めるために、crypto/x509
パッケージに DecryptPEMBlock
(コミットメッセージでは DecryptBlock
と記載されているが、実際のコードでは DecryptPEMBlock
に変更されている) 関数を導入し、パスワードで保護されたPEM形式の鍵をGoネイティブで安全に復号できるようにすることを目的としています。これにより、Goアプリケーションがより柔軟に、かつセキュアに鍵管理を行えるようになります。
前提知識の解説
PEM (Privacy-Enhanced Mail) 形式
PEM形式は、X.509証明書、秘密鍵、公開鍵などをASCIIテキスト形式で表現するための標準的なエンコーディング形式です。通常、-----BEGIN <LABEL>-----
と -----END <LABEL>-----
というヘッダとフッタで囲まれ、その間にBase64エンコードされたバイナリデータが格納されます。<LABEL>
は、格納されているデータの種類(例: RSA PRIVATE KEY
, CERTIFICATE
)を示します。
パスワードで保護されたPEMファイルの場合、ヘッダ部分に Proc-Type: 4,ENCRYPTED
や DEK-Info: <ALGORITHM>,<IV>
といった情報が含まれることがあります。
RFC 1423
RFC 1423は、「Privacy Enhancement for Internet Electronic Mail: Part III: Algorithms, Modes, and Identifiers」と題され、PEM形式における暗号化アルゴリズム、モード、識別子を定義しています。特に、パスワードで保護されたPEMブロックの暗号化方法について記述されており、DES-CBC、DES-EDE3-CBC、AES-CBCなどのアルゴリズムと、鍵導出のためのソルト(IVの一部)の使用が規定されています。
このRFCで定義されている暗号化方式は、現在では一部が脆弱であるとされていますが、古いシステムや既存の鍵ファイルとの互換性のために、その復号機能は依然として重要です。
DEK-Info ヘッダ
暗号化されたPEMブロックには、通常 DEK-Info
ヘッダが含まれます。このヘッダは、データの暗号化に使用されたアルゴリズムと、初期化ベクトル (IV) を指定します。形式は DEK-Info: <ALGORITHM>,<HEX_IV>
のようになります。例えば、DEK-Info: DES-CBC,34F09A4FC8DE22B5
は、DES-CBCアルゴリズムが使用され、初期化ベクトルが 34F09A4FC8DE22B5
であることを示します。
鍵導出関数 (Key Derivation Function, KDF) と OpenSSLのMD5ベース鍵導出
鍵導出関数 (KDF) は、パスワードなどの低エントロピーの秘密情報から、暗号化に使用できる高エントロピーの暗号鍵を生成するための関数です。
OpenSSLの古いバージョンでは、パスワードで保護されたPEM鍵の鍵導出に、MD5ハッシュ関数をベースとした独自のアルゴリズムが使用されていました。これは EVP_BytesToKey()
関数によって内部的に処理され、パスワードとソルト(DEK-Info
ヘッダから取得されるIVの最初の8バイト)を繰り返しMD5ハッシュにかけることで、必要な鍵とIVを生成します。
このMD5ベースの鍵導出は、反復回数が少ないため、現代のセキュリティ基準から見ると脆弱であり、ブルートフォース攻撃に対して弱いとされています。しかし、既存の多くのPEMファイルがこの方式で暗号化されているため、互換性のためにその復号ロジックを実装する必要があります。
CBC (Cipher Block Chaining) モード
CBCは、ブロック暗号の動作モードの一つです。このモードでは、各ブロックの平文が、暗号化される前に前のブロックの暗号文とXORされます。最初のブロックには初期化ベクトル (IV) が使用されます。CBCモードは、同じ平文ブロックが繰り返し出現しても、異なる暗号文ブロックを生成するため、パターンを隠蔽するのに役立ちます。復号時には、暗号文ブロックが復号された後、前の暗号文ブロックとXORすることで平文が復元されます。
パディング (Padding)
ブロック暗号は固定長のブロック単位でデータを処理します。もし平文の長さがブロックサイズの倍数でない場合、最後のブロックを埋めるために追加のデータ(パディング)が必要になります。復号後には、このパディングを正確に除去する必要があります。
RFC 1423で記述されているパディングスキームは、PKCS#5またはPKCS#7パディングと実質的に同じです。このスキームでは、パディングバイトの値が、追加されたパディングのバイト数を示します。例えば、ブロックサイズが8バイトで、最後のブロックに3バイトのパディングが必要な場合、3バイトの 0x03
が追加されます。復号時には、最後のバイトを読み取り、その値がパディングの長さを示していると解釈し、その長さ分のバイトをデータから除去します。パディングの検証は、復号されたデータの整合性を確認する重要なステップです。
crypto/x509
パッケージ
Go言語の標準ライブラリである crypto/x509
パッケージは、X.509証明書、証明書署名要求 (CSR)、証明書失効リスト (CRL) の解析、生成、検証、および公開鍵と秘密鍵のエンコード/デコード機能を提供します。このパッケージは、TLS/SSL通信やPKI (公開鍵基盤) において中心的な役割を果たします。
技術的詳細
このコミットで追加された DecryptPEMBlock
関数は、RFC 1423およびOpenSSLの実装に準拠して、パスワードで保護されたPEM形式の鍵を復号します。
-
DEK-Infoヘッダの解析:
pem.Block
からDEK-Info
ヘッダを読み取ります。- ヘッダの形式が
ALGORITHM,HEX_IV
であることを検証し、アルゴリズム名と16進数エンコードされたIVを抽出します。 - IVの長さが8バイト以上であることを確認します。
-
暗号化アルゴリズムの特定:
- 抽出したアルゴリズム名(例:
DES-CBC
,AES-128-CBC
)に基づいて、対応するブロック暗号関数と鍵サイズをrfc1423Algos
マップから取得します。 - サポートされるアルゴリズムは以下の通りです:
DES-CBC
(DES, 8バイト鍵)DES-EDE3-CBC
(Triple DES, 24バイト鍵)AES-128-CBC
(AES, 16バイト鍵)AES-192-CBC
(AES, 24バイト鍵)AES-256-CBC
(AES, 32バイト鍵)
- 抽出したアルゴリズム名(例:
-
鍵導出 (Key Derivation):
rfc1423Algo.deriveKey
メソッドが、パスワードとIVの最初の8バイト(ソルトとして使用)から暗号鍵を導出します。- この鍵導出アルゴリズムはOpenSSLの実装に由来しており、MD5ハッシュ関数を繰り返し適用して、必要な鍵サイズのバイト列を生成します。具体的には、
hash.Reset()
,hash.Write(digest)
,hash.Write(password)
,hash.Write(salt)
の順でデータをハッシュし、その結果を次の反復のdigest
として使用します。
-
CBC復号:
- 導出された鍵とIVを使用して、選択されたブロック暗号(DES, Triple DES, AES)のCBCデクリプタ (
cipher.NewCBCDecrypter
) を作成します。 CryptBlocks
メソッドを呼び出して、PEMブロックのバイト列を復号します。
- 導出された鍵とIVを使用して、選択されたブロック暗号(DES, Triple DES, AES)のCBCデクリプタ (
-
パディングの検証と除去:
- 復号されたデータは、RFC 1423で定義されたパディングスキーム(PKCS#5/PKCS#7と同様)に従ってパディングされています。
- 復号されたデータの最後のバイトを読み取り、それがパディングの長さ (
n
) を示していると解釈します。 - データの長さが
n
より短い場合や、n
が0または8より大きい場合は、パディングが不正であると判断し、IncorrectPasswordError
を返します。 - 最後の
n
バイトがすべてn
と同じ値であるかを検証します。これにより、パディングが正しいことを確認し、不正なパスワードやデータ破損を検出します。 - パディングが正しい場合、最後の
n
バイトを除去し、復号されたDERバイト列を返します。
この関数は、パスワードが間違っている場合や、PEMブロックの形式が不正な場合、またはパディングが不正な場合に、適切なエラーを返します。特に、IncorrectPasswordError
は、パスワードの誤りを明確に示します。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/crypto/x509/pem_decrypt.go
(新規追加):- パスワードで保護されたPEMブロックを復号するための主要なロジックが実装されています。
rfc1423Algo
構造体とrfc1423Algos
マップが定義され、サポートされる暗号化アルゴリズムとその鍵サイズがマッピングされています。deriveKey
メソッドが、OpenSSLのMD5ベースの鍵導出アルゴリズムを実装しています。IsEncryptedPEMBlock
関数が、PEMブロックが暗号化されているかどうかをDEK-Info
ヘッダの有無で判断します。IncorrectPasswordError
というカスタムエラーが定義されています。DecryptPEMBlock
関数が、PEMブロックの復号処理全体を統括します。
-
src/pkg/crypto/x509/pem_decrypt_test.go
(新規追加):pem_decrypt.go
で実装されたDecryptPEMBlock
関数のテストケースが含まれています。TestDecrypt
関数が、DES-CBC、DES-EDE3-CBC、AES-128-CBC、AES-192-CBC、AES-256-CBCで暗号化された様々なテストデータに対して復号処理を検証します。- テストデータには、実際の暗号化されたRSA秘密鍵のPEMデータと、それに対応するパスワードが含まれています。復号後、結果が有効なPKCS#1秘密鍵としてパースできるかどうかも検証しています。
-
src/pkg/go/build/deps_test.go
(変更):crypto/x509
パッケージの依存関係リストにencoding/hex
が追加されています。これは、pem_decrypt.go
でDEK-Info
ヘッダからIVを16進数デコードするためにencoding/hex
パッケージが使用されるようになったためです。
コアとなるコードの解説
src/pkg/crypto/x509/pem_decrypt.go
package x509
import (
"crypto/aes"
"crypto/cipher"
"crypto/des"
"crypto/md5"
"encoding/hex"
"encoding/pem"
"errors"
"strings"
)
// rfc1423Algos represents how to create a block cipher for a decryption mode.
type rfc1423Algo struct {
cipherFunc func([]byte) (cipher.Block, error)
keySize int
}
// 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.
func (c rfc1423Algo) deriveKey(password, salt []byte) []byte {
hash := md5.New()
out := make([]byte, c.keySize)
var digest []byte
for i := 0; i < len(out); i += len(digest) {
hash.Reset()
hash.Write(digest)
hash.Write(password)
hash.Write(salt)
digest = hash.Sum(digest[:0])
copy(out[i:], digest)
}
return out
}
// 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},
}
// IsEncryptedPEMBlock returns if the PEM block is password encrypted.
func IsEncryptedPEMBlock(b *pem.Block) bool {
_, ok := b.Headers["DEK-Info"]
return ok
}
// IncorrectPasswordError is returned when an incorrect password is detected.
var IncorrectPasswordError = errors.New("x509: decryption password incorrect")
// DecryptPEMBlock takes a password encrypted PEM block and the password used to
// encrypt it and returns a slice of decrypted DER encoded bytes. It inspects
// the DEK-Info header to determine the algorithm used for decryption. If no
// DEK-Info header is present, an error is returned. If an incorrect password
// is detected an IncorrectPasswordError is returned.
func DecryptPEMBlock(b *pem.Block, password []byte) ([]byte, error) {
dek, ok := b.Headers["DEK-Info"]
if !ok {
return nil, errors.New("x509: no DEK-Info header in block")
}
idx := strings.Index(dek, ",")
if idx == -1 {
return nil, errors.New("x509: malformed DEK-Info header")
}
mode, hexIV := dek[:idx], dek[idx+1:]
iv, err := hex.DecodeString(hexIV)
if err != nil {
return nil, err
}
if len(iv) < 8 {
return nil, errors.New("x509: not enough bytes in IV")
}
ciph, ok := rfc1423Algos[mode]
if !ok {
return nil, errors.New("x509: unknown encryption mode")
}
// Based on the OpenSSL implementation. The salt is the first 8 bytes
// of the initialization vector.
key := ciph.deriveKey(password, iv[:8])
block, err := ciph.cipherFunc(key)
if err != nil {
return nil, err
}
data := make([]byte, len(b.Bytes))
dec := cipher.NewCBCDecrypter(block, iv)
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.
// For example:
// [x y z 2 2]
// [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 {
return nil, errors.New("x509: invalid padding")
}
last := data[dlen-1]
if dlen < int(last) {
return nil, IncorrectPasswordError
}
if last == 0 || last > 8 {
return nil, IncorrectPasswordError
}
for _, val := range data[dlen-int(last):] {
if val != last {
return nil, IncorrectPasswordError
}
}
return data[:dlen-int(last)], nil
}
-
rfc1423Algo
構造体とrfc1423Algos
マップ:rfc1423Algo
は、特定の暗号化アルゴリズムに対応するブロック暗号の生成関数 (cipherFunc
) と鍵サイズ (keySize
) を保持します。rfc1423Algos
は、アルゴリズム名(例: "DES-CBC")をキーとしてrfc1423Algo
インスタンスをマッピングし、サポートされる暗号化方式を定義しています。
-
deriveKey
メソッド:- このメソッドは、OpenSSLの鍵導出アルゴリズムを模倣しています。
md5.New()
でMD5ハッシュインスタンスを作成し、パスワードとソルト(IVの最初の8バイト)を組み合わせて繰り返しハッシュ計算を行います。- 必要な鍵サイズ (
c.keySize
) に達するまで、ハッシュの出力を連結していきます。
-
IsEncryptedPEMBlock
関数:- 与えられた
pem.Block
がDEK-Info
ヘッダを持っているかどうかをチェックし、パスワードで暗号化されているかを判断します。
- 与えられた
-
IncorrectPasswordError
:- パスワードが間違っている場合に返される特定のエラーです。
-
DecryptPEMBlock
関数:- この関数が復号処理の主要なエントリポイントです。
- まず、
pem.Block
のHeaders
からDEK-Info
を取得し、アルゴリズムモードと16進数エンコードされたIVをパースします。 hex.DecodeString
を使用してIVをデコードします。rfc1423Algos
マップから対応するrfc1423Algo
を取得し、deriveKey
を呼び出して暗号鍵を導出します。- 導出された鍵とIV、そして
cipher.NewCBCDecrypter
を使用してCBCデクリプタを作成します。 dec.CryptBlocks
を呼び出して、PEMブロックのバイト列を復号します。- 最後に、RFC 1423で定義されたパディングスキームに従って、復号されたデータのパディングを検証し、除去します。パディングが不正な場合は
IncorrectPasswordError
を返します。
src/pkg/crypto/x509/pem_decrypt_test.go
このテストファイルは、DecryptPEMBlock
関数の正確性を検証するために、様々な暗号化アルゴリズム(DES-CBC, DES-EDE3-CBC, AES-128-CBC, AES-192-CBC, AES-256-CBC)で暗号化されたPEM形式のRSA秘密鍵と、それに対応するパスワードの組を testData
変数に定義しています。
TestDecrypt
関数は、これらのテストデータに対して pem.Decode
でPEMブロックをデコードし、DecryptPEMBlock
を呼び出して復号を試みます。復号が成功した場合、結果のDERバイト列が ParsePKCS1PrivateKey
で有効な秘密鍵としてパースできることを確認しています。これにより、復号されたデータが正しく、かつ利用可能であることを保証しています。
関連リンク
- GitHub上のコミットページ
- Go Code Review 6555052 (Goのコードレビューシステムでの変更履歴)
参考にした情報源リンク
- Go crypto/x509 package documentation
- RFC 1423: Privacy Enhancement for Internet Electronic Mail: Part III: Algorithms, Modes, and Identifiers
- OpenSSL EVP_BytesToKey() function
- PKCS #5: Password-Based Cryptography Specification Version 2.0 (PKCS#5/PKCS#7パディングに関する情報)
- Cipher Block Chaining (CBC) mode
- Padding (cryptography)
- Stack Overflow: How does OpenSSL derive key and IV from passphrase?
- Stack Overflow: PKCS5Padding vs PKCS7Padding