Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/acme_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (b *testBackend) startACMEServer(t *testing.T, opts ...AcmeServerOption) *a
// generate self-signed certificate for TLS
localIPs := []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}
expiration := time.Now().Add(365 * 24 * time.Hour)
cert, key := generateSelfSignedCert(t, "localhost", localIPs, expiration)
cert, key := generateSelfSignedCert(t, "localhost", localIPs, time.Now(), expiration)

// Create TLS config
tlsConfig := &tls.Config{
Expand Down
17 changes: 17 additions & 0 deletions backend/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ func getCert(ctx context.Context, storage logical.Storage, path string) (*cert,
return cert, nil
}

func (c *cert) NotBefore() time.Time {
if len(c.CertificateChain) > 0 {
return c.CertificateChain[0].NotBefore
}

return time.Time{}
}

func (c *cert) NotAfter() time.Time {
if len(c.CertificateChain) > 0 {
return c.CertificateChain[0].NotAfter
Expand All @@ -39,6 +47,15 @@ func (c *cert) NotAfter() time.Time {
return time.Time{}
}

func (c *cert) RenewalDeadline() time.Time {
var (
validDuration = c.NotAfter().Sub(c.NotBefore())
twoThirdsDuration = validDuration * 2 / 3
deadline = c.NotBefore().Add(twoThirdsDuration)
)
return deadline
}

func (c *cert) write(ctx context.Context, storage logical.Storage, path string) error {
entry, err := logical.StorageEntryJSON(path, c)
if err != nil {
Expand Down
9 changes: 5 additions & 4 deletions backend/cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestCert_Serialization(t *testing.T) {
storage := &logical.InmemStorage{}

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

originalCert := &cert{
CertificateChain: []*x509.Certificate{leaf, intermediate},
Expand All @@ -57,11 +57,12 @@ func assertCertEqual(t *testing.T, expected *cert, actual *cert) {
actualCert := actual.CertificateChain[ii]
assert.Equal(t, expectedCert.Raw, actualCert.Raw)
assert.Equal(t, expectedCert.Subject, actualCert.Subject)
assert.Equal(t, expectedCert.NotBefore, actualCert.NotBefore)
assert.Equal(t, expectedCert.NotAfter, actualCert.NotAfter)
}
}

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

Expand All @@ -75,7 +76,7 @@ func generateSelfSignedCert(t *testing.T, fqdn string, ips []net.IP, notAfter ti
StreetAddress: []string{""},
PostalCode: []string{""},
},
NotBefore: time.Now(),
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
Expand Down
8 changes: 4 additions & 4 deletions backend/path_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ func (b *backend) certsRead(ctx context.Context, req *logical.Request, data *fra
}

if c != nil && len(c.CertificateChain) > 0 && c.Key.PrivateKey != nil {
notAfter := c.NotAfter()
if !notAfter.IsZero() && time.Until(notAfter) > 30*24*time.Hour {

timeUntilRenwal := time.Until(c.RenewalDeadline())
if timeUntilRenwal > 0 {
return b.certResponse(ctx, c, req, account, provider)
}
}
Expand Down Expand Up @@ -198,8 +199,7 @@ func (b *backend) certResponse(ctx context.Context, c *cert, req *logical.Reques
}
keyPem := pem.EncodeToMemory(block)

ttlUntilExpiration := time.Until(c.NotAfter())
ttl := ttlUntilExpiration - 30*24*time.Hour
ttl := time.Until(c.RenewalDeadline())
if ttl < 1*time.Hour {
ttl = 1 * time.Hour
}
Expand Down
147 changes: 144 additions & 3 deletions backend/path_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestPathCerts_ExistingCertificate(t *testing.T) {

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

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

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

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

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

originalCert := &cert{
CertificateChain: []*x509.Certificate{leaf},
Expand Down Expand Up @@ -500,3 +500,144 @@ func TestPathCerts_IssuesNew_Wildcard(t *testing.T) {
assert.Contains(t, certcrypto.ExtractDomains(certs[0]), fqdn)
assertCertMatchesKey(t, certs[0], key)
}

func TestPathCerts_Renews_TwoThirds(t *testing.T) {
type testCase struct {
Desc string
NotBefore time.Time
NotAfter time.Time
ShouldRenew bool
}

tests := []testCase{
// Current 90 day certs
{
Desc: "90d-cert do-not-renew",
NotBefore: time.Now(),
NotAfter: time.Now().Add(90 * 24 * time.Hour),
ShouldRenew: false,
},
{
Desc: "90d-cert needs-renew",
NotBefore: time.Now().Add(-61 * 24 * time.Hour),
NotAfter: time.Now().Add(29 * 24 * time.Hour),
ShouldRenew: true,
},

// Current shortlived (6 day) certs
{
Desc: "6d-cert do-not-renew",
NotBefore: time.Now(),
NotAfter: time.Now().Add(6 * 24 * time.Hour),
ShouldRenew: false,
},
{
Desc: "6d-cert needs-renew",
NotBefore: time.Now().Add(-5 * 24 * time.Hour),
NotAfter: time.Now().Add(1 * 24 * time.Hour),
ShouldRenew: true,
},

// Future 64 day certs
{
Desc: "64d-cert do-not-renew",
NotBefore: time.Now(),
NotAfter: time.Now().Add(64 * 24 * time.Hour),
ShouldRenew: false,
},
{
Desc: "64d-cert needs-renew",
NotBefore: time.Now().Add(-41 * 24 * time.Hour),
NotAfter: time.Now().Add(20 * 24 * time.Hour),
ShouldRenew: true,
},

// Future 45 day certs
{
Desc: "45d-cert do-not-renew",
NotBefore: time.Now(),
NotAfter: time.Now().Add(45 * 24 * time.Hour),
ShouldRenew: false,
},
{
Desc: "45d-cert needs-renew",
NotBefore: time.Now().Add(-31 * 24 * time.Hour),
NotAfter: time.Now().Add(14 * 24 * time.Hour),
ShouldRenew: true,
},
}

for _, tc := range tests {

func() {
b := createTestBackend(t)

as := b.startACMEServer(t)
defer as.Close()

const (
account = "test-account"
provider = "test-dns"
fqdn = "test.example.com"
)

path := MakeDNS01Path(account, provider, fqdn)

b.RegisterDNSProvider(provider, func() (challenge.Provider, error) {
return as, nil
})

accountPath := "accounts/" + account
req := &logical.Request{
Path: accountPath,
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"email": "[email protected]",
"directory_url": as.DirectoryURL,
"tos_agreed": true,
},
}

resp, err := b.HandleRequest(t, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NoError(t, resp.Error())

leaf, key := generateSelfSignedCert(t, fqdn, nil, tc.NotBefore, tc.NotAfter)

originalCert := &cert{
CertificateChain: []*x509.Certificate{leaf},
Key: key,
}
err = originalCert.write(t.Context(), b.Storage, path)
require.NoError(t, err)

req = &logical.Request{
Path: path,
Operation: logical.ReadOperation,
}

resp, err = b.HandleRequest(t, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NoError(t, resp.Error())

certs, err := certcrypto.ParsePEMBundle([]byte(resp.Data["certificate"].(string)))
require.NoError(t, err)
require.NotEmpty(t, certs)

privKey, err := certcrypto.ParsePEMPrivateKey([]byte(resp.Data["private_key"].(string)))
require.NoError(t, err)
require.NotNil(t, privKey)

assert.Contains(t, certcrypto.ExtractDomains(certs[0]), fqdn)
if tc.ShouldRenew {
assertCertMatchesKey(t, certs[0], privKey)
assert.Truef(t, certs[0].NotAfter.After(tc.NotAfter), "%s", tc.Desc)
} else {
assert.Equalf(t, tc.NotAfter.Year(), certs[0].NotAfter.Year(), tc.Desc)
assert.Equalf(t, tc.NotAfter.YearDay(), certs[0].NotAfter.YearDay(), tc.Desc)
}
}()
}
}