From 28c3d96c016b6ff824daa1596a32d564b8fb7c3a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 12 Dec 2025 16:33:45 -0800 Subject: [PATCH 1/6] Platform KMS (WIP) --- kms/apiv1/options.go | 25 ++++++++++++ kms/platform/kms.go | 76 +++++++++++++++++++++++++++++++++++++ kms/platform/kms_darwin.go | 33 ++++++++++++++++ kms/platform/kms_other.go | 13 +++++++ kms/platform/kms_tpm.go | 41 ++++++++++++++++++++ kms/platform/kms_windows.go | 38 +++++++++++++++++++ 6 files changed, 226 insertions(+) create mode 100644 kms/platform/kms.go create mode 100644 kms/platform/kms_darwin.go create mode 100644 kms/platform/kms_other.go create mode 100644 kms/platform/kms_tpm.go create mode 100644 kms/platform/kms_windows.go diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 3b50b942..667d2fb5 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -18,6 +18,17 @@ type KeyManager interface { Close() error } +// KeyDeleter is an optional interface for KMS implementations that support +// deleting keys. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +type KeyDeleter interface { + DeleteKey(req *DeleteKeyRequest) error +} + // SearchableKeyManager is an optional interface for KMS implementations // that support searching for keys based on certain attributes. // @@ -54,6 +65,17 @@ type CertificateChainManager interface { StoreCertificateChain(req *StoreCertificateChainRequest) error } +// CertificateDeleter is an optional interface for KMS implementations that +// support deleting certificates. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +type CertificateDeleter interface { + DeleteCertificate(req *DeleteCertificateRequest) error +} + // NameValidator is an interface that KeyManager can implement to validate a // given name or URI. type NameValidator interface { @@ -151,6 +173,9 @@ const ( TPMKMS Type = "tpmkms" // MacKMS is the KMS implementation using macOS Keychain and Secure Enclave. MacKMS Type = "mackms" + // PlatformKMS is the KMS implementation that uses TPMKMS on Windows and + // Linux and MacKMS on macOS.. + PlatformKMS Type = "kms" ) // TypeOf returns the type of of the given uri. diff --git a/kms/platform/kms.go b/kms/platform/kms.go new file mode 100644 index 00000000..3015d09a --- /dev/null +++ b/kms/platform/kms.go @@ -0,0 +1,76 @@ +package platform + +import ( + "context" + "crypto" + "crypto/x509" + + "go.step.sm/crypto/kms/apiv1" +) + +const Scheme = "kms" + +func init() { + apiv1.Register(apiv1.PlatformKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +type extendedKeyManager interface { + apiv1.KeyManager + apiv1.KeyDeleter + apiv1.CertificateManager + apiv1.CertificateChainManager +} + +var _ apiv1.KeyManager = (*KMS)(nil) +var _ apiv1.CertificateManager = (*KMS)(nil) +var _ apiv1.CertificateChainManager = (*KMS)(nil) + +type KMS struct { + backend extendedKeyManager +} + +func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { + return newKMS(ctx, opts) +} + +func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + return k.backend.GetPublicKey(req) +} + +func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + return k.backend.CreateKey(req) +} + +func (k *KMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + return k.backend.CreateSigner(req) +} + +func (k *KMS) Close() error { + return k.backend.Close() +} + +func (k *KMS) DeleteKey(req *apiv1.DeleteKeyRequest) error { + return k.backend.DeleteKey(req) +} + +func (k *KMS) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { + return k.backend.LoadCertificate(req) +} + +func (k *KMS) StoreCertificate(req *apiv1.StoreCertificateRequest) error { + return k.backend.StoreCertificate(req) +} + +func (k *KMS) LoadCertificateChain(req *apiv1.LoadCertificateChainRequest) ([]*x509.Certificate, error) { + return k.backend.LoadCertificateChain(req) +} + +func (k *KMS) StoreCertificateChain(req *apiv1.StoreCertificateChainRequest) error { + if km, ok := k.backend.(apiv1.CertificateChainManager); ok { + return km.StoreCertificateChain(req) + } + + return apiv1.NotImplementedError{} +} diff --git a/kms/platform/kms_darwin.go b/kms/platform/kms_darwin.go new file mode 100644 index 00000000..cda75366 --- /dev/null +++ b/kms/platform/kms_darwin.go @@ -0,0 +1,33 @@ +package platform + +import ( + "context" + + "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/mackms" +) + +var _ apiv1.SearchableKeyManager = (*KMS)(nil) + +func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + if opts.Type == apiv1.TPMKMS { + return newTPMKMS(ctx, opts) + } + + km, err := mackms.New(ctx, opts) + if err != nil { + return nil, err + } + + return &KMS{ + backend: km, + }, nil +} + +func (k *KMS) SearchKeys(req *apiv1.SearchKeysRequest) (*apiv1.SearchKeysResponse, error) { + if km, ok := k.backend.(apiv1.SearchableKeyManager); ok { + return km.SearchKeys(req) + } + + return nil, apiv1.NotImplementedError{} +} diff --git a/kms/platform/kms_other.go b/kms/platform/kms_other.go new file mode 100644 index 00000000..fb844bad --- /dev/null +++ b/kms/platform/kms_other.go @@ -0,0 +1,13 @@ +//go:build !darwin && !windows + +package platform + +import ( + "context" + + "go.step.sm/crypto/kms/apiv1" +) + +func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + return newTPMKMS(ctx, opts) +} diff --git a/kms/platform/kms_tpm.go b/kms/platform/kms_tpm.go new file mode 100644 index 00000000..bca38f2f --- /dev/null +++ b/kms/platform/kms_tpm.go @@ -0,0 +1,41 @@ +package platform + +import ( + "context" + + "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/tpmkms" + "go.step.sm/crypto/tpm" +) + +var _ apiv1.Attester = (*KMS)(nil) + +func newTPMKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + km, err := tpmkms.New(ctx, opts) + if err != nil { + return nil, err + } + + return &KMS{ + backend: km, + }, nil +} + +func NewWithTPM(ctx context.Context, t *tpm.TPM, opts ...tpmkms.Option) (*KMS, error) { + km, err := tpmkms.NewWithTPM(ctx, t, opts...) + if err != nil { + return nil, err + } + + return &KMS{ + backend: km, + }, nil +} + +func (k *KMS) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1.CreateAttestationResponse, error) { + if km, ok := k.backend.(apiv1.Attester); ok { + return km.CreateAttestation(req) + } + + return nil, apiv1.NotImplementedError{} +} diff --git a/kms/platform/kms_windows.go b/kms/platform/kms_windows.go new file mode 100644 index 00000000..3de31245 --- /dev/null +++ b/kms/platform/kms_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package platform + +import ( + "context" + + "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/capi" + "go.step.sm/crypto/kms/uri" +) + +func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + if opts.Type == apiv1.CAPIKMS { + km, err := capi.New(ctx, opts) + if err != nil { + return nil, err + } + + return &KMS{ + backend: km, + }, nil + } + + if opts.URI != "" { + u, err := uri.Parse(opts.URI) + if err != nil { + return nil, err + } + + if !u.Has("enable-cng") { + u.Values.Set("enable-cng", "true") + } + opts.URI = u.String() + } + + return newTPMKMS(ctx, opts) +} From 597a1fc90ed51483a6410d1fc985b894671c11cc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 8 Jan 2026 13:05:04 -0800 Subject: [PATCH 2/6] wip --- kms/apiv1/options.go | 2 +- kms/platform/kms.go | 130 ++++++++++++++++++++++++++++++++++-- kms/platform/kms_darwin.go | 42 ++++++++++-- kms/platform/kms_tpm.go | 34 +++++++++- kms/platform/kms_windows.go | 59 ++++++++++------ 5 files changed, 234 insertions(+), 33 deletions(-) diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 667d2fb5..b557b812 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -206,7 +206,7 @@ func (t Type) Validate() error { return nil case YubiKey, PKCS11, TPMKMS: // Hardware based kms. return nil - case SSHAgentKMS, CAPIKMS, MacKMS: // Others + case SSHAgentKMS, CAPIKMS, MacKMS, PlatformKMS: // Others return nil } diff --git a/kms/platform/kms.go b/kms/platform/kms.go index 3015d09a..ef7237e5 100644 --- a/kms/platform/kms.go +++ b/kms/platform/kms.go @@ -4,8 +4,11 @@ import ( "context" "crypto" "crypto/x509" + "net/url" + "strings" "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/uri" ) const Scheme = "kms" @@ -16,6 +19,42 @@ func init() { }) } +const ( + backendKey = "backend" + nameKey = "name" + hwKey = "hw" +) + +type kmsURI struct { + uri *uri.URI + backend apiv1.Type + name string + hw bool + extraValues url.Values +} + +func parseURI(rawuri string) (*kmsURI, error) { + u, err := uri.ParseWithScheme(Scheme, rawuri) + if err != nil { + return nil, err + } + + extraValues := make(url.Values) + for k, v := range u.Values { + if k != nameKey && k != hwKey && k != backendKey { + extraValues[k] = v + } + } + + return &kmsURI{ + uri: u, + backend: apiv1.Type(strings.ToLower(u.Get(backendKey))), + name: u.Get(nameKey), + hw: u.GetBool(hwKey), + extraValues: extraValues, + }, nil +} + type extendedKeyManager interface { apiv1.KeyManager apiv1.KeyDeleter @@ -28,7 +67,8 @@ var _ apiv1.CertificateManager = (*KMS)(nil) var _ apiv1.CertificateChainManager = (*KMS)(nil) type KMS struct { - backend extendedKeyManager + backend extendedKeyManager + transformURI func(*kmsURI) string } func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { @@ -36,14 +76,34 @@ func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { } func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { - return k.backend.GetPublicKey(req) + name, err := k.transform(req.Name) + if err != nil { + return nil, err + } + return k.backend.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: name, + }) } func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + name, err := k.transform(req.Name) + if err != nil { + return nil, err + } + + req = clone(req) + req.Name = name return k.backend.CreateKey(req) } func (k *KMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + signingKey, err := k.transform(req.SigningKey) + if err != nil { + return nil, err + } + + req = clone(req) + req.SigningKey = signingKey return k.backend.CreateSigner(req) } @@ -52,25 +112,85 @@ func (k *KMS) Close() error { } func (k *KMS) DeleteKey(req *apiv1.DeleteKeyRequest) error { + name, err := k.transform(req.Name) + if err != nil { + return err + } + + req = clone(req) + req.Name = name return k.backend.DeleteKey(req) } func (k *KMS) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { + name, err := k.transform(req.Name) + if err != nil { + return nil, err + } + + req = clone(req) + req.Name = name return k.backend.LoadCertificate(req) } func (k *KMS) StoreCertificate(req *apiv1.StoreCertificateRequest) error { + name, err := k.transform(req.Name) + if err != nil { + return err + } + + req = clone(req) + req.Name = name return k.backend.StoreCertificate(req) } func (k *KMS) LoadCertificateChain(req *apiv1.LoadCertificateChainRequest) ([]*x509.Certificate, error) { + name, err := k.transform(req.Name) + if err != nil { + return nil, err + } + + req = clone(req) + req.Name = name return k.backend.LoadCertificateChain(req) } func (k *KMS) StoreCertificateChain(req *apiv1.StoreCertificateChainRequest) error { - if km, ok := k.backend.(apiv1.CertificateChainManager); ok { - return km.StoreCertificateChain(req) + name, err := k.transform(req.Name) + if err != nil { + return err } - return apiv1.NotImplementedError{} + req = clone(req) + req.Name = name + return k.backend.StoreCertificateChain(req) +} + +func (k *KMS) SearchKeys(req *apiv1.SearchKeysRequest) (*apiv1.SearchKeysResponse, error) { + if km, ok := k.backend.(apiv1.SearchableKeyManager); ok { + query, err := k.transform(req.Query) + if err != nil { + return nil, err + } + + req = clone(req) + req.Query = query + return km.SearchKeys(req) + } + + return nil, apiv1.NotImplementedError{} +} + +func (k *KMS) transform(rawuri string) (string, error) { + u, err := parseURI(rawuri) + if err != nil { + return "", err + } + + return k.transformURI(u), nil +} + +func clone[T any](v *T) *T { + c := *v + return &c } diff --git a/kms/platform/kms_darwin.go b/kms/platform/kms_darwin.go index cda75366..fb747af1 100644 --- a/kms/platform/kms_darwin.go +++ b/kms/platform/kms_darwin.go @@ -2,32 +2,62 @@ package platform import ( "context" + "fmt" + "net/url" "go.step.sm/crypto/kms/apiv1" "go.step.sm/crypto/kms/mackms" + "go.step.sm/crypto/kms/uri" ) var _ apiv1.SearchableKeyManager = (*KMS)(nil) func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { - if opts.Type == apiv1.TPMKMS { + if opts.URI == "" { + return newMacKMS(ctx, opts) + } + + u, err := parseURI(opts.URI) + if err != nil { + return nil, err + } + + switch u.backend { + case apiv1.TPMKMS: return newTPMKMS(ctx, opts) + case apiv1.DefaultKMS, apiv1.MacKMS: + opts.URI = transformToMacKMS(u) + return newMacKMS(ctx, opts) + default: + return nil, fmt.Errorf("failed parsing %q: unsupported backend %q", opts.URI, u.backend) } +} +func newMacKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { km, err := mackms.New(ctx, opts) if err != nil { return nil, err } return &KMS{ - backend: km, + backend: km, + transformURI: transformToMacKMS, }, nil } -func (k *KMS) SearchKeys(req *apiv1.SearchKeysRequest) (*apiv1.SearchKeysResponse, error) { - if km, ok := k.backend.(apiv1.SearchableKeyManager); ok { - return km.SearchKeys(req) +func transformToMacKMS(u *kmsURI) string { + uv := url.Values{ + "label": []string{u.name}, + } + if u.hw { + uv.Set("se", "true") + uv.Set("keychain", "dataProtection") + } + + // Add custom extra values that might be mackms specific. + for k, v := range u.extraValues { + uv[k] = v } - return nil, apiv1.NotImplementedError{} + return uri.New(mackms.Scheme, uv).String() } diff --git a/kms/platform/kms_tpm.go b/kms/platform/kms_tpm.go index bca38f2f..54a3b374 100644 --- a/kms/platform/kms_tpm.go +++ b/kms/platform/kms_tpm.go @@ -2,22 +2,35 @@ package platform import ( "context" + "net/url" "go.step.sm/crypto/kms/apiv1" "go.step.sm/crypto/kms/tpmkms" + "go.step.sm/crypto/kms/uri" "go.step.sm/crypto/tpm" ) var _ apiv1.Attester = (*KMS)(nil) func newTPMKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + if opts.URI == "" { + return newTPMKMS(ctx, opts) + } + + u, err := parseURI(opts.URI) + if err != nil { + return nil, err + } + + opts.URI = transformToTPMKMS(u) km, err := tpmkms.New(ctx, opts) if err != nil { return nil, err } return &KMS{ - backend: km, + backend: km, + transformURI: transformToTPMKMS, }, nil } @@ -28,7 +41,8 @@ func NewWithTPM(ctx context.Context, t *tpm.TPM, opts ...tpmkms.Option) (*KMS, e } return &KMS{ - backend: km, + backend: km, + transformURI: transformToTPMKMS, }, nil } @@ -39,3 +53,19 @@ func (k *KMS) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1.Cre return nil, apiv1.NotImplementedError{} } + +func transformToTPMKMS(u *kmsURI) string { + uv := url.Values{ + "name": []string{u.name}, + } + if u.hw { + uv.Set("ak", "true") + } + + // Add custom extra values that might be tpmkms specific. + for k, v := range u.extraValues { + uv[k] = v + } + + return uri.New(tpmkms.Scheme, uv).String() +} diff --git a/kms/platform/kms_windows.go b/kms/platform/kms_windows.go index 3de31245..82854d2e 100644 --- a/kms/platform/kms_windows.go +++ b/kms/platform/kms_windows.go @@ -4,6 +4,8 @@ package platform import ( "context" + "fmt" + "net/url" "go.step.sm/crypto/kms/apiv1" "go.step.sm/crypto/kms/capi" @@ -11,28 +13,47 @@ import ( ) func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { - if opts.Type == apiv1.CAPIKMS { - km, err := capi.New(ctx, opts) - if err != nil { - return nil, err - } - - return &KMS{ - backend: km, - }, nil + if opts.URI == "" { + return newTPMKMS(ctx, opts) } - if opts.URI != "" { - u, err := uri.Parse(opts.URI) - if err != nil { - return nil, err - } + u, err := parseURI(opts.URI) + if err != nil { + return nil, err + } + + switch u.backend { + case apiv1.CAPIKMS: + opts.URI = transformToCapiKMS(u) + return newCAPIKMS(ctx, opts) + case apiv1.DefaultKMS, apiv1.TPMKMS: + return newTPMKMS(ctx, opts) + default: + return nil, fmt.Errorf("failed parsing %q: unsupported backend %q", opts.URI, u.backend) + } +} + +func newCAPIKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + km, err := capi.New(ctx, opts) + if err != nil { + return nil, err + } + + return &KMS{ + backend: km, + transformURI: transformToCapiKMS, + }, nil +} + +func transformToCapiKMS(u *kmsURI) string { + uv := url.Values{ + "key": []string{u.name}, + } - if !u.Has("enable-cng") { - u.Values.Set("enable-cng", "true") - } - opts.URI = u.String() + // Add custom extra values that might be tpmkms specific. + for k, v := range u.extraValues { + uv[k] = v } - return newTPMKMS(ctx, opts) + return uri.New(capi.Scheme, uv).String() } From 2fb25b1d11330c0e64f32c796bd1c64c7f385673 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 21 Jan 2026 15:27:11 -0800 Subject: [PATCH 3/6] Fix transformation of URIs for search methods --- kms/platform/kms_darwin.go | 10 +++++----- kms/platform/kms_tpm.go | 10 +++++----- kms/platform/kms_windows.go | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/kms/platform/kms_darwin.go b/kms/platform/kms_darwin.go index fb747af1..c10887d8 100644 --- a/kms/platform/kms_darwin.go +++ b/kms/platform/kms_darwin.go @@ -3,6 +3,7 @@ package platform import ( "context" "fmt" + "maps" "net/url" "go.step.sm/crypto/kms/apiv1" @@ -46,8 +47,9 @@ func newMacKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { } func transformToMacKMS(u *kmsURI) string { - uv := url.Values{ - "label": []string{u.name}, + uv := url.Values{} + if u.name != "" { + uv.Set("label", u.name) } if u.hw { uv.Set("se", "true") @@ -55,9 +57,7 @@ func transformToMacKMS(u *kmsURI) string { } // Add custom extra values that might be mackms specific. - for k, v := range u.extraValues { - uv[k] = v - } + maps.Copy(uv, u.extraValues) return uri.New(mackms.Scheme, uv).String() } diff --git a/kms/platform/kms_tpm.go b/kms/platform/kms_tpm.go index 54a3b374..bfdf6f7a 100644 --- a/kms/platform/kms_tpm.go +++ b/kms/platform/kms_tpm.go @@ -2,6 +2,7 @@ package platform import ( "context" + "maps" "net/url" "go.step.sm/crypto/kms/apiv1" @@ -55,17 +56,16 @@ func (k *KMS) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1.Cre } func transformToTPMKMS(u *kmsURI) string { - uv := url.Values{ - "name": []string{u.name}, + uv := url.Values{} + if u.name != "" { + uv.Set("name", u.name) } if u.hw { uv.Set("ak", "true") } // Add custom extra values that might be tpmkms specific. - for k, v := range u.extraValues { - uv[k] = v - } + maps.Copy(uv, u.extraValues) return uri.New(tpmkms.Scheme, uv).String() } diff --git a/kms/platform/kms_windows.go b/kms/platform/kms_windows.go index 82854d2e..2c6beed6 100644 --- a/kms/platform/kms_windows.go +++ b/kms/platform/kms_windows.go @@ -5,6 +5,7 @@ package platform import ( "context" "fmt" + "maps" "net/url" "go.step.sm/crypto/kms/apiv1" @@ -46,14 +47,13 @@ func newCAPIKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { } func transformToCapiKMS(u *kmsURI) string { - uv := url.Values{ - "key": []string{u.name}, + uv := url.Values{} + if u.name != "" { + uv.Set("key", u.name) } - // Add custom extra values that might be tpmkms specific. - for k, v := range u.extraValues { - uv[k] = v - } + // Add custom extra values that might be CAPI specific. + maps.Copy(uv, u.extraValues) return uri.New(capi.Scheme, uv).String() } From 2d71e3ff8679bf76d8eb412c3f700049ae26e6d9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 29 Jan 2026 17:37:54 -0800 Subject: [PATCH 4/6] Add LoadCertificate and LoadCertificateChain to softkms --- kms/softkms/softkms.go | 26 +++++++++++++ kms/softkms/softkms_test.go | 68 +++++++++++++++++++++++++++++++++- kms/softkms/testdata/chain.crt | 12 ++++++ kms/softkms/testdata/chain.key | 5 +++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 kms/softkms/testdata/chain.crt create mode 100644 kms/softkms/testdata/chain.key diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index 46a73ebe..fb8ad78a 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -7,6 +7,7 @@ import ( "crypto/ed25519" "crypto/rsa" "crypto/x509" + "fmt" "github.com/pkg/errors" "go.step.sm/crypto/keyutil" @@ -190,6 +191,31 @@ func (k *SoftKMS) CreateDecrypter(req *apiv1.CreateDecrypterRequest) (crypto.Dec } } +// LoadCertificate returns a x509.Certificate from the file passed in the +// request name. +func (k *SoftKMS) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) { + if req.Name == "" { + return nil, fmt.Errorf("loadCertificateRequest 'name' cannot be empty") + } + + bundle, err := pemutil.ReadCertificateBundle(filename(req.Name)) + if err != nil { + return nil, err + } + + return bundle[0], nil +} + +// LoadCertificateChain returns a slice of x509.Certificate from the file passed +// in the request name. +func (k *SoftKMS) LoadCertificateChain(req *apiv1.LoadCertificateChainRequest) ([]*x509.Certificate, error) { + if req.Name == "" { + return nil, fmt.Errorf("loadCertificateChainRequest 'name' cannot be empty") + } + + return pemutil.ReadCertificateBundle(filename(req.Name)) +} + func filename(s string) string { if u, err := uri.ParseWithScheme(Scheme, s); err == nil { if f := u.Get("path"); f != "" { diff --git a/kms/softkms/softkms_test.go b/kms/softkms/softkms_test.go index 11aa4ddd..dc86ee67 100644 --- a/kms/softkms/softkms_test.go +++ b/kms/softkms/softkms_test.go @@ -17,7 +17,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - + "github.com/stretchr/testify/require" "go.step.sm/crypto/kms/apiv1" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x25519" @@ -439,3 +439,69 @@ func Test_filename(t *testing.T) { }) } } + +func TestSoftKMS_LoadCertificate(t *testing.T) { + cert, err := pemutil.ReadCertificate("testdata/cert.crt") + require.NoError(t, err) + + type args struct { + req *apiv1.LoadCertificateRequest + } + tests := []struct { + name string + k *SoftKMS + args args + want *x509.Certificate + assertion assert.ErrorAssertionFunc + }{ + {"ok", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{Name: "testdata/cert.crt"}}, cert, assert.NoError}, + {"ok uri", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{Name: "testdata/cert.crt"}}, cert, assert.NoError}, + {"ok uri with path", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{Name: "softkms:path=testdata/cert.crt"}}, cert, assert.NoError}, + {"fail empty", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{}}, nil, assert.Error}, + {"fail missing", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{Name: "testdata/missing.crt"}}, nil, assert.Error}, + {"fail not a certificate", &SoftKMS{}, args{&apiv1.LoadCertificateRequest{Name: "testdata/cert.key"}}, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + got, err := k.LoadCertificate(tt.args.req) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSoftKMS_LoadCertificateChain(t *testing.T) { + chain, err := pemutil.ReadCertificateBundle("testdata/chain.crt") + require.NoError(t, err) + + cert, err := pemutil.ReadCertificate("testdata/cert.crt") + require.NoError(t, err) + + type args struct { + req *apiv1.LoadCertificateChainRequest + } + tests := []struct { + name string + k *SoftKMS + args args + want []*x509.Certificate + assertion assert.ErrorAssertionFunc + }{ + {"ok", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "testdata/chain.crt"}}, chain, assert.NoError}, + {"ok uri", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "testdata/chain.crt"}}, chain, assert.NoError}, + {"ok uri with path", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "softkms:path=testdata/chain.crt"}}, chain, assert.NoError}, + {"ok cert", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "softkms:testdata/cert.crt"}}, []*x509.Certificate{cert}, assert.NoError}, + {"fail empty", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{}}, nil, assert.Error}, + {"fail missing", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "testdata/missing.crt"}}, nil, assert.Error}, + {"fail not a certificate", &SoftKMS{}, args{&apiv1.LoadCertificateChainRequest{Name: "testdata/cert.key"}}, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &SoftKMS{} + got, err := k.LoadCertificateChain(tt.args.req) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/kms/softkms/testdata/chain.crt b/kms/softkms/testdata/chain.crt new file mode 100644 index 00000000..4289f3e3 --- /dev/null +++ b/kms/softkms/testdata/chain.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB0TCCAXegAwIBAgIQLnp4754BEk4JSoK7md7K4DAKBggqhkjOPQQDAjAkMSIw +IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTI2MDEzMDAxMzM0 +M1oXDTI2MDEzMTAxMzM0M1owHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0ZXAuY29t +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIPeCsTFXBmkeJlX4dC6jo+2oe3US +m3Yt1LfJRrV2cBNB5t+OQqKZNajirkBCv/IqUBFJILtrNAZ8tSNwTtCTeKOBkTCB +jjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MB0GA1UdDgQWBBRv+K/kt+Ho6CDVpfvl3YRnbqxBjzAfBgNVHSMEGDAWgBQOlOB3 +dUUKfp7vXBdzBsMJREGiZTAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3RlcC5jb20w +CgYIKoZIzj0EAwIDSAAwRQIhAKzVQCITpTCITRhQ/VWrUHqhYuD+Q/a7yUrM1gix +ZJV3AiAgRBrPNWYAIK24hjVWE21OPaJnSZ7Q7VRJNE0/vQzrZQ== +-----END CERTIFICATE----- diff --git a/kms/softkms/testdata/chain.key b/kms/softkms/testdata/chain.key new file mode 100644 index 00000000..b9b8e11c --- /dev/null +++ b/kms/softkms/testdata/chain.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKDTRUVuCrQVHjAlUjaDkmfEayOiIkLk1SZPU7MPMhyxoAoGCCqGSM49 +AwEHoUQDQgAEIPeCsTFXBmkeJlX4dC6jo+2oe3USm3Yt1LfJRrV2cBNB5t+OQqKZ +NajirkBCv/IqUBFJILtrNAZ8tSNwTtCTeA== +-----END EC PRIVATE KEY----- From 81f376bd614bbf384467d13db56fa66c1cfe6d84 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 29 Jan 2026 17:39:05 -0800 Subject: [PATCH 5/6] Add softkms as a backend for platformkms --- kms/platform/kms.go | 12 ++++ kms/platform/kms_darwin.go | 2 + kms/platform/kms_other.go | 19 +++++- kms/platform/kms_softkms.go | 113 ++++++++++++++++++++++++++++++++++++ kms/platform/kms_windows.go | 2 + 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 kms/platform/kms_softkms.go diff --git a/kms/platform/kms.go b/kms/platform/kms.go index ef7237e5..a6b209fb 100644 --- a/kms/platform/kms.go +++ b/kms/platform/kms.go @@ -60,6 +60,7 @@ type extendedKeyManager interface { apiv1.KeyDeleter apiv1.CertificateManager apiv1.CertificateChainManager + apiv1.CertificateDeleter } var _ apiv1.KeyManager = (*KMS)(nil) @@ -166,6 +167,17 @@ func (k *KMS) StoreCertificateChain(req *apiv1.StoreCertificateChainRequest) err return k.backend.StoreCertificateChain(req) } +func (k *KMS) DeleteCertificater(req *apiv1.DeleteCertificateRequest) error { + name, err := k.transform(req.Name) + if err != nil { + return err + } + + req = clone(req) + req.Name = name + return k.backend.DeleteCertificate(req) +} + func (k *KMS) SearchKeys(req *apiv1.SearchKeysRequest) (*apiv1.SearchKeysResponse, error) { if km, ok := k.backend.(apiv1.SearchableKeyManager); ok { query, err := k.transform(req.Query) diff --git a/kms/platform/kms_darwin.go b/kms/platform/kms_darwin.go index c10887d8..32fdb981 100644 --- a/kms/platform/kms_darwin.go +++ b/kms/platform/kms_darwin.go @@ -26,6 +26,8 @@ func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { switch u.backend { case apiv1.TPMKMS: return newTPMKMS(ctx, opts) + case apiv1.SoftKMS: + return newSoftKMS(ctx, opts) case apiv1.DefaultKMS, apiv1.MacKMS: opts.URI = transformToMacKMS(u) return newMacKMS(ctx, opts) diff --git a/kms/platform/kms_other.go b/kms/platform/kms_other.go index fb844bad..9901783f 100644 --- a/kms/platform/kms_other.go +++ b/kms/platform/kms_other.go @@ -4,10 +4,27 @@ package platform import ( "context" + "fmt" "go.step.sm/crypto/kms/apiv1" ) func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { - return newTPMKMS(ctx, opts) + if opts.URI == "" { + return newTPMKMS(ctx, opts) + } + + u, err := parseURI(opts.URI) + if err != nil { + return nil, err + } + + switch u.backend { + case apiv1.SoftKMS: + return newSoftKMS(ctx, opts) + case apiv1.DefaultKMS, apiv1.TPMKMS: + return newTPMKMS(ctx, opts) + default: + return nil, fmt.Errorf("failed parsing %q: unsupported backend %q", opts.URI, u.backend) + } } diff --git a/kms/platform/kms_softkms.go b/kms/platform/kms_softkms.go new file mode 100644 index 00000000..0d6e4975 --- /dev/null +++ b/kms/platform/kms_softkms.go @@ -0,0 +1,113 @@ +package platform + +import ( + "bytes" + "context" + "encoding/pem" + "fmt" + "os" + + "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/softkms" + "go.step.sm/crypto/kms/uri" + "go.step.sm/crypto/pemutil" +) + +func newSoftKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { + km, err := softkms.New(ctx, opts) + if err != nil { + return nil, err + } + + return &KMS{ + backend: &softKMS{SoftKMS: km}, + transformURI: transformToSoftKMS, + }, nil +} + +type softKMS struct { + *softkms.SoftKMS +} + +func (k *softKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + resp, err := k.SoftKMS.CreateKey(req) + if err != nil { + return nil, err + } + + if _, err := pemutil.Serialize(resp.PrivateKey, pemutil.ToFile(resp.Name, 0o600)); err != nil { + return nil, err + } + + return resp, nil +} + +func (k *softKMS) DeleteKey(req *apiv1.DeleteKeyRequest) error { + if req.Name == "" { + return fmt.Errorf("deleteKeyRequest 'name' cannot be empty") + } + + return os.Remove(filename(req.Name)) +} + +func (k *softKMS) StoreCertificate(req *apiv1.StoreCertificateRequest) error { + switch { + case req.Name == "": + return fmt.Errorf("storeCertificateRequest 'name' cannot be empty") + case req.Certificate == nil: + return fmt.Errorf("storeCertificateRequest 'certificate' cannot be empty") + } + + return os.WriteFile(filename(req.Name), pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: req.Certificate.Raw, + }), 0o600) +} + +func (k *softKMS) StoreCertificateChain(req *apiv1.StoreCertificateChainRequest) error { + switch { + case req.Name == "": + return fmt.Errorf("storeCertificateChainRequest 'name' cannot be empty") + case len(req.CertificateChain) == 0: + return fmt.Errorf("storeCertificateChainRequest 'certificateChain' cannot be empty") + } + + var buf bytes.Buffer + for _, crt := range req.CertificateChain { + if err := pem.Encode(&buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + }); err != nil { + return err + } + } + + return os.WriteFile(filename(req.Name), buf.Bytes(), 0o600) +} + +func (k *softKMS) DeleteCertificate(req *apiv1.DeleteCertificateRequest) error { + if req.Name == "" { + return fmt.Errorf("deleteCertificateRequest 'name' cannot be empty") + } + + return os.Remove(filename(req.Name)) +} + +func filename(s string) string { + if u, err := uri.ParseWithScheme(Scheme, s); err == nil { + if f := u.Get("path"); f != "" { + return f + } + switch { + case u.Path != "": + return u.Path + default: + return u.Opaque + } + } + return s +} + +func transformToSoftKMS(u *kmsURI) string { + return uri.NewOpaque(softkms.Scheme, u.name).String() +} diff --git a/kms/platform/kms_windows.go b/kms/platform/kms_windows.go index 2c6beed6..30260f18 100644 --- a/kms/platform/kms_windows.go +++ b/kms/platform/kms_windows.go @@ -27,6 +27,8 @@ func newKMS(ctx context.Context, opts apiv1.Options) (*KMS, error) { case apiv1.CAPIKMS: opts.URI = transformToCapiKMS(u) return newCAPIKMS(ctx, opts) + case apiv1.SoftKMS: + return newSoftKMS(ctx, opts) case apiv1.DefaultKMS, apiv1.TPMKMS: return newTPMKMS(ctx, opts) default: From ccdf0272181fd56c8e861b3a095858ba653c45ac Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Feb 2026 11:53:28 -0800 Subject: [PATCH 6/6] Fix typo --- kms/platform/kms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/platform/kms.go b/kms/platform/kms.go index a6b209fb..a88162b3 100644 --- a/kms/platform/kms.go +++ b/kms/platform/kms.go @@ -167,7 +167,7 @@ func (k *KMS) StoreCertificateChain(req *apiv1.StoreCertificateChainRequest) err return k.backend.StoreCertificateChain(req) } -func (k *KMS) DeleteCertificater(req *apiv1.DeleteCertificateRequest) error { +func (k *KMS) DeleteCertificate(req *apiv1.DeleteCertificateRequest) error { name, err := k.transform(req.Name) if err != nil { return err