Skip to content

Commit 2df4153

Browse files
committed
acme/autocert: let automatic renewal work with short lifetime certs
Fixes golang/go#64997 Fixes golang/go#36548 Change-Id: Idb7a426ad3bfa6ac3b796f4b466da6e3154f1ffa Reviewed-on: https://go-review.googlesource.com/c/crypto/+/719080 Reviewed-by: Roland Shoemaker <roland@golang.org> Reviewed-by: Mark Freeman <markfreeman@google.com> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent bcf6a84 commit 2df4153

File tree

3 files changed

+76
-47
lines changed

3 files changed

+76
-47
lines changed

acme/autocert/autocert.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ type Manager struct {
134134
// RenewBefore optionally specifies how early certificates should
135135
// be renewed before they expire.
136136
//
137-
// If zero, they're renewed 30 days before expiration.
137+
// If zero, they're renewed at the lesser of 30 days or
138+
// 1/3 of the certificate lifetime.
138139
RenewBefore time.Duration
139140

140141
// Client is used to perform low-level operations, such as account registration
@@ -464,7 +465,7 @@ func (m *Manager) cert(ctx context.Context, ck certKey) (*tls.Certificate, error
464465
leaf: cert.Leaf,
465466
}
466467
m.state[ck] = s
467-
m.startRenew(ck, s.key, s.leaf.NotAfter)
468+
m.startRenew(ck, s.key, s.leaf.NotBefore, s.leaf.NotAfter)
468469
return cert, nil
469470
}
470471

@@ -610,7 +611,7 @@ func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate,
610611
}
611612
state.cert = der
612613
state.leaf = leaf
613-
m.startRenew(ck, state.key, state.leaf.NotAfter)
614+
m.startRenew(ck, state.key, state.leaf.NotBefore, state.leaf.NotAfter)
614615
return state.tlscert()
615616
}
616617

@@ -908,7 +909,7 @@ func httpTokenCacheKey(tokenPath string) string {
908909
//
909910
// The key argument is a certificate private key.
910911
// The exp argument is the cert expiration time (NotAfter).
911-
func (m *Manager) startRenew(ck certKey, key crypto.Signer, exp time.Time) {
912+
func (m *Manager) startRenew(ck certKey, key crypto.Signer, notBefore, notAfter time.Time) {
912913
m.renewalMu.Lock()
913914
defer m.renewalMu.Unlock()
914915
if m.renewal[ck] != nil {
@@ -920,7 +921,7 @@ func (m *Manager) startRenew(ck certKey, key crypto.Signer, exp time.Time) {
920921
}
921922
dr := &domainRenewal{m: m, ck: ck, key: key}
922923
m.renewal[ck] = dr
923-
dr.start(exp)
924+
dr.start(notBefore, notAfter)
924925
}
925926

926927
// stopRenew stops all currently running cert renewal timers.
@@ -1028,13 +1029,6 @@ func (m *Manager) hostPolicy() HostPolicy {
10281029
return defaultHostPolicy
10291030
}
10301031

1031-
func (m *Manager) renewBefore() time.Duration {
1032-
if m.RenewBefore > renewJitter {
1033-
return m.RenewBefore
1034-
}
1035-
return 720 * time.Hour // 30 days
1036-
}
1037-
10381032
func (m *Manager) now() time.Time {
10391033
if m.nowFunc != nil {
10401034
return m.nowFunc()

acme/autocert/renewal.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import (
1111
"time"
1212
)
1313

14-
// renewJitter is the maximum deviation from Manager.RenewBefore.
15-
const renewJitter = time.Hour
16-
1714
// domainRenewal tracks the state used by the periodic timers
1815
// renewing a single domain's cert.
1916
type domainRenewal struct {
@@ -30,13 +27,13 @@ type domainRenewal struct {
3027
// defined by the certificate expiration time exp.
3128
//
3229
// If the timer is already started, calling start is a noop.
33-
func (dr *domainRenewal) start(exp time.Time) {
30+
func (dr *domainRenewal) start(notBefore, notAfter time.Time) {
3431
dr.timerMu.Lock()
3532
defer dr.timerMu.Unlock()
3633
if dr.timer != nil {
3734
return
3835
}
39-
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
36+
dr.timer = time.AfterFunc(dr.next(notBefore, notAfter), dr.renew)
4037
}
4138

4239
// stop stops the cert renewal timer and waits for any in-flight calls to renew
@@ -79,7 +76,7 @@ func (dr *domainRenewal) renew() {
7976
// TODO: rotate dr.key at some point?
8077
next, err := dr.do(ctx)
8178
if err != nil {
82-
next = renewJitter / 2
79+
next = time.Hour / 2
8380
next += time.Duration(pseudoRand.int63n(int64(next)))
8481
}
8582
testDidRenewLoop(next, err)
@@ -107,8 +104,8 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
107104
// a race is likely unavoidable in a distributed environment
108105
// but we try nonetheless
109106
if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil {
110-
next := dr.next(tlscert.Leaf.NotAfter)
111-
if next > dr.m.renewBefore()+renewJitter {
107+
next := dr.next(tlscert.Leaf.NotBefore, tlscert.Leaf.NotAfter)
108+
if next > 0 {
112109
signer, ok := tlscert.PrivateKey.(crypto.Signer)
113110
if ok {
114111
state := &certState{
@@ -139,18 +136,23 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
139136
return 0, err
140137
}
141138
dr.updateState(state)
142-
return dr.next(leaf.NotAfter), nil
139+
return dr.next(leaf.NotBefore, leaf.NotAfter), nil
143140
}
144141

145-
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
146-
d := expiry.Sub(dr.m.now()) - dr.m.renewBefore()
147-
// add a bit of randomness to renew deadline
148-
n := pseudoRand.int63n(int64(renewJitter))
149-
d -= time.Duration(n)
150-
if d < 0 {
151-
return 0
142+
// next returns the wait time before the next renewal should start.
143+
// If manager.RenewBefore is set, it uses that capped at 30 days,
144+
// otherwise it uses a default of 1/3 of the cert lifetime.
145+
// It builds in a jitter of 10% of the renew threshold, capped at 1 hour.
146+
func (dr *domainRenewal) next(notBefore, notAfter time.Time) time.Duration {
147+
threshold := min(notAfter.Sub(notBefore)/3, 30*24*time.Hour)
148+
if dr.m.RenewBefore > 0 {
149+
threshold = min(dr.m.RenewBefore, 30*24*time.Hour)
152150
}
153-
return d
151+
maxJitter := min(threshold/10, time.Hour)
152+
jitter := pseudoRand.int63n(int64(maxJitter))
153+
renewAt := notAfter.Add(-(threshold - time.Duration(jitter)))
154+
renewWait := renewAt.Sub(dr.m.now())
155+
return max(0, renewWait)
154156
}
155157

156158
var testDidRenewLoop = func(next time.Duration, err error) {}

acme/autocert/renewal_test.go

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,60 @@ import (
1717

1818
func TestRenewalNext(t *testing.T) {
1919
now := time.Now()
20-
man := &Manager{
21-
RenewBefore: 7 * 24 * time.Hour,
22-
nowFunc: func() time.Time { return now },
23-
}
24-
defer man.stopRenew()
20+
nowFn := func() time.Time { return now }
2521
tt := []struct {
26-
expiry time.Time
27-
min, max time.Duration
22+
name string
23+
renewBefore time.Duration // arg to Manager
24+
// leaf cert validity
25+
notBefore time.Time
26+
validFor time.Duration
27+
// wait time
28+
waitMin, waitMax time.Duration
2829
}{
29-
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour},
30-
{now.Add(time.Hour), 0, 1},
31-
{now, 0, 1},
32-
{now.Add(-time.Hour), 0, 1},
30+
{"default renewal, 1h cert, valid",
31+
0, now, time.Hour, 40 * time.Minute, 50 * time.Minute},
32+
{"default renewal, 1h cert, should renew",
33+
0, now.Add(-50 * time.Minute), time.Hour, 0, 0},
34+
{"default renewal, 1h cert, expired",
35+
0, now.Add(-400 * 24 * time.Hour), time.Hour, 0, 0},
36+
{"default renewal, 6d cert, valid",
37+
0, now, 6 * 24 * time.Hour, 4 * 24 * time.Hour, (4*24 + 1) * time.Hour},
38+
{"default renewal, 6d cert, should renew",
39+
0, now.Add(-5 * 24 * time.Hour), 6 * 24 * time.Hour, 0, 0},
40+
{"default renewal, 6d cert, expired",
41+
0, now.Add(-400 * 24 * time.Hour), 6 * 24 * time.Hour, 0, 0},
42+
{"default renewal, 90d cert, valid",
43+
0, now, 90 * 24 * time.Hour, 60 * 24 * time.Hour, (60*24 + 1) * time.Hour},
44+
{"default renewal, 90d cert, should renew",
45+
0, now.Add(-70 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
46+
{"default renewal, 90d cert, expired",
47+
0, now.Add(-400 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
48+
{"default renewal, 398d cert, valid",
49+
0, now, 398 * 24 * time.Hour, (368 * 24) * time.Hour, (368*24 + 1) * time.Hour},
50+
{"default renewal, 398d cert, should renew",
51+
0, now.Add(-378 * 24 * time.Hour), 398 * 24 * time.Hour, 0, 0},
52+
{"default renewal, 398d cert, expired",
53+
0, now.Add(-400 * 24 * time.Hour), 398 * 24 * time.Hour, 0, 0},
54+
{"7d renewal, 90d cert, valid",
55+
7 * 24 * time.Hour, now, 90 * 24 * time.Hour, 83 * 24 * time.Hour, (83*24 + 1) * time.Hour},
56+
{"7d renewal, 90d cert, should not renew",
57+
7 * 24 * time.Hour, now.Add(-70 * 24 * time.Hour), 90 * 24 * time.Hour, 13 * 24 * time.Hour, (13*24 + 1) * time.Hour},
58+
{"7d renewal, 90d cert, should renew",
59+
7 * 24 * time.Hour, now.Add(-85 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
60+
{"7d renewal, 90d cert, expired",
61+
7 * 24 * time.Hour, now.Add(-400 * 24 * time.Hour), 90 * 24 * time.Hour, 0, 0},
3362
}
3463

35-
dr := &domainRenewal{m: man}
36-
for i, test := range tt {
37-
next := dr.next(test.expiry)
38-
if next < test.min || test.max < next {
39-
t.Errorf("%d: next = %v; want between %v and %v", i, next, test.min, test.max)
40-
}
64+
for _, test := range tt {
65+
t.Run(test.name, func(t *testing.T) {
66+
dr := &domainRenewal{m: &Manager{RenewBefore: test.renewBefore, nowFunc: nowFn}}
67+
defer dr.m.stopRenew()
68+
69+
next := dr.next(test.notBefore, test.notBefore.Add(test.validFor))
70+
if next < test.waitMin || next > test.waitMax {
71+
t.Errorf("expected wait time: %v <= %v <= %v", test.waitMin, next, test.waitMax)
72+
}
73+
})
4174
}
4275
}
4376

@@ -239,7 +272,7 @@ func TestRenewFromCacheAlreadyRenewed(t *testing.T) {
239272
}
240273

241274
// trigger renew
242-
man.startRenew(exampleCertKey, s.key, s.leaf.NotAfter)
275+
man.startRenew(exampleCertKey, s.key, s.leaf.NotBefore, s.leaf.NotAfter)
243276
<-renewed
244277
func() {
245278
man.renewalMu.Lock()

0 commit comments

Comments
 (0)