132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package acme
|
|
|
|
import (
|
|
_ "crypto/sha1"
|
|
_ "crypto/sha256"
|
|
_ "crypto/sha512"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GetRenewalInfo returns the renewal information (if present and supported by
|
|
// the ACME server), and a Retry-After time if indicated in the http response
|
|
// header.
|
|
func (c Client) GetRenewalInfo(cert *x509.Certificate) (RenewalInfo, error) {
|
|
if c.dir.RenewalInfo == "" {
|
|
return RenewalInfo{}, ErrRenewalInfoNotSupported
|
|
}
|
|
|
|
certID, err := GenerateARICertID(cert)
|
|
if err != nil {
|
|
return RenewalInfo{}, fmt.Errorf("acme: error generating certificate id: %v", err)
|
|
}
|
|
|
|
renewalURL := c.dir.RenewalInfo
|
|
if !strings.HasSuffix(renewalURL, "/") {
|
|
renewalURL += "/"
|
|
}
|
|
renewalURL += certID
|
|
var ri RenewalInfo
|
|
|
|
resp, err := c.get(renewalURL, &ri, http.StatusOK)
|
|
if err != nil {
|
|
return ri, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
ri.RetryAfter, err = parseRetryAfter(resp.Header.Get("Retry-After"))
|
|
return ri, err
|
|
}
|
|
|
|
// GenerateARICertID constructs a certificate identifier as described in
|
|
// draft-ietf-acme-ari-03, section 4.1.
|
|
func GenerateARICertID(cert *x509.Certificate) (string, error) {
|
|
if cert == nil {
|
|
return "", fmt.Errorf("certificate not found")
|
|
}
|
|
|
|
derBytes, err := asn1.Marshal(cert.SerialNumber)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(derBytes) < 3 {
|
|
return "", fmt.Errorf("invalid DER encoding of serial number")
|
|
}
|
|
|
|
// Extract only the integer bytes from the DER encoded Serial Number
|
|
// Skipping the first 2 bytes (tag and length). The result is base64url
|
|
// encoded without padding.
|
|
serial := base64.RawURLEncoding.EncodeToString(derBytes[2:])
|
|
|
|
// Convert the Authority Key Identifier to base64url encoding without
|
|
// padding.
|
|
aki := base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId)
|
|
|
|
// Construct the final identifier by concatenating AKI and Serial Number.
|
|
return fmt.Sprintf("%s.%s", aki, serial), nil
|
|
}
|
|
|
|
func (r RenewalInfo) 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)))
|
|
randomTime := start.Add(randomDuration)
|
|
|
|
// If the selected time is in the past, attempt renewal immediately.
|
|
if randomTime.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(randomTime) || willingToSleepUntil.Equal(randomTime) {
|
|
return &randomTime
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// timeNow and implementations support testing
|
|
type timeNow interface {
|
|
Now() time.Time
|
|
}
|
|
|
|
type currentTimeNow struct{}
|
|
|
|
func (currentTimeNow) Now() time.Time {
|
|
return time.Now()
|
|
}
|
|
|
|
var systemTime timeNow = currentTimeNow{}
|
|
|
|
func parseRetryAfter(ra string) (time.Time, error) {
|
|
retryAfterString := strings.TrimSpace(ra)
|
|
if len(retryAfterString) == 0 {
|
|
return time.Time{}, nil
|
|
}
|
|
|
|
if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterString); err == nil {
|
|
return retryAfterTime, nil
|
|
}
|
|
|
|
if retryAfterInt, err := strconv.Atoi(retryAfterString); err == nil {
|
|
return systemTime.Now().Add(time.Second * time.Duration(retryAfterInt)), nil
|
|
}
|
|
|
|
return time.Time{}, fmt.Errorf("invalid time format: %s", retryAfterString)
|
|
}
|