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

[インデックス 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,ENCRYPTEDDEK-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形式の鍵を復号します。

  1. DEK-Infoヘッダの解析:

    • pem.Block から DEK-Info ヘッダを読み取ります。
    • ヘッダの形式が ALGORITHM,HEX_IV であることを検証し、アルゴリズム名と16進数エンコードされたIVを抽出します。
    • IVの長さが8バイト以上であることを確認します。
  2. 暗号化アルゴリズムの特定:

    • 抽出したアルゴリズム名(例: 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バイト鍵)
  3. 鍵導出 (Key Derivation):

    • rfc1423Algo.deriveKey メソッドが、パスワードとIVの最初の8バイト(ソルトとして使用)から暗号鍵を導出します。
    • この鍵導出アルゴリズムはOpenSSLの実装に由来しており、MD5ハッシュ関数を繰り返し適用して、必要な鍵サイズのバイト列を生成します。具体的には、hash.Reset(), hash.Write(digest), hash.Write(password), hash.Write(salt) の順でデータをハッシュし、その結果を次の反復の digest として使用します。
  4. CBC復号:

    • 導出された鍵とIVを使用して、選択されたブロック暗号(DES, Triple DES, AES)のCBCデクリプタ (cipher.NewCBCDecrypter) を作成します。
    • CryptBlocks メソッドを呼び出して、PEMブロックのバイト列を復号します。
  5. パディングの検証と除去:

    • 復号されたデータは、RFC 1423で定義されたパディングスキーム(PKCS#5/PKCS#7と同様)に従ってパディングされています。
    • 復号されたデータの最後のバイトを読み取り、それがパディングの長さ (n) を示していると解釈します。
    • データの長さが n より短い場合や、n が0または8より大きい場合は、パディングが不正であると判断し、IncorrectPasswordError を返します。
    • 最後の n バイトがすべて n と同じ値であるかを検証します。これにより、パディングが正しいことを確認し、不正なパスワードやデータ破損を検出します。
    • パディングが正しい場合、最後の n バイトを除去し、復号されたDERバイト列を返します。

この関数は、パスワードが間違っている場合や、PEMブロックの形式が不正な場合、またはパディングが不正な場合に、適切なエラーを返します。特に、IncorrectPasswordError は、パスワードの誤りを明確に示します。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/crypto/x509/pem_decrypt.go (新規追加):

    • パスワードで保護されたPEMブロックを復号するための主要なロジックが実装されています。
    • rfc1423Algo 構造体と rfc1423Algos マップが定義され、サポートされる暗号化アルゴリズムとその鍵サイズがマッピングされています。
    • deriveKey メソッドが、OpenSSLのMD5ベースの鍵導出アルゴリズムを実装しています。
    • IsEncryptedPEMBlock 関数が、PEMブロックが暗号化されているかどうかを DEK-Info ヘッダの有無で判断します。
    • IncorrectPasswordError というカスタムエラーが定義されています。
    • DecryptPEMBlock 関数が、PEMブロックの復号処理全体を統括します。
  2. 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秘密鍵としてパースできるかどうかも検証しています。
  3. src/pkg/go/build/deps_test.go (変更):

    • crypto/x509 パッケージの依存関係リストに encoding/hex が追加されています。これは、pem_decrypt.goDEK-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.BlockDEK-Info ヘッダを持っているかどうかをチェックし、パスワードで暗号化されているかを判断します。
  • IncorrectPasswordError:

    • パスワードが間違っている場合に返される特定のエラーです。
  • DecryptPEMBlock 関数:

    • この関数が復号処理の主要なエントリポイントです。
    • まず、pem.BlockHeaders から 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 で有効な秘密鍵としてパースできることを確認しています。これにより、復号されたデータが正しく、かつ利用可能であることを保証しています。

関連リンク

参考にした情報源リンク