Skip to content

Commit 385b70e

Browse files
committed
feat: add option to define dynamically the renew date
1 parent 713acef commit 385b70e

File tree

2 files changed

+94
-14
lines changed

2 files changed

+94
-14
lines changed

cmd/cmd_renew.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import (
2020

2121
// Flag names.
2222
const (
23-
flgDays = "days"
23+
flgRenewDays = "days"
24+
flgRenewDynamic = "dynamic"
2425
flgARIDisable = "ari-disable"
2526
flgARIWaitToRenewDuration = "ari-wait-to-renew-duration"
2627
flgReuseKey = "reuse-key"
@@ -52,10 +53,16 @@ func createRenew() *cli.Command {
5253
},
5354
Flags: []cli.Flag{
5455
&cli.IntFlag{
55-
Name: flgDays,
56+
Name: flgRenewDays,
5657
Value: 30,
5758
Usage: "The number of days left on a certificate to renew it.",
5859
},
60+
// TODO(ldez): in v5, remove this flag, use this behavior as default.
61+
&cli.BoolFlag{
62+
Name: flgRenewDynamic,
63+
Value: false,
64+
Usage: "Dynamically compute the date renewal date. (1/3rd of the lifetime left or 1/2 of the lifetime left, if the lifetime is shorter than 10 days)",
65+
},
5966
&cli.BoolFlag{
6067
Name: flgARIDisable,
6168
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
@@ -187,7 +194,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
187194

188195
certDomains := certcrypto.ExtractDomains(cert)
189196

190-
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) &&
197+
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) &&
191198
(!forceDomains || slices.Equal(certDomains, domains)) {
192199
return nil
193200
}
@@ -304,7 +311,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
304311
}
305312
}
306313

307-
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) {
314+
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {
308315
return nil
309316
}
310317

@@ -342,21 +349,41 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
342349
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
343350
}
344351

345-
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
352+
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {
346353
if x509Cert.IsCA {
347354
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
348355
}
349356

350-
if days >= 0 {
351-
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
352-
if notAfter > days {
353-
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
354-
domain, notAfter, days)
355-
return false
356-
}
357+
if dynamic {
358+
return needRenewalDynamic(x509Cert, time.Now())
359+
}
360+
361+
if days < 0 {
362+
return true
363+
}
364+
365+
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
366+
if notAfter <= days {
367+
return true
357368
}
358369

359-
return true
370+
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
371+
domain, notAfter, days)
372+
373+
return false
374+
}
375+
376+
func needRenewalDynamic(x509Cert *x509.Certificate, now time.Time) bool {
377+
lifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore)
378+
379+
var divisor int64 = 3
380+
if lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 {
381+
divisor = 2
382+
}
383+
384+
dueDate := x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor))
385+
386+
return dueDate.Before(now)
360387
}
361388

362389
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.

cmd/cmd_renew_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,62 @@ func Test_needRenewal(t *testing.T) {
108108

109109
for _, test := range testCases {
110110
t.Run(test.desc, func(t *testing.T) {
111-
actual := needRenewal(test.x509Cert, "foo.com", test.days)
111+
actual := needRenewal(test.x509Cert, "foo.com", test.days, false)
112112

113113
assert.Equal(t, test.expected, actual)
114114
})
115115
}
116116
}
117+
118+
func Test_needRenewalDynamic(t *testing.T) {
119+
testCases := []struct {
120+
desc string
121+
now time.Time
122+
notBefore, notAfter time.Time
123+
expected assert.BoolAssertionFunc
124+
}{
125+
{
126+
desc: "higher than 1/3 of the certificate lifetime left (lifetime > 10 days)",
127+
now: time.Date(2025, 1, 19, 1, 1, 1, 1, time.UTC),
128+
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
129+
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
130+
expected: assert.False,
131+
},
132+
{
133+
desc: "lower than 1/3 of the certificate lifetime left(lifetime > 10 days)",
134+
now: time.Date(2025, 1, 21, 1, 1, 1, 1, time.UTC),
135+
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
136+
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
137+
expected: assert.True,
138+
},
139+
{
140+
desc: "higher than 1/2 of the certificate lifetime left (lifetime < 10 days)",
141+
now: time.Date(2025, 1, 4, 1, 1, 1, 1, time.UTC),
142+
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
143+
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
144+
expected: assert.False,
145+
},
146+
{
147+
desc: "lower than 1/2 of the certificate lifetime left (lifetime < 10 days)",
148+
now: time.Date(2025, 1, 6, 1, 1, 1, 1, time.UTC),
149+
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
150+
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
151+
expected: assert.True,
152+
},
153+
}
154+
155+
for _, test := range testCases {
156+
t.Run(test.desc, func(t *testing.T) {
157+
t.Parallel()
158+
159+
x509Cert := &x509.Certificate{
160+
NotBefore: test.notBefore,
161+
NotAfter: test.notAfter,
162+
}
163+
164+
ok := needRenewalDynamic(x509Cert, test.now)
165+
166+
test.expected(t, ok)
167+
})
168+
}
169+
}

0 commit comments

Comments
 (0)