diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 3b50b942..b557b812 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. @@ -181,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 new file mode 100644 index 00000000..a88162b3 --- /dev/null +++ b/kms/platform/kms.go @@ -0,0 +1,208 @@ +package platform + +import ( + "context" + "crypto" + "crypto/x509" + "net/url" + "strings" + + "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/uri" +) + +const Scheme = "kms" + +func init() { + apiv1.Register(apiv1.PlatformKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +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 + apiv1.CertificateManager + apiv1.CertificateChainManager + apiv1.CertificateDeleter +} + +var _ apiv1.KeyManager = (*KMS)(nil) +var _ apiv1.CertificateManager = (*KMS)(nil) +var _ apiv1.CertificateChainManager = (*KMS)(nil) + +type KMS struct { + backend extendedKeyManager + transformURI func(*kmsURI) string +} + +func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { + return newKMS(ctx, opts) +} + +func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + 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) +} + +func (k *KMS) Close() error { + return k.backend.Close() +} + +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 { + name, err := k.transform(req.Name) + if err != nil { + return err + } + + req = clone(req) + req.Name = name + return k.backend.StoreCertificateChain(req) +} + +func (k *KMS) DeleteCertificate(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) + 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 new file mode 100644 index 00000000..32fdb981 --- /dev/null +++ b/kms/platform/kms_darwin.go @@ -0,0 +1,65 @@ +package platform + +import ( + "context" + "fmt" + "maps" + "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.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.SoftKMS: + return newSoftKMS(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, + transformURI: transformToMacKMS, + }, nil +} + +func transformToMacKMS(u *kmsURI) string { + uv := url.Values{} + if u.name != "" { + uv.Set("label", u.name) + } + if u.hw { + uv.Set("se", "true") + uv.Set("keychain", "dataProtection") + } + + // Add custom extra values that might be mackms specific. + maps.Copy(uv, u.extraValues) + + return uri.New(mackms.Scheme, uv).String() +} diff --git a/kms/platform/kms_other.go b/kms/platform/kms_other.go new file mode 100644 index 00000000..9901783f --- /dev/null +++ b/kms/platform/kms_other.go @@ -0,0 +1,30 @@ +//go:build !darwin && !windows + +package platform + +import ( + "context" + "fmt" + + "go.step.sm/crypto/kms/apiv1" +) + +func newKMS(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 + } + + 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_tpm.go b/kms/platform/kms_tpm.go new file mode 100644 index 00000000..bfdf6f7a --- /dev/null +++ b/kms/platform/kms_tpm.go @@ -0,0 +1,71 @@ +package platform + +import ( + "context" + "maps" + "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, + transformURI: transformToTPMKMS, + }, 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, + transformURI: transformToTPMKMS, + }, 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{} +} + +func transformToTPMKMS(u *kmsURI) string { + 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. + 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 new file mode 100644 index 00000000..30260f18 --- /dev/null +++ b/kms/platform/kms_windows.go @@ -0,0 +1,61 @@ +//go:build windows + +package platform + +import ( + "context" + "fmt" + "maps" + "net/url" + + "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.URI == "" { + return newTPMKMS(ctx, opts) + } + + 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.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) + } +} + +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{} + if u.name != "" { + uv.Set("key", u.name) + } + + // Add custom extra values that might be CAPI specific. + maps.Copy(uv, u.extraValues) + + return uri.New(capi.Scheme, uv).String() +} 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-----