diff --git a/go.mod b/go.mod index c81244cf..49f6c511 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.13 require ( cloud.google.com/go v0.51.0 github.com/Masterminds/sprig/v3 v3.0.0 + github.com/aws/aws-sdk-go v1.30.29 github.com/go-chi/chi v4.0.2+incompatible github.com/go-piv/piv-go v1.5.0 github.com/googleapis/gax-go/v2 v2.0.5 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/newrelic/go-agent v2.15.0+incompatible - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 @@ -19,7 +20,7 @@ require ( github.com/smallstep/nosql v0.3.0 github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 google.golang.org/api v0.15.0 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb google.golang.org/grpc v1.26.0 diff --git a/go.sum b/go.sum index 7a9caaef..1bd57d61 100644 --- a/go.sum +++ b/go.sum @@ -43,7 +43,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.19.18 h1:Hb3+b9HCqrOrbAtFstUWg7H5TQ+/EcklJtE8VShVs8o= github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0= +github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -128,6 +131,8 @@ github.com/go-piv/piv-go v1.5.0 h1:UtHPfrJsZKY+Z3UIjmJLh6DY+KtmNOl/9b/zt4N81pM= github.com/go-piv/piv-go v1.5.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= @@ -268,7 +273,10 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -386,6 +394,8 @@ github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= @@ -500,6 +510,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= @@ -618,6 +630,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index c136e3f4..05421bd2 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -57,7 +57,7 @@ type Options struct { // The type of the KMS to use. Type string `json:"type"` - // Path to the credentials file used in CloudKMS. + // Path to the credentials file used in CloudKMS and AmazonKMS. CredentialsFile string `json:"credentialsFile"` // Path to the module used with PKCS11 KMS. @@ -65,6 +65,12 @@ type Options struct { // Pin used to access the PKCS11 module. Pin string `json:"pin"` + + // Region to use in AmazonKMS. + Region string `json:"region"` + + // Profile to use in AmazonKMS. + Profile string `json:"profile"` } // Validate checks the fields in Options. @@ -74,10 +80,8 @@ func (o *Options) Validate() error { } switch Type(strings.ToLower(o.Type)) { - case DefaultKMS, SoftKMS, CloudKMS: + case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS: case YubiKey: - case AmazonKMS: - return ErrNotImplemented{"support for AmazonKMS is not yet implemented"} case PKCS11: return ErrNotImplemented{"support for PKCS11 is not yet implemented"} default: diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go new file mode 100644 index 00000000..aea157b2 --- /dev/null +++ b/kms/awskms/awskms.go @@ -0,0 +1,237 @@ +package awskms + +import ( + "context" + "crypto" + "net/url" + "strings" + "time" + + "github.com/smallstep/certificates/kms/uri" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" +) + +// KMS implements a KMS using AWS Key Management Service. +type KMS struct { + session *session.Session + service *kms.KMS +} + +// customerMasterKeySpecMapping is a mapping between the step signature algorithm, +// and bits for RSA keys, with awskms CustomerMasterKeySpec. +var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{ + apiv1.UnspecifiedSignAlgorithm: kms.CustomerMasterKeySpecEccNistP256, + apiv1.SHA256WithRSA: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa3072, + 2048: kms.CustomerMasterKeySpecRsa2048, + 3072: kms.CustomerMasterKeySpecRsa3072, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA512WithRSA: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa4096, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA256WithRSAPSS: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa3072, + 2048: kms.CustomerMasterKeySpecRsa2048, + 3072: kms.CustomerMasterKeySpecRsa3072, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA512WithRSAPSS: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa4096, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.ECDSAWithSHA256: kms.CustomerMasterKeySpecEccNistP256, + apiv1.ECDSAWithSHA384: kms.CustomerMasterKeySpecEccNistP384, + apiv1.ECDSAWithSHA512: kms.CustomerMasterKeySpecEccNistP521, +} + +// New creates a new AWSKMS. By default, sessions will be created using the +// credentials in `~/.aws/credentials`, but this can be overridden using the +// CredentialsFile option, the Region and Profile can also be configured as +// options. +// +// AWS sessions can also be configured with environment variables, see docs at +// https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for all the options. +func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { + o := session.Options{} + if opts.Region != "" { + o.Config.Region = &opts.Region + } + if opts.Profile != "" { + o.Profile = opts.Profile + } + if opts.CredentialsFile != "" { + o.SharedConfigFiles = []string{opts.CredentialsFile} + } + + sess, err := session.NewSessionWithOptions(o) + if err != nil { + return nil, errors.Wrap(err, "error creating AWS session") + } + + return &KMS{ + session: sess, + service: kms.New(sess), + }, nil +} + +func init() { + apiv1.Register(apiv1.AmazonKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +// GetPublicKey returns a public key from KMS. +func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + if req.Name == "" { + return nil, errors.New("getPublicKey 'name' cannot be empty") + } + keyID, err := parseKeyID(req.Name) + if err != nil { + return nil, err + } + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{ + KeyId: &keyID, + }) + if err != nil { + return nil, errors.Wrap(err, "awskms GetPublicKeyWithContext failed") + } + + return pemutil.ParseDER(resp.PublicKey) +} + +// CreateKey generates a new key in KMS and returns the public key version +// of it. +func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + if req.Name == "" { + return nil, errors.New("createKeyRequest 'name' cannot be empty") + } + + keySpec, err := getCustomerMasterKeySpecMapping(req.SignatureAlgorithm, req.Bits) + if err != nil { + return nil, err + } + + tag := new(kms.Tag) + tag.SetTagKey("name") + tag.SetTagValue(req.Name) + + input := &kms.CreateKeyInput{ + Description: &req.Name, + CustomerMasterKeySpec: &keySpec, + Tags: []*kms.Tag{tag}, + } + input.SetKeyUsage(kms.KeyUsageTypeSignVerify) + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.service.CreateKeyWithContext(ctx, input) + if err != nil { + return nil, errors.Wrap(err, "awskms CreateKeyWithContext failed") + } + if err := k.createKeyAlias(*resp.KeyMetadata.KeyId, req.Name); err != nil { + return nil, err + } + + // Create uri for key + name := uri.New("awskms", url.Values{ + "key-id": []string{*resp.KeyMetadata.KeyId}, + }).String() + + publicKey, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: name, + }) + if err != nil { + return nil, err + } + + // Names uses Amazon Resource Name + // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + return &apiv1.CreateKeyResponse{ + Name: name, + PublicKey: publicKey, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: name, + }, + }, nil +} + +func (k *KMS) createKeyAlias(keyID, alias string) error { + alias = "alias/" + alias + "-" + keyID[:8] + + ctx, cancel := defaultContext() + defer cancel() + + _, err := k.service.CreateAliasWithContext(ctx, &kms.CreateAliasInput{ + AliasName: &alias, + TargetKeyId: &keyID, + }) + if err != nil { + return errors.Wrap(err, "awskms CreateAliasWithContext failed") + } + return nil +} + +// CreateSigner creates a new crypto.Signer with a previously configured key. +func (k *KMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.SigningKey == "" { + return nil, errors.New("createSigner 'signingKey' cannot be empty") + } + return NewSigner(k.service, req.SigningKey) +} + +// Close closes the connection of the KMS client. +func (k *KMS) Close() error { + return nil +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} + +// parseKeyID extracts the key-id from an uri. +func parseKeyID(name string) (string, error) { + name = strings.ToLower(name) + if strings.HasPrefix(name, "awskms:") || strings.HasPrefix(name, "aws:") { + u, err := uri.Parse(name) + if err != nil { + return "", err + } + if k := u.Get("key-id"); k != "" { + return k, nil + } + return "", errors.Errorf("failed to get key-id from %s", name) + } + return name, nil +} + +func getCustomerMasterKeySpecMapping(alg apiv1.SignatureAlgorithm, bits int) (string, error) { + v, ok := customerMasterKeySpecMapping[alg] + if !ok { + return "", errors.Errorf("awskms does not support signature algorithm '%s'", alg) + } + + switch v := v.(type) { + case string: + return v, nil + case map[int]string: + s, ok := v[bits] + if !ok { + return "", errors.Errorf("awskms does not support signature algorithm '%s' with '%d' bits", alg, bits) + } + return s, nil + default: + return "", errors.Errorf("unexpected error: this should not happen") + } +} diff --git a/kms/awskms/signer.go b/kms/awskms/signer.go new file mode 100644 index 00000000..194eeae4 --- /dev/null +++ b/kms/awskms/signer.go @@ -0,0 +1,121 @@ +package awskms + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "io" + + "github.com/aws/aws-sdk-go/service/kms" + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/pemutil" +) + +type Signer struct { + service *kms.KMS + keyID string + publicKey crypto.PublicKey +} + +// NewSigner creates a new signer using a key in the AWS KMS. +func NewSigner(svc *kms.KMS, signingKey string) (*Signer, error) { + keyID, err := parseKeyID(signingKey) + if err != nil { + return nil, err + } + + // Make sure that the key exists. + signer := &Signer{ + service: svc, + keyID: keyID, + } + if err := signer.preloadKey(keyID); err != nil { + return nil, err + } + + return signer, nil +} + +func (s *Signer) preloadKey(keyID string) error { + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{ + KeyId: &keyID, + }) + if err != nil { + return errors.Wrap(err, "awskms GetPublicKeyWithContext failed") + } + + s.publicKey, err = pemutil.ParseDER(resp.PublicKey) + return err +} + +// Public returns the public key of this signer or an error. +func (s *Signer) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key stored in the AWS KMS. +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + alg, err := getSigningAlgorithm(s.Public(), opts) + if err != nil { + return nil, err + } + + req := &kms.SignInput{ + KeyId: &s.keyID, + SigningAlgorithm: &alg, + Message: digest, + } + req.SetMessageType("DIGEST") + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.service.SignWithContext(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "awsKMS SignWithContext failed") + } + + return resp.Signature, nil +} + +func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (string, error) { + switch key.(type) { + case *rsa.PublicKey: + _, isPSS := opts.(*rsa.PSSOptions) + switch h := opts.HashFunc(); h { + case crypto.SHA256: + if isPSS { + return "RSASSA_PSS_SHA_256", nil + } + return "RSASSA_PKCS1_V1_5_SHA_256", nil + case crypto.SHA384: + if isPSS { + return "RSASSA_PSS_SHA_384", nil + } + return "RSASSA_PKCS1_V1_5_SHA_384", nil + case crypto.SHA512: + if isPSS { + return "RSASSA_PSS_SHA_512", nil + } + return "RSASSA_PKCS1_V1_5_SHA_512", nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + case *ecdsa.PublicKey: + switch h := opts.HashFunc(); h { + case crypto.SHA256: + return "ECDSA_SHA_256", nil + case crypto.SHA384: + return "ECDSA_SHA_384", nil + case crypto.SHA512: + return "ECDSA_SHA_512", nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + default: + return "", errors.Errorf("unsupported key type %T", key) + } +} diff --git a/kms/kms.go b/kms/kms.go index 310214f0..1a37135b 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -8,6 +8,7 @@ import ( "github.com/smallstep/certificates/kms/apiv1" // Enabled kms interfaces. + _ "github.com/smallstep/certificates/kms/awskms" _ "github.com/smallstep/certificates/kms/cloudkms" _ "github.com/smallstep/certificates/kms/softkms"