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

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

このコミットは、Go言語の crypto/tls パッケージにおいて、楕円曲線暗号(ECC)に基づくX.509証明書とそれに対応する秘密鍵のペアをロードする機能を追加するものです。これにより、TLS通信においてRSA鍵だけでなく、EC鍵も利用できるようになり、より多様な暗号方式への対応が強化されました。

コミット

commit aaf3b71288ac09953932025898a694300eab58cf
Author: Joel Sing <jsing@google.com>
Date:   Fri Nov 16 19:33:59 2012 +1100

    crypto/tls: add support for loading EC X.509 key pairs
    
    Add support for loading X.509 key pairs that consist of a certificate
    with an EC public key and its corresponding EC private key.
    
    R=agl
    CC=golang-dev
    https://golang.org/cl/6776043

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

https://github.com/golang/go/commit/aaf3b71288ac09953932025898a694300eab58cf

元コミット内容

crypto/tls: add support for loading EC X.509 key pairs

Add support for loading X.509 key pairs that consist of a certificate
with an EC public key and its corresponding EC private key.

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

変更の背景

TLS (Transport Layer Security) は、インターネット上での安全な通信を確立するためのプロトコルです。TLSハンドシェイクの過程で、サーバーは自身の身元を証明するためにX.509証明書を提示し、クライアントはその証明書を検証します。この証明書には公開鍵が含まれており、対応する秘密鍵はサーバー側で保持されます。

従来、多くのTLS実装ではRSA暗号方式が広く利用されてきましたが、楕円曲線暗号(ECC)は同等のセキュリティレベルをより短い鍵長で実現できるため、計算コストや帯域幅の削減に貢献します。特にモバイルデバイスやリソースが限られた環境での利用において、ECCは大きなメリットを提供します。

このコミット以前のGoの crypto/tls パッケージは、主にRSAベースの鍵ペアのロードに特化していました。しかし、ECCの普及と重要性の高まりに伴い、TLS通信においてEC鍵ペアを適切に処理できる機能が求められるようになりました。この変更は、GoのTLS実装が現代の暗号技術トレンドに対応し、より柔軟で効率的な鍵管理を可能にすることを目的としています。具体的には、X509KeyPair 関数がEC公開鍵を持つ証明書とEC秘密鍵を正しくペアリングし、ロードできるように拡張されました。

前提知識の解説

1. X.509証明書

X.509は、公開鍵証明書の標準フォーマットです。TLS/SSL、VPN、コード署名など、様々なセキュリティプロトコルで利用されます。X.509証明書には、公開鍵、所有者の識別情報(例: ドメイン名、組織名)、発行者の情報、有効期間、署名アルゴリズム、そして発行者のデジタル署名などが含まれます。この証明書によって、公開鍵が特定のエンティティに属していることが信頼できる第三者(認証局)によって保証されます。

2. 楕円曲線暗号 (ECC: Elliptic Curve Cryptography)

ECCは、楕円曲線上の点の数学的特性を利用した公開鍵暗号方式です。RSAと比較して、同等のセキュリティ強度をより短い鍵長で実現できるという利点があります。これにより、鍵の生成、署名、検証などの処理が高速になり、データサイズも小さくなるため、リソースが限られた環境やモバイルデバイスでの利用に適しています。TLS 1.2以降では、ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) や ECDSA (Elliptic Curve Digital Signature Algorithm) などのECCベースの鍵交換および署名アルゴリズムが広く利用されています。

3. 秘密鍵のフォーマット

秘密鍵は、その保存形式によっていくつかの標準的なフォーマットが存在します。

  • PKCS#1 (Public-Key Cryptography Standards #1): RSA秘密鍵の標準フォーマットです。通常、-----BEGIN RSA PRIVATE KEY----- で始まり、-----END RSA PRIVATE KEY----- で終わるPEMエンコードされた形式で表現されます。
  • PKCS#8 (Public-Key Cryptography Standards #8): 任意の種類の秘密鍵(RSA、DSA、ECCなど)を格納できる汎用的なフォーマットです。暗号化された形式と暗号化されていない形式の両方があります。PEMエンコードされた場合、-----BEGIN PRIVATE KEY----- または -----BEGIN ENCRYPTED PRIVATE KEY----- で始まります。OpenSSL 1.0.0以降では、デフォルトでPKCS#8形式が生成されることが多いです。
  • SEC1 (Standards for Efficient Cryptography Group): 楕円曲線秘密鍵に特化した標準フォーマットです。PEMエンコードされた場合、-----BEGIN EC PRIVATE KEY----- で始まります。OpenSSLの ecparam コマンドでEC秘密鍵を生成すると、この形式になることがあります。

crypto/tls パッケージの X509KeyPair 関数は、これらの異なるフォーマットの秘密鍵を適切に解析し、対応する公開鍵とペアリングできる必要があります。

技術的詳細

このコミットの主要な変更点は、crypto/tls パッケージ内の X509KeyPair 関数が、RSA秘密鍵だけでなく、楕円曲線(EC)秘密鍵もロードできるように拡張されたことです。

  1. parsePrivateKey 関数の導入:

    • 以前の X509KeyPair 関数は、秘密鍵の解析に x509.ParsePKCS1PrivateKeyx509.ParsePKCS8PrivateKey を直接呼び出し、主にRSA鍵を想定していました。
    • このコミットでは、新たに parsePrivateKey というヘルパー関数が導入されました。この関数は、与えられたDERエンコードされた秘密鍵ブロックを、以下の順序で解析を試みます。
      1. x509.ParsePKCS1PrivateKey: PKCS#1形式のRSA秘密鍵を解析します。
      2. x509.ParsePKCS8PrivateKey: PKCS#8形式の秘密鍵を解析します。この関数はRSA鍵とEC鍵の両方を返す可能性があるため、返された型が *rsa.PrivateKey または *ecdsa.PrivateKey であることを確認します。
      3. x509.ParseECPrivateKey: SEC1形式のEC秘密鍵を解析します。
    • これにより、X509KeyPair 関数は、OpenSSLのバージョンや鍵の生成方法によって異なる秘密鍵フォーマット(PKCS#1、PKCS#8、SEC1)に対応できるようになりました。
  2. 公開鍵と秘密鍵の型および値の整合性チェックの強化:

    • X509KeyPair 関数は、ロードされた証明書内の公開鍵と、解析された秘密鍵が一致するかどうかを検証します。
    • 変更前は、公開鍵がRSA型であることを前提とし、RSA公開鍵のモジュラス N と秘密鍵のモジュラス N を比較していました。
    • 変更後は、証明書内の公開鍵の型を switch ステートメントで判定します。
      • RSA公開鍵の場合: 秘密鍵も *rsa.PrivateKey 型であることを確認し、両者のモジュラス N を比較します。
      • ECDSA公開鍵の場合: 秘密鍵も *ecdsa.PrivateKey 型であることを確認し、両者の公開鍵の座標 XY を比較します。EC公開鍵は楕円曲線上の点であり、その座標 (X, Y) が公開鍵の値を表します。
      • その他の公開鍵アルゴリズムの場合: 未知の公開鍵アルゴリズムとしてエラーを返します。
    • この変更により、RSAとECの両方の鍵ペアに対して、公開鍵と秘密鍵の型の一致および値の整合性が厳密にチェックされるようになりました。
  3. テストケースの追加:

    • tls_test.go に、ECDSA証明書と秘密鍵のPEMブロックが追加されました。
    • TestX509KeyPair 関数が修正され、RSA鍵ペアとECDSA鍵ペアの両方について、証明書と秘密鍵の順序を入れ替えても正しくロードできることを検証するテストが追加されました。
    • TestX509MixedKeyPair 関数が追加され、RSA証明書とECDSA秘密鍵、またはECDSA証明書とRSA秘密鍵のように、公開鍵と秘密鍵の型が一致しない場合にエラーが返されることを検証するテストが追加されました。これにより、型ミスマッチによる不正な鍵ペアのロードを防ぐ堅牢性が向上しました。

これらの変更により、Goの crypto/tls パッケージは、より広範な暗号要件に対応できるようになり、特にECCベースのTLS通信のサポートが強化されました。

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

src/pkg/crypto/tls/tls.go

--- a/src/pkg/crypto/tls/tls.go
+++ b/src/pkg/crypto/tls/tls.go
@@ -6,6 +6,8 @@
 package tls
 
 import (
+	"crypto"
+	"crypto/ecdsa"
 	"crypto/rsa"
 	"crypto/x509"
 	"encoding/pem"
@@ -153,30 +155,16 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (cert Certificate, err error)\
 		err = errors.New("crypto/tls: failed to parse key PEM data")
 		return
 	}
-	if keyDERBlock.Type != "CERTIFICATE" {
+	if strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
 		break
 	}
 
-	// OpenSSL 0.9.8 generates PKCS#1 private keys by default, while
-	// OpenSSL 1.0.0 generates PKCS#8 keys. We try both.
-	var key *rsa.PrivateKey
-	if key, err = x509.ParsePKCS1PrivateKey(keyDERBlock.Bytes); err != nil {
-		var privKey interface{}
-		if privKey, err = x509.ParsePKCS8PrivateKey(keyDERBlock.Bytes); err != nil {
-			err = errors.New("crypto/tls: failed to parse key: " + err.Error())
-			return
-		}
-
-		var ok bool
-		if key, ok = privKey.(*rsa.PrivateKey); !ok {
-			err = errors.New("crypto/tls: found non-RSA private key in PKCS#8 wrapping")
-			return
-		}
+	cert.PrivateKey, err = parsePrivateKey(keyDERBlock.Bytes)
+	if err != nil {
+		return
 	}
 
-	cert.PrivateKey = key
-
 	// We don't need to parse the public key for TLS, but we so do anyway
 	// to check that it looks sane and matches the private key.
 	x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
@@ -184,10 +172,54 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (cert Certificate, err error)\
 		return
 	}
 
-	if x509Cert.PublicKeyAlgorithm != x509.RSA || x509Cert.PublicKey.(*rsa.PublicKey).N.Cmp(key.PublicKey.N) != 0 {
-		err = errors.New("crypto/tls: private key does not match public key")
+	switch pub := x509Cert.PublicKey.(type) {
+	case *rsa.PublicKey:
+		priv, ok := cert.PrivateKey.(*rsa.PrivateKey)
+		if !ok {
+			err = errors.New("crypto/tls: private key type does not match public key type")
+			return
+		}
+		if pub.N.Cmp(priv.N) != 0 {
+			err = errors.New("crypto/tls: private key does not match public key")
+			return
+		}
+	case *ecdsa.PublicKey:
+		priv, ok := cert.PrivateKey.(*ecdsa.PrivateKey)
+		if !ok {
+			err = errors.New("crypto/tls: private key type does not match public key type")
+			return
+
+		}
+		if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
+			err = errors.New("crypto/tls: private key does not match public key")
+			return
+		}
+	default:
+		err = errors.New("crypto/tls: unknown public key algorithm")
 		return
 	}
 
 	return
 }
 
+// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
+// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
+// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
+func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
+	if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
+		return key, nil
+	}
+	if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
+		switch key := key.(type) {
+		case *rsa.PrivateKey, *ecdsa.PrivateKey:
+			return key, nil
+		default:
+			return nil, errors.New("crypto/tls: found unknown private key type in PKCS#8 wrapping")
+		}
+	}
+	if key, err := x509.ParseECPrivateKey(der); err == nil {
+		return key, nil
+	}
+
+	return nil, errors.New("crypto/tls: failed to parse private key")
+}

src/pkg/crypto/tls/tls_test.go

--- a/src/pkg/crypto/tls/tls_test.go
+++ b/src/pkg/crypto/tls/tls_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 )
 
-var certPEM = `-----BEGIN CERTIFICATE-----
+var rsaCertPEM = `-----BEGIN CERTIFICATE-----
 MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
 GAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
 aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
@@ -22,7 +22,7 @@ r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
 -----END CERTIFICATE-----
 `
 
-var keyPEM = `-----BEGIN RSA PRIVATE KEY-----
+var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
 MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
 k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
 6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
@@ -33,15 +33,61 @@ D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
 -----END RSA PRIVATE KEY-----
 `
 
+var ecdsaCertPEM = `-----BEGIN CERTIFICATE-----
+MIIB/jCCAWICCQDscdUxw16XFDAJBgcqhkjOPQQBMEUxCzAJBgNVBAYTAkFVMRMw
+EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
+eSBMdGQwHhcNMTIxMTE0MTI0MDQ4WhcNMTUxMTE0MTI0MDQ4WjBFMQswCQYDVQQG
+EwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lk
+Z2l0cyBQdHkgTHRkMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBY9+my9OoeSUR
+lDQdV/x8LsOuLilthhiS1Tz4aGDHIPwC1mlvnf7fg5lecYpMCrLLhauAc1UJXcgl
+01xoLuzgtAEAgv2P/jgytzRSpUYvgLBt1UA0leLYBy6mQQbrNEuqT3INapKIcUv8
+XxYP0xMEUksLPq6Ca+CRSqTtrd/23uTnapkwCQYHKoZIzj0EAQOBigAwgYYCQXJo
+A7Sl2nLVf+4Iu/tAX/IF4MavARKC4PPHK3zfuGfPR3oCCcsAoz3kAzOeijvd0iXb
+H5jBImIxPL4WxQNiBTexAkF8D1EtpYuWdlVQ80/h/f4pBcGiXPqX5h2PQSQY7hP1
++jwM1FGS4fREIOvlBYr/SzzQRtwrvrzGYxDEDbsC0ZGRnA==
+-----END CERTIFICATE-----
+`
+
+var ecdsaKeyPEM = `-----BEGIN EC PARAMETERS-----
+BgUrgQQAIw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIBrsoKp0oqcv6/JovJJDoDVSGWdirrkgCWxrprGlzB9o0X8fV675X0
+NwuBenXFfeZvVcwluO7/Q9wkYoPd/t3jGImgBwYFK4EEACOhgYkDgYYABAFj36bL
+06h5JRGUNB1X/Hwuw64uKW2GGJLVPPhoYMcg/ALWaW+d/t+DmV5xikwKssuFq4Bz
+VQldyCXTXGgu7OC0AQCC/Y/+ODK3NFKlRi+AsG3VQDSV4tgHLqZBBus0S6pPcg1q
+kohxS/xfFg/TEwRSSws+roJr4JFKpO2t3/23uTnapkw==
+-----END EC PRIVATE KEY-----
+`
+
+var keyPairTests = []struct {
+	algo string
+	cert *string
+	key  *string
+}{
+	{"ECDSA", &ecdsaCertPEM, &ecdsaKeyPEM},
+	{"RSA", &rsaCertPEM, &rsaKeyPEM},
+}
+
 func TestX509KeyPair(t *testing.T) {\
-	_, err := X509KeyPair([]byte(keyPEM+certPEM), []byte(keyPEM+certPEM))\
-	if err != nil {\
-		t.Errorf("Failed to load key followed by cert: %s", err)\
+	var pem []byte
+	for _, test := range keyPairTests {
+		pem = []byte(*test.cert + *test.key)
+		if _, err := X509KeyPair(pem, pem); err != nil {
+			t.Errorf("Failed to load %s cert followed by %s key: %s", test.algo, test.algo, err)
+		}
+		pem = []byte(*test.key + *test.cert)
+		if _, err := X509KeyPair(pem, pem); err != nil {
+			t.Errorf("Failed to load %s key followed by %s cert: %s", test.algo, test.algo, err)
+		}
 	}
-+\
-	_, err = X509KeyPair([]byte(certPEM+keyPEM), []byte(certPEM+keyPEM))\
-	if err != nil {\
-		t.Errorf("Failed to load cert followed by key: %s", err)\
-		println(err.Error())\
+
+}
+
+func TestX509MixedKeyPair(t *testing.T) {
+	if _, err := X509KeyPair([]byte(rsaCertPEM), []byte(ecdsaKeyPEM)); err == nil {
+		t.Error("Load of RSA certificate succeeded with ECDSA private key")
+	}
+	if _, err := X509KeyPair([]byte(ecdsaCertPEM), []byte(rsaKeyPEM)); err == nil {
+		t.Error("Load of ECDSA certificate succeeded with RSA private key")
 	}
 }

コアとなるコードの解説

parsePrivateKey 関数

この関数は、PEMブロックから抽出されたDERエンコードされた秘密鍵バイト列を受け取り、Goの crypto.PrivateKey インターフェースを満たす具体的な秘密鍵型(*rsa.PrivateKey または *ecdsa.PrivateKey)に解析しようとします。

  1. x509.ParsePKCS1PrivateKey(der): まず、PKCS#1形式のRSA秘密鍵として解析を試みます。成功すればその鍵を返します。
  2. x509.ParsePKCS8PrivateKey(der): PKCS#1での解析に失敗した場合、次にPKCS#8形式として解析を試みます。PKCS#8は汎用的なフォーマットであり、RSA鍵もEC鍵も格納できます。解析に成功した場合、返された鍵の型が *rsa.PrivateKey または *ecdsa.PrivateKey であることを確認し、それ以外の場合は「未知の秘密鍵タイプ」としてエラーを返します。
  3. x509.ParseECPrivateKey(der): PKCS#8での解析にも失敗した場合、最後にSEC1形式のEC秘密鍵として解析を試みます。成功すればその鍵を返します。

これらの試行がすべて失敗した場合、"crypto/tls: failed to parse private key" というエラーを返します。この関数により、X509KeyPair は様々な形式の秘密鍵に対応できるようになりました。

X509KeyPair 関数の公開鍵検証ロジック

X509KeyPair 関数は、証明書から抽出された公開鍵と、解析された秘密鍵が本当にペアになっているかを検証します。

	switch pub := x509Cert.PublicKey.(type) {
	case *rsa.PublicKey:
		priv, ok := cert.PrivateKey.(*rsa.PrivateKey)
		if !ok {
			err = errors.New("crypto/tls: private key type does not match public key type")
			return
		}
		if pub.N.Cmp(priv.N) != 0 {
			err = errors.New("crypto/tls: private key does not match public key")
			return
		}
	case *ecdsa.PublicKey:
		priv, ok := cert.PrivateKey.(*ecdsa.PrivateKey)
		if !ok {
			err = errors.New("crypto/tls: private key type does not match public key type")
			return

		}
		if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
			err = errors.New("crypto/tls: private key does not match public key")
			return
		}
	default:
		err = errors.New("crypto/tls: unknown public key algorithm")
		return
	}

この switch ステートメントは、証明書内の公開鍵の具体的な型に基づいて処理を分岐させます。

  • *rsa.PublicKey の場合:
    • cert.PrivateKey*rsa.PrivateKey 型にキャストできるかを確認します。できなければ、公開鍵と秘密鍵の型が一致しないとしてエラーを返します。
    • 両者がRSA鍵であれば、公開鍵のモジュラス pub.N と秘密鍵の公開部分のモジュラス priv.N を比較します。これらが一致しない場合、鍵ペアが一致しないとしてエラーを返します。
  • *ecdsa.PublicKey の場合:
    • cert.PrivateKey*ecdsa.PrivateKey 型にキャストできるかを確認します。できなければ、公開鍵と秘密鍵の型が一致しないとしてエラーを返します。
    • 両者がECDSA鍵であれば、公開鍵のX座標 pub.X とY座標 pub.Y を、秘密鍵の公開部分のX座標 priv.X とY座標 priv.Y と比較します。これらが一致しない場合、鍵ペアが一致しないとしてエラーを返します。
  • default (その他の型):
    • RSAでもECDSAでもない未知の公開鍵アルゴリズムの場合、エラーを返します。

このロジックにより、X509KeyPair は、ロードされた証明書と秘密鍵が、その暗号アルゴリズム(RSAまたはEC)に応じて正しくペアになっていることを厳密に検証できるようになりました。

関連リンク

参考にした情報源リンク