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>
This commit is contained in:
Alex Vaghin
2016-08-30 15:40:08 +00:00
parent 351dc6a5bf
commit b13fc1fd38
2 changed files with 96 additions and 10 deletions
+53 -10
View File
@@ -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
View File
@@ -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"