[インデックス 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)秘密鍵もロードできるように拡張されたことです。
-
parsePrivateKey
関数の導入:- 以前の
X509KeyPair
関数は、秘密鍵の解析にx509.ParsePKCS1PrivateKey
とx509.ParsePKCS8PrivateKey
を直接呼び出し、主にRSA鍵を想定していました。 - このコミットでは、新たに
parsePrivateKey
というヘルパー関数が導入されました。この関数は、与えられたDERエンコードされた秘密鍵ブロックを、以下の順序で解析を試みます。x509.ParsePKCS1PrivateKey
: PKCS#1形式のRSA秘密鍵を解析します。x509.ParsePKCS8PrivateKey
: PKCS#8形式の秘密鍵を解析します。この関数はRSA鍵とEC鍵の両方を返す可能性があるため、返された型が*rsa.PrivateKey
または*ecdsa.PrivateKey
であることを確認します。x509.ParseECPrivateKey
: SEC1形式のEC秘密鍵を解析します。
- これにより、
X509KeyPair
関数は、OpenSSLのバージョンや鍵の生成方法によって異なる秘密鍵フォーマット(PKCS#1、PKCS#8、SEC1)に対応できるようになりました。
- 以前の
-
公開鍵と秘密鍵の型および値の整合性チェックの強化:
X509KeyPair
関数は、ロードされた証明書内の公開鍵と、解析された秘密鍵が一致するかどうかを検証します。- 変更前は、公開鍵がRSA型であることを前提とし、RSA公開鍵のモジュラス
N
と秘密鍵のモジュラスN
を比較していました。 - 変更後は、証明書内の公開鍵の型を
switch
ステートメントで判定します。- RSA公開鍵の場合: 秘密鍵も
*rsa.PrivateKey
型であることを確認し、両者のモジュラスN
を比較します。 - ECDSA公開鍵の場合: 秘密鍵も
*ecdsa.PrivateKey
型であることを確認し、両者の公開鍵の座標X
とY
を比較します。EC公開鍵は楕円曲線上の点であり、その座標(X, Y)
が公開鍵の値を表します。 - その他の公開鍵アルゴリズムの場合: 未知の公開鍵アルゴリズムとしてエラーを返します。
- RSA公開鍵の場合: 秘密鍵も
- この変更により、RSAとECの両方の鍵ペアに対して、公開鍵と秘密鍵の型の一致および値の整合性が厳密にチェックされるようになりました。
-
テストケースの追加:
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
)に解析しようとします。
x509.ParsePKCS1PrivateKey(der)
: まず、PKCS#1形式のRSA秘密鍵として解析を試みます。成功すればその鍵を返します。x509.ParsePKCS8PrivateKey(der)
: PKCS#1での解析に失敗した場合、次にPKCS#8形式として解析を試みます。PKCS#8は汎用的なフォーマットであり、RSA鍵もEC鍵も格納できます。解析に成功した場合、返された鍵の型が*rsa.PrivateKey
または*ecdsa.PrivateKey
であることを確認し、それ以外の場合は「未知の秘密鍵タイプ」としてエラーを返します。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)に応じて正しくペアになっていることを厳密に検証できるようになりました。
関連リンク
- Go CL 6776043: https://golang.org/cl/6776043
参考にした情報源リンク
- X.509: https://ja.wikipedia.org/wiki/X.509
- 楕円曲線暗号: https://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9A%E6%9A%97%E5%8F%B7
- PKCS #1: https://ja.wikipedia.org/wiki/PKCS_1
- PKCS #8: https://ja.wikipedia.org/wiki/PKCS_8
- SEC 1: https://www.secg.org/sec1-v2.pdf (PDF)
- Go
crypto/tls
package documentation: https://pkg.go.dev/crypto/tls - Go
crypto/x509
package documentation: https://pkg.go.dev/crypto/x509 - Go
crypto/ecdsa
package documentation: https://pkg.go.dev/crypto/ecdsa - Go
crypto/rsa
package documentation: https://pkg.go.dev/crypto/rsa - OpenSSL
ecparam
command: https://www.openssl.org/docs/manmaster/man1/openssl-ecparam.html