Skip to content

Commit

Permalink
acme: implement ListCertAlternates
Browse files Browse the repository at this point in the history
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 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
  • Loading branch information
jameshartig authored and 4a6f656c committed Sep 17, 2021
1 parent c084706 commit 90f61a9
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 0 deletions.
26 changes: 26 additions & 0 deletions acme/rfc8555.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,29 @@ func isAlreadyRevoked(err error) bool {
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
}
32 changes: 32 additions & 0 deletions acme/rfc8555_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,35 @@ func TestRFC_AlreadyRevokedCert(t *testing.T) {
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)
}
exp := []string{"https://example.com/crt/2", "https://example.com/crt/3"}
if !reflect.DeepEqual(crts, exp) {
t.Errorf("ListCertAlternates(/crt): %v; want %v", crts, exp)
}
crts, err = cl.ListCertAlternates(context.Background(), s.url("/crt2"))
if err != nil {
t.Fatalf("ListCertAlternates: %v", err)
}
if len(crts) != 0 {
t.Errorf("ListCertAlternates(/crt2): %v; want []", crts)
}
}

0 comments on commit 90f61a9

Please sign in to comment.