diff --git a/acme/internal/acme/acme.go b/acme/internal/acme/acme.go index 0439f7e..be5b09d 100644 --- a/acme/internal/acme/acme.go +++ b/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, diff --git a/acme/internal/acme/acme_test.go b/acme/internal/acme/acme_test.go index 2ae69a9..c85a689 100644 --- a/acme/internal/acme/acme_test.go +++ b/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"