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>
This commit is contained in:
committed by
Filippo Valsorda
parent
84f357641f
commit
089bfa5675
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user