diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 6b19f60b1e..16beef63e5 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -41,6 +41,7 @@ import ( "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/pgp" + "github.com/getsops/sops/v3/stackitkms" "github.com/getsops/sops/v3/stores" "github.com/getsops/sops/v3/stores/dotenv" "github.com/getsops/sops/v3/stores/json" @@ -91,14 +92,14 @@ func main() { }, } app.Name = "sops" - app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, Azure Key Vault, age, and GPG support" + app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, STACKIT KMS, Azure Key Vault, age, and GPG support" app.ArgsUsage = "sops [options] file" app.Version = version.Version app.Authors = []cli.Author{ {Name: "CNCF Maintainers"}, } - app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, AZKV, - PGP, and Age + app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, HuaweiCloud KMS, STACKIT KMS, + AZKV, PGP, and Age To encrypt or decrypt a document with AWS KMS, specify the KMS ARN in the -k flag or in the SOPS_KMS_ARN environment variable. @@ -117,6 +118,12 @@ func main() { HUAWEICLOUD_SDK_AK, HUAWEICLOUD_SDK_SK, HUAWEICLOUD_SDK_PROJECT_ID, or use credentials file at ~/.huaweicloud/credentials) + To encrypt or decrypt a document with STACKIT KMS, specify the + STACKIT KMS resource ID in the --stackit-kms flag or in the + SOPS_STACKIT_KMS_IDS environment variable. + (Authentication is handled by the STACKIT SDK via environment variables, + service account key files, or credentials file at ~/.stackit/credentials.json) + To encrypt or decrypt a document with HashiCorp Vault's Transit Secret Engine, specify the Vault key URI name in the --hc-vault-transit flag or in the SOPS_VAULT_URIS environment variable (for example @@ -142,12 +149,12 @@ func main() { To use multiple KMS or PGP keys, separate them by commas. For example: $ sops -p "10F2...0A, 85D...B3F21" file.yaml - The -p, -k, --gcp-kms, --hckms, --hc-vault-transit, and --azure-kv flags are only + The -p, -k, --gcp-kms, --hckms, --stackit-kms, --hc-vault-transit, and --azure-kv flags are only used to encrypt new documents. Editing or decrypting existing documents can be done with "sops file" or "sops decrypt file" respectively. The KMS and PGP keys listed in the encrypted documents are used then. To manage master - keys in existing documents, use the "add-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" - and "rm-{kms,pgp,gcp-kms,hckms,azure-kv,hc-vault-transit}" flags with --rotate + keys in existing documents, use the "add-{kms,pgp,gcp-kms,hckms,stackit-kms,azure-kv,hc-vault-transit}" + and "rm-{kms,pgp,gcp-kms,hckms,stackit-kms,azure-kv,hc-vault-transit}" flags with --rotate or the updatekeys command. To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC. @@ -582,6 +589,10 @@ func main() { Name: "hckms", Usage: "the HuaweiCloud KMS key ID (format: region:key-uuid) the new group should contain. Can be specified more than once", }, + cli.StringSliceFlag{ + Name: "stackit-kms", + Usage: "the STACKIT KMS resource ID the new group should contain. Can be specified more than once", + }, cli.StringSliceFlag{ Name: "azure-kv", Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once", @@ -635,6 +646,15 @@ func main() { } group = append(group, k) } + stackitKmsIds := c.StringSlice("stackit-kms") + for _, resID := range stackitKmsIds { + k, err := stackitkms.NewMasterKey(resID) + if err != nil { + log.WithError(err).Error("Failed to add key") + continue + } + group = append(group, k) + } for _, url := range azkvs { k, err := azkv.NewMasterKeyFromURL(url) if err != nil { @@ -950,6 +970,11 @@ func main() { Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", }, + cli.StringFlag{ + Name: "stackit-kms", + Usage: "comma separated list of STACKIT KMS resource IDs", + EnvVar: "SOPS_STACKIT_KMS_IDS", + }, cli.StringFlag{ Name: "azure-kv", Usage: "comma separated list of Azure Key Vault URLs", @@ -1143,6 +1168,14 @@ func main() { Name: "rm-hckms", Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", }, + cli.StringFlag{ + Name: "add-stackit-kms", + Usage: "add the provided comma-separated list of STACKIT KMS resource IDs to the list of master keys on the given file", + }, + cli.StringFlag{ + Name: "rm-stackit-kms", + Usage: "remove the provided comma-separated list of STACKIT KMS resource IDs from the list of master keys on the given file", + }, cli.StringFlag{ Name: "add-azure-kv", Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", @@ -1209,8 +1242,8 @@ func main() { return toExitError(err) } if _, err := os.Stat(fileName); os.IsNotExist(err) { - if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || - c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { + if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hckms") != "" || c.String("add-stackit-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || + c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hckms") != "" || c.String("rm-stackit-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } } @@ -1301,6 +1334,11 @@ func main() { Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", }, + cli.StringFlag{ + Name: "stackit-kms", + Usage: "comma separated list of STACKIT KMS resource IDs", + EnvVar: "SOPS_STACKIT_KMS_IDS", + }, cli.StringFlag{ Name: "azure-kv", Usage: "comma separated list of Azure Key Vault URLs", @@ -1714,6 +1752,11 @@ func main() { Usage: "comma separated list of HuaweiCloud KMS key IDs (format: region:key-uuid)", EnvVar: "SOPS_HUAWEICLOUD_KMS_IDS", }, + cli.StringFlag{ + Name: "stackit-kms", + Usage: "comma separated list of STACKIT KMS resource IDs", + EnvVar: "SOPS_STACKIT_KMS_IDS", + }, cli.StringFlag{ Name: "azure-kv", Usage: "comma separated list of Azure Key Vault URLs", @@ -1770,6 +1813,14 @@ func main() { Name: "rm-hckms", Usage: "remove the provided comma-separated list of HuaweiCloud KMS key IDs (format: region:key-uuid) from the list of master keys on the given file", }, + cli.StringFlag{ + Name: "add-stackit-kms", + Usage: "add the provided comma-separated list of STACKIT KMS resource IDs to the list of master keys on the given file", + }, + cli.StringFlag{ + Name: "rm-stackit-kms", + Usage: "remove the provided comma-separated list of STACKIT KMS resource IDs from the list of master keys on the given file", + }, cli.StringFlag{ Name: "add-azure-kv", Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file", @@ -2235,7 +2286,7 @@ func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, }, nil } -func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) { +func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, hckmsOptionName string, stackitKmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) { var masterKeys []keys.MasterKey for _, k := range kms.MasterKeysFromArnString(c.String(kmsOptionName), kmsEncryptionContext, c.String("aws-profile")) { masterKeys = append(masterKeys, k) @@ -2253,6 +2304,13 @@ func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsO for _, k := range hckmsKeys { masterKeys = append(masterKeys, k) } + stackitKmsKeys, err := stackitkms.NewMasterKeyFromResourceIDString(c.String(stackitKmsOptionName)) + if err != nil { + return nil, err + } + for _, k := range stackitKmsKeys { + masterKeys = append(masterKeys, k) + } azureKeys, err := azkv.MasterKeysFromURLs(c.String(azureKvOptionName)) if err != nil { return nil, err @@ -2279,11 +2337,11 @@ func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsO func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) { kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) - addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-hckms", "add-azure-kv", "add-hc-vault-transit", "add-age") + addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-hckms", "add-stackit-kms", "add-azure-kv", "add-hc-vault-transit", "add-age") if err != nil { return rotateOpts{}, err } - rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-hckms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age") + rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-hckms", "rm-stackit-kms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age") if err != nil { return rotateOpts{}, err } @@ -2432,6 +2490,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so var azkvKeys []keys.MasterKey var hcVaultMkKeys []keys.MasterKey var hckmsMkKeys []keys.MasterKey + var stackitKmsMkKeys []keys.MasterKey var ageMasterKeys []keys.MasterKey kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) if c.String("encryption-context") != "" && kmsEncryptionContext == nil { @@ -2456,6 +2515,15 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so hckmsMkKeys = append(hckmsMkKeys, k) } } + if c.String("stackit-kms") != "" { + stackitKmsKeys, err := stackitkms.NewMasterKeyFromResourceIDString(c.String("stackit-kms")) + if err != nil { + return nil, err + } + for _, k := range stackitKmsKeys { + stackitKmsMkKeys = append(stackitKmsMkKeys, k) + } + } if c.String("azure-kv") != "" { azureKeys, err := azkv.MasterKeysFromURLs(c.String("azure-kv")) if err != nil { @@ -2488,7 +2556,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so ageMasterKeys = append(ageMasterKeys, k) } } - if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("hckms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" { + if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("hckms") == "" && c.String("stackit-kms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" { conf := optionalConfig var err error if conf == nil { @@ -2508,6 +2576,7 @@ func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]so group = append(group, kmsKeys...) group = append(group, cloudKmsKeys...) group = append(group, hckmsMkKeys...) + group = append(group, stackitKmsMkKeys...) group = append(group, azkvKeys...) group = append(group, pgpKeys...) group = append(group, hcVaultMkKeys...) diff --git a/config/config.go b/config/config.go index 511df1bc15..5a5574617e 100644 --- a/config/config.go +++ b/config/config.go @@ -20,6 +20,7 @@ import ( "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/publish" + "github.com/getsops/sops/v3/stackitkms" "go.yaml.in/yaml/v3" ) @@ -130,14 +131,15 @@ type configFile struct { } type keyGroup struct { - Merge []keyGroup `yaml:"merge"` - KMS []kmsKey `yaml:"kms"` - GCPKMS []gcpKmsKey `yaml:"gcp_kms"` - HCKms []hckmsKey `yaml:"hckms"` - AzureKV []azureKVKey `yaml:"azure_keyvault"` - Vault []string `yaml:"hc_vault"` - Age []string `yaml:"age"` - PGP []string `yaml:"pgp"` + Merge []keyGroup `yaml:"merge"` + KMS []kmsKey `yaml:"kms"` + GCPKMS []gcpKmsKey `yaml:"gcp_kms"` + HCKms []hckmsKey `yaml:"hckms"` + STACKITKms []stackitKmsKey `yaml:"stackit_kms"` + AzureKV []azureKVKey `yaml:"azure_keyvault"` + Vault []string `yaml:"hc_vault"` + Age []string `yaml:"age"` + PGP []string `yaml:"pgp"` } type gcpKmsKey struct { @@ -161,6 +163,10 @@ type hckmsKey struct { KeyID string `yaml:"key_id"` } +type stackitKmsKey struct { + ResourceID string `yaml:"resource_id"` +} + type destinationRule struct { PathRegex string `yaml:"path_regex"` S3Bucket string `yaml:"s3_bucket"` @@ -183,6 +189,7 @@ type creationRule struct { PGP interface{} `yaml:"pgp"` // string or []string GCPKMS interface{} `yaml:"gcp_kms"` // string or []string HCKms []string `yaml:"hckms"` + STACKITKms interface{} `yaml:"stackit_kms"` // string or []string AzureKeyVault interface{} `yaml:"azure_keyvault"` // string or []string VaultURI interface{} `yaml:"hc_vault_transit_uri"` // string or []string KeyGroups []keyGroup `yaml:"key_groups"` @@ -213,6 +220,10 @@ func (c *creationRule) GetGCPKMSKeys() ([]string, error) { return parseKeyField(c.GCPKMS, "gcp_kms") } +func (c *creationRule) GetSTACKITKmsKeys() ([]string, error) { + return parseKeyField(c.STACKITKms, "stackit_kms") +} + func (c *creationRule) GetAzureKeyVaultKeys() ([]string, error) { return parseKeyField(c.AzureKeyVault, "azure_keyvault") } @@ -343,6 +354,13 @@ func extractMasterKeys(group keyGroup) (sops.KeyGroup, error) { } keyGroup = append(keyGroup, key) } + for _, k := range group.STACKITKms { + key, err := stackitkms.NewMasterKey(k.ResourceID) + if err != nil { + return nil, err + } + keyGroup = append(keyGroup, key) + } for _, k := range group.AzureKV { if key, err := azkv.NewMasterKeyWithOptionalVersion(k.VaultURL, k.Key, k.Version); err == nil { keyGroup = append(keyGroup, key) @@ -423,6 +441,17 @@ func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[ for _, k := range hckmsMasterKeys { keyGroup = append(keyGroup, k) } + stackitKmsKeys, err := getKeysWithValidation(cRule.GetSTACKITKmsKeys, "stackit_kms") + if err != nil { + return nil, err + } + stackitKmsMasterKeys, err := stackitkms.NewMasterKeyFromResourceIDString(strings.Join(stackitKmsKeys, ",")) + if err != nil { + return nil, err + } + for _, k := range stackitKmsMasterKeys { + keyGroup = append(keyGroup, k) + } azKeys, err := getKeysWithValidation(cRule.GetAzureKeyVaultKeys, "azure_keyvault") if err != nil { return nil, err diff --git a/go.mod b/go.mod index 7a1db25766..99fff2561d 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,8 @@ require ( github.com/ory/dockertest/v3 v3.12.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.4 + github.com/stackitcloud/stackit-sdk-go/core v0.22.0 + github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2 github.com/stretchr/testify v1.11.1 github.com/urfave/cli v1.22.17 go.yaml.in/yaml/v3 v3.0.4 diff --git a/go.sum b/go.sum index 022f2892ba..670ed8c779 100644 --- a/go.sum +++ b/go.sum @@ -314,6 +314,10 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stackitcloud/stackit-sdk-go/core v0.22.0 h1:6rViz7GnNwXSh51Lur5xuDzO8EWSZfN9J0HvEkBKq6c= +github.com/stackitcloud/stackit-sdk-go/core v0.22.0/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI= +github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2 h1:2ulSL2IkIAKND59eAjbEhVkOoBMyvm48ojwz1a3t0U0= +github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2/go.mod h1:cuIaMMiHeHQsbvy7BOFMutoV3QtN+ZBx7Tg3GmYUw7s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/keyservice/keyservice.go b/keyservice/keyservice.go index 04125f7510..5235d0d697 100644 --- a/keyservice/keyservice.go +++ b/keyservice/keyservice.go @@ -15,6 +15,7 @@ import ( "github.com/getsops/sops/v3/keys" "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/pgp" + "github.com/getsops/sops/v3/stackitkms" ) // KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can be serialized with Protocol Buffers @@ -87,6 +88,14 @@ func KeyFromMasterKey(mk keys.MasterKey) Key { }, }, } + case *stackitkms.MasterKey: + return Key{ + KeyType: &Key_STACKITKmsKey{ + STACKITKmsKey: &STACKITKmsKey{ + ResourceId: mk.ResourceID, + }, + }, + } default: panic(fmt.Sprintf("Tried to convert unknown MasterKey type %T to keyservice.Key", mk)) } diff --git a/keyservice/keyservice.proto b/keyservice/keyservice.proto index 3a471a34fd..75670e548a 100644 --- a/keyservice/keyservice.proto +++ b/keyservice/keyservice.proto @@ -11,6 +11,7 @@ message Key { VaultKey vault_key = 5; AgeKey age_key = 6; HckmsKey hckms_key = 7; + STACKITKmsKey stackit_kms_key = 8; } } @@ -49,6 +50,10 @@ message HckmsKey { string key_id = 1; } +message STACKITKmsKey { + string resource_id = 1; +} + message EncryptRequest { Key key = 1; bytes plaintext = 2; diff --git a/keyservice/server.go b/keyservice/server.go index c1f1e8ce86..ad08a194d5 100644 --- a/keyservice/server.go +++ b/keyservice/server.go @@ -10,6 +10,7 @@ import ( "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/pgp" + "github.com/getsops/sops/v3/stackitkms" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -88,6 +89,18 @@ func (ks *Server) encryptWithVault(key *VaultKey, plaintext []byte) ([]byte, err return []byte(vaultKey.EncryptedKey), nil } +func (ks *Server) encryptWithSTACKITKms(key *STACKITKmsKey, plaintext []byte) ([]byte, error) { + stackitKmsKey, err := stackitkms.NewMasterKey(key.ResourceId) + if err != nil { + return nil, err + } + err = stackitKmsKey.Encrypt(plaintext) + if err != nil { + return nil, err + } + return []byte(stackitKmsKey.EncryptedKey), nil +} + func (ks *Server) encryptWithAge(key *AgeKey, plaintext []byte) ([]byte, error) { ageKey := age.MasterKey{ Recipient: key.Recipient, @@ -158,6 +171,19 @@ func (ks *Server) decryptWithVault(key *VaultKey, ciphertext []byte) ([]byte, er return []byte(plaintext), err } +func (ks *Server) decryptWithSTACKITKms(key *STACKITKmsKey, ciphertext []byte) ([]byte, error) { + stackitKmsKey, err := stackitkms.NewMasterKey(key.ResourceId) + if err != nil { + return nil, err + } + stackitKmsKey.EncryptedKey = string(ciphertext) + plaintext, err := stackitKmsKey.Decrypt() + if err != nil { + return nil, err + } + return plaintext, nil +} + func (ks *Server) decryptWithAge(key *AgeKey, ciphertext []byte) ([]byte, error) { ageKey := age.MasterKey{ Recipient: key.Recipient, @@ -230,6 +256,14 @@ func (ks Server) Encrypt(ctx context.Context, response = &EncryptResponse{ Ciphertext: ciphertext, } + case *Key_STACKITKmsKey: + ciphertext, err := ks.encryptWithSTACKITKms(k.STACKITKmsKey, req.Plaintext) + if err != nil { + return nil, err + } + response = &EncryptResponse{ + Ciphertext: ciphertext, + } case nil: return nil, status.Errorf(codes.NotFound, "Must provide a key") default: @@ -258,6 +292,8 @@ func keyToString(key *Key) string { return fmt.Sprintf("Hashicorp Vault key with URI %s/v1/%s/keys/%s", k.VaultKey.VaultAddress, k.VaultKey.EnginePath, k.VaultKey.KeyName) case *Key_HckmsKey: return fmt.Sprintf("HuaweiCloud KMS key with ID %s", k.HckmsKey.KeyId) + case *Key_STACKITKmsKey: + return fmt.Sprintf("STACKIT KMS key with resource ID %s", k.STACKITKmsKey.ResourceId) default: return "Unknown key type" } @@ -342,6 +378,14 @@ func (ks Server) Decrypt(ctx context.Context, response = &DecryptResponse{ Plaintext: plaintext, } + case *Key_STACKITKmsKey: + plaintext, err := ks.decryptWithSTACKITKms(k.STACKITKmsKey, req.Ciphertext) + if err != nil { + return nil, err + } + response = &DecryptResponse{ + Plaintext: plaintext, + } case nil: return nil, status.Errorf(codes.NotFound, "Must provide a key") default: diff --git a/keyservice/stackitkms.go b/keyservice/stackitkms.go new file mode 100644 index 0000000000..5298d9caf7 --- /dev/null +++ b/keyservice/stackitkms.go @@ -0,0 +1,30 @@ +package keyservice + +// STACKITKmsKey is used to serialize a STACKIT KMS key over the keyservice protocol. +// This is defined separately from the protobuf-generated types to avoid +// requiring protobuf regeneration. It follows the same pattern as other key types. +type STACKITKmsKey struct { + ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` +} + +func (x *STACKITKmsKey) GetResourceId() string { + if x != nil { + return x.ResourceId + } + return "" +} + +// Key_STACKITKmsKey is a wrapper for STACKITKmsKey to be used in Key.KeyType oneof. +type Key_STACKITKmsKey struct { + STACKITKmsKey *STACKITKmsKey `protobuf:"bytes,8,opt,name=stackit_kms_key,json=stackitKmsKey,proto3,oneof"` +} + +func (*Key_STACKITKmsKey) isKey_KeyType() {} + +// GetSTACKITKmsKey returns the STACKITKmsKey from the Key, if set. +func (x *Key) GetSTACKITKmsKey() *STACKITKmsKey { + if x, ok := x.GetKeyType().(*Key_STACKITKmsKey); ok { + return x.STACKITKmsKey + } + return nil +} diff --git a/stackitkms/keysource.go b/stackitkms/keysource.go new file mode 100644 index 0000000000..97e1fe04f7 --- /dev/null +++ b/stackitkms/keysource.go @@ -0,0 +1,274 @@ +/* +Package stackitkms contains an implementation of the github.com/getsops/sops/v3/keys.MasterKey +interface that encrypts and decrypts the data key using STACKIT KMS. +*/ +package stackitkms // import "github.com/getsops/sops/v3/stackitkms" + +import ( + "context" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" + + "github.com/sirupsen/logrus" + stackitconfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/kms" + + "github.com/getsops/sops/v3/logging" +) + +const ( + // KeyTypeIdentifier is the string used to identify a STACKIT KMS MasterKey. + KeyTypeIdentifier = "stackit_kms" + // stackitKmsTTL is the duration after which a MasterKey requires rotation. + stackitKmsTTL = time.Hour * 24 * 30 * 6 +) + +var ( + // log is the global logger for any STACKIT KMS MasterKey. + log *logrus.Logger +) + +func init() { + log = logging.NewLogger("STACKITKMS") +} + +// MasterKey is a STACKIT KMS key used to encrypt and decrypt SOPS' data key. +type MasterKey struct { + // ResourceID is the full resource identifier in format: + // projects//regions//keyRings//keys//versions/ + ResourceID string + // ProjectID is the STACKIT project UUID. + ProjectID string + // RegionID is the STACKIT region (e.g., "eu01"). + RegionID string + // KeyRingID is the key ring UUID. + KeyRingID string + // KeyID is the key UUID. + KeyID string + // VersionNumber is the key version number. + VersionNumber int64 + // EncryptedKey stores the data key in its encrypted form. + EncryptedKey string + // CreationDate is when this MasterKey was created. + CreationDate time.Time + + // configOpts holds STACKIT SDK configuration options. + // They can be injected by a (local) keyservice.KeyServiceServer using + // Credentials.ApplyToMasterKey. + // If nil, the default credential provider chain is used. + configOpts []stackitconfig.ConfigurationOption +} + +// NewMasterKey creates a new MasterKey from a resource ID string, setting +// the creation date to the current date. +func NewMasterKey(resourceID string) (*MasterKey, error) { + projectID, regionID, keyRingID, keyID, versionNumber, err := parseResourceID(resourceID) + if err != nil { + return nil, err + } + return &MasterKey{ + ResourceID: resourceID, + ProjectID: projectID, + RegionID: regionID, + KeyRingID: keyRingID, + KeyID: keyID, + VersionNumber: versionNumber, + CreationDate: time.Now().UTC(), + }, nil +} + +// NewMasterKeyFromResourceIDString takes a comma separated list of STACKIT KMS +// resource IDs and returns a slice of new MasterKeys. +func NewMasterKeyFromResourceIDString(resourceID string) ([]*MasterKey, error) { + var keys []*MasterKey + if resourceID == "" { + return keys, nil + } + for _, s := range strings.Split(resourceID, ",") { + s = strings.TrimSpace(s) + if s == "" { + continue + } + k, err := NewMasterKey(s) + if err != nil { + return nil, err + } + keys = append(keys, k) + } + return keys, nil +} + +// parseResourceID parses a resource ID in format: +// projects//regions//keyRings//keys//versions/ +func parseResourceID(resourceID string) (projectID, regionID, keyRingID, keyID string, versionNumber int64, err error) { + resourceID = strings.TrimSpace(resourceID) + parts := strings.Split(resourceID, "/") + if len(parts) != 10 { + return "", "", "", "", 0, fmt.Errorf("invalid STACKIT KMS resource ID format: expected 'projects//regions//keyRings//keys//versions/', got %q", resourceID) + } + if parts[0] != "projects" || parts[2] != "regions" || parts[4] != "keyRings" || parts[6] != "keys" || parts[8] != "versions" { + return "", "", "", "", 0, fmt.Errorf("invalid STACKIT KMS resource ID format: expected 'projects//regions//keyRings//keys//versions/', got %q", resourceID) + } + projectID = parts[1] + regionID = parts[3] + keyRingID = parts[5] + keyID = parts[7] + versionNumber, err = strconv.ParseInt(parts[9], 10, 64) + if err != nil { + return "", "", "", "", 0, fmt.Errorf("invalid version number in STACKIT KMS resource ID %q: %w", resourceID, err) + } + if projectID == "" || regionID == "" || keyRingID == "" || keyID == "" { + return "", "", "", "", 0, fmt.Errorf("all components must be non-empty in STACKIT KMS resource ID %q", resourceID) + } + return projectID, regionID, keyRingID, keyID, versionNumber, nil +} + +// Credentials is a wrapper around STACKIT SDK configuration options used +// for authentication towards STACKIT KMS. +type Credentials struct { + opts []stackitconfig.ConfigurationOption +} + +// NewCredentials returns a Credentials object with the provided configuration options. +func NewCredentials(opts ...stackitconfig.ConfigurationOption) *Credentials { + return &Credentials{opts: opts} +} + +// ApplyToMasterKey configures the credentials on the provided key. +func (c Credentials) ApplyToMasterKey(key *MasterKey) { + key.configOpts = c.opts +} + +// Encrypt takes a SOPS data key, encrypts it with STACKIT KMS and stores the result +// in the EncryptedKey field. +func (key *MasterKey) Encrypt(dataKey []byte) error { + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext takes a SOPS data key, encrypts it with STACKIT KMS and stores the result +// in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { + client, err := key.createKMSClient() + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Encryption failed") + return fmt.Errorf("failed to create STACKIT KMS client: %w", err) + } + + plaintext := base64.StdEncoding.EncodeToString(dataKey) + plaintextBytes := []byte(plaintext) + + result, err := client.Encrypt(ctx, key.ProjectID, key.RegionID, key.KeyRingID, key.KeyID, key.VersionNumber). + EncryptPayload(kms.EncryptPayload{ + Data: &plaintextBytes, + }).Execute() + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Encryption failed") + return fmt.Errorf("failed to encrypt sops data key with STACKIT KMS: %w", err) + } + + encryptedData := result.GetData() + if len(encryptedData) == 0 { + return fmt.Errorf("encryption response missing ciphertext") + } + key.EncryptedKey = base64.StdEncoding.EncodeToString(encryptedData) + log.WithField("resourceID", key.ResourceID).Info("Encryption succeeded") + return nil +} + +// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been +// encrypted yet. +func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { + if key.EncryptedKey == "" { + return key.Encrypt(dataKey) + } + return nil +} + +// EncryptedDataKey returns the encrypted data key this master key holds. +func (key *MasterKey) EncryptedDataKey() []byte { + return []byte(key.EncryptedKey) +} + +// SetEncryptedDataKey sets the encrypted data key for this master key. +func (key *MasterKey) SetEncryptedDataKey(enc []byte) { + key.EncryptedKey = string(enc) +} + +// Decrypt decrypts the EncryptedKey with STACKIT KMS and returns the result. +func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey with STACKIT KMS and returns the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { + client, err := key.createKMSClient() + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Decryption failed") + return nil, fmt.Errorf("failed to create STACKIT KMS client: %w", err) + } + + encryptedBytes, err := base64.StdEncoding.DecodeString(key.EncryptedKey) + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Decryption failed") + return nil, fmt.Errorf("failed to base64 decode encrypted data key: %w", err) + } + + result, err := client.Decrypt(ctx, key.ProjectID, key.RegionID, key.KeyRingID, key.KeyID, key.VersionNumber). + DecryptPayload(kms.DecryptPayload{ + Data: &encryptedBytes, + }).Execute() + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Decryption failed") + return nil, fmt.Errorf("failed to decrypt sops data key with STACKIT KMS: %w", err) + } + + decryptedData := result.GetData() + if len(decryptedData) == 0 { + return nil, fmt.Errorf("decryption response missing plaintext") + } + + plaintext, err := base64.StdEncoding.DecodeString(string(decryptedData)) + if err != nil { + log.WithField("resourceID", key.ResourceID).Info("Decryption failed") + return nil, fmt.Errorf("failed to base64 decode decrypted data key: %w", err) + } + + log.WithField("resourceID", key.ResourceID).Info("Decryption succeeded") + return plaintext, nil +} + +// NeedsRotation returns whether the data key needs to be rotated or not. +func (key *MasterKey) NeedsRotation() bool { + return time.Since(key.CreationDate) > stackitKmsTTL +} + +// ToString converts the key to a string representation. +func (key *MasterKey) ToString() string { + return key.ResourceID +} + +// ToMap converts the MasterKey to a map for serialization purposes. +func (key MasterKey) ToMap() map[string]interface{} { + out := make(map[string]interface{}) + out["resource_id"] = key.ResourceID + out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) + out["enc"] = key.EncryptedKey + return out +} + +// TypeToIdentifier returns the string identifier for the MasterKey type. +func (key *MasterKey) TypeToIdentifier() string { + return KeyTypeIdentifier +} + +// createKMSClient creates a STACKIT KMS client with the appropriate credentials. +func (key *MasterKey) createKMSClient() (*kms.APIClient, error) { + client, err := kms.NewAPIClient(key.configOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create STACKIT KMS API client: %w", err) + } + return client, nil +} diff --git a/stackitkms/keysource_test.go b/stackitkms/keysource_test.go new file mode 100644 index 0000000000..ae530db969 --- /dev/null +++ b/stackitkms/keysource_test.go @@ -0,0 +1,188 @@ +package stackitkms + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const ( + testResourceID1 = "projects/test-project-id/regions/eu01/keyRings/test-keyring-id/keys/test-key-id/versions/1" + testResourceID2 = "projects/test-project-id/regions/eu01/keyRings/test-keyring-id/keys/test-key-id-2/versions/2" +) + +func TestNewMasterKey(t *testing.T) { + tests := []struct { + name string + resID string + expectErr bool + expectKey MasterKey + }{ + { + name: "valid resource ID", + resID: testResourceID1, + expectKey: MasterKey{ + ResourceID: testResourceID1, + ProjectID: "test-project-id", + RegionID: "eu01", + KeyRingID: "test-keyring-id", + KeyID: "test-key-id", + VersionNumber: 1, + }, + }, + { + name: "invalid format - too few parts", + resID: "projects/foo/regions/bar", + expectErr: true, + }, + { + name: "invalid format - wrong prefix", + resID: "proj/foo/regions/bar/keyRings/baz/keys/qux/versions/1", + expectErr: true, + }, + { + name: "invalid format - bad version number", + resID: "projects/foo/regions/bar/keyRings/baz/keys/qux/versions/abc", + expectErr: true, + }, + { + name: "invalid format - empty project", + resID: "projects//regions/bar/keyRings/baz/keys/qux/versions/1", + expectErr: true, + }, + { + name: "invalid format - empty string", + resID: "", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := NewMasterKey(tt.resID) + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, key) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expectKey.ResourceID, key.ResourceID) + assert.Equal(t, tt.expectKey.ProjectID, key.ProjectID) + assert.Equal(t, tt.expectKey.RegionID, key.RegionID) + assert.Equal(t, tt.expectKey.KeyRingID, key.KeyRingID) + assert.Equal(t, tt.expectKey.KeyID, key.KeyID) + assert.Equal(t, tt.expectKey.VersionNumber, key.VersionNumber) + assert.NotNil(t, key.CreationDate) + }) + } +} + +func TestNewMasterKeyFromResourceIDString(t *testing.T) { + tests := []struct { + name string + resIDString string + expectErr bool + expectKeyCount int + }{ + { + name: "single resource ID", + resIDString: testResourceID1, + expectKeyCount: 1, + }, + { + name: "multiple resource IDs", + resIDString: testResourceID1 + "," + testResourceID2, + expectKeyCount: 2, + }, + { + name: "multiple with spaces", + resIDString: " " + testResourceID1 + " , " + testResourceID2 + " ", + expectKeyCount: 2, + }, + { + name: "empty string", + resIDString: "", + expectKeyCount: 0, + }, + { + name: "invalid resource ID in list", + resIDString: testResourceID1 + ",invalid-id", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keys, err := NewMasterKeyFromResourceIDString(tt.resIDString) + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, keys) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expectKeyCount, len(keys)) + }) + } +} + +func TestMasterKey_ToString(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + assert.Equal(t, testResourceID1, key.ToString()) +} + +func TestMasterKey_ToMap(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + key.EncryptedKey = "test-encrypted-key" + key.CreationDate = time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + + m := key.ToMap() + assert.Equal(t, testResourceID1, m["resource_id"]) + assert.Equal(t, "test-encrypted-key", m["enc"]) + assert.Equal(t, "2025-01-01T12:00:00Z", m["created_at"]) +} + +func TestMasterKey_TypeToIdentifier(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + assert.Equal(t, KeyTypeIdentifier, key.TypeToIdentifier()) +} + +func TestMasterKey_NeedsRotation(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + + // New key should not need rotation + assert.False(t, key.NeedsRotation()) + + // Key older than TTL should need rotation + key.CreationDate = time.Now().UTC().Add(-stackitKmsTTL - time.Hour) + assert.True(t, key.NeedsRotation()) +} + +func TestMasterKey_EncryptedDataKey(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + key.EncryptedKey = "test-encrypted-data" + assert.Equal(t, []byte("test-encrypted-data"), key.EncryptedDataKey()) +} + +func TestMasterKey_SetEncryptedDataKey(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + key.SetEncryptedDataKey([]byte("test-encrypted-data")) + assert.Equal(t, "test-encrypted-data", key.EncryptedKey) +} + +func TestMasterKey_EncryptIfNeeded(t *testing.T) { + key, err := NewMasterKey(testResourceID1) + assert.NoError(t, err) + + // Key with encrypted data should not attempt encryption + key.EncryptedKey = "already-encrypted" + err = key.EncryptIfNeeded([]byte("test-data")) + assert.NoError(t, err) + assert.Equal(t, "already-encrypted", key.EncryptedKey) +} diff --git a/stores/stores.go b/stores/stores.go index b1b496dc76..82377b7f07 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -23,6 +23,7 @@ import ( "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/pgp" + "github.com/getsops/sops/v3/stackitkms" ) const ( @@ -35,35 +36,37 @@ const ( // in order to allow the binary format to stay backwards compatible over time, but at the same time allow the internal // representation SOPS uses to change over time. type metadata struct { - ShamirThreshold int `mapstructure:"shamir_threshold,omitempty"` - KeyGroups []keygroup `mapstructure:"key_groups,omitempty,deep"` - KMSKeys []kmskey `mapstructure:"kms,omitempty,deep"` - GCPKMSKeys []gcpkmskey `mapstructure:"gcp_kms,omitempty,deep"` - HCKmsKeys []hckmskey `mapstructure:"hckms,omitempty,deep"` - AzureKeyVaultKeys []azkvkey `mapstructure:"azure_kv,omitempty,deep"` - VaultKeys []vaultkey `mapstructure:"hc_vault,omitempty,deep"` - AgeKeys []agekey `mapstructure:"age,omitempty,deep"` - LastModified string `mapstructure:"lastmodified"` - MessageAuthenticationCode string `mapstructure:"mac"` - PGPKeys []pgpkey `mapstructure:"pgp,omitempty,deep"` - UnencryptedSuffix string `mapstructure:"unencrypted_suffix,omitempty"` - EncryptedSuffix string `mapstructure:"encrypted_suffix,omitempty"` - UnencryptedRegex string `mapstructure:"unencrypted_regex,omitempty"` - EncryptedRegex string `mapstructure:"encrypted_regex,omitempty"` - UnencryptedCommentRegex string `mapstructure:"unencrypted_comment_regex,omitempty"` - EncryptedCommentRegex string `mapstructure:"encrypted_comment_regex,omitempty"` - MACOnlyEncrypted bool `mapstructure:"mac_only_encrypted,omitempty"` - Version string `mapstructure:"version"` + ShamirThreshold int `mapstructure:"shamir_threshold,omitempty"` + KeyGroups []keygroup `mapstructure:"key_groups,omitempty,deep"` + KMSKeys []kmskey `mapstructure:"kms,omitempty,deep"` + GCPKMSKeys []gcpkmskey `mapstructure:"gcp_kms,omitempty,deep"` + HCKmsKeys []hckmskey `mapstructure:"hckms,omitempty,deep"` + STACKITKmsKeys []stackitkmskey `mapstructure:"stackit_kms,omitempty,deep"` + AzureKeyVaultKeys []azkvkey `mapstructure:"azure_kv,omitempty,deep"` + VaultKeys []vaultkey `mapstructure:"hc_vault,omitempty,deep"` + AgeKeys []agekey `mapstructure:"age,omitempty,deep"` + LastModified string `mapstructure:"lastmodified"` + MessageAuthenticationCode string `mapstructure:"mac"` + PGPKeys []pgpkey `mapstructure:"pgp,omitempty,deep"` + UnencryptedSuffix string `mapstructure:"unencrypted_suffix,omitempty"` + EncryptedSuffix string `mapstructure:"encrypted_suffix,omitempty"` + UnencryptedRegex string `mapstructure:"unencrypted_regex,omitempty"` + EncryptedRegex string `mapstructure:"encrypted_regex,omitempty"` + UnencryptedCommentRegex string `mapstructure:"unencrypted_comment_regex,omitempty"` + EncryptedCommentRegex string `mapstructure:"encrypted_comment_regex,omitempty"` + MACOnlyEncrypted bool `mapstructure:"mac_only_encrypted,omitempty"` + Version string `mapstructure:"version"` } type keygroup struct { - PGPKeys []pgpkey `mapstructure:"pgp,omitempty,deep"` - KMSKeys []kmskey `mapstructure:"kms,omitempty,deep"` - GCPKMSKeys []gcpkmskey `mapstructure:"gcp_kms,omitempty,deep"` - HCKmsKeys []hckmskey `mapstructure:"hckms,omitempty,deep"` - AzureKeyVaultKeys []azkvkey `mapstructure:"azure_kv,omitempty,deep"` - VaultKeys []vaultkey `mapstructure:"hc_vault,deep"` - AgeKeys []agekey `mapstructure:"age,deep"` + PGPKeys []pgpkey `mapstructure:"pgp,omitempty,deep"` + KMSKeys []kmskey `mapstructure:"kms,omitempty,deep"` + GCPKMSKeys []gcpkmskey `mapstructure:"gcp_kms,omitempty,deep"` + HCKmsKeys []hckmskey `mapstructure:"hckms,omitempty,deep"` + STACKITKmsKeys []stackitkmskey `mapstructure:"stackit_kms,omitempty,deep"` + AzureKeyVaultKeys []azkvkey `mapstructure:"azure_kv,omitempty,deep"` + VaultKeys []vaultkey `mapstructure:"hc_vault,deep"` + AgeKeys []agekey `mapstructure:"age,deep"` } type pgpkey struct { @@ -114,6 +117,12 @@ type hckmskey struct { EncryptedDataKey string `mapstructure:"enc"` } +type stackitkmskey struct { + ResourceID string `mapstructure:"resource_id"` + CreatedAt string `mapstructure:"created_at"` + EncryptedDataKey string `mapstructure:"enc"` +} + // metadataFromInternal converts an internal SOPS metadata representation to a // representation appropriate for storage. func metadataFromInternal(sopsMetadata sops.Metadata) metadata { @@ -135,6 +144,7 @@ func metadataFromInternal(sopsMetadata sops.Metadata) metadata { m.KMSKeys = kmsKeysFromGroup(group) m.GCPKMSKeys = gcpkmsKeysFromGroup(group) m.HCKmsKeys = hckmsKeysFromGroup(group) + m.STACKITKmsKeys = stackitKmsKeysFromGroup(group) m.VaultKeys = vaultKeysFromGroup(group) m.AzureKeyVaultKeys = azkvKeysFromGroup(group) m.AgeKeys = ageKeysFromGroup(group) @@ -145,6 +155,7 @@ func metadataFromInternal(sopsMetadata sops.Metadata) metadata { PGPKeys: pgpKeysFromGroup(group), GCPKMSKeys: gcpkmsKeysFromGroup(group), HCKmsKeys: hckmsKeysFromGroup(group), + STACKITKmsKeys: stackitKmsKeysFromGroup(group), VaultKeys: vaultKeysFromGroup(group), AzureKeyVaultKeys: azkvKeysFromGroup(group), AgeKeys: ageKeysFromGroup(group), @@ -258,6 +269,20 @@ func hckmsKeysFromGroup(group sops.KeyGroup) (keys []hckmskey) { return } +func stackitKmsKeysFromGroup(group sops.KeyGroup) (keys []stackitkmskey) { + for _, key := range group { + switch key := key.(type) { + case *stackitkms.MasterKey: + keys = append(keys, stackitkmskey{ + ResourceID: key.ResourceID, + CreatedAt: key.CreationDate.Format(time.RFC3339), + EncryptedDataKey: key.EncryptedKey, + }) + } + } + return +} + // ToInternal converts a storage-appropriate Metadata struct to a SOPS internal representation func (m *metadata) ToInternal() (sops.Metadata, error) { lastModified, err := time.Parse(time.RFC3339, m.LastModified) @@ -312,7 +337,7 @@ func (m *metadata) ToInternal() (sops.Metadata, error) { }, nil } -func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmskey, hckmsKeys []hckmskey, azkvKeys []azkvkey, vaultKeys []vaultkey, ageKeys []agekey) (sops.KeyGroup, error) { +func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmskey, hckmsKeys []hckmskey, stackitKmsKeys []stackitkmskey, azkvKeys []azkvkey, vaultKeys []vaultkey, ageKeys []agekey) (sops.KeyGroup, error) { var internalGroup sops.KeyGroup for _, kmsKey := range kmsKeys { k, err := kmsKey.toInternal() @@ -335,6 +360,13 @@ func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmske } internalGroup = append(internalGroup, k) } + for _, stackitKmsKey := range stackitKmsKeys { + k, err := stackitKmsKey.toInternal() + if err != nil { + return nil, err + } + internalGroup = append(internalGroup, k) + } for _, azkvKey := range azkvKeys { k, err := azkvKey.toInternal() if err != nil { @@ -368,8 +400,8 @@ func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmske func (m *metadata) internalKeygroups() ([]sops.KeyGroup, error) { var internalGroups []sops.KeyGroup - if len(m.PGPKeys) > 0 || len(m.KMSKeys) > 0 || len(m.GCPKMSKeys) > 0 || len(m.HCKmsKeys) > 0 || len(m.AzureKeyVaultKeys) > 0 || len(m.VaultKeys) > 0 || len(m.AgeKeys) > 0 { - internalGroup, err := internalGroupFrom(m.KMSKeys, m.PGPKeys, m.GCPKMSKeys, m.HCKmsKeys, m.AzureKeyVaultKeys, m.VaultKeys, m.AgeKeys) + if len(m.PGPKeys) > 0 || len(m.KMSKeys) > 0 || len(m.GCPKMSKeys) > 0 || len(m.HCKmsKeys) > 0 || len(m.STACKITKmsKeys) > 0 || len(m.AzureKeyVaultKeys) > 0 || len(m.VaultKeys) > 0 || len(m.AgeKeys) > 0 { + internalGroup, err := internalGroupFrom(m.KMSKeys, m.PGPKeys, m.GCPKMSKeys, m.HCKmsKeys, m.STACKITKmsKeys, m.AzureKeyVaultKeys, m.VaultKeys, m.AgeKeys) if err != nil { return nil, err } @@ -377,7 +409,7 @@ func (m *metadata) internalKeygroups() ([]sops.KeyGroup, error) { return internalGroups, nil } else if len(m.KeyGroups) > 0 { for _, group := range m.KeyGroups { - internalGroup, err := internalGroupFrom(group.KMSKeys, group.PGPKeys, group.GCPKMSKeys, group.HCKmsKeys, group.AzureKeyVaultKeys, group.VaultKeys, group.AgeKeys) + internalGroup, err := internalGroupFrom(group.KMSKeys, group.PGPKeys, group.GCPKMSKeys, group.HCKmsKeys, group.STACKITKmsKeys, group.AzureKeyVaultKeys, group.VaultKeys, group.AgeKeys) if err != nil { return nil, err } @@ -477,6 +509,20 @@ func (hckmsKey *hckmskey) toInternal() (*hckms.MasterKey, error) { return key, nil } +func (stackitKmsKey *stackitkmskey) toInternal() (*stackitkms.MasterKey, error) { + creationDate, err := time.Parse(time.RFC3339, stackitKmsKey.CreatedAt) + if err != nil { + return nil, err + } + key, err := stackitkms.NewMasterKey(stackitKmsKey.ResourceID) + if err != nil { + return nil, err + } + key.EncryptedKey = stackitKmsKey.EncryptedDataKey + key.CreationDate = creationDate + return key, nil +} + // ExampleComplexTree is an example sops.Tree object exhibiting complex relationships var ExampleComplexTree = sops.Tree{ Branches: sops.TreeBranches{