@@ -18,12 +18,15 @@ import (
18
18
"context"
19
19
"crypto/tls"
20
20
"crypto/x509"
21
+ "encoding/json"
21
22
"fmt"
23
+ "math/rand"
22
24
"net"
23
25
"os"
24
26
"strings"
25
27
"time"
26
28
29
+ "github.com/mholt/acmez/v2/acme"
27
30
"go.uber.org/zap"
28
31
"golang.org/x/crypto/ocsp"
29
32
)
@@ -56,6 +59,9 @@ type Certificate struct {
56
59
57
60
// The unique string identifying the issuer of this certificate.
58
61
issuerKey string
62
+
63
+ // ACME Renewal Information, if available
64
+ ari acme.RenewalInfo
59
65
}
60
66
61
67
// Empty returns true if the certificate struct is not filled out; at
@@ -67,10 +73,106 @@ func (cert Certificate) Empty() bool {
67
73
// Hash returns a checksum of the certificate chain's DER-encoded bytes.
68
74
func (cert Certificate ) Hash () string { return cert .hash }
69
75
70
- // NeedsRenewal returns true if the certificate is
71
- // expiring soon (according to cfg) or has expired.
76
+ // NeedsRenewal returns true if the certificate is expiring
77
+ // soon (according to ARI and/or cfg) or has expired.
72
78
func (cert Certificate ) NeedsRenewal (cfg * Config ) bool {
73
- return currentlyInRenewalWindow (cert .Leaf .NotBefore , expiresAt (cert .Leaf ), cfg .RenewalWindowRatio )
79
+ return cfg .certNeedsRenewal (cert .Leaf , cert .ari , true )
80
+ }
81
+
82
+ // certNeedsRenewal consults ACME Renewal Info (ARI) and certificate expiration to determine
83
+ // whether the leaf certificate needs to be renewed yet. If true is returned, the certificate
84
+ // should be renewed as soon as possible. The reasoning for a true return value is logged
85
+ // unless emitLogs is false; this can be useful to suppress noisy logs in the case where you
86
+ // first call this to determine if a cert in memory needs renewal, and then right after you
87
+ // call it again to see if the cert in storage still needs renewal -- you probably don't want
88
+ // to log the second time for checking the cert in storage which is mainly for synchronization.
89
+ func (cfg * Config ) certNeedsRenewal (leaf * x509.Certificate , ari acme.RenewalInfo , emitLogs bool ) bool {
90
+ expiration := expiresAt (leaf )
91
+
92
+ var logger * zap.Logger
93
+ if emitLogs {
94
+ logger = cfg .Logger .With (
95
+ zap .Strings ("subjects" , leaf .DNSNames ),
96
+ zap .Time ("expiration" , expiration ),
97
+ zap .String ("ari_cert_id" , ari .UniqueIdentifier ),
98
+ zap .Timep ("next_ari_update" , ari .RetryAfter ),
99
+ zap .Duration ("renew_check_interval" , cfg .certCache .options .RenewCheckInterval ),
100
+ zap .Time ("window_start" , ari .SuggestedWindow .Start ),
101
+ zap .Time ("window_end" , ari .SuggestedWindow .End ))
102
+ } else {
103
+ logger = zap .NewNop ()
104
+ }
105
+
106
+ // first check ARI: if it says it's time to renew, it's time to renew
107
+ // (notice that we don't strictly require an ARI window to also exist; we presume
108
+ // that if a time has been selected, a window does or did exist, even if it didn't
109
+ // get stored/encoded for some reason - but also: this allows administrators to
110
+ // manually or explicitly schedule a renewal time indepedently of ARI which could
111
+ // be useful)
112
+ selectedTime := ari .SelectedTime
113
+
114
+ // if, for some reason a random time in the window hasn't been selected yet, but an ARI
115
+ // window does exist, we can always improvise one... even if this is called repeatedly,
116
+ // a random time is a random time, whether you generate it once or more :D
117
+ // (code borrowed from our acme package)
118
+ if selectedTime .IsZero () &&
119
+ (! ari .SuggestedWindow .Start .IsZero () && ! ari .SuggestedWindow .End .IsZero ()) {
120
+ start , end := ari .SuggestedWindow .Start .Unix ()+ 1 , ari .SuggestedWindow .End .Unix ()
121
+ selectedTime = time .Unix (rand .Int63n (end - start )+ start , 0 ).UTC ()
122
+ logger .Warn ("no renewal time had been selected with ARI; chose an ephemeral one for now" ,
123
+ zap .Time ("ephemeral_selected_time" , selectedTime ))
124
+ }
125
+
126
+ // if a renewal time has been selected, start with that
127
+ if ! selectedTime .IsZero () {
128
+ // ARI spec recommends an algorithm that renews after the randomly-selected
129
+ // time OR just before it if the next waking time would be after it; this
130
+ // cutoff can actually be before the start of the renewal window, but the spec
131
+ // author says that's OK: https://github.com/aarongable/draft-acme-ari/issues/71
132
+ cutoff := ari .SelectedTime .Add (- cfg .certCache .options .RenewCheckInterval )
133
+ if time .Now ().After (cutoff ) {
134
+ logger .Info ("certificate needs renewal based on ARI window" ,
135
+ zap .Time ("selected_time" , selectedTime ),
136
+ zap .Time ("renewal_cutoff" , cutoff ))
137
+ return true
138
+ }
139
+
140
+ // according to ARI, we are not ready to renew; however, we do not rely solely on
141
+ // ARI calculations... what if there is a bug in our implementation, or in the
142
+ // server's, or the stored metadata? for redundancy, give credence to the expiration
143
+ // date; ignore ARI if we are past a "dangerously close" limit, to avoid any
144
+ // possibility of a bug in ARI compromising a site's uptime: we should always always
145
+ // always give heed to actual validity period
146
+ if currentlyInRenewalWindow (leaf .NotBefore , expiration , 1.0 / 20.0 ) {
147
+ logger .Warn ("certificate is in emergency renewal window; superceding ARI" ,
148
+ zap .Duration ("remaining" , time .Until (expiration )),
149
+ zap .Time ("renewal_cutoff" , cutoff ))
150
+ return true
151
+ }
152
+
153
+ }
154
+
155
+ // the normal check, in the absence of ARI, is to determine if we're near enough (or past)
156
+ // the expiration date based on the configured remaining:lifetime ratio
157
+ if currentlyInRenewalWindow (leaf .NotBefore , expiration , cfg .RenewalWindowRatio ) {
158
+ logger .Info ("certificate is in configured renewal window based on expiration date" ,
159
+ zap .Duration ("remaining" , time .Until (expiration )))
160
+ return true
161
+ }
162
+
163
+ // finally, if the certificate is expiring imminently, always attempt a renewal;
164
+ // we check both a (very low) lifetime ratio and also a strict difference between
165
+ // the time until expiration and the interval at which we run the standard maintenance
166
+ // routine to check for renewals, to accommodate both exceptionally long and short
167
+ // cert lifetimes
168
+ if currentlyInRenewalWindow (leaf .NotBefore , expiration , 1.0 / 50.0 ) ||
169
+ time .Until (expiration ) < cfg .certCache .options .RenewCheckInterval * 5 {
170
+ logger .Warn ("certificate is in emergency renewal window; expiration imminent" ,
171
+ zap .Duration ("remaining" , time .Until (expiration )))
172
+ return true
173
+ }
174
+
175
+ return false
74
176
}
75
177
76
178
// Expired returns true if the certificate has expired.
@@ -85,10 +187,12 @@ func (cert Certificate) Expired() bool {
85
187
return time .Now ().After (expiresAt (cert .Leaf ))
86
188
}
87
189
88
- // currentlyInRenewalWindow returns true if the current time is
89
- // within the renewal window, according to the given start/end
190
+ // currentlyInRenewalWindow returns true if the current time is within
191
+ // (or after) the renewal window, according to the given start/end
90
192
// dates and the ratio of the renewal window. If true is returned,
91
- // the certificate being considered is due for renewal.
193
+ // the certificate being considered is due for renewal. The ratio
194
+ // is remaining:total time, i.e. 1/3 = 1/3 of lifetime remaining,
195
+ // or 9/10 = 9/10 of time lifetime remaining.
92
196
func currentlyInRenewalWindow (notBefore , notAfter time.Time , renewalWindowRatio float64 ) bool {
93
197
if notAfter .IsZero () {
94
198
return false
@@ -154,9 +258,37 @@ func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (C
154
258
}
155
259
cert .managed = true
156
260
cert .issuerKey = certRes .issuerKey
261
+ if ari , err := certRes .getARI (); err == nil && ari != nil {
262
+ cert .ari = * ari
263
+ }
157
264
return cert , nil
158
265
}
159
266
267
+ // getARI unpacks ACME Renewal Information from the issuer data, if available.
268
+ // It is only an error if there is invalid JSON.
269
+ func (certRes CertificateResource ) getARI () (* acme.RenewalInfo , error ) {
270
+ acmeData , err := certRes .getACMEData ()
271
+ if err != nil {
272
+ return nil , err
273
+ }
274
+ return acmeData .RenewalInfo , nil
275
+ }
276
+
277
+ // getACMEData returns the ACME certificate metadata from the IssuerData, but
278
+ // note that a non-ACME-issued certificate may return an empty value and nil
279
+ // since the JSON may still decode successfully but just not match any or all
280
+ // of the fields. Remember that the IssuerKey is used to store and access the
281
+ // cert files in the first place (it is part of the path) so in theory if you
282
+ // load a CertificateResource from an ACME issuer it should work as expected.
283
+ func (certRes CertificateResource ) getACMEData () (acme.Certificate , error ) {
284
+ if len (certRes .IssuerData ) == 0 {
285
+ return acme.Certificate {}, nil
286
+ }
287
+ var acmeCert acme.Certificate
288
+ err := json .Unmarshal (certRes .IssuerData , & acmeCert )
289
+ return acmeCert , err
290
+ }
291
+
160
292
// CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
161
293
// and keyFile, which must be in PEM format. It stores the certificate in
162
294
// the in-memory cache and returns the hash, useful for removing from the cache.
@@ -329,21 +461,22 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
329
461
return nil
330
462
}
331
463
332
- // managedCertInStorageExpiresSoon returns true if cert (being a
333
- // managed certificate) is expiring within RenewDurationBefore.
334
- // It returns false if there was an error checking the expiration
335
- // of the certificate as found in storage, or if the certificate
336
- // in storage is NOT expiring soon. A certificate that is expiring
464
+ // managedCertInStorageNeedsRenewal returns true if cert (being a
465
+ // managed certificate) is expiring soon (according to cfg) or if
466
+ // ACME Renewal Information (ARI) is available and says that it is
467
+ // time to renew (it uses existing ARI; it does not update it).
468
+ // It returns false if there was an error, the cert is not expiring
469
+ // soon, and ARI window is still future. A certificate that is expiring
337
470
// soon in our cache but is not expiring soon in storage probably
338
471
// means that another instance renewed the certificate in the
339
472
// meantime, and it would be a good idea to simply load the cert
340
473
// into our cache rather than repeating the renewal process again.
341
- func (cfg * Config ) managedCertInStorageExpiresSoon (ctx context.Context , cert Certificate ) (bool , error ) {
474
+ func (cfg * Config ) managedCertInStorageNeedsRenewal (ctx context.Context , cert Certificate ) (bool , error ) {
342
475
certRes , err := cfg .loadCertResourceAnyIssuer (ctx , cert .Names [0 ])
343
476
if err != nil {
344
477
return false , err
345
478
}
346
- _ , needsRenew := cfg .managedCertNeedsRenewal (certRes )
479
+ _ , _ , needsRenew := cfg .managedCertNeedsRenewal (certRes , false )
347
480
return needsRenew , nil
348
481
}
349
482
0 commit comments