acme/autocert: add an option for RSA-based certs Currently, autocert.Manager always generates EC-based certificates. This change adds an optional field forcing the Manager to use RSA instead. An alternative idea, a "double" certificate, where the Manager presents either RSA or EC certificate based on client's compatibility, doesn't seem to be worth the implementation time given the constant increase in Elliptic Curve cryptography. Fixes golang/go#17744 Change-Id: Idc68abfc698bcff4aad99715baefc06f8fae50ad Reviewed-on: https://go-review.googlesource.com/34570 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 877bfba..4b15816 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go
@@ -141,6 +141,12 @@ // If the Client's account key is already registered, Email is not used. Email string + // ForceRSA makes the Manager generate certificates with 2048-bit RSA keys. + // + // If false, a default is used. Currently the default + // is EC-based keys using the P-256 curve. + ForceRSA bool + clientMu sync.Mutex client *acme.Client // initialized by acmeClient method @@ -385,11 +391,21 @@ if state, ok := m.state[domain]; ok { return state, nil } + // new locked state - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + var ( + err error + key crypto.Signer + ) + if m.ForceRSA { + key, err = rsa.GenerateKey(rand.Reader, 2048) + } else { + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } if err != nil { return nil, err } + state := &certState{ key: key, locked: true,
diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index 295f702..4bcd6d5 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go
@@ -108,18 +108,41 @@ } func TestGetCertificate(t *testing.T) { - testGetCertificate(t, false) + man := &Manager{Prompt: AcceptTOS} + defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + testGetCertificate(t, man, "example.org", hello) } func TestGetCertificate_trailingDot(t *testing.T) { - testGetCertificate(t, true) -} - -func testGetCertificate(t *testing.T, trailingDot bool) { - const domain = "example.org" man := &Manager{Prompt: AcceptTOS} defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org."} + testGetCertificate(t, man, "example.org", hello) +} +func TestGetCertificate_ForceRSA(t *testing.T) { + man := &Manager{ + Prompt: AcceptTOS, + Cache: make(memCache), + ForceRSA: true, + } + defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + testGetCertificate(t, man, "example.org", hello) + + cert, err := man.cacheGet("example.org") + if err != nil { + t.Fatalf("man.cacheGet: %v", err) + } + if _, ok := cert.PrivateKey.(*rsa.PrivateKey); !ok { + t.Errorf("cert.PrivateKey is %T; want *rsa.PrivateKey", cert.PrivateKey) + } +} + +// tests man.GetCertificate flow using the provided hello argument. +// The domain argument is the expected domain name of a certificate request. +func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) { // echo token-02 | shasum -a 256 // then divide result in 2 parts separated by dot tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid" @@ -212,14 +235,10 @@ // simulate tls.Config.GetCertificate var tlscert *tls.Certificate done := make(chan struct{}) - go func(serverName string) { - if trailingDot { - serverName += "." - } - hello := &tls.ClientHelloInfo{ServerName: serverName} + go func() { tlscert, err = man.GetCertificate(hello) close(done) - }(domain) + }() select { case <-time.After(time.Minute): t.Fatal("man.GetCertificate took too long to return")