acme/autocert: return error if Prompt not set
Without this, autocert will panic with an unhelpful nil pointer inside
the acme client.
Reorganized the test suite's ACME server stub creation, as I needed to
be able to stand up a test where GetCertificate was expected to fail.
Change-Id: Ie5e19c6e7766b4578c9b3c16789d7b27bd3be163
Reviewed-on: https://go-review.googlesource.com/40951
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go
index ce2f647..98842b4 100644
--- a/acme/autocert/autocert.go
+++ b/acme/autocert/autocert.go
@@ -162,6 +162,10 @@
// The error is propagated back to the caller of GetCertificate and is user-visible.
// This does not affect cached certs. See HostPolicy field description for more details.
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ if m.Prompt == nil {
+ return nil, errors.New("acme/autocert: Manager.Prompt not set")
+ }
+
name := hello.ServerName
if name == "" {
return nil, errors.New("acme/autocert: missing server name")
diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go
index acef162..6df4cf3 100644
--- a/acme/autocert/autocert_test.go
+++ b/acme/autocert/autocert_test.go
@@ -159,9 +159,28 @@
}
}
-// tests man.GetCertificate flow using the provided hello argument.
+func TestGetCertificate_nilPrompt(t *testing.T) {
+ man := &Manager{}
+ defer man.stopRenew()
+ url, finish := startACMEServerStub(t, man, "example.org")
+ defer finish()
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ man.Client = &acme.Client{
+ Key: key,
+ DirectoryURL: url,
+ }
+ hello := &tls.ClientHelloInfo{ServerName: "example.org"}
+ if _, err := man.GetCertificate(hello); err == nil {
+ t.Error("got certificate for example.org; wanted error")
+ }
+}
+
+// startACMEServerStub runs an ACME server
// The domain argument is the expected domain name of a certificate request.
-func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) {
+func startACMEServerStub(t *testing.T, man *Manager, domain string) (url string, finish func()) {
// echo token-02 | shasum -a 256
// then divide result in 2 parts separated by dot
tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid"
@@ -187,7 +206,7 @@
// discovery
case "/":
if err := discoTmpl.Execute(w, ca.URL); err != nil {
- t.Fatalf("discoTmpl: %v", err)
+ t.Errorf("discoTmpl: %v", err)
}
// client key registration
case "/new-reg":
@@ -197,7 +216,7 @@
w.Header().Set("location", ca.URL+"/authz/1")
w.WriteHeader(http.StatusCreated)
if err := authzTmpl.Execute(w, ca.URL); err != nil {
- t.Fatalf("authzTmpl: %v", err)
+ t.Errorf("authzTmpl: %v", err)
}
// accept tls-sni-02 challenge
case "/challenge/2":
@@ -215,14 +234,14 @@
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
csr, err := x509.ParseCertificateRequest(b)
if err != nil {
- t.Fatalf("new-cert: CSR: %v", err)
+ t.Errorf("new-cert: CSR: %v", err)
}
if csr.Subject.CommonName != domain {
t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain)
}
der, err := dummyCert(csr.PublicKey, domain)
if err != nil {
- t.Fatalf("new-cert: dummyCert: %v", err)
+ t.Errorf("new-cert: dummyCert: %v", err)
}
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
w.Header().Set("link", chainUp)
@@ -232,14 +251,51 @@
case "/ca-cert":
der, err := dummyCert(nil, "ca")
if err != nil {
- t.Fatalf("ca-cert: dummyCert: %v", err)
+ t.Errorf("ca-cert: dummyCert: %v", err)
}
w.Write(der)
default:
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
}
}))
- defer ca.Close()
+ finish = func() {
+ ca.Close()
+
+ // make sure token cert was removed
+ cancel := make(chan struct{})
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ tick := time.NewTicker(100 * time.Millisecond)
+ defer tick.Stop()
+ for {
+ hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
+ if _, err := man.GetCertificate(hello); err != nil {
+ return
+ }
+ select {
+ case <-tick.C:
+ case <-cancel:
+ return
+ }
+ }
+ }()
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ close(cancel)
+ t.Error("token cert was not removed")
+ <-done
+ }
+ }
+ return ca.URL, finish
+}
+
+// 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) {
+ url, finish := startACMEServerStub(t, man, domain)
+ defer finish()
// use EC key to run faster on 386
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -248,7 +304,7 @@
}
man.Client = &acme.Client{
Key: key,
- DirectoryURL: ca.URL,
+ DirectoryURL: url,
}
// simulate tls.Config.GetCertificate
@@ -279,23 +335,6 @@
t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, domain)
}
- // make sure token cert was removed
- done = make(chan struct{})
- go func() {
- for {
- hello := &tls.ClientHelloInfo{ServerName: tokenCertName}
- if _, err := man.GetCertificate(hello); err != nil {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
- close(done)
- }()
- select {
- case <-time.After(5 * time.Second):
- t.Error("token cert was not removed")
- case <-done:
- }
}
func TestAccountKeyCache(t *testing.T) {