acme: fetch fresh nonces from newNonce resource

Previously, nonce values were fetched from Directory URL.
RFC8555 and some recent drafts provide specific URL to fetch
new nonce values from.

This CL makes the client always use new nonce URL when available
and fall back to the previous behavior otherwise.

Updates golang/go#21081

Change-Id: I6442004b01d46aa015c193ca4c80daa712b78790
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/191603
Run-TryBot: Alex Vaghin <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/acme/acme.go b/acme/acme.go
index 3cf7486..9e28b32 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -750,12 +750,16 @@
 }
 
 // popNonce returns a nonce value previously stored with c.addNonce
-// or fetches a fresh one from a URL by issuing a HEAD request.
-// It first tries c.directoryURL() and then the provided url if the former fails.
+// or fetches a fresh one from c.dir.NonceURL.
+// If NonceURL is empty, it first tries c.directoryURL() and, failing that,
+// the provided url.
 func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
 	c.noncesMu.Lock()
 	defer c.noncesMu.Unlock()
 	if len(c.nonces) == 0 {
+		if c.dir != nil && c.dir.NonceURL != "" {
+			return c.fetchNonce(ctx, c.dir.NonceURL)
+		}
 		dirURL := c.directoryURL()
 		v, err := c.fetchNonce(ctx, dirURL)
 		if err != nil && url != dirURL {
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index bfb5e53..4a8d9f5 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -84,3 +84,40 @@
 		t.Error("dir.Meta.ExternalAccountRequired is false")
 	}
 }
+
+func TestRFC_popNonce(t *testing.T) {
+	var count int
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// The Client uses only Directory.NonceURL when specified.
+		// Expect no other URL paths.
+		if r.URL.Path != "/new-nonce" {
+			t.Errorf("r.URL.Path = %q; want /new-nonce", r.URL.Path)
+		}
+		if count > 0 {
+			w.WriteHeader(http.StatusTooManyRequests)
+			return
+		}
+		count++
+		w.Header().Set("Replay-Nonce", "second")
+	}))
+	cl := &Client{
+		DirectoryURL: ts.URL,
+		dir:          &Directory{NonceURL: ts.URL + "/new-nonce"},
+	}
+	cl.addNonce(http.Header{"Replay-Nonce": {"first"}})
+
+	for i, nonce := range []string{"first", "second"} {
+		v, err := cl.popNonce(context.Background(), "")
+		if err != nil {
+			t.Errorf("%d: cl.popNonce: %v", i, err)
+		}
+		if v != nonce {
+			t.Errorf("%d: cl.popNonce = %q; want %q", i, v, nonce)
+		}
+	}
+	// No more nonces and server replies with an error past first nonce fetch.
+	// Expected to fail.
+	if _, err := cl.popNonce(context.Background(), ""); err == nil {
+		t.Error("last cl.popNonce returned nil error")
+	}
+}