Skip to content

Commit 73e1040

Browse files
authored
Merge pull request #1028 from smallstep/mariano/cleanup-certificates
Refactor the Cleanup method
2 parents 468452a + 875d451 commit 73e1040

10 files changed

Lines changed: 244 additions & 95 deletions

File tree

‎kms/apiv1/options.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ type CertificateDeleter interface {
7676
DeleteCertificate(req *DeleteCertificateRequest) error
7777
}
7878

79-
// CleaningCertificateManager is an optional interface for KMS implementations
80-
// that support cleaning up expired certificates from a certificate store.
79+
// CredentialsCleaner is an optional interface for KMS implementations that
80+
// support cleaning up expired certificates from a certificate store.
8181
//
8282
// # Experimental
8383
//
8484
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
8585
// release.
86-
type CleaningCertificateManager interface {
87-
Cleanup(req *CleanupCertificatesRequest) error
86+
type CredentialsCleaner interface {
87+
CleanupCredentials(req *CleanupCredentialsRequest) error
8888
}
8989

9090
// NameValidator is an interface that KeyManager can implement to validate a

‎kms/apiv1/requests.go‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -351,16 +351,16 @@ type DeleteCertificateRequest struct {
351351
Name string
352352
}
353353

354-
// CleanupCertificatesRequest is the parameter used in the Cleanup method of a
355-
// CleaningCertificateManager. It identifies a certificate-store scope (issuer,
356-
// store location, store name) and a subject for which expired certificates
357-
// should be removed.
354+
// CleanupCredentialsRequest is the parameter used in the CleanupCredentials
355+
// method of a CredentialsCleaner. It identifies a certificate-store scope
356+
// (issuer, store location, store name) and a subject for which expired
357+
// certificates should be removed.
358358
//
359359
// # Experimental
360360
//
361361
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
362362
// release.
363-
type CleanupCertificatesRequest struct {
363+
type CleanupCredentialsRequest struct {
364364
Issuer string
365365
StoreLocation string
366366
Store string

‎kms/capi/capi.go‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ import (
1313
"crypto/x509"
1414
"encoding/binary"
1515
"encoding/hex"
16+
"errors"
1617
"fmt"
1718
"io"
1819
"math/big"
1920
"net/url"
2021
"reflect"
2122
"strconv"
2223
"strings"
24+
"time"
2325
"unsafe"
2426

25-
"github.com/pkg/errors"
26-
2727
"golang.org/x/crypto/cryptobyte"
2828
"golang.org/x/crypto/cryptobyte/asn1"
2929
"golang.org/x/sys/windows"
@@ -1140,6 +1140,41 @@ func (k *CAPIKMS) DeleteCertificate(req *apiv1.DeleteCertificateRequest) error {
11401140
}
11411141
}
11421142

1143+
// CleanupCredentials implements [apiv1.CredentialsCleaner]. It finds all
1144+
// certificates in the Windows certificate store issued to the subject in req by
1145+
// the issuer in req, and deletes any that have already expired.
1146+
func (k *CAPIKMS) CleanupCredentials(req *apiv1.CleanupCredentialsRequest) error {
1147+
certs, err := k.FindCertificatesByIssuer(&apiv1.LoadCertificateRequest{
1148+
Name: uri.New("capi", url.Values{
1149+
"issuer": []string{req.Issuer},
1150+
"store-location": []string{req.StoreLocation},
1151+
"store": []string{req.Store},
1152+
}).String(),
1153+
}, req.RawSubject)
1154+
if err != nil {
1155+
return fmt.Errorf("failed loading certificates by issuer %q: %w", req.Issuer, err)
1156+
}
1157+
1158+
var deleteErrors []error
1159+
now := time.Now()
1160+
for _, cert := range certs {
1161+
if cert.NotAfter.Before(now) {
1162+
deleteURI := uri.New("capi", url.Values{
1163+
"store-location": []string{req.StoreLocation},
1164+
"store": []string{req.Store},
1165+
"issuer": []string{req.Issuer},
1166+
"serial": []string{"0x" + cert.SerialNumber.Text(16)},
1167+
}).String()
1168+
1169+
if err := k.DeleteCertificate(&apiv1.DeleteCertificateRequest{Name: deleteURI}); err != nil {
1170+
deleteErrors = append(deleteErrors, fmt.Errorf("failed deleting expired certificate (serial %s): %w", cert.SerialNumber.Text(16), err))
1171+
}
1172+
}
1173+
}
1174+
1175+
return errors.Join(deleteErrors...)
1176+
}
1177+
11431178
func (k *CAPIKMS) getKeyFlags(u *uriAttributes) (uint32, error) {
11441179
keyFlags := uint32(0)
11451180

‎kms/platform/kms.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ var (
9393
_ apiv1.KeyManager = (*KMS)(nil)
9494
_ apiv1.CertificateManager = (*KMS)(nil)
9595
_ apiv1.CertificateChainManager = (*KMS)(nil)
96+
_ apiv1.SearchableKeyManager = (*KMS)(nil)
97+
_ apiv1.CredentialsCleaner = (*KMS)(nil)
9698
)
9799

98100
type KMS struct {
@@ -291,9 +293,9 @@ func (k *KMS) SearchKeys(req *apiv1.SearchKeysRequest) (*apiv1.SearchKeysRespons
291293
return nil, apiv1.NotImplementedError{}
292294
}
293295

294-
func (k *KMS) Cleanup(req *apiv1.CleanupCertificatesRequest) error {
295-
if km, ok := k.backend.(apiv1.CleaningCertificateManager); ok {
296-
return km.Cleanup(req)
296+
func (k *KMS) CleanupCredentials(req *apiv1.CleanupCredentialsRequest) error {
297+
if km, ok := k.backend.(apiv1.CredentialsCleaner); ok {
298+
return km.CleanupCredentials(req)
297299
}
298300

299301
return apiv1.NotImplementedError{}

‎kms/platform/kms_darwin_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package platform
22

33
import (
4+
"crypto/x509"
45
"testing"
6+
"time"
57

68
"github.com/stretchr/testify/assert"
9+
"go.step.sm/crypto/kms/apiv1"
710
)
811

912
func mustPlatformKMS(t *testing.T) *KMS {
@@ -68,3 +71,35 @@ func Test_transformFromMacKMS(t *testing.T) {
6871
})
6972
}
7073
}
74+
75+
func TestKMS_CleanupCredentials_mackms(t *testing.T) {
76+
platformKMS := mustPlatformKMS(t)
77+
// Use an expired certificate
78+
chain := mustCreatePlatformCertificate(t, platformKMS, withTemplateModifier(func(c *x509.Certificate) *x509.Certificate {
79+
c.NotBefore = time.Now().Add(-time.Minute).Truncate(time.Second)
80+
c.NotAfter = time.Now().Add(-time.Second).Truncate(time.Second)
81+
return c
82+
}))
83+
84+
type args struct {
85+
req *apiv1.CleanupCredentialsRequest
86+
}
87+
tests := []struct {
88+
name string
89+
kms *KMS
90+
args args
91+
assertion assert.ErrorAssertionFunc
92+
}{
93+
{"not implemented", platformKMS, args{&apiv1.CleanupCredentialsRequest{
94+
Issuer: chain[0].Issuer.CommonName,
95+
RawSubject: chain[0].RawSubject,
96+
}}, func(tt assert.TestingT, err error, i ...interface{}) bool {
97+
return assert.ErrorIs(tt, err, apiv1.NotImplementedError{})
98+
}},
99+
}
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
tt.assertion(t, tt.kms.CleanupCredentials(tt.args.req))
103+
})
104+
}
105+
}

‎kms/platform/kms_other_test.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
package platform
44

55
import (
6+
"crypto/x509"
67
"net/url"
78
"testing"
9+
"time"
810

11+
"github.com/stretchr/testify/assert"
912
"go.step.sm/crypto/kms/apiv1"
1013
"go.step.sm/crypto/kms/uri"
1114
"go.step.sm/crypto/tpm/available"
@@ -26,3 +29,37 @@ func mustPlatformKMS(t *testing.T) *KMS {
2629
func (k *KMS) SkipTests() bool {
2730
return k.Type() == apiv1.DefaultKMS
2831
}
32+
33+
func TestKMS_CleanupCredentials_other(t *testing.T) {
34+
platformKMS := mustPlatformKMS(t)
35+
// Use an expired certificate
36+
chain := mustCreatePlatformCertificate(t, platformKMS, withTemplateModifier(func(c *x509.Certificate) *x509.Certificate {
37+
c.NotBefore = time.Now().Add(-time.Minute).Truncate(time.Second)
38+
c.NotAfter = time.Now().Add(-time.Second).Truncate(time.Second)
39+
return c
40+
}))
41+
42+
type args struct {
43+
req *apiv1.CleanupCredentialsRequest
44+
}
45+
tests := []struct {
46+
name string
47+
kms *KMS
48+
args args
49+
assertion assert.ErrorAssertionFunc
50+
}{
51+
{"not implemented", platformKMS, args{&apiv1.CleanupCredentialsRequest{
52+
RawSubject: chain[0].RawSubject,
53+
}}, func(tt assert.TestingT, err error, i ...interface{}) bool {
54+
if platformKMS.Type() == apiv1.TPMKMS {
55+
return assert.NoError(t, err)
56+
}
57+
return assert.ErrorIs(tt, err, apiv1.NotImplementedError{})
58+
}},
59+
}
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
tt.assertion(t, tt.kms.CleanupCredentials(tt.args.req))
63+
})
64+
}
65+
}

‎kms/platform/kms_test.go‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"path/filepath"
2121
"runtime"
2222
"testing"
23+
"time"
2324

2425
"github.com/stretchr/testify/assert"
2526
"github.com/stretchr/testify/require"
@@ -101,21 +102,27 @@ func mustReadSigner(t *testing.T, path string) crypto.Signer {
101102
return signer
102103
}
103104

104-
func mustCertificate(t *testing.T, path string) []*x509.Certificate {
105+
func mustCertificate(t *testing.T, path string, opts ...createFuncOption) []*x509.Certificate {
105106
t.Helper()
106107

108+
o := new(createOptions)
109+
o.templateModifier = func(c *x509.Certificate) *x509.Certificate { return c }
110+
for _, fn := range opts {
111+
fn(o)
112+
}
113+
107114
ca, err := minica.New()
108115
require.NoError(t, err)
109116

110117
signer, err := keyutil.GenerateDefaultSigner()
111118
require.NoError(t, err)
112119

113-
cert, err := ca.Sign(&x509.Certificate{
120+
cert, err := ca.Sign(o.templateModifier(&x509.Certificate{
114121
KeyUsage: x509.KeyUsageDigitalSignature,
115122
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
116123
PublicKey: signer.Public(),
117124
DNSNames: []string{"example.com"},
118-
})
125+
}))
119126
require.NoError(t, err)
120127

121128
if path != "" {
@@ -186,6 +193,7 @@ type createOptions struct {
186193
name string
187194
noCleanup bool
188195
noCleanupCertificate bool
196+
templateModifier func(*x509.Certificate) *x509.Certificate
189197
}
190198

191199
type createFuncOption func(*createOptions)
@@ -209,6 +217,12 @@ func withNoCleanupCertificate() createFuncOption {
209217
}
210218
}
211219

220+
func withTemplateModifier(fn func(*x509.Certificate) *x509.Certificate) createFuncOption {
221+
return func(co *createOptions) {
222+
co.templateModifier = fn
223+
}
224+
}
225+
212226
func mustCreatePlatformKey(t *testing.T, km *KMS, opts ...createFuncOption) *apiv1.CreateKeyResponse {
213227
t.Helper()
214228

@@ -242,6 +256,7 @@ func mustCreatePlatformCertificate(t *testing.T, km *KMS, opts ...createFuncOpti
242256
t.Helper()
243257

244258
o := new(createOptions)
259+
o.templateModifier = func(c *x509.Certificate) *x509.Certificate { return c }
245260
o.name = platformCertName
246261
for _, fn := range opts {
247262
fn(o)
@@ -257,12 +272,12 @@ func mustCreatePlatformCertificate(t *testing.T, km *KMS, opts ...createFuncOpti
257272
}
258273

259274
key := mustCreatePlatformKey(t, km, opts...)
260-
cert, err := ca.Sign(&x509.Certificate{
275+
cert, err := ca.Sign(o.templateModifier(&x509.Certificate{
261276
KeyUsage: x509.KeyUsageDigitalSignature,
262277
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
263278
PublicKey: key.PublicKey,
264279
DNSNames: []string{"example.com"},
265-
})
280+
}))
266281
require.NoError(t, err)
267282

268283
require.NoError(t, km.StoreCertificateChain(&apiv1.StoreCertificateChainRequest{
@@ -1289,6 +1304,39 @@ func TestKMS_SearchKeys(t *testing.T) {
12891304
}
12901305
}
12911306

1307+
func TestKMS_CleanupCredentials(t *testing.T) {
1308+
dir := t.TempDir()
1309+
softKMS := mustKMS(t, "kms:backend=softkms")
1310+
// Create an expired certificate
1311+
chain := mustCertificate(t, filepath.Join(dir, "chain.crt"), withTemplateModifier(func(c *x509.Certificate) *x509.Certificate {
1312+
c.NotBefore = time.Now().Add(-time.Minute).Truncate(time.Second)
1313+
c.NotAfter = time.Now().Add(-time.Second).Truncate(time.Second)
1314+
return c
1315+
}))
1316+
1317+
type args struct {
1318+
req *apiv1.CleanupCredentialsRequest
1319+
}
1320+
tests := []struct {
1321+
name string
1322+
kms *KMS
1323+
args args
1324+
assertion assert.ErrorAssertionFunc
1325+
}{
1326+
{"not implemented", softKMS, args{&apiv1.CleanupCredentialsRequest{
1327+
Issuer: chain[0].Issuer.CommonName,
1328+
RawSubject: chain[0].RawSubject,
1329+
}}, func(tt assert.TestingT, err error, i ...interface{}) bool {
1330+
return assert.ErrorIs(tt, err, apiv1.NotImplementedError{})
1331+
}},
1332+
}
1333+
for _, tt := range tests {
1334+
t.Run(tt.name, func(t *testing.T) {
1335+
tt.assertion(t, tt.kms.CleanupCredentials(tt.args.req))
1336+
})
1337+
}
1338+
}
1339+
12921340
func Test_getBackend(t *testing.T) {
12931341
type args struct {
12941342
opts apiv1.Options

0 commit comments

Comments
 (0)