2023-05-27 17:05:22 +02:00
package certificate
import (
"crypto/x509"
2024-03-08 17:22:09 +02:00
"encoding/asn1"
2023-05-27 17:05:22 +02:00
"encoding/base64"
"encoding/json"
2024-01-24 23:02:50 +02:00
"errors"
2023-05-27 17:05:22 +02:00
"fmt"
"math/rand"
"time"
"github.com/go-acme/lego/v4/acme"
)
// RenewalInfoRequest contains the necessary renewal information.
type RenewalInfoRequest struct {
2024-01-24 23:02:50 +02:00
Cert * x509 . Certificate
2023-05-27 17:05:22 +02:00
}
// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
type RenewalInfoResponse struct {
acme . RenewalInfoResponse
2024-05-07 19:01:12 +02:00
// RetryAfter header indicating the polling interval that the ACME server recommends.
// Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed,
// as the server may provide a different suggestedWindow.
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
RetryAfter time . Duration
2023-05-27 17:05:22 +02:00
}
// ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.
// 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.
// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari.
//
// - (4.1-11. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
func ( r * RenewalInfoResponse ) ShouldRenewAt ( now time . Time , willingToSleep time . Duration ) * time . Time {
// Explicitly convert all times to UTC.
now = now . UTC ( )
start := r . SuggestedWindow . Start . UTC ( )
end := r . SuggestedWindow . End . UTC ( )
// Select a uniform random time within the suggested window.
window := end . Sub ( start )
randomDuration := time . Duration ( rand . Int63n ( int64 ( window ) ) )
rt := start . Add ( randomDuration )
// If the selected time is in the past, attempt renewal immediately.
if rt . Before ( now ) {
return & now
}
// Otherwise, if the client can schedule itself to attempt renewal at exactly the selected time, do so.
willingToSleepUntil := now . Add ( willingToSleep )
if willingToSleepUntil . After ( rt ) || willingToSleepUntil . Equal ( rt ) {
return & rt
}
// TODO: Otherwise, if the selected time is before the next time that the client would wake up normally, attempt renewal immediately.
// Otherwise, sleep until the next normal wake time, re-check ARI, and return to Step 1.
return nil
}
// GetRenewalInfo sends a request to the ACME server's renewalInfo endpoint to obtain a suggested renewal window.
// The caller MUST provide the certificate and issuer certificate for the certificate they wish to renew.
// The caller should attempt to renew the certificate at the time indicated by the ShouldRenewAt method of the returned RenewalInfoResponse object.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func ( c * Certifier ) GetRenewalInfo ( req RenewalInfoRequest ) ( * RenewalInfoResponse , error ) {
2024-03-08 17:22:09 +02:00
certID , err := MakeARICertID ( req . Cert )
2023-05-27 17:05:22 +02:00
if err != nil {
return nil , fmt . Errorf ( "error making certID: %w" , err )
}
resp , err := c . core . Certificates . GetRenewalInfo ( certID )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
var info RenewalInfoResponse
err = json . NewDecoder ( resp . Body ) . Decode ( & info )
if err != nil {
return nil , err
}
2024-05-07 19:01:12 +02:00
if retry := resp . Header . Get ( "Retry-After" ) ; retry != "" {
info . RetryAfter , err = time . ParseDuration ( retry + "s" )
if err != nil {
return nil , err
}
}
2023-05-27 17:05:22 +02:00
return & info , nil
}
2024-03-08 17:22:09 +02:00
// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1.
func MakeARICertID ( leaf * x509 . Certificate ) ( string , error ) {
if leaf == nil {
return "" , errors . New ( "leaf certificate is nil" )
2023-05-27 17:05:22 +02:00
}
2024-03-08 17:22:09 +02:00
// Marshal the Serial Number into DER.
der , err := asn1 . Marshal ( leaf . SerialNumber )
2023-05-27 17:05:22 +02:00
if err != nil {
2024-03-08 17:22:09 +02:00
return "" , err
2023-05-27 17:05:22 +02:00
}
2024-03-08 17:22:09 +02:00
// Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,
// length, and value).
if len ( der ) < 3 {
return "" , errors . New ( "invalid DER encoding of serial number" )
2023-05-27 17:05:22 +02:00
}
2024-03-08 17:22:09 +02:00
// Extract only the integer bytes from the DER encoded Serial Number
// Skipping the first 2 bytes (tag and length).
serial := base64 . RawURLEncoding . EncodeToString ( der [ 2 : ] )
// Convert the Authority Key Identifier to base64url encoding without
// padding.
aki := base64 . RawURLEncoding . EncodeToString ( leaf . AuthorityKeyId )
// Construct the final identifier by concatenating AKI and Serial Number.
return fmt . Sprintf ( "%s.%s" , aki , serial ) , nil
2023-05-27 17:05:22 +02:00
}