Scale ARI suggested window to cert lifetime (#8024)
Compute the width of the ARI suggested renewal window as 2% of the validity period. This means that 90-day certificates have their suggested window shrink slightly from 48 hours to 43.2 hours, and gives six-day (160h) certs a suggested window 3.2 hours wide. Also move the center of that window to the midpoint of the certificate validity period for certs which are valid for less than 10 days, so that operators have (proportionally) a little more time to respond to renewal issues. Fixes https://github.com/letsencrypt/boulder/issues/7996
This commit is contained in:
parent
6b85b3480b
commit
a00821ada6
|
@ -465,16 +465,21 @@ type RenewalInfo struct {
|
|||
|
||||
// RenewalInfoSimple constructs a `RenewalInfo` object and suggested window
|
||||
// using a very simple renewal calculation: calculate a point 2/3rds of the way
|
||||
// through the validity period, then give a 2-day window around that. Both the
|
||||
// `issued` and `expires` timestamps are expected to be UTC.
|
||||
// through the validity period (or halfway through, for short-lived certs), then
|
||||
// give a 2%-of-validity wide window around that. Both the `issued` and
|
||||
// `expires` timestamps are expected to be UTC.
|
||||
func RenewalInfoSimple(issued time.Time, expires time.Time) RenewalInfo {
|
||||
validity := expires.Add(time.Second).Sub(issued)
|
||||
renewalOffset := validity / time.Duration(3)
|
||||
if validity < 10*24*time.Hour {
|
||||
renewalOffset = validity / time.Duration(2)
|
||||
}
|
||||
idealRenewal := expires.Add(-renewalOffset)
|
||||
margin := validity / time.Duration(100)
|
||||
return RenewalInfo{
|
||||
SuggestedWindow: SuggestedWindow{
|
||||
Start: idealRenewal.Add(-24 * time.Hour).Truncate(time.Second),
|
||||
End: idealRenewal.Add(24 * time.Hour).Truncate(time.Second),
|
||||
Start: idealRenewal.Add(-1 * margin).Truncate(time.Second),
|
||||
End: idealRenewal.Add(margin).Truncate(time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,20 @@
|
|||
"ignoredLints": [
|
||||
"w_ext_subject_key_identifier_missing_sub_cert"
|
||||
]
|
||||
},
|
||||
"shortlived": {
|
||||
"allowMustStaple": true,
|
||||
"omitCommonName": true,
|
||||
"omitKeyEncipherment": true,
|
||||
"omitClientAuth": true,
|
||||
"omitSKID": true,
|
||||
"includeCRLDistributionPoints": true,
|
||||
"maxValidityPeriod": "160h",
|
||||
"maxValidityBackdate": "1h5m",
|
||||
"lintConfig": "test/config-next/zlint.toml",
|
||||
"ignoredLints": [
|
||||
"w_ext_subject_key_identifier_missing_sub_cert"
|
||||
]
|
||||
}
|
||||
},
|
||||
"crlProfile": {
|
||||
|
|
|
@ -48,6 +48,12 @@
|
|||
"validAuthzLifetime": "7h",
|
||||
"orderLifetime": "7h",
|
||||
"maxNames": 10
|
||||
},
|
||||
"shortlived": {
|
||||
"pendingAuthzLifetime": "7h",
|
||||
"validAuthzLifetime": "7h",
|
||||
"orderLifetime": "7h",
|
||||
"maxNames": 10
|
||||
}
|
||||
},
|
||||
"defaultProfileName": "legacy",
|
||||
|
|
|
@ -129,7 +129,8 @@
|
|||
},
|
||||
"certProfiles": {
|
||||
"legacy": "The normal profile you know and love",
|
||||
"modern": "Profile 2: Electric Boogaloo"
|
||||
"modern": "Profile 2: Electric Boogaloo",
|
||||
"shortlived": "Like modern, but smaller"
|
||||
},
|
||||
"unpause": {
|
||||
"hmacKey": {
|
||||
|
|
|
@ -68,6 +68,19 @@
|
|||
"ignoredLints": [
|
||||
"w_ext_subject_key_identifier_missing_sub_cert"
|
||||
]
|
||||
},
|
||||
"shortlived": {
|
||||
"allowMustStaple": true,
|
||||
"omitCommonName": true,
|
||||
"omitKeyEncipherment": true,
|
||||
"omitClientAuth": true,
|
||||
"omitSKID": true,
|
||||
"maxValidityPeriod": "160h",
|
||||
"maxValidityBackdate": "1h5m",
|
||||
"lintConfig": "test/config-next/zlint.toml",
|
||||
"ignoredLints": [
|
||||
"w_ext_subject_key_identifier_missing_sub_cert"
|
||||
]
|
||||
}
|
||||
},
|
||||
"crlProfile": {
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
"pendingAuthzLifetime": "7h",
|
||||
"validAuthzLifetime": "7h",
|
||||
"orderLifetime": "7h"
|
||||
},
|
||||
"shortlived": {
|
||||
"pendingAuthzLifetime": "7h",
|
||||
"validAuthzLifetime": "7h",
|
||||
"orderLifetime": "7h"
|
||||
}
|
||||
},
|
||||
"defaultProfileName": "legacy",
|
||||
|
|
|
@ -134,7 +134,8 @@
|
|||
},
|
||||
"certProfiles": {
|
||||
"legacy": "The normal profile you know and love",
|
||||
"modern": "Profile 2: Electric Boogaloo"
|
||||
"modern": "Profile 2: Electric Boogaloo",
|
||||
"shortlived": "Like modern, but smaller"
|
||||
},
|
||||
"unpause": {
|
||||
"hmacKey": {
|
||||
|
|
|
@ -24,14 +24,12 @@ type certID struct {
|
|||
SerialNumber *big.Int
|
||||
}
|
||||
|
||||
func TestARI(t *testing.T) {
|
||||
func TestARIAndReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create an account.
|
||||
// Setup
|
||||
client, err := makeClient("mailto:example@letsencrypt.org")
|
||||
test.AssertNotError(t, err, "creating acme client")
|
||||
|
||||
// Create a private key.
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "creating random cert key")
|
||||
|
||||
|
@ -45,11 +43,12 @@ func TestARI(t *testing.T) {
|
|||
cert := ir.certs[0]
|
||||
ari, err := client.GetRenewalInfo(cert)
|
||||
test.AssertNotError(t, err, "ARI request should have succeeded")
|
||||
test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 1415*time.Hour)
|
||||
test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 1463*time.Hour)
|
||||
test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 1418*time.Hour)
|
||||
test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 1461*time.Hour)
|
||||
test.AssertEquals(t, ari.RetryAfter.Sub(time.Now()).Round(time.Hour), 6*time.Hour)
|
||||
|
||||
// Make a new order which indicates that it replaces the cert issued above.
|
||||
// Make a new order which indicates that it replaces the cert issued above,
|
||||
// and verify that the replacement order succeeds.
|
||||
_, order, err := makeClientAndOrder(client, key, []string{name}, true, "", cert)
|
||||
test.AssertNotError(t, err, "failed to issue test cert")
|
||||
replaceID, err := acme.GenerateARICertID(cert)
|
||||
|
@ -57,34 +56,83 @@ func TestARI(t *testing.T) {
|
|||
test.AssertEquals(t, order.Replaces, replaceID)
|
||||
test.AssertNotEquals(t, order.Replaces, "")
|
||||
|
||||
// Try it again and verify it fails
|
||||
// Try another replacement order and verify that it fails.
|
||||
_, order, err = makeClientAndOrder(client, key, []string{name}, true, "", cert)
|
||||
test.AssertError(t, err, "subsequent ARI replacements for a replaced cert should fail, but didn't")
|
||||
}
|
||||
|
||||
// Revoke the cert and re-request ARI. The renewal window should now be in
|
||||
// the past indicating to the client that a renewal should happen
|
||||
// immediately.
|
||||
func TestARIShortLived(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup
|
||||
client, err := makeClient("mailto:example@letsencrypt.org")
|
||||
test.AssertNotError(t, err, "creating acme client")
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "creating random cert key")
|
||||
|
||||
// Issue a short-lived cert, request ARI, and check that both the suggested
|
||||
// window and the retry-after header are approximately the right amount of
|
||||
// time in the future.
|
||||
name := random_domain()
|
||||
ir, err := authAndIssue(client, key, []string{name}, true, "shortlived")
|
||||
test.AssertNotError(t, err, "failed to issue test cert")
|
||||
|
||||
cert := ir.certs[0]
|
||||
ari, err := client.GetRenewalInfo(cert)
|
||||
test.AssertNotError(t, err, "ARI request should have succeeded")
|
||||
test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 78*time.Hour)
|
||||
test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 81*time.Hour)
|
||||
test.AssertEquals(t, ari.RetryAfter.Sub(time.Now()).Round(time.Hour), 6*time.Hour)
|
||||
}
|
||||
|
||||
func TestARIRevoked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup
|
||||
client, err := makeClient("mailto:example@letsencrypt.org")
|
||||
test.AssertNotError(t, err, "creating acme client")
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "creating random cert key")
|
||||
|
||||
// Issue a cert, revoke it, request ARI, and check that the suggested window
|
||||
// is in the past, indicating that a renewal should happen immediately.
|
||||
name := random_domain()
|
||||
ir, err := authAndIssue(client, key, []string{name}, true, "")
|
||||
test.AssertNotError(t, err, "failed to issue test cert")
|
||||
|
||||
cert := ir.certs[0]
|
||||
err = client.RevokeCertificate(client.Account, cert, client.PrivateKey, 0)
|
||||
test.AssertNotError(t, err, "failed to revoke cert")
|
||||
|
||||
ari, err = client.GetRenewalInfo(cert)
|
||||
ari, err := client.GetRenewalInfo(cert)
|
||||
test.AssertNotError(t, err, "ARI request should have succeeded")
|
||||
test.Assert(t, ari.SuggestedWindow.End.Before(time.Now()), "suggested window should end in the past")
|
||||
test.Assert(t, ari.SuggestedWindow.Start.Before(ari.SuggestedWindow.End), "suggested window should start before it ends")
|
||||
}
|
||||
|
||||
func TestARIForPrecert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup
|
||||
client, err := makeClient("mailto:example@letsencrypt.org")
|
||||
test.AssertNotError(t, err, "creating acme client")
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "creating random cert key")
|
||||
|
||||
// Try to make a new cert for a new domain, but sabotage the CT logs so
|
||||
// issuance fails. Recover the precert from CT, then request ARI and check
|
||||
// that it fails, because we don't serve ARI for non-issued certs.
|
||||
name = random_domain()
|
||||
// issuance fails.
|
||||
name := random_domain()
|
||||
err = ctAddRejectHost(name)
|
||||
test.AssertNotError(t, err, "failed to add ct-test-srv reject host")
|
||||
_, err = authAndIssue(client, key, []string{name}, true, "")
|
||||
test.AssertError(t, err, "expected error from authAndIssue, was nil")
|
||||
|
||||
cert, err = ctFindRejection([]string{name})
|
||||
// Recover the precert from CT, then request ARI and check
|
||||
// that it fails, because we don't serve ARI for non-issued certs.
|
||||
cert, err := ctFindRejection([]string{name})
|
||||
test.AssertNotError(t, err, "failed to find rejected precert")
|
||||
|
||||
ari, err = client.GetRenewalInfo(cert)
|
||||
_, err = client.GetRenewalInfo(cert)
|
||||
test.AssertError(t, err, "ARI request should have failed")
|
||||
test.AssertEquals(t, err.(acme.Problem).Status, 404)
|
||||
}
|
||||
|
|
|
@ -3939,19 +3939,19 @@ func makeARICertID(leaf *x509.Certificate) (string, error) {
|
|||
}
|
||||
|
||||
func TestCountNewOrderWithReplaces(t *testing.T) {
|
||||
wfe, _, signer := setupWFE(t)
|
||||
wfe, fc, signer := setupWFE(t)
|
||||
|
||||
expectExpiry := time.Now().AddDate(0, 0, 1)
|
||||
// Pick a random issuer to "issue" expectCert.
|
||||
var issuer *issuance.Certificate
|
||||
for _, v := range wfe.issuerCertificates {
|
||||
issuer = v
|
||||
break
|
||||
}
|
||||
testKey, _ := rsa.GenerateKey(rand.Reader, 1024)
|
||||
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
expectSerial := big.NewInt(1337)
|
||||
expectCert := &x509.Certificate{
|
||||
NotAfter: expectExpiry,
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().AddDate(0, 0, 90),
|
||||
DNSNames: []string{"example.com"},
|
||||
SerialNumber: expectSerial,
|
||||
AuthorityKeyId: issuer.SubjectKeyId,
|
||||
|
@ -3967,11 +3967,18 @@ func TestCountNewOrderWithReplaces(t *testing.T) {
|
|||
RegistrationID: 1,
|
||||
Serial: core.SerialToString(expectSerial),
|
||||
Der: expectDer,
|
||||
Issued: timestamppb.New(expectCert.NotBefore),
|
||||
Expires: timestamppb.New(expectCert.NotAfter),
|
||||
},
|
||||
}
|
||||
mux := wfe.Handler(metrics.NoopRegisterer)
|
||||
responseWriter := httptest.NewRecorder()
|
||||
|
||||
// Set the fake clock forward to 1s past the suggested renewal window start
|
||||
// time.
|
||||
renewalWindowStart := core.RenewalInfoSimple(expectCert.NotBefore, expectCert.NotAfter).SuggestedWindow.Start
|
||||
fc.Set(renewalWindowStart.Add(time.Second))
|
||||
|
||||
body := fmt.Sprintf(`
|
||||
{
|
||||
"Identifiers": [
|
||||
|
|
Loading…
Reference in New Issue