浏览代码

acme: take a user provided key in TLSSNIxChallengeCert

Some systems can only operate with a certain key type, for instance RSA,
which makes it impossible for them to use Client provided certificates
to fulfill tls-sni-x challenges since TLSSNIxChallengeCert methods
of the Client always generate ECDSA-based certificates.

At the same time we want to keep using ECDSA-based certificates
in autocert.Manager.

This change allows for a user supplied key when generating tls-sni-x
challenge certficates.

Change-Id: I04f617ff99794a71ef23ad259cb333839f13ae1c
Reviewed-on: https://go-review.googlesource.com/27750
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Alex Vaghin 9 年之前
父节点
当前提交
b13fc1fd38
共有 2 个文件被更改,包括 96 次插入10 次删除
  1. 53 10
      acme/internal/acme/acme.go
  2. 43 0
      acme/internal/acme/acme_test.go

+ 53 - 10
acme/internal/acme/acme.go

@@ -45,6 +45,24 @@ const (
 	maxCertSize = 1 << 20 // max size of a certificate, in bytes
 )
 
+// CertOption is an optional argument type for Client methods which manipulate
+// certificate data.
+type CertOption interface {
+	privateCertOpt()
+}
+
+// WithKey creates an option holding a private/public key pair.
+// The private part signs a certificate, and the public part represents the signee.
+func WithKey(key crypto.Signer) CertOption {
+	return &certOptKey{key}
+}
+
+type certOptKey struct {
+	key crypto.Signer
+}
+
+func (co *certOptKey) privateCertOpt() {}
+
 // Client is an ACME client.
 // The only required field is Key. An example of creating a client with a new key
 // is as follows:
@@ -514,10 +532,13 @@ func (c *Client) HTTP01ChallengePath(token string) string {
 // For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3.
 //
 // The token argument is a Challenge.Token value.
+// If a WithKey option is provided, its private part signs the returned cert,
+// and the public part is used to specify the signee.
+// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
 //
 // The returned certificate is valid for the next 24 hours and must be presented only when
 // the server name of the client hello matches exactly the returned name value.
-func (c *Client) TLSSNI01ChallengeCert(token string) (cert tls.Certificate, name string, err error) {
+func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
 	ka, err := keyAuth(c.Key.Public(), token)
 	if err != nil {
 		return tls.Certificate{}, "", err
@@ -525,7 +546,7 @@ func (c *Client) TLSSNI01ChallengeCert(token string) (cert tls.Certificate, name
 	b := sha256.Sum256([]byte(ka))
 	h := hex.EncodeToString(b[:])
 	name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
-	cert, err = tlsChallengeCert(name)
+	cert, err = tlsChallengeCert([]string{name}, opt)
 	if err != nil {
 		return tls.Certificate{}, "", err
 	}
@@ -538,10 +559,13 @@ func (c *Client) TLSSNI01ChallengeCert(token string) (cert tls.Certificate, name
 // https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3.
 //
 // The token argument is a Challenge.Token value.
+// If a WithKey option is provided, its private part signs the returned cert,
+// and the public part is used to specify the signee.
+// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
 //
 // The returned certificate is valid for the next 24 hours and must be presented only when
 // the server name in the client hello matches exactly the returned name value.
-func (c *Client) TLSSNI02ChallengeCert(token string) (cert tls.Certificate, name string, err error) {
+func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
 	b := sha256.Sum256([]byte(token))
 	h := hex.EncodeToString(b[:])
 	sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
@@ -554,7 +578,7 @@ func (c *Client) TLSSNI02ChallengeCert(token string) (cert tls.Certificate, name
 	h = hex.EncodeToString(b[:])
 	sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
 
-	cert, err = tlsChallengeCert(sanA, sanB)
+	cert, err = tlsChallengeCert([]string{sanA, sanB}, opt)
 	if err != nil {
 		return tls.Certificate{}, "", err
 	}
@@ -815,11 +839,27 @@ func keyAuth(pub crypto.PublicKey, token string) (string, error) {
 }
 
 // tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
-// with the given SANs.
-func tlsChallengeCert(san ...string) (tls.Certificate, error) {
-	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		return tls.Certificate{}, err
+// with the given SANs and auto-generated public/private key pair.
+// To create a cert with a custom key pair, specify WithKey option.
+func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
+	var key crypto.Signer
+	for _, o := range opt {
+		switch o := o.(type) {
+		case *certOptKey:
+			if key != nil {
+				return tls.Certificate{}, errors.New("acme: duplicate key option")
+			}
+			key = o.key
+		default:
+			// package's fault, if we let this happen:
+			panic(fmt.Sprintf("unsupported option type %T", o))
+		}
+	}
+	if key == nil {
+		var err error
+		if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil {
+			return tls.Certificate{}, err
+		}
 	}
 	t := x509.Certificate{
 		SerialNumber:          big.NewInt(1),
@@ -829,7 +869,10 @@ func tlsChallengeCert(san ...string) (tls.Certificate, error) {
 		KeyUsage:              x509.KeyUsageKeyEncipherment,
 		DNSNames:              san,
 	}
-	der, err := x509.CreateCertificate(rand.Reader, &t, &t, &key.PublicKey, key)
+	der, err := x509.CreateCertificate(rand.Reader, &t, &t, key.Public(), key)
+	if err != nil {
+		return tls.Certificate{}, err
+	}
 	return tls.Certificate{
 		Certificate: [][]byte{der},
 		PrivateKey:  key,

+ 43 - 0
acme/internal/acme/acme_test.go

@@ -7,6 +7,8 @@ package acme
 import (
 	"bytes"
 	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/base64"
@@ -1024,6 +1026,47 @@ func TestTLSSNI02ChallengeCert(t *testing.T) {
 	}
 }
 
+func TestTLSChallengeCertRSA(t *testing.T) {
+	key, err := rsa.GenerateKey(rand.Reader, 512)
+	if err != nil {
+		t.Fatal(err)
+	}
+	client := &Client{Key: testKeyEC}
+	cert1, _, err := client.TLSSNI01ChallengeCert("token", WithKey(key))
+	if err != nil {
+		t.Fatal(err)
+	}
+	cert2, _, err := client.TLSSNI02ChallengeCert("token", WithKey(key))
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i, tlscert := range []tls.Certificate{cert1, cert2} {
+		// verify generated cert private key
+		tlskey, ok := tlscert.PrivateKey.(*rsa.PrivateKey)
+		if !ok {
+			t.Errorf("%d: tlscert.PrivateKey is %T; want *rsa.PrivateKey", i, tlscert.PrivateKey)
+			continue
+		}
+		if tlskey.D.Cmp(key.D) != 0 {
+			t.Errorf("%d: tlskey.D = %v; want %v", i, tlskey.D, key.D)
+		}
+		// verify generated cert public key
+		x509Cert, err := x509.ParseCertificate(tlscert.Certificate[0])
+		if err != nil {
+			t.Errorf("%d: %v", i, err)
+			continue
+		}
+		tlspub, ok := x509Cert.PublicKey.(*rsa.PublicKey)
+		if !ok {
+			t.Errorf("%d: x509Cert.PublicKey is %T; want *rsa.PublicKey", i, x509Cert.PublicKey)
+			continue
+		}
+		if tlspub.N.Cmp(key.N) != 0 {
+			t.Errorf("%d: tlspub.N = %v; want %v", i, tlspub.N, key.N)
+		}
+	}
+}
+
 func TestHTTP01Challenge(t *testing.T) {
 	const (
 		token = "xxx"