acme: implement Client.ListCertAlternates

Let's Encrypt is defaulting to a longer cross-signed chain on May 4th,
2021 but will offer the ability to download the shorter chain via an
alternate URL via a link header [1]. The shorter chain can be selected
to workaround a validation bug in legacy versions of OpenSSL, GnuTLS,
and LibreSSL. The alternate relation is described in section 7.4.2 of
RFC 8555.

ListCertAlternates should be passed the original certificate chain URL
and will return a list of alternate chain URLs that can be passed to
FetchCert to download.

Fixes golang/go#42437

[1] https://community.letsencrypt.org/t/production-chain-changes/150739

Change-Id: Iaa32e49cb1322ac79ac1a5b4b7980d5401f4b86e
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/277294
Trust: Filippo Valsorda <filippo@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/acme/rfc8555.go b/acme/rfc8555.go
index 073cee5..f9d3011 100644
--- a/acme/rfc8555.go
+++ b/acme/rfc8555.go
@@ -410,3 +410,29 @@
 	e, ok := err.(*Error)
 	return ok && e.ProblemType == "urn:ietf:params:acme:error:alreadyRevoked"
 }
+
+// ListCertAlternates retrieves any alternate certificate chain URLs for the
+// given certificate chain URL. These alternate URLs can be passed to FetchCert
+// in order to retrieve the alternate certificate chains.
+//
+// If there are no alternate issuer certificate chains, a nil slice will be
+// returned.
+func (c *Client) ListCertAlternates(ctx context.Context, url string) ([]string, error) {
+	if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
+		return nil, err
+	}
+
+	res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	// We don't need the body but we need to discard it so we don't end up
+	// preventing keep-alive
+	if _, err := io.Copy(ioutil.Discard, res.Body); err != nil {
+		return nil, fmt.Errorf("acme: cert alternates response stream: %v", err)
+	}
+	alts := linkHeader(res.Header, "alternate")
+	return alts, nil
+}
diff --git a/acme/rfc8555_test.go b/acme/rfc8555_test.go
index c35118e..07e2f29 100644
--- a/acme/rfc8555_test.go
+++ b/acme/rfc8555_test.go
@@ -882,3 +882,35 @@
 		t.Fatalf("RevokeCert: %v", err)
 	}
 }
+
+func TestRFC_ListCertAlternates(t *testing.T) {
+	s := newACMEServer()
+	s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/pem-certificate-chain")
+		w.Header().Add("Link", `<https://example.com/crt/2>;rel="alternate"`)
+		w.Header().Add("Link", `<https://example.com/crt/3>; rel="alternate"`)
+		w.Header().Add("Link", `<https://example.com/acme>; rel="index"`)
+	})
+	s.handle("/crt2", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/pem-certificate-chain")
+	})
+	s.start()
+	defer s.close()
+
+	cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
+	crts, err := cl.ListCertAlternates(context.Background(), s.url("/crt"))
+	if err != nil {
+		t.Fatalf("ListCertAlternates: %v", err)
+	}
+	want := []string{"https://example.com/crt/2", "https://example.com/crt/3"}
+	if !reflect.DeepEqual(crts, want) {
+		t.Errorf("ListCertAlternates(/crt): %v; want %v", crts, want)
+	}
+	crts, err = cl.ListCertAlternates(context.Background(), s.url("/crt2"))
+	if err != nil {
+		t.Fatalf("ListCertAlternates: %v", err)
+	}
+	if crts != nil {
+		t.Errorf("ListCertAlternates(/crt2): %v; want nil", crts)
+	}
+}