Skip to content

Commit 089bfa5

Browse files
jameshartigFiloSottile
authored andcommitted
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 <[email protected]> Run-TryBot: Filippo Valsorda <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> TryBot-Result: Go Bot <[email protected]>
1 parent 84f3576 commit 089bfa5

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

acme/rfc8555.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,29 @@ func isAlreadyRevoked(err error) bool {
410410
e, ok := err.(*Error)
411411
return ok && e.ProblemType == "urn:ietf:params:acme:error:alreadyRevoked"
412412
}
413+
414+
// ListCertAlternates retrieves any alternate certificate chain URLs for the
415+
// given certificate chain URL. These alternate URLs can be passed to FetchCert
416+
// in order to retrieve the alternate certificate chains.
417+
//
418+
// If there are no alternate issuer certificate chains, a nil slice will be
419+
// returned.
420+
func (c *Client) ListCertAlternates(ctx context.Context, url string) ([]string, error) {
421+
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
422+
return nil, err
423+
}
424+
425+
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
426+
if err != nil {
427+
return nil, err
428+
}
429+
defer res.Body.Close()
430+
431+
// We don't need the body but we need to discard it so we don't end up
432+
// preventing keep-alive
433+
if _, err := io.Copy(ioutil.Discard, res.Body); err != nil {
434+
return nil, fmt.Errorf("acme: cert alternates response stream: %v", err)
435+
}
436+
alts := linkHeader(res.Header, "alternate")
437+
return alts, nil
438+
}

acme/rfc8555_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,3 +882,35 @@ func TestRFC_AlreadyRevokedCert(t *testing.T) {
882882
t.Fatalf("RevokeCert: %v", err)
883883
}
884884
}
885+
886+
func TestRFC_ListCertAlternates(t *testing.T) {
887+
s := newACMEServer()
888+
s.handle("/crt", func(w http.ResponseWriter, r *http.Request) {
889+
w.Header().Set("Content-Type", "application/pem-certificate-chain")
890+
w.Header().Add("Link", `<https://example.com/crt/2>;rel="alternate"`)
891+
w.Header().Add("Link", `<https://example.com/crt/3>; rel="alternate"`)
892+
w.Header().Add("Link", `<https://example.com/acme>; rel="index"`)
893+
})
894+
s.handle("/crt2", func(w http.ResponseWriter, r *http.Request) {
895+
w.Header().Set("Content-Type", "application/pem-certificate-chain")
896+
})
897+
s.start()
898+
defer s.close()
899+
900+
cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")}
901+
crts, err := cl.ListCertAlternates(context.Background(), s.url("/crt"))
902+
if err != nil {
903+
t.Fatalf("ListCertAlternates: %v", err)
904+
}
905+
want := []string{"https://example.com/crt/2", "https://example.com/crt/3"}
906+
if !reflect.DeepEqual(crts, want) {
907+
t.Errorf("ListCertAlternates(/crt): %v; want %v", crts, want)
908+
}
909+
crts, err = cl.ListCertAlternates(context.Background(), s.url("/crt2"))
910+
if err != nil {
911+
t.Fatalf("ListCertAlternates: %v", err)
912+
}
913+
if crts != nil {
914+
t.Errorf("ListCertAlternates(/crt2): %v; want nil", crts)
915+
}
916+
}

0 commit comments

Comments
 (0)