renewal.go raw

   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