1 package certificate
2 3 import (
4 "crypto/x509"
5 "encoding/asn1"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "math/rand"
11 "time"
12 13 "github.com/go-acme/lego/v4/acme"
14 )
15 16 // RenewalInfoRequest contains the necessary renewal information.
17 type RenewalInfoRequest struct {
18 Cert *x509.Certificate
19 }
20 21 // RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
22 type RenewalInfoResponse struct {
23 acme.RenewalInfoResponse
24 25 // RetryAfter header indicating the polling interval that the ACME server recommends.
26 // Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed,
27 // as the server may provide a different suggestedWindow.
28 // https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
29 RetryAfter time.Duration
30 }
31 32 // ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.
33 // It returns a pointer to a time.Time value indicating when the renewal should be attempted or nil if deferred until the next normal wake time.
34 // This method implements the RECOMMENDED algorithm described in RFC 9773.
35 //
36 // - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
37 func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {
38 // Explicitly convert all times to UTC.
39 now = now.UTC()
40 start := r.SuggestedWindow.Start.UTC()
41 end := r.SuggestedWindow.End.UTC()
42 43 // Select a uniform random time within the suggested window.
44 rt := start
45 if window := end.Sub(start); window > 0 {
46 randomDuration := time.Duration(rand.Int63n(int64(window)))
47 rt = rt.Add(randomDuration)
48 }
49 50 // If the selected time is in the past, attempt renewal immediately.
51 if rt.Before(now) {
52 return &now
53 }
54 55 // Otherwise, if the client can schedule itself to attempt renewal at exactly the selected time, do so.
56 willingToSleepUntil := now.Add(willingToSleep)
57 if willingToSleepUntil.After(rt) || willingToSleepUntil.Equal(rt) {
58 return &rt
59 }
60 61 // TODO: Otherwise, if the selected time is before the next time that the client would wake up normally, attempt renewal immediately.
62 63 // Otherwise, sleep until the next normal wake time, re-check ARI, and return to Step 1.
64 return nil
65 }
66 67 // GetRenewalInfo sends a request to the ACME server's renewalInfo endpoint to obtain a suggested renewal window.
68 // The caller MUST provide the certificate and issuer certificate for the certificate they wish to renew.
69 // The caller should attempt to renew the certificate at the time indicated by the ShouldRenewAt method of the returned RenewalInfoResponse object.
70 //
71 // Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
72 // This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
73 //
74 // https://www.rfc-editor.org/rfc/rfc9773.html
75 func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
76 certID, err := MakeARICertID(req.Cert)
77 if err != nil {
78 return nil, fmt.Errorf("error making certID: %w", err)
79 }
80 81 resp, err := c.core.Certificates.GetRenewalInfo(certID)
82 if err != nil {
83 return nil, err
84 }
85 defer resp.Body.Close()
86 87 var info RenewalInfoResponse
88 89 err = json.NewDecoder(resp.Body).Decode(&info)
90 if err != nil {
91 return nil, err
92 }
93 94 if retry := resp.Header.Get("Retry-After"); retry != "" {
95 info.RetryAfter, err = time.ParseDuration(retry + "s")
96 if err != nil {
97 return nil, err
98 }
99 }
100 101 return &info, nil
102 }
103 104 // MakeARICertID constructs a certificate identifier as described in RFC 9773, section 4.1.
105 func MakeARICertID(leaf *x509.Certificate) (string, error) {
106 if leaf == nil {
107 return "", errors.New("leaf certificate is nil")
108 }
109 110 // Marshal the Serial Number into DER.
111 der, err := asn1.Marshal(leaf.SerialNumber)
112 if err != nil {
113 return "", err
114 }
115 116 // Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,
117 // length, and value).
118 if len(der) < 3 {
119 return "", errors.New("invalid DER encoding of serial number")
120 }
121 122 // Extract only the integer bytes from the DER encoded Serial Number
123 // Skipping the first 2 bytes (tag and length).
124 serial := base64.RawURLEncoding.EncodeToString(der[2:])
125 126 // Convert the Authority Key Identifier to base64url encoding without
127 // padding.
128 aki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId)
129 130 // Construct the final identifier by concatenating AKI and Serial Number.
131 return fmt.Sprintf("%s.%s", aki, serial), nil
132 }
133