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

[インデックス 14539] ファイルの概要

このコミットは、Go言語の標準ライブラリ crypto/tls パッケージにおける、PEMエンコードされた秘密鍵の読み込みに関するバグ修正です。具体的には、秘密鍵のPEMブロックが「PRIVATE KEY」というラベルのみを持つ場合に、X509KeyPair 関数が正しく鍵をロードできない問題を解決します。

コミット

commit 444b7b53e02487e3292c9db04b584915a21de54b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sat Dec 1 11:02:08 2012 -0800

    crypto/tls: fix loading keys labeled just "PRIVATE KEY"
    
    Worked in Go 1, broken in f440e65f93fe.
    
    Fixes #4477
    
    R=golang-dev, agl
    CC=golang-dev
    https://golang.org/cl/6865043
---
 src/pkg/crypto/tls/tls.go      |  2 +-\
 src/pkg/crypto/tls/tls_test.go | 26 ++++++++++++++++++++------
 2 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/src/pkg/crypto/tls/tls.go b/src/pkg/crypto/tls/tls.go
index 182506c59e..9230656d6a 100644
--- a/src/pkg/crypto/tls/tls.go
+++ b/src/pkg/crypto/tls/tls.go
@@ -155,7 +155,7 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (cert Certificate, err error)\n 		err = errors.New("crypto/tls: failed to parse key PEM data")
 		return
 	}\n-\t\tif strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {\n+\t\tif keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {\n \t\t\tbreak
 \t\t}\
 	}\
diff --git a/src/pkg/crypto/tls/tls_test.go b/src/pkg/crypto/tls/tls_test.go
index 31b858d832..38229014cd 100644
--- a/src/pkg/crypto/tls/tls_test.go
+++ b/src/pkg/crypto/tls/tls_test.go
@@ -33,6 +33,19 @@ D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
 -----END RSA PRIVATE KEY-----\n `
 
+// keyPEM is the same as rsaKeyPEM, but declares itself as just
+// "PRIVATE KEY", not "RSA PRIVATE KEY".  http://golang.org/issue/4477
+var keyPEM = `-----BEGIN PRIVATE KEY-----
+MIIBOwIBAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
+k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
+6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
+MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
+SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
+xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
+D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
+-----END PRIVATE KEY-----
+`
+
 var ecdsaCertPEM = `-----BEGIN CERTIFICATE-----\n MIIB/jCCAWICCQDscdUxw16XFDAJBgcqhkjOPQQBMEUxCzAJBgNVBAYTAkFVMRMw\n EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0\n@@ -62,21 +75,22 @@ kohxS/xfFg/TEwRSSws+roJr4JFKpO2t3/be5OdqmQ==
 
 var keyPairTests = []struct {\n 	algo string\n-\tcert *string\n-\tkey  *string\n+\tcert string\n+\tkey  string\n }{\n-\t{\"ECDSA\", &ecdsaCertPEM, &ecdsaKeyPEM},\n-\t{\"RSA\", &rsaCertPEM, &rsaKeyPEM},\n+\t{\"ECDSA\", ecdsaCertPEM, ecdsaKeyPEM},\n+\t{\"RSA\", rsaCertPEM, rsaKeyPEM},\n+\t{\"RSA-untyped\", rsaCertPEM, keyPEM}, // golang.org/issue/4477\n }\n \n func TestX509KeyPair(t *testing.T) {\n 	var pem []byte\n 	for _, test := range keyPairTests {\n-\t\tpem = []byte(*test.cert + *test.key)\n+\t\tpem = []byte(test.cert + test.key)\n \t\tif _, err := X509KeyPair(pem, pem); err != nil {\n \t\t\tt.Errorf(\"Failed to load %s cert followed by %s key: %s\", test.algo, test.algo, err)\n \t\t}\n-\t\tpem = []byte(*test.key + *test.cert)\n+\t\tpem = []byte(test.key + test.cert)\n \t\tif _, err := X509KeyPair(pem, pem); err != nil {\n \t\t\tt.Errorf(\"Failed to load %s key followed by %s cert: %s\", test.algo, test.algo, err)\n \t\t}\

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/444b7b53e02487e3292c9db04b584915a21de54b

元コミット内容

crypto/tls: fix loading keys labeled just "PRIVATE KEY"

Worked in Go 1, broken in f440e65f93fe.

Fixes #4477

R=golang-dev, agl
CC=golang-dev
https://golang.org/cl/6865043

変更の背景

このコミットは、Go言語の crypto/tls パッケージが、PEMエンコードされた秘密鍵を正しく読み込めないというバグを修正するために行われました。具体的には、秘密鍵のPEMブロックが「-----BEGIN PRIVATE KEY-----」という汎用的なラベルを持つ場合に、X509KeyPair 関数がその鍵を認識できず、ロードに失敗するという問題がありました。

コミットメッセージによると、この機能はGo 1では動作していましたが、コミット f440e65f93fe によって壊れてしまったとされています。これは、おそらく特定のアルゴリズム(例: RSA)が明示された「-----BEGIN RSA PRIVATE KEY-----」のようなラベルのみを期待するように変更されたためと考えられます。しかし、RFC 5958で定義されているPKCS#8形式の秘密鍵など、アルゴリズムを特定しない「PRIVATE KEY」ラベルも広く使用されており、これに対応する必要がありました。

この問題は、GoのTLSクライアントやサーバーが、特定の形式でエクスポートされた秘密鍵を読み込めないという互換性の問題を引き起こしていました。

前提知識の解説

1. PEMエンコーディング

PEM (Privacy-Enhanced Mail) は、X.509証明書、秘密鍵、公開鍵などをASCIIテキスト形式で表現するための標準的なエンコーディング形式です。バイナリデータをBase64でエンコードし、ヘッダーとフッターで囲むことで、テキストファイルとして簡単に扱えるようにします。

PEMブロックの一般的な構造は以下の通りです。

-----BEGIN <TYPE>-----
<Base64 encoded data>
-----END <TYPE>-----

ここで <TYPE> は、エンコードされているデータの種類を示します。例えば、証明書であれば CERTIFICATE、RSA秘密鍵であれば RSA PRIVATE KEY、汎用的な秘密鍵であれば PRIVATE KEY などがあります。

2. crypto/tls パッケージ

crypto/tls は、Go言語におけるTLS (Transport Layer Security) プロトコルの実装を提供する標準ライブラリです。このパッケージは、TLSクライアントおよびサーバーの実装、証明書の検証、秘密鍵の管理など、セキュアな通信に必要な機能を提供します。

3. X509KeyPair 関数

crypto/tls パッケージ内の X509KeyPair 関数は、PEMエンコードされた証明書と秘密鍵のペアを解析し、tls.Certificate 構造体としてロードするために使用されます。この関数は、TLS接続を確立する際にサーバーが自身の身元を証明するため、またはクライアントがクライアント証明書認証を行うために必要となります。

X509KeyPair 関数は、以下の引数を取ります。

  • certPEMBlock []byte: PEMエンコードされた証明書データ。
  • keyPEMBlock []byte: PEMエンコードされた秘密鍵データ。

この関数は、内部で encoding/pem パッケージを使用してPEMブロックをデコードし、それぞれのブロックの Type フィールド(例: "RSA PRIVATE KEY", "PRIVATE KEY")を検査して、それが適切な種類の鍵であるかどうかを判断します。

4. 秘密鍵のPEMラベルの種類

秘密鍵のPEMブロックには、いくつかの異なるラベルが存在します。

  • RSA PRIVATE KEY: RSAアルゴリズムに特化した秘密鍵を示します。これはPKCS#1形式のRSA秘密鍵によく見られます。
  • EC PRIVATE KEY: 楕円曲線暗号 (ECC) の秘密鍵を示します。
  • PRIVATE KEY: これはPKCS#8形式の秘密鍵を示す汎用的なラベルです。PKCS#8は、秘密鍵のアルゴリズムに依存しない標準的な形式であり、様々な種類の秘密鍵(RSA、EC、DSAなど)を格納できます。多くのツールやシステムがこの形式で秘密鍵をエクスポートします。

このコミットの背景にある問題は、X509KeyPair 関数が RSA PRIVATE KEY のような特定のアルゴリズムを示すラベルは認識するものの、PRIVATE KEY という汎用的なラベルを認識できなかったことにあります。

技術的詳細

このコミットの技術的な核心は、crypto/tls パッケージの X509KeyPair 関数における秘密鍵のPEMブロックのタイプチェックのロジック変更にあります。

変更前、X509KeyPair 関数は、秘密鍵のPEMブロックを解析する際に、その Type フィールドが strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") という条件を満たすかどうかをチェックしていました。この条件は、「RSA PRIVATE KEY」や「EC PRIVATE KEY」のように「 PRIVATE KEY」で終わるタイプを捕捉することを意図していました。しかし、この条件では「PRIVATE KEY」という正確な文字列を持つタイプは捕捉できませんでした。strings.HasSuffix は文字列の末尾をチェックするため、「PRIVATE KEY」自体は「 PRIVATE KEY」で終わるわけではないからです。

変更後、この条件は keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") と変更されました。これにより、以下の2つのケースが考慮されるようになりました。

  1. keyDERBlock.Type == "PRIVATE KEY": 秘密鍵のPEMブロックのタイプが正確に「PRIVATE KEY」である場合。これはPKCS#8形式の秘密鍵でよく見られます。
  2. strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY"): 秘密鍵のPEMブロックのタイプが「 PRIVATE KEY」で終わる場合。これは「RSA PRIVATE KEY」や「EC PRIVATE KEY」のような、アルゴリズムが明示された秘密鍵のタイプをカバーします。

この変更により、X509KeyPair 関数は、より広範な種類のPEMエンコードされた秘密鍵を正しく解析し、ロードできるようになりました。

テストファイル src/pkg/crypto/tls/tls_test.go の変更も重要です。新しいテストケース keyPEM が追加され、これは「-----BEGIN PRIVATE KEY-----」で始まる秘密鍵のPEMブロックを含んでいます。そして、keyPairTests スライスに {"RSA-untyped", rsaCertPEM, keyPEM} という新しいエントリが追加されました。これにより、汎用的な「PRIVATE KEY」ラベルを持つ秘密鍵が X509KeyPair 関数によって正しく処理されることを保証するテストカバレッジが追加されました。また、keyPairTests 構造体の certkey フィールドが *string から string に変更され、それに伴いテストコード内の参照も変更されています。これは、文字列リテラルをポインタで渡す必要がないため、コードの簡素化と可読性の向上を目的としたリファクタリングです。

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

src/pkg/crypto/tls/tls.go

--- a/src/pkg/crypto/tls/tls.go
+++ b/src/pkg/crypto/tls/tls.go
@@ -155,7 +155,7 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (cert Certificate, err error)\n 		err = errors.New("crypto/tls: failed to parse key PEM data")
 		return
 	}\n-\t\tif strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {\n+\t\tif keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {\n \t\t\tbreak
 \t\t}\
 	}\

src/pkg/crypto/tls/tls_test.go

--- a/src/pkg/crypto/tls/tls_test.go
+++ b/src/pkg/crypto/tls/tls_test.go
@@ -33,6 +33,19 @@ D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
 -----END RSA PRIVATE KEY-----\n `
 
+// keyPEM is the same as rsaKeyPEM, but declares itself as just
+// "PRIVATE KEY", not "RSA PRIVATE KEY".  http://golang.org/issue/4477
+var keyPEM = `-----BEGIN PRIVATE KEY-----
+MIIBOwIBAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
+k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
+6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
+MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
+SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
+xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
+D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNNUN9B2N2g==
+-----END PRIVATE KEY-----
+`
+
 var ecdsaCertPEM = `-----BEGIN CERTIFICATE-----\n MIIB/jCCAWICCQDscdUxw16XFDAJBgcqhkjOPQQBMEUxCzAJBgNVBAYTAkFVMRMw\n EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0\n@@ -62,21 +75,22 @@ kohxS/xfFg/TEwRSSws+roJr4JFKpO2t3/be5OdqmQ==
 
 var keyPairTests = []struct {\n 	algo string\n-\tcert *string\n-\tkey  *string\n+\tcert string\n+\tkey  string\n }{\n-\t{\"ECDSA\", &ecdsaCertPEM, &ecdsaKeyPEM},\n-\t{\"RSA\", &rsaCertPEM, &rsaKeyPEM},\n+\t{\"ECDSA\", ecdsaCertPEM, ecdsaKeyPEM},\n+\t{\"RSA\", rsaCertPEM, rsaKeyPEM},\n+\t{\"RSA-untyped\", rsaCertPEM, keyPEM}, // golang.org/issue/4477\n }\n \n func TestX509KeyPair(t *testing.T) {\n 	var pem []byte\n 	for _, test := range keyPairTests {\n-\t\tpem = []byte(*test.cert + *test.key)\n+\t\tpem = []byte(test.cert + test.key)\n \t\tif _, err := X509KeyPair(pem, pem); err != nil {\n \t\t\tt.Errorf(\"Failed to load %s cert followed by %s key: %s\", test.algo, test.algo, err)\n \t\t}\n-\t\tpem = []byte(*test.key + *test.cert)\n+\t\tpem = []byte(test.key + test.cert)\n \t\tif _, err := X509KeyPair(pem, pem); err != nil {\n \t\t\tt.Errorf(\"Failed to load %s key followed by %s cert: %s\", test.algo, test.algo, err)\n \t\t}\

コアとなるコードの解説

src/pkg/crypto/tls/tls.go の変更

この変更は、X509KeyPair 関数内で秘密鍵のPEMブロックをループで処理し、適切な鍵タイプを見つける部分にあります。

元のコード:

		if strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
			break
		}

この行は、デコードされたPEMブロックの Type フィールドが「 PRIVATE KEY」で終わる場合にのみ、そのブロックを有効な秘密鍵として認識し、ループを抜けていました。例えば、「RSA PRIVATE KEY」や「EC PRIVATE KEY」はこれに該当しますが、「PRIVATE KEY」という正確な文字列は該当しませんでした。

変更後のコード:

		if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
			break
		}

この変更により、条件に keyDERBlock.Type == "PRIVATE KEY" が追加されました。これは論理OR (||) で結合されているため、以下のいずれかの条件が真であれば、そのPEMブロックが有効な秘密鍵として扱われます。

  1. keyDERBlock.Type が正確に "PRIVATE KEY" である。
  2. keyDERBlock.Type" PRIVATE KEY" で終わる(例: "RSA PRIVATE KEY")。

この修正により、PKCS#8形式の秘密鍵など、汎用的な「PRIVATE KEY」ラベルを持つ鍵も正しく認識され、X509KeyPair 関数がそれらをロードできるようになりました。

src/pkg/crypto/tls/tls_test.go の変更

テストファイルでは、主に以下の3つの変更が行われました。

  1. 新しい keyPEM 変数の追加:

    // keyPEM is the same as rsaKeyPEM, but declares itself as just
    // "PRIVATE KEY", not "RSA PRIVATE KEY".  http://golang.org/issue/4477
    var keyPEM = `-----BEGIN PRIVATE KEY-----
    ... (Base64 encoded key data) ...
    -----END PRIVATE KEY-----
    `
    

    この新しい変数は、-----BEGIN PRIVATE KEY----- で始まるPEMエンコードされた秘密鍵を含んでいます。これは、修正が対象とする「PRIVATE KEY」ラベルの鍵を具体的に表すものです。

  2. keyPairTests 構造体のフィールド型変更:

    -	cert *string
    -	key  *string
    +	cert string
    +	key  string
    

    keyPairTests スライス内の各テストケースの certkey フィールドが、文字列ポインタ (*string) から直接文字列 (string) に変更されました。これは、文字列リテラルをポインタで保持する必要がないため、コードを簡潔にするためのリファクタリングです。

  3. 新しいテストケースの追加:

    +	{"RSA-untyped", rsaCertPEM, keyPEM}, // golang.org/issue/4477
    

    keyPairTests スライスに、"RSA-untyped" という名前の新しいテストケースが追加されました。このテストケースは、既存のRSA証明書 (rsaCertPEM) と、新しく定義された汎用的な秘密鍵 (keyPEM) を組み合わせています。これにより、X509KeyPair 関数が「PRIVATE KEY」ラベルを持つ秘密鍵を正しくロードできることを検証します。コメント // golang.org/issue/4477 は、このテストケースが関連するGoのイシュー番号を示しています。

これらのテストの変更は、バグ修正が意図した通りに機能し、将来のリグレッションを防ぐための重要なステップです。

関連リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/444b7b53e02487e3292c9db04b584915a21de54b
  • Go CL (Code Review): https://golang.org/cl/6865043
  • Go Issue #4477: コミットメッセージに Fixes #4477 と記載されていますが、現在のGoのイシュートラッカーではこの番号のイシューは見つかりませんでした。これは、イシューが非常に古いか、別のトラッカーで管理されていた可能性、または番号が変更された可能性が考えられます。

参考にした情報源リンク

  • コミットの差分情報 (diff)
  • PEMエンコーディングに関する一般的な知識
  • Go言語の crypto/tls および encoding/pem パッケージのドキュメント(一般的な機能理解のため)
  • PKCS#8 秘密鍵形式に関する一般的な知識