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:
+53
-10
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user