Skip to content

Commit 1452fa1

Browse files
authored
Merge pull request #15 from PlayEveryWare/mendsley/dynamic_cert_renewal_time
Renew certs when they reach the 2/3 mark to expiry
2 parents f2c2c42 + d15e134 commit 1452fa1

File tree

5 files changed

+171
-12
lines changed

5 files changed

+171
-12
lines changed

backend/acme_server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (b *testBackend) startACMEServer(t *testing.T, opts ...AcmeServerOption) *a
103103
// generate self-signed certificate for TLS
104104
localIPs := []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}
105105
expiration := time.Now().Add(365 * 24 * time.Hour)
106-
cert, key := generateSelfSignedCert(t, "localhost", localIPs, expiration)
106+
cert, key := generateSelfSignedCert(t, "localhost", localIPs, time.Now(), expiration)
107107

108108
// Create TLS config
109109
tlsConfig := &tls.Config{

backend/cert.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ func getCert(ctx context.Context, storage logical.Storage, path string) (*cert,
3131
return cert, nil
3232
}
3333

34+
func (c *cert) NotBefore() time.Time {
35+
if len(c.CertificateChain) > 0 {
36+
return c.CertificateChain[0].NotBefore
37+
}
38+
39+
return time.Time{}
40+
}
41+
3442
func (c *cert) NotAfter() time.Time {
3543
if len(c.CertificateChain) > 0 {
3644
return c.CertificateChain[0].NotAfter
@@ -39,6 +47,15 @@ func (c *cert) NotAfter() time.Time {
3947
return time.Time{}
4048
}
4149

50+
func (c *cert) RenewalDeadline() time.Time {
51+
var (
52+
validDuration = c.NotAfter().Sub(c.NotBefore())
53+
twoThirdsDuration = validDuration * 2 / 3
54+
deadline = c.NotBefore().Add(twoThirdsDuration)
55+
)
56+
return deadline
57+
}
58+
4259
func (c *cert) write(ctx context.Context, storage logical.Storage, path string) error {
4360
entry, err := logical.StorageEntryJSON(path, c)
4461
if err != nil {

backend/cert_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ func TestCert_Serialization(t *testing.T) {
3030
storage := &logical.InmemStorage{}
3131

3232
notAfter := time.Now().Add(265 * 24 * time.Hour)
33-
leaf, key := generateSelfSignedCert(t, "test.example.com", nil, notAfter)
34-
intermediate, _ := generateSelfSignedCert(t, "ca.example.com", nil, notAfter)
33+
leaf, key := generateSelfSignedCert(t, "test.example.com", nil, time.Now(), notAfter)
34+
intermediate, _ := generateSelfSignedCert(t, "ca.example.com", nil, time.Now(), notAfter)
3535

3636
originalCert := &cert{
3737
CertificateChain: []*x509.Certificate{leaf, intermediate},
@@ -57,11 +57,12 @@ func assertCertEqual(t *testing.T, expected *cert, actual *cert) {
5757
actualCert := actual.CertificateChain[ii]
5858
assert.Equal(t, expectedCert.Raw, actualCert.Raw)
5959
assert.Equal(t, expectedCert.Subject, actualCert.Subject)
60+
assert.Equal(t, expectedCert.NotBefore, actualCert.NotBefore)
6061
assert.Equal(t, expectedCert.NotAfter, actualCert.NotAfter)
6162
}
6263
}
6364

64-
func generateSelfSignedCert(t *testing.T, fqdn string, ips []net.IP, notAfter time.Time) (*x509.Certificate, accountKey) {
65+
func generateSelfSignedCert(t *testing.T, fqdn string, ips []net.IP, notBefore, notAfter time.Time) (*x509.Certificate, accountKey) {
6566
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
6667
require.NoError(t, err)
6768

@@ -75,7 +76,7 @@ func generateSelfSignedCert(t *testing.T, fqdn string, ips []net.IP, notAfter ti
7576
StreetAddress: []string{""},
7677
PostalCode: []string{""},
7778
},
78-
NotBefore: time.Now(),
79+
NotBefore: notBefore,
7980
NotAfter: notAfter,
8081
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
8182
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},

backend/path_cert.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ func (b *backend) certsRead(ctx context.Context, req *logical.Request, data *fra
7777
}
7878

7979
if c != nil && len(c.CertificateChain) > 0 && c.Key.PrivateKey != nil {
80-
notAfter := c.NotAfter()
81-
if !notAfter.IsZero() && time.Until(notAfter) > 30*24*time.Hour {
80+
81+
timeUntilRenwal := time.Until(c.RenewalDeadline())
82+
if timeUntilRenwal > 0 {
8283
return b.certResponse(ctx, c, req, account, provider)
8384
}
8485
}
@@ -198,8 +199,7 @@ func (b *backend) certResponse(ctx context.Context, c *cert, req *logical.Reques
198199
}
199200
keyPem := pem.EncodeToMemory(block)
200201

201-
ttlUntilExpiration := time.Until(c.NotAfter())
202-
ttl := ttlUntilExpiration - 30*24*time.Hour
202+
ttl := time.Until(c.RenewalDeadline())
203203
if ttl < 1*time.Hour {
204204
ttl = 1 * time.Hour
205205
}

backend/path_cert_test.go

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestPathCerts_ExistingCertificate(t *testing.T) {
2525

2626
path := MakeDNS01Path(account, provider, fqdn)
2727
notAfter := time.Now().Add(90 * 24 * time.Hour)
28-
leaf, key := generateSelfSignedCert(t, fqdn, nil, notAfter)
28+
leaf, key := generateSelfSignedCert(t, fqdn, nil, time.Now(), notAfter)
2929

3030
originalCert := &cert{
3131
CertificateChain: []*x509.Certificate{leaf},
@@ -236,7 +236,7 @@ func TestPathCerts_Renew(t *testing.T) {
236236
require.NoError(t, resp.Error())
237237

238238
notAfter := time.Now().Add(-time.Hour)
239-
leaf, key := generateSelfSignedCert(t, fqdn, nil, notAfter)
239+
leaf, key := generateSelfSignedCert(t, fqdn, nil, time.Now(), notAfter)
240240

241241
originalCert := &cert{
242242
CertificateChain: []*x509.Certificate{leaf},
@@ -354,7 +354,7 @@ func TestPathCerts_LeaseRevoke(t *testing.T) {
354354

355355
path := MakeDNS01Path(account, provider, fqdn)
356356
notAfter := time.Now().Add(90 * 24 * time.Hour)
357-
leaf, key := generateSelfSignedCert(t, fqdn, nil, notAfter)
357+
leaf, key := generateSelfSignedCert(t, fqdn, nil, time.Now(), notAfter)
358358

359359
originalCert := &cert{
360360
CertificateChain: []*x509.Certificate{leaf},
@@ -500,3 +500,144 @@ func TestPathCerts_IssuesNew_Wildcard(t *testing.T) {
500500
assert.Contains(t, certcrypto.ExtractDomains(certs[0]), fqdn)
501501
assertCertMatchesKey(t, certs[0], key)
502502
}
503+
504+
func TestPathCerts_Renews_TwoThirds(t *testing.T) {
505+
type testCase struct {
506+
Desc string
507+
NotBefore time.Time
508+
NotAfter time.Time
509+
ShouldRenew bool
510+
}
511+
512+
tests := []testCase{
513+
// Current 90 day certs
514+
{
515+
Desc: "90d-cert do-not-renew",
516+
NotBefore: time.Now(),
517+
NotAfter: time.Now().Add(90 * 24 * time.Hour),
518+
ShouldRenew: false,
519+
},
520+
{
521+
Desc: "90d-cert needs-renew",
522+
NotBefore: time.Now().Add(-61 * 24 * time.Hour),
523+
NotAfter: time.Now().Add(29 * 24 * time.Hour),
524+
ShouldRenew: true,
525+
},
526+
527+
// Current shortlived (6 day) certs
528+
{
529+
Desc: "6d-cert do-not-renew",
530+
NotBefore: time.Now(),
531+
NotAfter: time.Now().Add(6 * 24 * time.Hour),
532+
ShouldRenew: false,
533+
},
534+
{
535+
Desc: "6d-cert needs-renew",
536+
NotBefore: time.Now().Add(-5 * 24 * time.Hour),
537+
NotAfter: time.Now().Add(1 * 24 * time.Hour),
538+
ShouldRenew: true,
539+
},
540+
541+
// Future 64 day certs
542+
{
543+
Desc: "64d-cert do-not-renew",
544+
NotBefore: time.Now(),
545+
NotAfter: time.Now().Add(64 * 24 * time.Hour),
546+
ShouldRenew: false,
547+
},
548+
{
549+
Desc: "64d-cert needs-renew",
550+
NotBefore: time.Now().Add(-41 * 24 * time.Hour),
551+
NotAfter: time.Now().Add(20 * 24 * time.Hour),
552+
ShouldRenew: true,
553+
},
554+
555+
// Future 45 day certs
556+
{
557+
Desc: "45d-cert do-not-renew",
558+
NotBefore: time.Now(),
559+
NotAfter: time.Now().Add(45 * 24 * time.Hour),
560+
ShouldRenew: false,
561+
},
562+
{
563+
Desc: "45d-cert needs-renew",
564+
NotBefore: time.Now().Add(-31 * 24 * time.Hour),
565+
NotAfter: time.Now().Add(14 * 24 * time.Hour),
566+
ShouldRenew: true,
567+
},
568+
}
569+
570+
for _, tc := range tests {
571+
572+
func() {
573+
b := createTestBackend(t)
574+
575+
as := b.startACMEServer(t)
576+
defer as.Close()
577+
578+
const (
579+
account = "test-account"
580+
provider = "test-dns"
581+
fqdn = "test.example.com"
582+
)
583+
584+
path := MakeDNS01Path(account, provider, fqdn)
585+
586+
b.RegisterDNSProvider(provider, func() (challenge.Provider, error) {
587+
return as, nil
588+
})
589+
590+
accountPath := "accounts/" + account
591+
req := &logical.Request{
592+
Path: accountPath,
593+
Operation: logical.UpdateOperation,
594+
Data: map[string]interface{}{
595+
"email": "[email protected]",
596+
"directory_url": as.DirectoryURL,
597+
"tos_agreed": true,
598+
},
599+
}
600+
601+
resp, err := b.HandleRequest(t, req)
602+
require.NoError(t, err)
603+
require.NotNil(t, resp)
604+
require.NoError(t, resp.Error())
605+
606+
leaf, key := generateSelfSignedCert(t, fqdn, nil, tc.NotBefore, tc.NotAfter)
607+
608+
originalCert := &cert{
609+
CertificateChain: []*x509.Certificate{leaf},
610+
Key: key,
611+
}
612+
err = originalCert.write(t.Context(), b.Storage, path)
613+
require.NoError(t, err)
614+
615+
req = &logical.Request{
616+
Path: path,
617+
Operation: logical.ReadOperation,
618+
}
619+
620+
resp, err = b.HandleRequest(t, req)
621+
require.NoError(t, err)
622+
require.NotNil(t, resp)
623+
require.NoError(t, resp.Error())
624+
625+
certs, err := certcrypto.ParsePEMBundle([]byte(resp.Data["certificate"].(string)))
626+
require.NoError(t, err)
627+
require.NotEmpty(t, certs)
628+
629+
privKey, err := certcrypto.ParsePEMPrivateKey([]byte(resp.Data["private_key"].(string)))
630+
require.NoError(t, err)
631+
require.NotNil(t, privKey)
632+
633+
assert.Contains(t, certcrypto.ExtractDomains(certs[0]), fqdn)
634+
if tc.ShouldRenew {
635+
assertCertMatchesKey(t, certs[0], privKey)
636+
assert.Truef(t, certs[0].NotAfter.After(tc.NotAfter), "%s", tc.Desc)
637+
} else {
638+
assert.Equalf(t, tc.NotAfter.Year(), certs[0].NotAfter.Year(), tc.Desc)
639+
assert.Equalf(t, tc.NotAfter.YearDay(), certs[0].NotAfter.YearDay(), tc.Desc)
640+
}
641+
}()
642+
}
643+
}

0 commit comments

Comments
 (0)