diff --git a/acme/acme.go b/acme/acme.go index 45b3f78..be18ad7 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -65,7 +65,21 @@ type certOptKey struct { key crypto.Signer } -func (co *certOptKey) privateCertOpt() {} +func (*certOptKey) privateCertOpt() {} + +// WithTemplate creates an option for specifying a certificate template. +// See x509.CreateCertificate for template usage details. +// +// In TLSSNIxChallengeCert methods, the template is also used as parent, +// resulting in a self-signed certificate. +// The DNSNames field of t is always overwritten for tls-sni challenge certs. +func WithTemplate(t *x509.Certificate) CertOption { + return (*certOptTemplate)(t) +} + +type certOptTemplate x509.Certificate + +func (*certOptTemplate) privateCertOpt() {} // Client is an ACME client. // The only required field is Key. An example of creating a client with a new key @@ -874,7 +888,10 @@ func keyAuth(pub crypto.PublicKey, token string) (string, error) { // 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 + var ( + key crypto.Signer + tmpl *x509.Certificate + ) for _, o := range opt { switch o := o.(type) { case *certOptKey: @@ -882,6 +899,9 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) { return tls.Certificate{}, errors.New("acme: duplicate key option") } key = o.key + case *certOptTemplate: + var t = *(*x509.Certificate)(o) // shallow copy is ok + tmpl = &t default: // package's fault, if we let this happen: panic(fmt.Sprintf("unsupported option type %T", o)) @@ -893,15 +913,18 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) { return tls.Certificate{}, err } } - t := x509.Certificate{ - SerialNumber: big.NewInt(1), - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - BasicConstraintsValid: true, - KeyUsage: x509.KeyUsageKeyEncipherment, - DNSNames: san, + if tmpl == nil { + tmpl = &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageKeyEncipherment, + } } - der, err := x509.CreateCertificate(rand.Reader, &t, &t, key.Public(), key) + tmpl.DNSNames = san + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) if err != nil { return tls.Certificate{}, err } diff --git a/acme/acme_test.go b/acme/acme_test.go index 10e72c4..e552984 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -1060,20 +1060,28 @@ func TestTLSSNI02ChallengeCert(t *testing.T) { } } -func TestTLSChallengeCertRSA(t *testing.T) { +func TestTLSChallengeCertOpt(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 512) if err != nil { t.Fatal(err) } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{Organization: []string{"Test"}}, + DNSNames: []string{"should-be-overwritten"}, + } + opts := []CertOption{WithKey(key), WithTemplate(tmpl)} + client := &Client{Key: testKeyEC} - cert1, _, err := client.TLSSNI01ChallengeCert("token", WithKey(key)) + cert1, _, err := client.TLSSNI01ChallengeCert("token", opts...) if err != nil { t.Fatal(err) } - cert2, _, err := client.TLSSNI02ChallengeCert("token", WithKey(key)) + cert2, _, err := client.TLSSNI02ChallengeCert("token", opts...) 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) @@ -1098,6 +1106,20 @@ func TestTLSChallengeCertRSA(t *testing.T) { if tlspub.N.Cmp(key.N) != 0 { t.Errorf("%d: tlspub.N = %v; want %v", i, tlspub.N, key.N) } + // verify template option + sn := big.NewInt(2) + if x509Cert.SerialNumber.Cmp(sn) != 0 { + t.Errorf("%d: SerialNumber = %v; want %v", i, x509Cert.SerialNumber, sn) + } + org := []string{"Test"} + if !reflect.DeepEqual(x509Cert.Subject.Organization, org) { + t.Errorf("%d: Subject.Organization = %+v; want %+v", i, x509Cert.Subject.Organization, org) + } + for _, v := range x509Cert.DNSNames { + if !strings.HasSuffix(v, ".acme.invalid") { + t.Errorf("%d: invalid DNSNames element: %q", i, v) + } + } } }