-
Notifications
You must be signed in to change notification settings - Fork 22
/
ari.go
131 lines (106 loc) · 3.43 KB
/
ari.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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)
}