diff --git a/go.mod b/go.mod index 8ca56a4b9..ff736d950 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e github.com/tailscale/depaware v0.0.0-20250112153213-b748de04d81b github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 - github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca + github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tailscale/mkctr v0.0.0-20250228050937-c75ea1476830 diff --git a/go.sum b/go.sum index ca1b5e30c..06fad5d6d 100644 --- a/go.sum +++ b/go.sum @@ -910,8 +910,8 @@ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8 github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= diff --git a/tempfork/acme/acme.go b/tempfork/acme/acme.go index 94234efe3..bbddb9551 100644 --- a/tempfork/acme/acme.go +++ b/tempfork/acme/acme.go @@ -270,10 +270,7 @@ func (c *Client) FetchRenewalInfo(ctx context.Context, leaf []byte) (*RenewalInf return nil, fmt.Errorf("parsing leaf certificate: %w", err) } - renewalURL, err := c.getRenewalURL(parsedLeaf) - if err != nil { - return nil, fmt.Errorf("generating renewal info URL: %w", err) - } + renewalURL := c.getRenewalURL(parsedLeaf) res, err := c.get(ctx, renewalURL, wantStatus(http.StatusOK)) if err != nil { @@ -288,16 +285,20 @@ func (c *Client) FetchRenewalInfo(ctx context.Context, leaf []byte) (*RenewalInf return &info, nil } -func (c *Client) getRenewalURL(cert *x509.Certificate) (string, error) { +func (c *Client) getRenewalURL(cert *x509.Certificate) string { // See https://www.ietf.org/archive/id/draft-ietf-acme-ari-04.html#name-the-renewalinfo-resource // for how the request URL is built. url := c.dir.RenewalInfoURL if !strings.HasSuffix(url, "/") { url += "/" } + return url + certRenewalIdentifier(cert) +} + +func certRenewalIdentifier(cert *x509.Certificate) string { aki := base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId) serial := base64.RawURLEncoding.EncodeToString(cert.SerialNumber.Bytes()) - return fmt.Sprintf("%s%s.%s", url, aki, serial), nil + return aki + "." + serial } // AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service diff --git a/tempfork/acme/acme_test.go b/tempfork/acme/acme_test.go index 5473bbc2b..f0c45aea9 100644 --- a/tempfork/acme/acme_test.go +++ b/tempfork/acme/acme_test.go @@ -549,10 +549,7 @@ func TestGetRenewalURL(t *testing.T) { } client := newTestClientWithMockDirectory() - urlString, err := client.getRenewalURL(parsedLeaf) - if err != nil { - t.Fatal(err) - } + urlString := client.getRenewalURL(parsedLeaf) parsedURL, err := url.Parse(urlString) if err != nil { diff --git a/tempfork/acme/rfc8555.go b/tempfork/acme/rfc8555.go index 3152e531b..3eaf935fd 100644 --- a/tempfork/acme/rfc8555.go +++ b/tempfork/acme/rfc8555.go @@ -7,6 +7,7 @@ package acme import ( "context" "crypto" + "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" @@ -205,6 +206,7 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO Identifiers []wireAuthzID `json:"identifiers"` NotBefore string `json:"notBefore,omitempty"` NotAfter string `json:"notAfter,omitempty"` + Replaces string `json:"replaces,omitempty"` }{} for _, v := range id { req.Identifiers = append(req.Identifiers, wireAuthzID{ @@ -218,6 +220,14 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO req.NotBefore = time.Time(o).Format(time.RFC3339) case orderNotAfterOpt: req.NotAfter = time.Time(o).Format(time.RFC3339) + case orderReplacesCert: + req.Replaces = certRenewalIdentifier(o.cert) + case orderReplacesCertDER: + cert, err := x509.ParseCertificate(o) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate being replaced: %w", err) + } + req.Replaces = certRenewalIdentifier(cert) default: // Package's fault if we let this happen. panic(fmt.Sprintf("unsupported order option type %T", o)) diff --git a/tempfork/acme/rfc8555_test.go b/tempfork/acme/rfc8555_test.go index d65720a35..ec51a7a5e 100644 --- a/tempfork/acme/rfc8555_test.go +++ b/tempfork/acme/rfc8555_test.go @@ -766,10 +766,17 @@ func TestRFC_AuthorizeOrder(t *testing.T) { s.start() defer s.close() + prevCertDER, _ := pem.Decode([]byte(leafPEM)) + prevCert, err := x509.ParseCertificate(prevCertDER.Bytes) + if err != nil { + t.Fatal(err) + } + cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/")} o, err := cl.AuthorizeOrder(context.Background(), DomainIDs("example.org"), WithOrderNotBefore(time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC)), WithOrderNotAfter(time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC)), + WithOrderReplacesCert(prevCert), ) if err != nil { t.Fatal(err) diff --git a/tempfork/acme/types.go b/tempfork/acme/types.go index 518fa2440..0142469d8 100644 --- a/tempfork/acme/types.go +++ b/tempfork/acme/types.go @@ -391,6 +391,30 @@ type orderNotAfterOpt time.Time func (orderNotAfterOpt) privateOrderOpt() {} +// WithOrderReplacesCert indicates that this Order is for a replacement of an +// existing certificate. +// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 +func WithOrderReplacesCert(cert *x509.Certificate) OrderOption { + return orderReplacesCert{cert} +} + +type orderReplacesCert struct { + cert *x509.Certificate +} + +func (orderReplacesCert) privateOrderOpt() {} + +// WithOrderReplacesCertDER indicates that this Order is for a replacement of +// an existing DER-encoded certificate. +// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 +func WithOrderReplacesCertDER(der []byte) OrderOption { + return orderReplacesCertDER(der) +} + +type orderReplacesCertDER []byte + +func (orderReplacesCertDER) privateOrderOpt() {} + // Authorization encodes an authorization response. type Authorization struct { // URI uniquely identifies a authorization.