mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
feat: add option to define dynamically the renew date (#2574)
Co-authored-by: Dominik Menke <git@dmke.org>
This commit is contained in:
parent
713acefd7f
commit
96b18d764d
3 changed files with 96 additions and 14 deletions
|
|
@ -20,7 +20,8 @@ import (
|
|||
|
||||
// Flag names.
|
||||
const (
|
||||
flgDays = "days"
|
||||
flgRenewDays = "days"
|
||||
flgRenewDynamic = "dynamic"
|
||||
flgARIDisable = "ari-disable"
|
||||
flgARIWaitToRenewDuration = "ari-wait-to-renew-duration"
|
||||
flgReuseKey = "reuse-key"
|
||||
|
|
@ -52,10 +53,16 @@ func createRenew() *cli.Command {
|
|||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: flgDays,
|
||||
Name: flgRenewDays,
|
||||
Value: 30,
|
||||
Usage: "The number of days left on a certificate to renew it.",
|
||||
},
|
||||
// TODO(ldez): in v5, remove this flag, use this behavior as default.
|
||||
&cli.BoolFlag{
|
||||
Name: flgRenewDynamic,
|
||||
Value: false,
|
||||
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgARIDisable,
|
||||
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
|
|||
|
||||
certDomains := certcrypto.ExtractDomains(cert)
|
||||
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) &&
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) &&
|
||||
(!forceDomains || slices.Equal(certDomains, domains)) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -304,7 +311,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
|
|||
}
|
||||
}
|
||||
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) {
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -342,21 +349,41 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
|
|||
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
|
||||
}
|
||||
|
||||
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
|
||||
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {
|
||||
if x509Cert.IsCA {
|
||||
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
|
||||
}
|
||||
|
||||
if days >= 0 {
|
||||
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
||||
if notAfter > days {
|
||||
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
|
||||
domain, notAfter, days)
|
||||
return false
|
||||
}
|
||||
if dynamic {
|
||||
return needRenewalDynamic(x509Cert, time.Now())
|
||||
}
|
||||
|
||||
return true
|
||||
if days < 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
||||
if notAfter <= days {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
|
||||
domain, notAfter, days)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func needRenewalDynamic(x509Cert *x509.Certificate, now time.Time) bool {
|
||||
lifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore)
|
||||
|
||||
var divisor int64 = 3
|
||||
if lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 {
|
||||
divisor = 2
|
||||
}
|
||||
|
||||
dueDate := x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor))
|
||||
|
||||
return dueDate.Before(now)
|
||||
}
|
||||
|
||||
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
|
||||
|
|
|
|||
|
|
@ -108,9 +108,62 @@ func Test_needRenewal(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
actual := needRenewal(test.x509Cert, "foo.com", test.days)
|
||||
actual := needRenewal(test.x509Cert, "foo.com", test.days, false)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_needRenewalDynamic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
now time.Time
|
||||
notBefore, notAfter time.Time
|
||||
expected assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "higher than 1/3 of the certificate lifetime left (lifetime > 10 days)",
|
||||
now: time.Date(2025, 1, 19, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "lower than 1/3 of the certificate lifetime left(lifetime > 10 days)",
|
||||
now: time.Date(2025, 1, 21, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "higher than 1/2 of the certificate lifetime left (lifetime < 10 days)",
|
||||
now: time.Date(2025, 1, 4, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "lower than 1/2 of the certificate lifetime left (lifetime < 10 days)",
|
||||
now: time.Date(2025, 1, 6, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.True,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
x509Cert := &x509.Certificate{
|
||||
NotBefore: test.notBefore,
|
||||
NotAfter: test.notAfter,
|
||||
}
|
||||
|
||||
ok := needRenewalDynamic(x509Cert, test.now)
|
||||
|
||||
test.expected(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ GLOBAL OPTIONS:
|
|||
--server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") [$LEGO_SERVER]
|
||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. (default: false)
|
||||
--email value, -m value Email used for registration and recovery contact. [$LEGO_EMAIL]
|
||||
--disable-cn value Disable the use of the common name in the CSR. [$disable-cn]
|
||||
--csr value, -c value Certificate signing request filename, if an external CSR is to be used.
|
||||
--eab Use External Account Binding for account registration. Requires --kid and --hmac. (default: false) [$LEGO_EAB]
|
||||
--kid value Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
|
||||
|
|
@ -93,6 +94,7 @@ USAGE:
|
|||
|
||||
OPTIONS:
|
||||
--days value The number of days left on a certificate to renew it. (default: 30)
|
||||
--dynamic Dynamically defines the renewal date. (1/3rd of the lifetime left or 1/2 of the lifetime left, if the lifetime is shorter than 10 days) (default: false)
|
||||
--ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed. (default: false)
|
||||
--ari-wait-to-renew-duration value The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
|
||||
--reuse-key Used to indicate you want to reuse your current private key for the new certificate. (default: false)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue