From 34d234996ac072ad55fe7d622c4b2a4bd7df0f22 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Sat, 28 Dec 2024 05:04:25 +0100 Subject: [PATCH 01/12] feat: OCI KMS provider Signed-off-by: Alessandro De Blasis --- .gitignore | 1 + README.rst | 71 ++++++++- cmd/sops/main.go | 33 +++- config/config.go | 12 ++ go.mod | 3 + go.sum | 8 + keyservice/keyservice.go | 10 ++ keyservice/keyservice.pb.go | 249 ++++++++++++++++++++----------- keyservice/keyservice.proto | 5 + keyservice/keyservice_grpc.pb.go | 2 +- keyservice/server.go | 39 +++++ ocikms/keysource.go | 191 ++++++++++++++++++++++++ ocikms/keysource_test.go | 36 +++++ stores/stores.go | 52 ++++++- 14 files changed, 602 insertions(+), 110 deletions(-) create mode 100644 ocikms/keysource.go create mode 100644 ocikms/keysource_test.go diff --git a/.gitignore b/.gitignore index 5eb4a860c2..eb11be7ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ dist/ functional-tests/sops +functional-tests/target vendor/ profile.out diff --git a/README.rst b/README.rst index a4a5e7b654..074dc08efc 100644 --- a/README.rst +++ b/README.rst @@ -255,7 +255,7 @@ It is also possible to use ``updatekeys``, when adding or removing age recipient +++ age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw Is this okay? (y/n):y 2022/02/09 16:32:04 File /iac/solution1/secret.enc.yaml synced with new keys - + Encrypting using GCP KMS ~~~~~~~~~~~~~~~~~~~~~~~~ GCP KMS uses `Application Default Credentials @@ -418,7 +418,7 @@ Encrypting using Hashicorp Vault We assume you have an instance (or more) of Vault running and you have privileged access to it. For instructions on how to deploy a secure instance of Vault, refer to Hashicorp's official documentation. -To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!) +To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!) .. code:: sh @@ -428,11 +428,11 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!) .. code:: sh $ # Substitute this with the address Vault is running on - $ export VAULT_ADDR=http://127.0.0.1:8200 + $ export VAULT_ADDR=http://127.0.0.1:8200 $ # this may not be necessary in case you previously used `vault login` for production use - $ export VAULT_TOKEN=toor - + $ export VAULT_TOKEN=toor + $ # to check if Vault started and is configured correctly $ vault status Key Value @@ -471,7 +471,62 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!) hc_vault_transit_uri: "$VAULT_ADDR/v1/sops/keys/thirdkey" EOF - $ sops encrypt --verbose prod/raw.yaml > prod/encrypted.yaml + $ sops --verbose -e prod/raw.yaml > prod/encrypted.yaml + +Encrypting using OCI KMS +~~~~~~~~~~~~~~~~~~~~~~~~ + +OCI KMS uses the `DefaultConfigProvider `_. +It will look for the `DEFAULT` profile in the `~/.oci/config` file. + +Make sure to authenticate and to have a valid session via: + +.. code:: bash + + $ oci session authenticate + +Encrypting/decrypting with OCI KMS requires a KMS OCID. You can use the +cloud console the get the OCID of an existing key or you can create one using the `oci` +CLI: + +.. code:: bash + + $ export compartment_id= + $ export display_name= + $ export vault_type= + $ OCI_CLI_AUTH=security_token oci kms management vault create --compartment-id $compartment_id --display-name $display_name --vault-type $vault_type + # you should see a JSON summarizing the created resource + # for help: https://docs.cloud.oracle.com/en-us/iaas/tools/oci-cli/latest/oci_cli_docs/cmdref/kms/management/vault/create.html + +Now we need to create a key. First of all we need to define a shape for it with: + +.. code:: bash + + $ cat << EOF > key-shape.json + { + "algorithm": "AES", + "length": 32 + } + EOF + +Now we can create the key with + +.. code:: bash + + $ export compartment_id= + $ export display_name= + # you can grab the endpoint from the vault page on the portal, it should be something like: https://asdadsasdagz5aacmg-management.kms..oraclecloud.com + $ OCI_CLI_AUTH=security_token oci kms management key create --compartment-id $compartment_id --display-name $display_name --endpoint --key-shape file://key-shape.json + # you should see a JSON summarizing the created resource, we need to grab the OCID of the key from it + # for help: https://docs.cloud.oracle.com/en-us/iaas/tools/oci-cli/latest/oci_cli_docs/cmdref/kms/management/key/create.html + +Now you can encrypt a file using:: + + $ sops --encrypt --oci-kms ocid1.key.oc1..asdadsasdagz5aacmg.abwgiljtjasdasdasdagugpfe7wrtngukihgkybqxcoozz7sbh6lq test.yaml > test.enc.yaml + +And decrypt it using:: + + $ sops --decrypt test.enc.yaml Adding and removing keys ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1596,8 +1651,8 @@ will encrypt the values under the ``data`` and ``stringData`` keys in a YAML fil containing kubernetes secrets. It will not encrypt other values that help you to navigate the file, like ``metadata`` which contains the secrets' names. -Conversely, you can opt in to only leave certain keys without encrypting by using the -``--unencrypted-regex`` option, which will leave the values unencrypted of those keys +Conversely, you can opt in to only leave certain keys without encrypting by using the +``--unencrypted-regex`` option, which will leave the values unencrypted of those keys that match the supplied regular expression. For example, this command: .. code:: sh diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 42883ff371..94204fdb2d 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -38,6 +38,7 @@ import ( "github.com/getsops/sops/v3/keyservice" "github.com/getsops/sops/v3/kms" "github.com/getsops/sops/v3/logging" + "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/stores/dotenv" "github.com/getsops/sops/v3/stores/json" @@ -1092,8 +1093,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-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-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-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || c.String("add-oci-kms") != "" || + c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" || c.String("rm-oci-kms") != "" { return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } } @@ -1554,6 +1555,11 @@ func main() { Usage: "comma separated list of age recipients", EnvVar: "SOPS_AGE_RECIPIENTS", }, + cli.StringFlag{ + Name: "oci-kms", + Usage: "comma separated list of OCI KMS OCIDs", + EnvVar: "SOPS_OCI_KMS_OCIDS", + }, cli.BoolFlag{ Name: "in-place, i", Usage: "write output back to the same file instead of stdout", @@ -1614,6 +1620,14 @@ func main() { Name: "rm-age", Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file", }, + cli.StringFlag{ + Name: "add-oci-kms", + Usage: "add the provided comma-separated list of OCI KMS keys OCIDs to the list of master keys on the given file", + }, + cli.StringFlag{ + Name: "rm-oci-kms", + Usage: "remove the provided comma-separated list of OCI KMS keys OCIDs from the list of master keys on the given file", + }, cli.StringFlag{ Name: "add-pgp", Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file", @@ -2004,7 +2018,7 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) { }, nil } -func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName 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, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string, ociOptionName 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) @@ -2041,11 +2055,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-azure-kv", "add-hc-vault-transit", "add-age") + addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-azure-kv", "add-hc-vault-transit", "add-age", "add-oci-kms") if err != nil { return rotateOpts{}, err } - rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age") + rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age", "rm-oci-kms") if err != nil { return rotateOpts{}, err } @@ -2180,6 +2194,7 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { var azkvKeys []keys.MasterKey var hcVaultMkKeys []keys.MasterKey var ageMasterKeys []keys.MasterKey + var ociMasterKeys []keys.MasterKey kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context")) if c.String("encryption-context") != "" && kmsEncryptionContext == nil { return nil, common.NewExitError("Invalid KMS encryption context format", codes.ErrorInvalidKMSEncryptionContextFormat) @@ -2226,7 +2241,12 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { ageMasterKeys = append(ageMasterKeys, k) } } - if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" { + if c.String("oci-kms") != "" { + for _, k := range ocikms.MasterKeysFromOCIDString(c.String("oci-kms")) { + ociMasterKeys = append(ociMasterKeys, k) + } + } + if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" && c.String("oci-kms") == "" { conf, err := loadConfig(c, file, kmsEncryptionContext) // config file might just not be supplied, without any error if conf == nil { @@ -2245,6 +2265,7 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { group = append(group, pgpKeys...) group = append(group, hcVaultMkKeys...) group = append(group, ageMasterKeys...) + group = append(group, ociMasterKeys...) log.Debugf("Master keys available: %+v", group) return []sops.KeyGroup{group}, nil } diff --git a/config/config.go b/config/config.go index 8d3dc4dd19..0a55017204 100644 --- a/config/config.go +++ b/config/config.go @@ -17,6 +17,7 @@ import ( "github.com/getsops/sops/v3/gcpkms" "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/publish" "gopkg.in/yaml.v3" @@ -92,6 +93,7 @@ type keyGroup struct { AzureKV []azureKVKey `yaml:"azure_keyvault"` Vault []string `yaml:"hc_vault"` Age []string `yaml:"age"` + OCIKMS []string `yaml:"oci_kms"` PGP []string } @@ -131,6 +133,7 @@ type creationRule struct { KMS string AwsProfile string `yaml:"aws_profile"` Age string `yaml:"age"` + OCIKMS string `yaml:"oci_kms"` PGP string GCPKMS string `yaml:"gcp_kms"` AzureKeyVault string `yaml:"azure_keyvault"` @@ -214,6 +217,9 @@ func extractMasterKeys(group keyGroup) (sops.KeyGroup, error) { keyGroup = append(keyGroup, key) } } + for _, k := range group.OCIKMS { + keyGroup = append(keyGroup, ocikms.NewMasterKeyFromOCID(k)) + } for _, k := range group.PGP { keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k)) } @@ -244,6 +250,9 @@ func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[ if err != nil { return nil, err } + for _, k := range group.OCIKMS { + keyGroup = append(keyGroup, ocikms.NewMasterKeyFromOCID(k)) + } groups = append(groups, keyGroup) } } else { @@ -267,6 +276,9 @@ func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[ for _, k := range gcpkms.MasterKeysFromResourceIDString(cRule.GCPKMS) { keyGroup = append(keyGroup, k) } + for _, k := range ocikms.MasterKeysFromOCIDString(cRule.OCIKMS) { + keyGroup = append(keyGroup, k) + } azureKeys, err := azkv.MasterKeysFromURLs(cRule.AzureKeyVault) if err != nil { return nil, err diff --git a/go.mod b/go.mod index f45dd3817e..b2dc35efd4 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/lib/pq v1.10.9 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 + github.com/oracle/oci-go-sdk/v65 v65.81.1 github.com/ory/dockertest/v3 v3.11.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 @@ -97,6 +98,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -127,6 +129,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index 77f45ecf28..81aa286add 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -240,6 +242,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/oracle/oci-go-sdk/v65 v65.81.1 h1:JYc47bk8n/MUchA2KHu1ggsCQzlJZQLJ+tTKfOho00E= +github.com/oracle/oci-go-sdk/v65 v65.81.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -260,9 +264,12 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 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= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -333,6 +340,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= diff --git a/keyservice/keyservice.go b/keyservice/keyservice.go index 321af79420..13d884be8a 100644 --- a/keyservice/keyservice.go +++ b/keyservice/keyservice.go @@ -13,6 +13,7 @@ import ( "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/keys" "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" ) @@ -78,6 +79,15 @@ func KeyFromMasterKey(mk keys.MasterKey) Key { }, }, } + case *ocikms.MasterKey: + return Key{ + KeyType: &Key_OciKey{ + OciKey: &OciKey{ + Ocid: mk.Ocid, + }, + }, + } + default: panic(fmt.Sprintf("Tried to convert unknown MasterKey type %T to keyservice.Key", mk)) } diff --git a/keyservice/keyservice.pb.go b/keyservice/keyservice.pb.go index a810b28053..8be514d9c3 100644 --- a/keyservice/keyservice.pb.go +++ b/keyservice/keyservice.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.2 -// protoc v5.28.3 +// protoc v5.29.2 // source: keyservice/keyservice.proto package keyservice @@ -33,6 +33,7 @@ type Key struct { // *Key_AzureKeyvaultKey // *Key_VaultKey // *Key_AgeKey + // *Key_OciKey KeyType isKey_KeyType `protobuf_oneof:"key_type"` } @@ -115,6 +116,13 @@ func (x *Key) GetAgeKey() *AgeKey { return nil } +func (x *Key) GetOciKey() *OciKey { + if x, ok := x.GetKeyType().(*Key_OciKey); ok { + return x.OciKey + } + return nil +} + type isKey_KeyType interface { isKey_KeyType() } @@ -143,6 +151,10 @@ type Key_AgeKey struct { AgeKey *AgeKey `protobuf:"bytes,6,opt,name=age_key,json=ageKey,proto3,oneof"` } +type Key_OciKey struct { + OciKey *OciKey `protobuf:"bytes,7,opt,name=oci_key,json=ociKey,proto3,oneof"` +} + func (*Key_KmsKey) isKey_KeyType() {} func (*Key_PgpKey) isKey_KeyType() {} @@ -155,6 +167,8 @@ func (*Key_VaultKey) isKey_KeyType() {} func (*Key_AgeKey) isKey_KeyType() {} +func (*Key_OciKey) isKey_KeyType() {} + type PgpKey struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -481,6 +495,51 @@ func (x *AgeKey) GetRecipient() string { return "" } +type OciKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ocid string `protobuf:"bytes,1,opt,name=ocid,proto3" json:"ocid,omitempty"` +} + +func (x *OciKey) Reset() { + *x = OciKey{} + mi := &file_keyservice_keyservice_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OciKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OciKey) ProtoMessage() {} + +func (x *OciKey) ProtoReflect() protoreflect.Message { + mi := &file_keyservice_keyservice_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OciKey.ProtoReflect.Descriptor instead. +func (*OciKey) Descriptor() ([]byte, []int) { + return file_keyservice_keyservice_proto_rawDescGZIP(), []int{7} +} + +func (x *OciKey) GetOcid() string { + if x != nil { + return x.Ocid + } + return "" +} + type EncryptRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -492,7 +551,7 @@ type EncryptRequest struct { func (x *EncryptRequest) Reset() { *x = EncryptRequest{} - mi := &file_keyservice_keyservice_proto_msgTypes[7] + mi := &file_keyservice_keyservice_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -504,7 +563,7 @@ func (x *EncryptRequest) String() string { func (*EncryptRequest) ProtoMessage() {} func (x *EncryptRequest) ProtoReflect() protoreflect.Message { - mi := &file_keyservice_keyservice_proto_msgTypes[7] + mi := &file_keyservice_keyservice_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -517,7 +576,7 @@ func (x *EncryptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EncryptRequest.ProtoReflect.Descriptor instead. func (*EncryptRequest) Descriptor() ([]byte, []int) { - return file_keyservice_keyservice_proto_rawDescGZIP(), []int{7} + return file_keyservice_keyservice_proto_rawDescGZIP(), []int{8} } func (x *EncryptRequest) GetKey() *Key { @@ -544,7 +603,7 @@ type EncryptResponse struct { func (x *EncryptResponse) Reset() { *x = EncryptResponse{} - mi := &file_keyservice_keyservice_proto_msgTypes[8] + mi := &file_keyservice_keyservice_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -556,7 +615,7 @@ func (x *EncryptResponse) String() string { func (*EncryptResponse) ProtoMessage() {} func (x *EncryptResponse) ProtoReflect() protoreflect.Message { - mi := &file_keyservice_keyservice_proto_msgTypes[8] + mi := &file_keyservice_keyservice_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -569,7 +628,7 @@ func (x *EncryptResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EncryptResponse.ProtoReflect.Descriptor instead. func (*EncryptResponse) Descriptor() ([]byte, []int) { - return file_keyservice_keyservice_proto_rawDescGZIP(), []int{8} + return file_keyservice_keyservice_proto_rawDescGZIP(), []int{9} } func (x *EncryptResponse) GetCiphertext() []byte { @@ -590,7 +649,7 @@ type DecryptRequest struct { func (x *DecryptRequest) Reset() { *x = DecryptRequest{} - mi := &file_keyservice_keyservice_proto_msgTypes[9] + mi := &file_keyservice_keyservice_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -602,7 +661,7 @@ func (x *DecryptRequest) String() string { func (*DecryptRequest) ProtoMessage() {} func (x *DecryptRequest) ProtoReflect() protoreflect.Message { - mi := &file_keyservice_keyservice_proto_msgTypes[9] + mi := &file_keyservice_keyservice_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -615,7 +674,7 @@ func (x *DecryptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DecryptRequest.ProtoReflect.Descriptor instead. func (*DecryptRequest) Descriptor() ([]byte, []int) { - return file_keyservice_keyservice_proto_rawDescGZIP(), []int{9} + return file_keyservice_keyservice_proto_rawDescGZIP(), []int{10} } func (x *DecryptRequest) GetKey() *Key { @@ -642,7 +701,7 @@ type DecryptResponse struct { func (x *DecryptResponse) Reset() { *x = DecryptResponse{} - mi := &file_keyservice_keyservice_proto_msgTypes[10] + mi := &file_keyservice_keyservice_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -654,7 +713,7 @@ func (x *DecryptResponse) String() string { func (*DecryptResponse) ProtoMessage() {} func (x *DecryptResponse) ProtoReflect() protoreflect.Message { - mi := &file_keyservice_keyservice_proto_msgTypes[10] + mi := &file_keyservice_keyservice_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -667,7 +726,7 @@ func (x *DecryptResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DecryptResponse.ProtoReflect.Descriptor instead. func (*DecryptResponse) Descriptor() ([]byte, []int) { - return file_keyservice_keyservice_proto_rawDescGZIP(), []int{10} + return file_keyservice_keyservice_proto_rawDescGZIP(), []int{11} } func (x *DecryptResponse) GetPlaintext() []byte { @@ -681,7 +740,7 @@ var File_keyservice_keyservice_proto protoreflect.FileDescriptor var file_keyservice_keyservice_proto_rawDesc = []byte{ 0x0a, 0x1b, 0x6b, 0x65, 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x6b, 0x65, 0x79, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x98, 0x02, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbc, 0x02, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x6b, 0x6d, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x70, 0x67, 0x70, @@ -698,64 +757,69 @@ var file_keyservice_keyservice_proto_rawDesc = []byte{ 0x0b, 0x32, 0x09, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x61, 0x67, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x41, 0x67, 0x65, 0x4b, - 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x61, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, - 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2a, 0x0a, 0x06, 0x50, 0x67, 0x70, 0x4b, - 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, - 0x72, 0x69, 0x6e, 0x74, 0x22, 0xbb, 0x01, 0x0a, 0x06, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x61, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x72, - 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x2e, - 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x77, 0x73, 0x5f, 0x70, 0x72, 0x6f, - 0x66, 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x77, 0x73, 0x50, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x09, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12, - 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, - 0x22, 0x6b, 0x0a, 0x08, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, - 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x50, 0x61, - 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5d, 0x0a, - 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, - 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x06, - 0x41, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, - 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, - 0x69, 0x65, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1c, - 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x31, 0x0a, 0x0f, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, - 0x48, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04, - 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, - 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, - 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, 0x0f, 0x44, 0x65, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, - 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x32, 0x6c, 0x0a, 0x0a, 0x4b, 0x65, - 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44, 0x65, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2f, 0x6b, 0x65, - 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x61, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, + 0x6f, 0x63, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, + 0x4f, 0x63, 0x69, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x6f, 0x63, 0x69, 0x4b, 0x65, 0x79, + 0x42, 0x0a, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2a, 0x0a, 0x06, + 0x50, 0x67, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, + 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x22, 0xbb, 0x01, 0x0a, 0x06, 0x4b, 0x6d, 0x73, + 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x61, 0x72, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x4b, 0x6d, 0x73, + 0x4b, 0x65, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x77, 0x73, + 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x61, 0x77, 0x73, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x09, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, + 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x08, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, + 0x12, 0x23, 0x0a, 0x0d, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5f, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x67, 0x69, + 0x6e, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0x5d, 0x0a, 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, + 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x55, + 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x22, 0x26, 0x0a, 0x06, 0x41, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, + 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, + 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x1c, 0x0a, 0x06, 0x4f, 0x63, 0x69, 0x4b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6f, 0x63, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6f, 0x63, 0x69, 0x64, 0x22, 0x46, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x31, + 0x0a, 0x0f, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, + 0x74, 0x22, 0x48, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, + 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, 0x0f, 0x44, + 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x32, 0x6c, 0x0a, 0x0a, + 0x4b, 0x65, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2f, + 0x6b, 0x65, 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -770,7 +834,7 @@ func file_keyservice_keyservice_proto_rawDescGZIP() []byte { return file_keyservice_keyservice_proto_rawDescData } -var file_keyservice_keyservice_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_keyservice_keyservice_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_keyservice_keyservice_proto_goTypes = []any{ (*Key)(nil), // 0: Key (*PgpKey)(nil), // 1: PgpKey @@ -779,11 +843,12 @@ var file_keyservice_keyservice_proto_goTypes = []any{ (*VaultKey)(nil), // 4: VaultKey (*AzureKeyVaultKey)(nil), // 5: AzureKeyVaultKey (*AgeKey)(nil), // 6: AgeKey - (*EncryptRequest)(nil), // 7: EncryptRequest - (*EncryptResponse)(nil), // 8: EncryptResponse - (*DecryptRequest)(nil), // 9: DecryptRequest - (*DecryptResponse)(nil), // 10: DecryptResponse - nil, // 11: KmsKey.ContextEntry + (*OciKey)(nil), // 7: OciKey + (*EncryptRequest)(nil), // 8: EncryptRequest + (*EncryptResponse)(nil), // 9: EncryptResponse + (*DecryptRequest)(nil), // 10: DecryptRequest + (*DecryptResponse)(nil), // 11: DecryptResponse + nil, // 12: KmsKey.ContextEntry } var file_keyservice_keyservice_proto_depIdxs = []int32{ 2, // 0: Key.kms_key:type_name -> KmsKey @@ -792,18 +857,19 @@ var file_keyservice_keyservice_proto_depIdxs = []int32{ 5, // 3: Key.azure_keyvault_key:type_name -> AzureKeyVaultKey 4, // 4: Key.vault_key:type_name -> VaultKey 6, // 5: Key.age_key:type_name -> AgeKey - 11, // 6: KmsKey.context:type_name -> KmsKey.ContextEntry - 0, // 7: EncryptRequest.key:type_name -> Key - 0, // 8: DecryptRequest.key:type_name -> Key - 7, // 9: KeyService.Encrypt:input_type -> EncryptRequest - 9, // 10: KeyService.Decrypt:input_type -> DecryptRequest - 8, // 11: KeyService.Encrypt:output_type -> EncryptResponse - 10, // 12: KeyService.Decrypt:output_type -> DecryptResponse - 11, // [11:13] is the sub-list for method output_type - 9, // [9:11] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 7, // 6: Key.oci_key:type_name -> OciKey + 12, // 7: KmsKey.context:type_name -> KmsKey.ContextEntry + 0, // 8: EncryptRequest.key:type_name -> Key + 0, // 9: DecryptRequest.key:type_name -> Key + 8, // 10: KeyService.Encrypt:input_type -> EncryptRequest + 10, // 11: KeyService.Decrypt:input_type -> DecryptRequest + 9, // 12: KeyService.Encrypt:output_type -> EncryptResponse + 11, // 13: KeyService.Decrypt:output_type -> DecryptResponse + 12, // [12:14] is the sub-list for method output_type + 10, // [10:12] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_keyservice_keyservice_proto_init() } @@ -818,6 +884,7 @@ func file_keyservice_keyservice_proto_init() { (*Key_AzureKeyvaultKey)(nil), (*Key_VaultKey)(nil), (*Key_AgeKey)(nil), + (*Key_OciKey)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -825,7 +892,7 @@ func file_keyservice_keyservice_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_keyservice_keyservice_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/keyservice/keyservice.proto b/keyservice/keyservice.proto index 8bf62f89bf..54bb3f4d65 100644 --- a/keyservice/keyservice.proto +++ b/keyservice/keyservice.proto @@ -10,6 +10,7 @@ message Key { AzureKeyVaultKey azure_keyvault_key = 4; VaultKey vault_key = 5; AgeKey age_key = 6; + OciKey oci_key = 7; } } @@ -44,6 +45,10 @@ message AgeKey { string recipient = 1; } +message OciKey { + string ocid = 1; +} + message EncryptRequest { Key key = 1; bytes plaintext = 2; diff --git a/keyservice/keyservice_grpc.pb.go b/keyservice/keyservice_grpc.pb.go index d278b82d97..f0ab3b45e3 100644 --- a/keyservice/keyservice_grpc.pb.go +++ b/keyservice/keyservice_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.28.3 +// - protoc v5.29.2 // source: keyservice/keyservice.proto package keyservice diff --git a/keyservice/server.go b/keyservice/server.go index 9f2b486a67..565723c267 100644 --- a/keyservice/server.go +++ b/keyservice/server.go @@ -8,6 +8,7 @@ import ( "github.com/getsops/sops/v3/gcpkms" "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" "golang.org/x/net/context" "google.golang.org/grpc/codes" @@ -87,6 +88,17 @@ func (ks *Server) encryptWithAge(key *AgeKey, plaintext []byte) ([]byte, error) return []byte(ageKey.EncryptedKey), nil } +func (ks *Server) encryptWithOciKms(key *OciKey, plaintext []byte) ([]byte, error) { + ociKmsKey := ocikms.MasterKey{ + Ocid: key.Ocid, + } + err := ociKmsKey.Encrypt(plaintext) + if err != nil { + return nil, err + } + return []byte(ociKmsKey.EncryptedKey), nil +} + func (ks *Server) decryptWithPgp(key *PgpKey, ciphertext []byte) ([]byte, error) { pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint) pgpKey.EncryptedKey = string(ciphertext) @@ -141,6 +153,15 @@ func (ks *Server) decryptWithAge(key *AgeKey, ciphertext []byte) ([]byte, error) return []byte(plaintext), err } +func (ks *Server) decryptWithOciKms(key *OciKey, ciphertext []byte) ([]byte, error) { + ociKmsKey := ocikms.MasterKey{ + Ocid: key.Ocid, + } + ociKmsKey.EncryptedKey = string(ciphertext) + plaintext, err := ociKmsKey.Decrypt() + return []byte(plaintext), err +} + // Encrypt takes an encrypt request and encrypts the provided plaintext with the provided key, returning the encrypted // result func (ks Server) Encrypt(ctx context.Context, @@ -196,6 +217,14 @@ func (ks Server) Encrypt(ctx context.Context, response = &EncryptResponse{ Ciphertext: ciphertext, } + case *Key_OciKey: + ciphertext, err := ks.encryptWithOciKms(k.OciKey, 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: @@ -222,6 +251,8 @@ func keyToString(key *Key) string { return fmt.Sprintf("Azure Key Vault key with URL %s/keys/%s/%s", k.AzureKeyvaultKey.VaultUrl, k.AzureKeyvaultKey.Name, k.AzureKeyvaultKey.Version) case *Key_VaultKey: return fmt.Sprintf("Hashicorp Vault key with URI %s/v1/%s/keys/%s", k.VaultKey.VaultAddress, k.VaultKey.EnginePath, k.VaultKey.KeyName) + case *Key_OciKey: + return fmt.Sprintf("OCI KMS key with OCID %s", k.OciKey.Ocid) default: return "Unknown key type" } @@ -298,6 +329,14 @@ func (ks Server) Decrypt(ctx context.Context, response = &DecryptResponse{ Plaintext: plaintext, } + case *Key_OciKey: + plaintext, err := ks.decryptWithOciKms(k.OciKey, 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/ocikms/keysource.go b/ocikms/keysource.go new file mode 100644 index 0000000000..c8d5de00fb --- /dev/null +++ b/ocikms/keysource.go @@ -0,0 +1,191 @@ +package ocikms + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/getsops/sops/v3/logging" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/keymanagement" + "github.com/sirupsen/logrus" +) + +var log *logrus.Logger + +const ( + // cryptoEndpointTemplate is the template for the OCI KMS crypto endpoint that is constructed using parts of the key OCID + cryptoEndpointTemplate = "https://%s-crypto.kms.%s.oraclecloud.com" + // ocidParts is the number of parts in an OCID, separated by ".", eg: "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" + ocidParts = 6 + // KeyTypeIdentifier is the string used to identify an OCI KMS MasterKey. + KeyTypeIdentifier = "oci_kms" +) + +func init() { + log = logging.NewLogger("OCIKMS") +} + +// MasterKey is an Oracle Cloud KMS key used to encrypt and decrypt sops' data key. +type MasterKey struct { + Ocid string + EncryptedKey string + CreationDate time.Time +} + +func NewMasterKeyFromOCID(ocid string) *MasterKey { + return &MasterKey{ + Ocid: strings.Replace(ocid, " ", "", -1), + CreationDate: time.Now().UTC(), + } +} + +func MasterKeysFromOCIDString(ocids string) []*MasterKey { + var keys []*MasterKey + if ocids == "" { + return keys + } + for _, s := range strings.Split(ocids, ",") { + keys = append(keys, NewMasterKeyFromOCID(s)) + } + return keys +} + +// createKeyManagementClient creates a new OCI KMS client +func (key *MasterKey) createCryptoClient() (client keymanagement.KmsCryptoClient, err error) { + region, vault_ref, err := extractRefs(key) + if err != nil { + log.WithField("ocid", key.Ocid).Errorf("Cannot extract region and vault_ref from OCID: %s", err) + } + + endpoint := fmt.Sprintf(cryptoEndpointTemplate, vault_ref, region) + log.WithField("endpoint", endpoint).Info("Creating OCI KMS client") + // The client is created using the default OCI config provider, using the default profile in the default config file (~/.oci/config) + // There is currently no straightforward way to pass a custom config provider to the client. + // The oci-go-sdk provides a way to pass a custom config provider to the client, but there's no environment variable to feature-flag it. + // Related: https://github.com/oracle/oci-go-sdk/issues/318 + + // In order to use a custom provider, the client would need to be created like this: + // client, err := keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.CustomProfileConfigProvider("/home//.oci/config", ""), endpoint) + // Sticking with the defaults for now. + + client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.DefaultConfigProvider(), endpoint) + if err != nil { + return client, fmt.Errorf("Cannot create OCI KMS client: %w", err) + } + return client, nil +} + +func extractRefs(key *MasterKey) (string, string, error) { + parts := strings.Split(key.Ocid, ".") + if len(parts) != ocidParts { + return "", "", fmt.Errorf("OCID length is %s, expected %d", key.Ocid, ocidParts) + } + region := parts[3] + vault_ref := parts[4] + return region, vault_ref, 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) +} + +// Encrypt takes a sops data key, encrypts it with Key Vault and stores the result in the EncryptedKey field +func (key *MasterKey) Encrypt(dataKey []byte) error { + c, err := key.createCryptoClient() + if err != nil { + log.WithField("ocid", key.Ocid).Info("Encryption failed") + return fmt.Errorf("cannot create OCI KMS service: %w", err) + } + data := base64.StdEncoding.EncodeToString(dataKey) + + res, err := c.Encrypt(context.TODO(), keymanagement.EncryptRequest{ + EncryptDataDetails: keymanagement.EncryptDataDetails{ + KeyId: common.String(key.Ocid), + Plaintext: &data, + }, + RequestMetadata: common.RequestMetadata{}, + }) + + if err != nil { + log.WithError(err).WithField("ocid", key.Ocid). + Error("Encryption failed") + return fmt.Errorf("failed to encrypt data: %w", err) + } + + key.EncryptedKey = *res.EncryptedData.Ciphertext + log.WithField("ocid", key.Ocid).Info("Encryption succeeded") + + return nil +} + +// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet +func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { + if key.EncryptedKey == "" { + return key.Encrypt(dataKey) + } + return nil +} + +// Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns the result. +func (key *MasterKey) Decrypt() ([]byte, error) { + c, err := key.createCryptoClient() + if err != nil { + log.WithField("ocid", key.Ocid).Info("Decryption failed") + return nil, err + } + + res, err := c.Decrypt(context.TODO(), keymanagement.DecryptRequest{ + DecryptDataDetails: keymanagement.DecryptDataDetails{ + Ciphertext: &key.EncryptedKey, + KeyId: &key.Ocid, + }, + }) + + if err != nil { + log.WithError(err).WithField("ocid", key.Ocid).Error("Decryption failed") + return nil, fmt.Errorf("error decrypting key: %w", err) + } + + plaintext, err := base64.StdEncoding.DecodeString(*res.Plaintext) + if err != nil { + log.WithError(err).WithField("ocid", key.Ocid).Error("Decryption failed") + return nil, err + } + + log.WithField("ocid", key.Ocid).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) > (time.Hour * 24 * 30 * 6) +} + +// ToString converts the key to a string representation +func (key *MasterKey) ToString() string { + return key.Ocid +} + +// ToMap converts the MasterKey to a map for serialization purposes +func (key MasterKey) ToMap() map[string]interface{} { + out := make(map[string]interface{}) + out["ocid"] = key.Ocid + 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 +} diff --git a/ocikms/keysource_test.go b/ocikms/keysource_test.go new file mode 100644 index 0000000000..e3efe1c629 --- /dev/null +++ b/ocikms/keysource_test.go @@ -0,0 +1,36 @@ +package ocikms + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMasterKeysFromOCIDString(t *testing.T) { + s := "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq, ocid1.key.oc1.uk-london-1.bbbblgz5aacmg.bbbbiljtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" + ks := MasterKeysFromOCIDString(s) + k1 := ks[0] + k2 := ks[1] + expectedOcid1 := "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" + expectedOcid2 := "ocid1.key.oc1.uk-london-1.bbbblgz5aacmg.bbbbiljtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" + if k1.Ocid != expectedOcid1 { + t.Errorf("Ocid mismatch. Expected %s, found %s", expectedOcid1, k1.Ocid) + } + if k2.Ocid != expectedOcid2 { + t.Errorf("Ocid mismatch. Expected %s, found %s", expectedOcid2, k2.Ocid) + } +} + +func TestKeyToMap(t *testing.T) { + key := MasterKey{ + CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC), + Ocid: "foo", + EncryptedKey: "this is encrypted", + } + assert.Equal(t, map[string]interface{}{ + "ocid": "foo", + "enc": "this is encrypted", + "created_at": "2016-10-31T10:00:00Z", + }, key.ToMap()) +} diff --git a/stores/stores.go b/stores/stores.go index 169e8dbb52..12b0e784fd 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -20,6 +20,7 @@ import ( "github.com/getsops/sops/v3/gcpkms" "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" ) @@ -49,6 +50,7 @@ type Metadata struct { AzureKeyVaultKeys []azkvkey `yaml:"azure_kv" json:"azure_kv"` VaultKeys []vaultkey `yaml:"hc_vault" json:"hc_vault"` AgeKeys []agekey `yaml:"age" json:"age"` + OCIKMSKeys []ocikmskey `yaml:"oci_kms" json:"oci_kms"` LastModified string `yaml:"lastmodified" json:"lastmodified"` MessageAuthenticationCode string `yaml:"mac" json:"mac"` PGPKeys []pgpkey `yaml:"pgp" json:"pgp"` @@ -69,6 +71,7 @@ type keygroup struct { AzureKeyVaultKeys []azkvkey `yaml:"azure_kv,omitempty" json:"azure_kv,omitempty"` VaultKeys []vaultkey `yaml:"hc_vault" json:"hc_vault"` AgeKeys []agekey `yaml:"age" json:"age"` + OCIKMSKeys []ocikmskey `yaml:"oci_kms" json:"oci_kms"` } type pgpkey struct { @@ -113,6 +116,12 @@ type agekey struct { EncryptedDataKey string `yaml:"enc" json:"enc"` } +type ocikmskey struct { + Ocid string `yaml:"ocid" json:"ocid"` + CreatedAt string `yaml:"created_at" json:"created_at"` + EncryptedDataKey string `yaml:"enc" json:"enc"` +} + // MetadataFromInternal converts an internal SOPS metadata representation to a representation appropriate for storage func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata { var m Metadata @@ -135,6 +144,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata { m.VaultKeys = vaultKeysFromGroup(group) m.AzureKeyVaultKeys = azkvKeysFromGroup(group) m.AgeKeys = ageKeysFromGroup(group) + m.OCIKMSKeys = ocikmsKeysFromGroup(group) } else { for _, group := range sopsMetadata.KeyGroups { m.KeyGroups = append(m.KeyGroups, keygroup{ @@ -144,6 +154,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata { VaultKeys: vaultKeysFromGroup(group), AzureKeyVaultKeys: azkvKeysFromGroup(group), AgeKeys: ageKeysFromGroup(group), + OCIKMSKeys: ocikmsKeysFromGroup(group), }) } } @@ -240,6 +251,20 @@ func ageKeysFromGroup(group sops.KeyGroup) (keys []agekey) { return } +func ocikmsKeysFromGroup(group sops.KeyGroup) (keys []ocikmskey) { + for _, key := range group { + switch key := key.(type) { + case *ocikms.MasterKey: + keys = append(keys, ocikmskey{ + Ocid: key.Ocid, + 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) @@ -294,7 +319,7 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) { }, nil } -func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmskey, azkvKeys []azkvkey, vaultKeys []vaultkey, ageKeys []agekey) (sops.KeyGroup, error) { +func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmskey, azkvKeys []azkvkey, vaultKeys []vaultkey, ageKeys []agekey, ociKmsKeys []ocikmskey) (sops.KeyGroup, error) { var internalGroup sops.KeyGroup for _, kmsKey := range kmsKeys { k, err := kmsKey.toInternal() @@ -338,13 +363,20 @@ func internalGroupFrom(kmsKeys []kmskey, pgpKeys []pgpkey, gcpKmsKeys []gcpkmske } internalGroup = append(internalGroup, k) } + for _, ociKmsKey := range ociKmsKeys { + k, err := ociKmsKey.toInternal() + if err != nil { + return nil, err + } + internalGroup = append(internalGroup, k) + } return internalGroup, nil } 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.AzureKeyVaultKeys) > 0 || len(m.VaultKeys) > 0 || len(m.AgeKeys) > 0 { - internalGroup, err := internalGroupFrom(m.KMSKeys, m.PGPKeys, m.GCPKMSKeys, m.AzureKeyVaultKeys, m.VaultKeys, m.AgeKeys) + if len(m.PGPKeys) > 0 || len(m.KMSKeys) > 0 || len(m.GCPKMSKeys) > 0 || len(m.AzureKeyVaultKeys) > 0 || len(m.VaultKeys) > 0 || len(m.AgeKeys) > 0 || len(m.OCIKMSKeys) > 0 { + internalGroup, err := internalGroupFrom(m.KMSKeys, m.PGPKeys, m.GCPKMSKeys, m.AzureKeyVaultKeys, m.VaultKeys, m.AgeKeys, m.OCIKMSKeys) if err != nil { return nil, err } @@ -352,7 +384,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.AzureKeyVaultKeys, group.VaultKeys, group.AgeKeys) + internalGroup, err := internalGroupFrom(group.KMSKeys, group.PGPKeys, group.GCPKMSKeys, group.AzureKeyVaultKeys, group.VaultKeys, group.AgeKeys, group.OCIKMSKeys) if err != nil { return nil, err } @@ -438,6 +470,18 @@ func (ageKey *agekey) toInternal() (*age.MasterKey, error) { }, nil } +func (ociKmsKey *ocikmskey) toInternal() (*ocikms.MasterKey, error) { + creationDate, err := time.Parse(time.RFC3339, ociKmsKey.CreatedAt) + if err != nil { + return nil, err + } + return &ocikms.MasterKey{ + Ocid: ociKmsKey.Ocid, + EncryptedKey: ociKmsKey.EncryptedDataKey, + CreationDate: creationDate, + }, nil +} + // ExampleComplexTree is an example sops.Tree object exhibiting complex relationships var ExampleComplexTree = sops.Tree{ Branches: sops.TreeBranches{ From 7c4db9e690584b64d677074b73913cf303e6e9c3 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Tue, 31 Dec 2024 17:41:28 +0100 Subject: [PATCH 02/12] docs: rstcheck fix Signed-off-by: Alessandro De Blasis --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 074dc08efc..db3ed9593c 100644 --- a/README.rst +++ b/README.rst @@ -481,7 +481,7 @@ It will look for the `DEFAULT` profile in the `~/.oci/config` file. Make sure to authenticate and to have a valid session via: -.. code:: bash +.. code:: sh $ oci session authenticate @@ -489,7 +489,7 @@ Encrypting/decrypting with OCI KMS requires a KMS OCID. You can use the cloud console the get the OCID of an existing key or you can create one using the `oci` CLI: -.. code:: bash +.. code:: sh $ export compartment_id= $ export display_name= @@ -511,7 +511,7 @@ Now we need to create a key. First of all we need to define a shape for it with: Now we can create the key with -.. code:: bash +.. code:: sh $ export compartment_id= $ export display_name= From 6407abcdf0acadb0df61767730338daad36325f9 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Wed, 8 Jan 2025 15:30:01 +0100 Subject: [PATCH 03/12] Merge branch 'main' of github.com:mozilla/sops into issue/#981-oci-kms Signed-off-by: Alessandro De Blasis --- functional-tests/Cargo.lock | 22 ++++++++++++++++++++-- functional-tests/Cargo.toml | 2 +- go.mod | 11 +++++------ go.sum | 20 ++++++++++---------- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/functional-tests/Cargo.lock b/functional-tests/Cargo.lock index 35223ce575..4c676e7ab8 100644 --- a/functional-tests/Cargo.lock +++ b/functional-tests/Cargo.lock @@ -48,6 +48,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -195,12 +206,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys", @@ -218,6 +230,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/functional-tests/Cargo.toml b/functional-tests/Cargo.toml index a114919186..18e9b89de8 100644 --- a/functional-tests/Cargo.toml +++ b/functional-tests/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Adrian Utrilla "] [dependencies] -tempfile = "3.12.0" +tempfile = "3.15.0" serde = "1.0" serde_json = "1.0.134" serde_yaml = "0.9.34" diff --git a/go.mod b/go.mod index b2dc35efd4..af130b6809 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module github.com/getsops/sops/v3 go 1.22 - toolchain go1.22.9 require ( - cloud.google.com/go/kms v1.20.3 + cloud.google.com/go/kms v1.20.4 cloud.google.com/go/storage v1.49.0 filippo.io/age v1.2.1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 @@ -15,9 +14,9 @@ require ( github.com/aws/aws-sdk-go-v2 v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.28.7 github.com/aws/aws-sdk-go-v2/credentials v1.17.48 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.45 github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 - github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0 github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 github.com/blang/semver v3.5.1+incompatible github.com/fatih/color v1.18.0 @@ -37,8 +36,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.16 golang.org/x/net v0.33.0 - golang.org/x/sys v0.28.0 - golang.org/x/term v0.27.0 + golang.org/x/sys v0.29.0 + golang.org/x/term v0.28.0 google.golang.org/api v0.214.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 google.golang.org/grpc v1.69.2 diff --git a/go.sum b/go.sum index 81aa286add..ea49bc2d07 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/iam v1.3.0 h1:4Wo2qTaGKFtajbLpF6I4mywg900u3TLlHDb6mriLDPU= cloud.google.com/go/iam v1.3.0/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/kms v1.20.3 h1:a61yIN5LN8ozWxOC6tjUx5V5SEzfkS+b69kYMQfzGzE= -cloud.google.com/go/kms v1.20.3/go.mod h1:YvX+xhp2E2Sc3vol5IcRlBhH14Ecl3kegUY/DtH7EWQ= +cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A= +cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc= cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= @@ -73,8 +73,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4af github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44 h1:2zxMLXLedpB4K1ilbJFxtMKsVKaexOqDttOhc0QGm3Q= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44/go.mod h1:VuLHdqwjSvgftNC7yqPWyGVhEwPmJpeRi07gOgOfHF8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.45 h1:ZxB8WFVYwolhDZxuZXoesHkl+L9cXLWy0K/G0QkNATc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.45/go.mod h1:1krrbyoFFDqaNldmltPTP+mK3sAXLHPoaFtISOw2Hkk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= @@ -93,8 +93,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 h1:Hi0KGbrnr57bEH github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7/go.mod h1:wKNgWgExdjjrm4qvfbTorkvocEstaoDl4WCvGfeCy9c= github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk= github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 h1:aOVVZJgWbaH+EJYPvEgkNhCEbXXvH7+oML36oaPK3zE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= +github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0 h1:SAfh4pNx5LuTafKKWR02Y+hL3A+3TX8cTKG1OIAJaBk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.72.0/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= @@ -341,10 +341,10 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= From efadca10b7c831fb454a46ad4431a11455dc8010 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 26 Sep 2025 02:41:36 +0200 Subject: [PATCH 04/12] Merge branch 'issue/#981-oci-kms' of github.com:deblasis/sops into issue/#981-oci-kms Signed-off-by: Alessandro De Blasis --- .git-blame-ignore-revs | 2 + .github/utils/patch-go.mod.py | 77 ---- .github/workflows/cli.yml | 21 +- .github/workflows/codeql.yml | 6 +- .github/workflows/docs.yml | 2 +- .github/workflows/linters.yml | 2 +- .github/workflows/release.yml | 26 +- .goreleaser.yaml | 3 +- .release/alpine.Dockerfile | 2 +- CHANGELOG.md | 128 ++++++ Makefile | 2 +- README.rst | 141 +++++-- aes/cipher.go | 11 + aes/cipher_test.go | 93 ++++- age/encrypted_keys.go | 190 +++++++++ age/keysource.go | 256 ++++++++++-- age/keysource_test.go | 247 ++++++++++- age/ssh_parse.go | 84 ++++ age/tui.go | 173 ++++++++ audit/audit.go | 2 +- azkv/keysource.go | 90 +++- cmd/sops/common/common.go | 30 +- cmd/sops/completion.go | 59 +++ cmd/sops/decrypt.go | 12 +- cmd/sops/edit.go | 15 +- cmd/sops/encrypt.go | 26 +- cmd/sops/main.go | 300 ++++++++++---- cmd/sops/set.go | 15 +- cmd/sops/subcommand/exec/exec.go | 31 +- cmd/sops/subcommand/exec/exec_unix.go | 5 + cmd/sops/subcommand/exec/exec_windows.go | 5 + cmd/sops/subcommand/updatekeys/updatekeys.go | 6 +- config/config.go | 224 ++++++++-- config/config_test.go | 191 ++++++++- example.yaml | 39 +- functional-tests/Cargo.lock | 37 +- functional-tests/Cargo.toml | 4 +- functional-tests/src/lib.rs | 381 ++++++++++++++++- gcpkms/keysource.go | 99 ++++- gcpkms/keysource_test.go | 56 ++- go.mod | 190 ++++----- go.sum | 406 +++++++++---------- hcvault/keysource.go | 49 ++- hcvault/keysource_test.go | 16 +- kms/keysource.go | 56 ++- kms/keysource_test.go | 8 +- pgp/keysource.go | 57 ++- pgp/keysource_test.go | 21 +- publish/vault.go | 2 +- rust-toolchain.toml | 2 +- shamir/shamir.go | 130 +++--- shamir/shamir_test.go | 16 + shamir/tables.go | 77 ---- shamir/tables_test.go | 13 - sops.go | 78 +++- sops_test.go | 118 ++++-- stores/dotenv/store.go | 8 +- stores/flatten.go | 29 +- stores/ini/store.go | 24 +- stores/ini/store_test.go | 51 ++- stores/json/store.go | 2 + stores/json/store_test.go | 40 +- stores/stores.go | 39 +- stores/stores_test.go | 27 ++ stores/yaml/store.go | 9 +- stores/yaml/store_test.go | 30 +- version/version.go | 16 +- 67 files changed, 3550 insertions(+), 1057 deletions(-) create mode 100644 .git-blame-ignore-revs delete mode 100644 .github/utils/patch-go.mod.py create mode 100644 age/encrypted_keys.go create mode 100644 age/ssh_parse.go create mode 100644 age/tui.go create mode 100644 cmd/sops/completion.go delete mode 100644 shamir/tables.go delete mode 100644 shamir/tables_test.go create mode 100644 stores/stores_test.go diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..b2222d430b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Update formatting. +72cebfd8a13ff59dd72712b711f08787c9cc6b0a diff --git a/.github/utils/patch-go.mod.py b/.github/utils/patch-go.mod.py deleted file mode 100644 index 892dfbba44..0000000000 --- a/.github/utils/patch-go.mod.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Patch go.mod so that the lines 'go xxx' to 'toolchain xxx' are as in git's -HEAD. - -This is necessary since newer 'go mod tidy' versions tend to modify these -lines. Since we check in CI that 'go mod tidy' does not change go.mod, this -causes CI to fail. -""" - -import subprocess - - -def split_go_mod(contents: str) -> tuple[list[str], list[str], list[str]]: - """ - Given the contents of go.mod, splits it into three lists of lines - (with endings): - 1. The lines before 'go'; - 2. The lines starting with 'go' and ending with 'toolchain'; - 3. The lines after 'toolchain'. - """ - parts: tuple[list[str], list[str], list[str]] = ([], [], []) - index = 0 - for line in contents.splitlines(keepends=True): - next_index = index - if line.startswith('go '): - index = next_index = 1 - if line.startswith('toolchain '): - next_index = 2 - parts[index].append(line) - index = next_index - return parts - - -def get_file_contents_from_git_revision(filename: str, revision: str) -> str: - """ - Get the file contents of ``filename`` from Git revision ``revision``. - """ - p = subprocess.run( - ['git', 'show', f'{revision}:{filename}'], - stdout=subprocess.PIPE, - check=True, - encoding='utf-8', - ) - return p.stdout - - -def read_file(filename: str) -> str: - """ - Read the file's contents. - """ - with open(filename, 'r', encoding='utf-8') as f: - return f.read() - - -def write_file(filename: str, contents: str) -> None: - """ - Write the file's contents. - """ - with open(filename, 'w', encoding='utf-8') as f: - f.write(contents) - - -def main(): - """ - Patches go.mod. - """ - filename = 'go.mod' - _, go_versions, __ = split_go_mod( - get_file_contents_from_git_revision(filename, 'HEAD') - ) - head, _, tail = split_go_mod(read_file(filename)) - lines = head + go_versions + tail - write_file(filename, ''.join(lines)) - - -if __name__ == '__main__': - main() diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 6a738959ba..0659e93b1d 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -19,7 +19,7 @@ jobs: matrix: os: [linux, darwin, windows] arch: [amd64, arm64] - go-version: ['1.22', '1.23'] + go-version: ['1.23', '1.24'] exclude: - os: windows arch: arm64 @@ -29,17 +29,17 @@ jobs: VAULT_ADDR: "http://127.0.0.1:8200" steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -49,9 +49,6 @@ jobs: - name: Vendor Go Modules run: make vendor - - name: Restore go/toolchain lines of go.mod - run: python3 .github/utils/patch-go.mod.py - - name: Ensure clean working tree run: git diff --exit-code @@ -71,14 +68,14 @@ jobs: - name: Upload artifact for ${{ matrix.os }} if: matrix.os != 'windows' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} path: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} - name: Upload artifact for ${{ matrix.os }} if: matrix.os == 'windows' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ github.sha }} path: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ github.sha }} @@ -88,14 +85,14 @@ jobs: needs: [build] strategy: matrix: - go-version: ['1.22'] + go-version: ['1.24'] env: VAULT_VERSION: "1.14.0" VAULT_TOKEN: "root" VAULT_ADDR: "http://127.0.0.1:8200" steps: - name: Check out code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -108,7 +105,7 @@ jobs: - name: Show Rust version run: cargo --version - - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: sops-${{ matrix.go-version }}-linux-amd64-${{ github.sha }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b9bfcabfc5..9b6351d358 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,13 +29,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 with: languages: go # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs @@ -52,6 +52,6 @@ jobs: make install - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 with: category: "/language:go" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 65b7d49aaf..87ec3ba773 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index d0626a2cbc..91852997e1 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9715f72d6..452917f986 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,38 +25,38 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 persist-credentials: false - name: Setup Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v4.0.1 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v4.0.1 with: - go-version-file: go.mod + go-version: 1.24 cache: false - name: Setup Syft - uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 + uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6 - name: Setup Cosign - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - name: Setup QEMU - uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Setup Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Quay.io - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: quay.io username: ${{ secrets.QUAY_BOT_USERNAME }} @@ -64,7 +64,7 @@ jobs: - name: Run GoReleaser id: goreleaser - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: # Note that the following is the version of goreleaser, and NOT a Go version! # When bumping it, make sure to check out goreleaser's changelog first! @@ -169,7 +169,7 @@ jobs: id-token: write # For creating OIDC tokens for signing. contents: write # For adding assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: "${{ needs.combine-subjects.outputs.all-subjects }}" upload-assets: true @@ -186,7 +186,7 @@ jobs: strategy: matrix: ${{ fromJSON(needs.release.outputs.container-subjects) }} - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: ghcr.io/${{ matrix.image }} digest: ${{ matrix.digest }} @@ -205,7 +205,7 @@ jobs: strategy: matrix: ${{ fromJSON(needs.release.outputs.container-subjects) }} - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: quay.io/${{ matrix.image }} digest: ${{ matrix.digest }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b28f8d52c6..87bb3b2d18 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -69,6 +69,7 @@ builds: - windows goarch: - amd64 + - arm64 # Modified timestamp on the binary, set to ensure reproducible builds. mod_timestamp: "{{ .CommitTimestamp }}" @@ -118,7 +119,7 @@ archives: builds: - binary-windows # NB: specifically crafted to ensure compatibility with release artifacts < v3.8.0. - name_template: '{{ .ProjectName }}-v{{ .Version }}' + name_template: '{{ .ProjectName }}-v{{ .Version }}.{{ .Arch }}' - id: archive-darwin-universal format: binary diff --git a/.release/alpine.Dockerfile b/.release/alpine.Dockerfile index 94e205694c..58d5b418a4 100644 --- a/.release/alpine.Dockerfile +++ b/.release/alpine.Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.22 RUN apk --no-cache add \ ca-certificates \ diff --git a/CHANGELOG.md b/CHANGELOG.md index f27b5904a3..f5358b44c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,133 @@ # Changelog +## 3.10.2 + +Bugfixes: + +* Remove reserved keyword check from YAML store, which hid a better check ([#1829](https://github.com/getsops/sops/pull/1829)). + +Improvements: + +* Dependency updates ([#1834](https://github.com/getsops/sops/pull/1834), [#1839](https://github.com/getsops/sops/pull/1839)). +* Use latest 1.24 Go version for release build ([#1836](https://github.com/getsops/sops/pull/1836)). + +Project changes: + +* CI dependency updates ([#1840](https://github.com/getsops/sops/pull/1840)). + +## 3.10.1 + +This is a re-release of 3.10.0 with no code changes. + +Due to a failure during the 3.10.0 release, the +[commit cached by the Go infrastructure for 3.10.0](https://github.com/getsops/sops/commit/200bb6d8ab4063330bc99697255b3583501b3877) +is different from +[the commit tagged in the repository](https://github.com/getsops/sops/commit/4ed7060298fbcd00cafa359121ca62091b85bb6f). +To avoid confusion, we decided to push another release where the tag in the repository +will coincide with the commit cached by Go. + +Project changes: + +* CI dependency updates ([#1826](https://github.com/getsops/sops/pull/1826)). + +## 3.10.0 + +Security fixes: + +* Cherry-pick a fix for a timing vulnerability in the Shamir Secret Sharing code. + The code was vendored from HashiCorp's Vault project, and the issue was fixed + there two years ago; see [GHSA-vq4h-9ghm-qmrr](https://github.com/advisories/GHSA-vq4h-9ghm-qmrr) + for details ([#1813](https://github.com/getsops/sops/pull/1813)). + +Features: + +* Add `--input-type` option for `sops filestatus` subcommand ([#1601](https://github.com/getsops/sops/pull/1601)). +* Allow to set the editor `sops` should use with the `SOPS_EDITOR` environment variable. + If not set, `sops` falls back to `EDITOR` as before ([#1611](https://github.com/getsops/sops/pull/1611)). +* Allow users to disable the latest version check with the environment variable `SOPS_DISABLE_VERSION_CHECK`. + Setting it to `1`, `t`, `T`, `TRUE`, `true`, or `True` explicitly + disables the check ([#1684](https://github.com/getsops/sops/pull/1684)). +* Allow users to explicitly enable the latest version check with the `--check-for-updates` + option ([#1816](https://github.com/getsops/sops/pull/1816)). +* Add duplicate section support for INI store ([#1452](https://github.com/getsops/sops/pull/1452)). +* Add check to prevent duplicate keys in YAML files ([#1203](https://github.com/getsops/sops/pull/1203)). +* Add `--same-process` option for the `sops exec-env` to use the `execve` syscall + instead of starting the command in a child process ([#880](https://github.com/getsops/sops/pull/880)). +* Add `--idempotent` option for the `sops set` subcommand that will only + write the file if a change happened ([#1754](https://github.com/getsops/sops/pull/1754)). +* Encrypt and decrypt `time.Time` objects that can appear in YAML files + when using dates and timestamps ([#1759](https://github.com/getsops/sops/pull/1759)). +* Allow to encrypt and decrypt from `stdin` without having to provide + platform-specific device names. This only works when using the + `sops encrypt` and `sops decrypt` subcommands ([#1690](https://github.com/getsops/sops/pull/1690)). +* Allow to set the SOPS config location with the environment variable + `SOPS_CONFIG` ([#1701](https://github.com/getsops/sops/pull/1701)). +* Support the `--config` option in the `sops publish` subcommand ([#1779](https://github.com/getsops/sops/pull/1779)). +* Omit empty master key metadata from encrypted files ([#1571](https://github.com/getsops/sops/pull/1571)). +* Add SSH support for Age ([#1692](https://github.com/getsops/sops/pull/1692)). +* Support Age identities with passphrases ([#1400](https://github.com/getsops/sops/pull/1400)). +* Add Age plugin support ([#1641](https://github.com/getsops/sops/pull/1641)). +* Allow to set the `SOPS_AGE_KEY_CMD` environment variable to an executable that + returns Age keys ([#1811](https://github.com/getsops/sops/pull/1811)). +* Add support for `oauth2.TokenSource` injection from key service clients in + GCP KMS ([#1794](https://github.com/getsops/sops/pull/1794)). +* Support `GOOGLE_OAUTH_ACCESS_TOKEN` for GCP KMS ([#1578](https://github.com/getsops/sops/pull/1578)). + +Improvements: + +* Dependency updates ([#1743](https://github.com/getsops/sops/pull/1743), [#1745](https://github.com/getsops/sops/pull/1745), + [#1751](https://github.com/getsops/sops/pull/1751), [#1763](https://github.com/getsops/sops/pull/1763), + [#1769](https://github.com/getsops/sops/pull/1769), [#1773](https://github.com/getsops/sops/pull/1773), + [#1784](https://github.com/getsops/sops/pull/1784), [#1797](https://github.com/getsops/sops/pull/1797), + [#1802](https://github.com/getsops/sops/pull/1802), [#1806](https://github.com/getsops/sops/pull/1806), + [#1809](https://github.com/getsops/sops/pull/1809), [#1814](https://github.com/getsops/sops/pull/1814)). +* Fix typos ([#1765](https://github.com/getsops/sops/pull/1765)). +* Make sure that tests do not pick up `keys.txt` from user's `$HOME` dir ([#1766](https://github.com/getsops/sops/pull/1766)). +* Consolidate passphrase reading functionality in Age code ([#1775](https://github.com/getsops/sops/pull/1775)). +* Fix some problems reported by the `staticcheck` linter ([#1780](https://github.com/getsops/sops/pull/1780)). +* Improve documentation of Shamir Secret Sharing code to ease maintenance ([#1813](https://github.com/getsops/sops/pull/1813)). +* Make sure all files are properly formatted ([#1817](https://github.com/getsops/sops/pull/1817)). +* `sops` now warns if it finds a `.sops.yml` file while searching for a + `.sops.yaml` config file ([#1820](https://github.com/getsops/sops/pull/1820)). + +Bugfixes: + +* Add trailing newline at the end of JSON files ([#1476](https://github.com/getsops/sops/pull/1476)). +* Check GnuPG decryption result for non-empty size. Certain older versions return + an empty result with a successful return code when a AEAD cipher from a newer + version was used ([#1776](https://github.com/getsops/sops/pull/1776)). +* Fix caching of `Metadata.DataKey` ([#1781](https://github.com/getsops/sops/pull/1781)). +* If `--filename-override` is specified, convert it to an absolute path same as regular + filenames ([#1793](https://github.com/getsops/sops/pull/1793)). + +Deprecations: + +* The current behavior that `sops --version` always checks whether the current + version is the latest is deprecated and will no longer be the default eventually. + It is best to right now always specify `--disable-version-check` or `--check-for-updates` + to `sops --version`, or alternatively set the environment variable `SOPS_DISABLE_VERSION_CHECK=true` + to already get the planned default behavior today. ([#1816](https://github.com/getsops/sops/pull/1816)). + +Project changes: + +* Go 1.22 is no longer support; CI now also builds with Go 1.24 ([#1819](https://github.com/getsops/sops/pull/1819)). +* CI dependency updates ([#1746](https://github.com/getsops/sops/pull/1746), + [#1750](https://github.com/getsops/sops/pull/1750), [#1770](https://github.com/getsops/sops/pull/1770), + [#1782](https://github.com/getsops/sops/pull/1782), [#1795](https://github.com/getsops/sops/pull/1795), + [#1801](https://github.com/getsops/sops/pull/1801), [#1808](https://github.com/getsops/sops/pull/1808)). +* Rust dependency updates for functional tests ([#1744](https://github.com/getsops/sops/pull/1744), + [#1762](https://github.com/getsops/sops/pull/1762), [#1768](https://github.com/getsops/sops/pull/1768), + [#1783](https://github.com/getsops/sops/pull/1783), [#1796](https://github.com/getsops/sops/pull/1796), + [#1800](https://github.com/getsops/sops/pull/1800), [#1807](https://github.com/getsops/sops/pull/1807)). +* Bump Rust version for functional tests to 1.85 ([#1783](https://github.com/getsops/sops/pull/1783)). +* Release environment updates ([#1700](https://github.com/getsops/sops/pull/1700), + [#1761](https://github.com/getsops/sops/pull/1761)). +* The changelog is now a MarkDown document ([#1741](https://github.com/getsops/sops/pull/1741)). +* We now also build a Windows ARM64 binary ([#1791](https://github.com/getsops/sops/pull/1791)). +* In the `updatekey.Opts` structure, `GroupQuorum` was renamed to `ShamirThreshold` + ([#1631](https://github.com/getsops/sops/pull/1631)). +* Produce multiple Windows binaries ([#1823](https://github.com/getsops/sops/pull/1823)). + ## 3.9.4 Improvements: diff --git a/Makefile b/Makefile index 1712c17d33..e0fbd3cbb1 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ checkmd: $(MD_FILES) .PHONY: test test: vendor gpg --import pgp/sops_functional_tests_key.asc 2>&1 1>/dev/null || exit 0 - unset SOPS_AGE_KEY_FILE; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... + unset SOPS_AGE_KEY_FILE; unset SOPS_AGE_KEY_CMD; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... .PHONY: showcoverage showcoverage: test diff --git a/README.rst b/README.rst index db3ed9593c..a86cc9628b 100644 --- a/README.rst +++ b/README.rst @@ -106,7 +106,8 @@ encryption/decryption transparently and open the cleartext file in an editor please wait while an encryption key is being generated and stored in a secure fashion file written to mynewtestfile.yaml -Editing will happen in whatever ``$EDITOR`` is set to, or, if it's not set, in vim. +Editing will happen in whatever ``$SOPS_EDITOR`` or ``$EDITOR`` is set to, or, if it's +not set, in vim, nano, or vi. Keep in mind that SOPS will wait for the editor to exit, and then try to reencrypt the file. Some GUI editors (atom, sublime) spawn a child process and then exit immediately. They usually have an option to wait for the main editor window to be @@ -220,19 +221,39 @@ the ``--age`` option or the **SOPS_AGE_RECIPIENTS** environment variable: When decrypting a file with the corresponding identity, SOPS will look for a text file name ``keys.txt`` located in a ``sops`` subdirectory of your user -configuration directory. On Linux, this would be ``$XDG_CONFIG_HOME/sops/age/keys.txt``. -If ``$XDG_CONFIG_HOME`` is not set ``$HOME/.config/sops/age/keys.txt`` is used instead. -On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``. On -Windows, this would be ``%AppData%\sops\age\keys.txt``. You can specify the location -of this file manually by setting the environment variable **SOPS_AGE_KEY_FILE**. -Alternatively, you can provide the key(s) directly by setting the **SOPS_AGE_KEY** -environment variable. +configuration directory. + +- **Linux** + + - Looks for ``keys.txt`` in ``$XDG_CONFIG_HOME/sops/age/keys.txt``; + - Falls back to ``$HOME/.config/sops/age/keys.txt`` if ``$XDG_CONFIG_HOME`` isn’t set. + +- **macOS** + + - Looks for ``keys.txt`` in ``$XDG_CONFIG_HOME/sops/age/keys.txt``; + - Falls back to ``$HOME/Library/Application Support/sops/age/keys.txt`` if ``$XDG_CONFIG_HOME`` isn’t set. + +- **Windows** + + - Looks for ``keys.txt`` in `%AppData%\\sops\\age\\keys.txt``. + +You can override the default lookup by: + +- setting the environment variable **SOPS_AGE_KEY_FILE**; +- setting the **SOPS_AGE_KEY** environment variable; +- providing a command to output the age keys by setting the **SOPS_AGE_KEY_CMD** environment variable.. The contents of this key file should be a list of age X25519 identities, one per line. Lines beginning with ``#`` are considered comments and ignored. Each identity will be tried in sequence until one is able to decrypt the data. -Encrypting with SSH keys via age is not yet supported by SOPS. +Encrypting with SSH keys via age is also supported by SOPS. You can use SSH public keys +("ssh-ed25519 AAAA...", "ssh-rsa AAAA...") as age recipients when encrypting a file. +When decrypting a file, SOPS will look for ``~/.ssh/id_ed25519`` and falls back to +``~/.ssh/id_rsa``. You can specify the location of the private key manually by setting +the environment variable **SOPS_AGE_SSH_PRIVATE_KEY_FILE**. + +Note that only ``ssh-rsa`` and ``ssh-ed25519`` are supported. A list of age recipients can be added to the ``.sops.yaml``: @@ -258,8 +279,12 @@ It is also possible to use ``updatekeys``, when adding or removing age recipient Encrypting using GCP KMS ~~~~~~~~~~~~~~~~~~~~~~~~ -GCP KMS uses `Application Default Credentials -`_. +GCP KMS has support for authorization with the use of `Application Default Credentials +`_ and using an OAuth 2.0 token. +Application default credentials precedes the use of access token. + +Using Application Default Credentials you can authorize by doing this: + If you already logged in using .. code:: sh @@ -272,6 +297,18 @@ you can enable application default credentials using the sdk: $ gcloud auth application-default login +Using OAauth tokens you can authorize by doing this: + +.. code:: sh + + $ export GOOGLE_OAUTH_ACCESS_TOKEN= + +Or if you are logged in you can authorize by generating an access token: + +.. code:: sh + + $ export GOOGLE_OAUTH_ACCESS_TOKEN="$(gcloud auth print-access-token)" + Encrypting/decrypting with GCP KMS requires a KMS ResourceID. You can use the cloud console the get the ResourceID or you can create one using the gcloud sdk: @@ -341,6 +378,11 @@ a key. This has the following form:: https://${VAULT_URL}/keys/${KEY_NAME}/${KEY_VERSION} +You can omit the version, and have just a trailing slash, and this will use +whatever the latest version of the key is:: + + https://${VAULT_URL}/keys/${KEY_NAME}/ + To create a Key Vault and assign your service principal permissions on it from the commandline: @@ -364,6 +406,10 @@ Now you can encrypt a file using:: $ sops encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/some-string test.yaml > test.enc.yaml +or, without the version:: + + $ sops encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/ test.yaml > test.enc.yaml + And decrypt it using:: $ sops decrypt test.enc.yaml @@ -375,33 +421,38 @@ Encrypting and decrypting from other programs When using ``sops`` in scripts or from other programs, there are often situations where you do not want to write encrypted or decrypted data to disk. The best way to avoid this is to pass data to SOPS via stdin, and to let SOPS write data to stdout. By default, the encrypt and decrypt operations write data to stdout already. To pass -data via stdin, you need to pass ``/dev/stdin`` as the input filename. Please note that this only works on -Unix-like operating systems such as macOS and Linux. On Windows, you have to use named pipes. +data via stdin, you need to not provide an input filename. For encryption, you also must provide the +``--filename-override`` option with the file's filename. The filename will be used to determine the input and output +types, and to select the correct creation rule. -To decrypt data, you can simply do: +The simplest way to decrypt data from stdin is as follows: .. code:: sh - $ cat encrypted-data | sops decrypt /dev/stdin > decrypted-data + $ cat encrypted-data | sops decrypt > decrypted-data -To control the input and output format, pass ``--input-type`` and ``--output-type`` as appropriate. By default, -``sops`` determines the input and output format from the provided filename, which is ``/dev/stdin`` here, and -thus will use the binary store which expects JSON input and outputs binary data on decryption. +By default, ``sops`` determines the input and output format from the provided filename. Since in this case, +no filename is provided, ``sops`` will use the binary store which expects JSON input and outputs binary data +on decryption. This is often not what you want. -For example, to decrypt YAML data and obtain the decrypted result as YAML, use: +To avoid this, you can either provide a filename with ``--filename-override``, or explicitly control +the input and output formats by passing ``--input-type`` and ``--output-type`` as appropriate: .. code:: sh - $ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml /dev/stdin > decrypted-data + $ cat encrypted-data | sops decrypt --filename-override filename.yaml > decrypted-data + $ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml > decrypted-data + +In both cases, ``sops`` will assume that the data you provide is in YAML format, and will encode the decrypted +data in YAML as well. The second form allows to use different formats for input and output. To encrypt, it is important to note that SOPS also uses the filename to look up the correct creation rule from -``.sops.yaml``. Likely ``/dev/stdin`` will not match a creation rule, or only match the fallback rule without -``path_regex``, which is usually not what you want. For that, ``sops`` provides the ``--filename-override`` -parameter which allows you to tell SOPS which filename to use to match creation rules: +``.sops.yaml``. Therefore, you must provide the ``--filename-override`` parameter which allows you to tell +SOPS which filename to use to match creation rules: .. code:: sh - $ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml /dev/stdin > encrypted-data + $ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml > encrypted-data SOPS will find a matching creation rule for ``path/filename.sops.yaml`` in ``.sops.yaml`` and use that one to encrypt the data from stdin. This filename will also be used to determine the input and output store. As always, @@ -410,7 +461,7 @@ the input store type can be adjusted by passing ``--input-type``, and the output .. code:: sh - $ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv /dev/stdin > encrypted-data + $ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv > encrypted-data Encrypting using Hashicorp Vault @@ -577,7 +628,7 @@ disabled by supplying the ``-y`` flag. ****************** The ``rotate`` command generates a new data encryption key and reencrypt all values -with the new key. At te same time, the command line flag ``--add-kms``, ``--add-pgp``, +with the new key. At the same time, the command line flag ``--add-kms``, ``--add-pgp``, ``--add-gcp-kms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms`` and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms`` and ``--azure-kv`` @@ -859,14 +910,6 @@ Example: place the following in your ``~/.bashrc`` SOPS_GPG_EXEC = 'your_gpg_client_wrapper' -Specify a different GPG key server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, SOPS uses the key server ``keys.openpgp.org`` to retrieve the GPG -keys that are not present in the local keyring. -This is no longer configurable. You can learn more about why from this write-up: `SKS Keyserver Network Under Attack `_. - - Key groups ~~~~~~~~~~ @@ -1027,7 +1070,7 @@ service exposed on the unix socket located in ``/tmp/sops.sock``, you can run: .. code:: sh - $ sops decrypt --keyservice unix:///tmp/sops.sock file.yaml` + $ sops decrypt --keyservice unix:///tmp/sops.sock file.yaml And if you only want to use the key service exposed on the unix socket located in ``/tmp/sops.sock`` and not the local key service, you can run: @@ -1123,6 +1166,11 @@ written to disk. $ echo your password: $database_password your password: +If you want process signals to be sent to the command, for example if you are +running ``exec-env`` to launch a server and your server handles SIGTERM, then the +``--same-process`` flag can be used to instruct ``sops`` to start your command in +the same process instead of a child process. This uses the ``execve`` system call +and is supported on Unix-like systems. If the command you want to run only operates on files, you can use ``exec-file`` instead. By default, SOPS will use a FIFO to pass the contents of the @@ -1308,7 +1356,7 @@ When operating on stdin, use the ``--input-type`` and ``--output-type`` flags as .. code:: sh - $ cat myfile.json | sops decrypt --input-type json --output-type json /dev/stdin + $ cat myfile.json | sops decrypt --input-type json --output-type json JSON and JSON_binary indentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1485,7 +1533,7 @@ original file after encrypting or decrypting it. Encrypting binary files ~~~~~~~~~~~~~~~~~~~~~~~ -SOPS primary use case is encrypting YAML and JSON configuration files, but it +SOPS primary use case is encrypting YAML, JSON, ENV, and INI configuration files, but it also has the ability to manage binary files. When encrypting a binary, SOPS will read the data as bytes, encrypt it, store the encrypted base64 under ``tree['data']`` and write the result as JSON. @@ -1568,6 +1616,17 @@ The value must be formatted as json. $ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '{"uid1":null,"uid2":1000,"uid3":["bob"]}' +You can also provide the value from a file or stdin: + +.. code:: sh + + # Provide the value from a file + $ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' > /tmp/example-value + $ sops set --value-file ~/git/svc/sops/example.yaml '["an_array"][1]' /tmp/example-value + + # Provide the value from stdin + $ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' | sops set --value-stdin ~/git/svc/sops/example.yaml '["an_array"][1]' + Unset a sub-part in a document tree ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1619,9 +1678,9 @@ git client interfaces, because they call git diff under the hood! Encrypting only parts of a file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Note: this only works on YAML and JSON files, not on BINARY files. +Note: this only works on YAML, JSON, ENV, and INI files, not on BINARY files. -By default, SOPS encrypts all the values of a YAML or JSON file and leaves the +By default, SOPS encrypts all the values of a YAML, JSON, ENV, or INI file and leaves the keys in cleartext. In some instances, you may want to exclude some values from being encrypted. This can be accomplished by adding the suffix **_unencrypted** to any key of a file. When set, all values underneath the key that set the @@ -1832,9 +1891,9 @@ automation, we found this to be a hard problem with a number of prerequisites: git repo, jenkins and S3) and only be decrypted on the target systems -SOPS can be used to encrypt YAML, JSON and BINARY files. In BINARY mode, the +SOPS can be used to encrypt YAML, JSON, ENV, INI, and BINARY files. In BINARY mode, the content of the file is treated as a blob, the same way PGP would encrypt an -entire file. In YAML and JSON modes, however, the content of the file is +entire file. In YAML, JSON, ENV, and INI modes, however, the content of the file is manipulated as a tree where keys are stored in cleartext, and values are encrypted. hiera-eyaml does something similar, and over the years we learned to appreciate its benefits, namely: diff --git a/aes/cipher.go b/aes/cipher.go index 291f2fedfb..e1009f2a5b 100644 --- a/aes/cipher.go +++ b/aes/cipher.go @@ -11,6 +11,7 @@ import ( "fmt" "regexp" "strconv" + "time" "github.com/getsops/sops/v3" "github.com/getsops/sops/v3/logging" @@ -110,6 +111,10 @@ func (c Cipher) Decrypt(ciphertext string, key []byte, additionalData string) (p plaintext = decryptedBytes case "bool": plaintext, err = strconv.ParseBool(decryptedValue) + case "time": + var value time.Time + err = value.UnmarshalText(decryptedBytes) + plaintext = value case "comment": plaintext = sops.Comment{Value: decryptedValue} default: @@ -176,6 +181,12 @@ func (c Cipher) Encrypt(plaintext interface{}, key []byte, additionalData string } else { plainBytes = []byte("False") } + case time.Time: + encryptedType = "time" + plainBytes, err = value.MarshalText() + if err != nil { + return "", fmt.Errorf("Error marshaling timestamp %q: %w", value, err) + } case sops.Comment: encryptedType = "comment" plainBytes = []byte(value.Value) diff --git a/aes/cipher_test.go b/aes/cipher_test.go index 4d53510aab..2c2421faf5 100644 --- a/aes/cipher_test.go +++ b/aes/cipher_test.go @@ -1,13 +1,16 @@ package aes import ( + "bytes" "crypto/rand" + "reflect" "strings" "testing" "testing/quick" + "time" - "github.com/stretchr/testify/assert" "github.com/getsops/sops/v3" + "github.com/stretchr/testify/assert" ) func TestDecrypt(t *testing.T) { @@ -108,6 +111,36 @@ func TestRoundtripBool(t *testing.T) { } } +func TestRoundtripTime(t *testing.T) { + key := []byte(strings.Repeat("f", 32)) + parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + assert.Nil(t, err) + loc := time.FixedZone("", 12300) // offset must be divisible by 60, otherwise won't survive a round-trip + values := []time.Time{ + time.UnixMilli(0).In(time.UTC), + time.UnixMilli(123456).In(time.UTC), + time.UnixMilli(123456).In(loc), + time.UnixMilli(123456789).In(time.UTC), + time.UnixMilli(123456789).In(loc), + time.UnixMilli(1234567890).In(time.UTC), + time.UnixMilli(1234567890).In(loc), + parsedTime, + } + for _, value := range values { + s, err := NewCipher().Encrypt(value, key, "foo") + assert.Nil(t, err) + if err != nil { + continue + } + d, err := NewCipher().Decrypt(s, key, "foo") + assert.Nil(t, err) + if err != nil { + continue + } + assert.Equal(t, value, d) + } +} + func TestEncryptEmptyComment(t *testing.T) { key := []byte(strings.Repeat("f", 32)) s, err := NewCipher().Encrypt(sops.Comment{}, key, "") @@ -121,3 +154,61 @@ func TestDecryptEmptyValue(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "", s) } + +// This test would belong more in sops_test.go, but from there we cannot access +// the aes package to get a cipher which can actually handle time.Time objects. +func TestTimestamps(t *testing.T) { + unixTime := time.UnixMilli(123456789).In(time.UTC) + parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + assert.Nil(t, err) + branches := sops.TreeBranches{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: unixTime, + }, + sops.TreeItem{ + Key: "bar", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: parsedTime, + }, + }, + }, + }, + } + tree := sops.Tree{Branches: branches, Metadata: sops.Metadata{UnencryptedSuffix: "_unencrypted"}} + expected := sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: unixTime, + }, + sops.TreeItem{ + Key: "bar", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: parsedTime, + }, + }, + }, + } + cipher := NewCipher() + _, err = tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Encrypting the tree failed: %s", err) + } + if reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees do match: \ngot \t\t%+v,\n not expected \t\t%+v", tree.Branches[0], expected) + } + _, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Decrypting the tree failed: %s", err) + } + assert.Equal(t, tree.Branches[0][0].Value, unixTime) + assert.Equal(t, tree.Branches[0], expected) + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected) + } +} diff --git a/age/encrypted_keys.go b/age/encrypted_keys.go new file mode 100644 index 0000000000..922564da71 --- /dev/null +++ b/age/encrypted_keys.go @@ -0,0 +1,190 @@ +// These functions have been copied from the age project +// https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/encrypted_keys.go +// https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/parse.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + + "filippo.io/age" + "filippo.io/age/armor" + + gpgagent "github.com/getsops/gopgagent" +) + +type EncryptedIdentity struct { + Contents []byte + Passphrase func() (string, error) + NoMatchWarning func() + IncorrectPassphrase func() + + identities []age.Identity +} + +var _ age.Identity = &EncryptedIdentity{} + +func (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { + if i.identities == nil { + if err := i.decrypt(); err != nil { + return nil, err + } + } + + for _, id := range i.identities { + fileKey, err = id.Unwrap(stanzas) + if errors.Is(err, age.ErrIncorrectIdentity) { + continue + } + if err != nil { + return nil, err + } + return fileKey, nil + } + i.NoMatchWarning() + return nil, age.ErrIncorrectIdentity +} + +func (i *EncryptedIdentity) decrypt() error { + d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase}) + if e := new(age.NoIdentityMatchError); errors.As(err, &e) { + // ScryptIdentity returns ErrIncorrectIdentity for an incorrect + // passphrase, which would lead Decrypt to returning "no identity + // matched any recipient". That makes sense in the API, where there + // might be multiple configured ScryptIdentity. Since in cmd/age there + // can be only one, return a better error message. + i.IncorrectPassphrase() + return fmt.Errorf("incorrect passphrase") + } + if err != nil { + return fmt.Errorf("failed to decrypt identity file: %v", err) + } + i.identities, err = age.ParseIdentities(d) + return err +} + +// LazyScryptIdentity is an age.Identity that requests a passphrase only if it +// encounters an scrypt stanza. After obtaining a passphrase, it delegates to +// ScryptIdentity. +type LazyScryptIdentity struct { + Passphrase func() (string, error) +} + +var _ age.Identity = &LazyScryptIdentity{} + +func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { + for _, s := range stanzas { + if s.Type == "scrypt" && len(stanzas) != 1 { + return nil, errors.New("an scrypt recipient must be the only one") + } + } + if len(stanzas) != 1 || stanzas[0].Type != "scrypt" { + return nil, age.ErrIncorrectIdentity + } + pass, err := i.Passphrase() + if err != nil { + return nil, fmt.Errorf("could not read passphrase: %v", err) + } + ii, err := age.NewScryptIdentity(pass) + if err != nil { + return nil, err + } + fileKey, err = ii.Unwrap(stanzas) + return fileKey, err +} + +func unwrapIdentities(location string, reader io.Reader) (ParsedIdentities, error) { + b := bufio.NewReader(reader) + p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE" + peeked := string(p) + + switch { + // An age encrypted file, plain or armored. + case peeked == "age-encryption" || peeked == "-----BEGIN AGE": + var r io.Reader = b + if peeked == "-----BEGIN AGE" { + r = armor.NewReader(r) + } + const privateKeySizeLimit = 1 << 24 // 16 MiB + contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit)) + if err != nil { + return nil, fmt.Errorf("failed to read '%s': %w", location, err) + } + if len(contents) == privateKeySizeLimit { + return nil, fmt.Errorf("failed to read '%s': file too long", location) + } + IncorrectPassphrase := func() { + conn, err := gpgagent.NewConn() + if err != nil { + return + } + defer func(conn *gpgagent.Conn) { + if err := conn.Close(); err != nil { + log.Errorf("failed to close connection with gpg-agent: %s", err) + } + }(conn) + err = conn.RemoveFromCache(location) + if err != nil { + log.Warnf("gpg-agent remove cache request errored: %s", err) + return + } + } + ids := []age.Identity{&EncryptedIdentity{ + Contents: contents, + Passphrase: func() (string, error) { + conn, err := gpgagent.NewConn() + if err != nil { + passphrase, err := readSecret(fmt.Sprintf("Enter passphrase for identity '%s':", location)) + if err != nil { + return "", err + } + return string(passphrase), nil + } + defer func(conn *gpgagent.Conn) { + if err := conn.Close(); err != nil { + log.Errorf("failed to close connection with gpg-agent: %s", err) + } + }(conn) + + req := gpgagent.PassphraseRequest{ + // TODO is the cachekey good enough? + CacheKey: location, + Prompt: "Passphrase", + Desc: fmt.Sprintf("Enter passphrase for identity '%s':", location), + } + pass, err := conn.GetPassphrase(&req) + if err != nil { + return "", fmt.Errorf("gpg-agent passphrase request errored: %s", err) + } + //make sure that we won't store empty pass + if len(pass) == 0 { + IncorrectPassphrase() + } + return pass, nil + }, + IncorrectPassphrase: IncorrectPassphrase, + NoMatchWarning: func() { + log.Warnf("encrypted identity '%s' didn't match file's recipients", location) + }, + }} + return ids, nil + // An unencrypted age identity file. + default: + ids, err := parseIdentities(b) + if err != nil { + return nil, fmt.Errorf("failed to parse '%s' age identities: %w", location, err) + } + return ids, nil + } +} diff --git a/age/keysource.go b/age/keysource.go index 83bdbe0a6d..35ca024cfd 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -1,20 +1,25 @@ package age import ( + "bufio" "bytes" "errors" "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "strings" "filippo.io/age" + "filippo.io/age/agessh" "filippo.io/age/armor" + "filippo.io/age/plugin" "github.com/sirupsen/logrus" "github.com/getsops/sops/v3/logging" + "github.com/google/shlex" ) const ( @@ -24,6 +29,12 @@ const ( // SopsAgeKeyFileEnv can be set as an environment variable pointing to an // age keys file. SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE" + // SopsAgeKeyCmdEnv can be set as an environment variable with a command + // to execute that returns the age keys. + SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD" + // SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to + // a private SSH key file. + SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE" // SopsAgeKeyUserConfigPath is the default age keys file path in // getUserConfigDir(). SopsAgeKeyUserConfigPath = "sops/age/keys.txt" @@ -60,7 +71,7 @@ type MasterKey struct { parsedIdentities []age.Identity // parsedRecipient contains a parsed age public key. // It is used to lazy-load the Recipient at-most once. - parsedRecipient *age.X25519Recipient + parsedRecipient age.Recipient } // MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded @@ -83,6 +94,18 @@ func MasterKeysFromRecipients(commaSeparatedRecipients string) ([]*MasterKey, er return keys, nil } +// errSet is a collection of captured errors. +type errSet []error + +// Error joins the errors into a "; " separated string. +func (e errSet) Error() string { + str := make([]string, len(e)) + for i, err := range e { + str[i] = err.Error() + } + return strings.Join(str, "; ") +} + // MasterKeyFromRecipient takes a Bech32-encoded age public key, parses it, and // returns a new MasterKey. func MasterKeyFromRecipient(recipient string) (*MasterKey, error) { @@ -111,7 +134,10 @@ type ParsedIdentities []age.Identity // parsing (using age.ParseIdentities) and appending to the slice yourself, in // combination with e.g. a sync.Mutex. func (i *ParsedIdentities) Import(identity ...string) error { - identities, err := parseIdentities(identity...) + // one identity per line + r := strings.NewReader(strings.Join(identity, "\n")) + + identities, err := parseIdentities(r) if err != nil { return fmt.Errorf("failed to parse and add to age identities: %w", err) } @@ -180,14 +206,41 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { key.EncryptedKey = string(enc) } +func formatError(msg string, err error, errs errSet, unusedLocations []string) error { + var loadSuffix string + if len(errs) > 0 { + loadSuffix = fmt.Sprintf(". Errors while loading age identities: %s", errs.Error()) + } + var unusedSuffix string + if len(unusedLocations) > 0 { + count := len(unusedLocations) + if count == 1 { + unusedSuffix = fmt.Sprintf(" '%s'", unusedLocations[0]) + } else if count == 2 { + unusedSuffix = fmt.Sprintf("s '%s' and '%s'", unusedLocations[0], unusedLocations[1]) + } else { + unusedSuffix = fmt.Sprintf("s '%s', and '%s'", strings.Join(unusedLocations[:count - 1], "', '"), unusedLocations[count - 1]) + } + unusedSuffix = fmt.Sprintf(". Did not find keys in location%s.", unusedSuffix) + } + if err != nil { + return fmt.Errorf("%s: %w%s%s", msg, err, loadSuffix, unusedSuffix) + } else { + return fmt.Errorf("%s%s%s", msg, loadSuffix, unusedSuffix) + } +} + // Decrypt decrypts the EncryptedKey with the parsed or loaded identities, and // returns the result. func (key *MasterKey) Decrypt() ([]byte, error) { + var errs errSet + var unusedLocations []string if len(key.parsedIdentities) == 0 { - ids, err := key.loadIdentities() - if err != nil { + var ids ParsedIdentities + ids, unusedLocations, errs = key.loadIdentities() + if len(ids) == 0 { log.Info("Decryption failed") - return nil, fmt.Errorf("failed to load age identities: %w", err) + return nil, formatError("failed to load age identities", nil, errs, unusedLocations) } ids.ApplyToMasterKey(key) } @@ -197,7 +250,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { r, err := age.Decrypt(ar, key.parsedIdentities...) if err != nil { log.Info("Decryption failed") - return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w", err) + return nil, formatError("failed to create reader for decrypting sops data key with age", err, errs, unusedLocations) } var b bytes.Buffer @@ -233,6 +286,61 @@ func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } +// loadAgeSSHIdentity attempts to load the age SSH identity based on an SSH +// private key from the SopsAgeSshPrivateKeyFileEnv environment variable. If the +// environment variable is not present, it will fall back to `~/.ssh/id_ed25519` +// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil. +func loadAgeSSHIdentities() ([]age.Identity, []string, errSet) { + var identities []age.Identity + var unusedLocations []string + var errs errSet + + sshKeyFilePath, ok := os.LookupEnv(SopsAgeSshPrivateKeyFileEnv) + if ok { + identity, err := parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath) + if err != nil { + errs = append(errs, err) + } else { + identities = append(identities, identity) + } + } else { + unusedLocations = append(unusedLocations, SopsAgeSshPrivateKeyFileEnv) + } + + userHomeDir, err := os.UserHomeDir() + if err != nil { + errs = append(errs, err) + } else if userHomeDir == "" { + log.Warnf("could not determine the user home directory: %v", err) + } else { + sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519") + if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil { + identity, err := parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath) + if err != nil { + errs = append(errs, err) + } else { + identities = append(identities, identity) + } + } else { + unusedLocations = append(unusedLocations, sshEd25519PrivateKeyPath) + } + + sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa") + if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil { + identity, err := parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath) + if err != nil { + errs = append(errs, err) + } else { + identities = append(identities, identity) + } + } else { + unusedLocations = append(unusedLocations, sshRsaPrivateKeyPath) + } + } + + return identities, unusedLocations, errs +} + func getUserConfigDir() (string, error) { if runtime.GOOS == "darwin" { if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" { @@ -244,76 +352,138 @@ func getUserConfigDir() (string, error) { // loadIdentities attempts to load the age identities based on runtime // environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv, -// SopsAgeKeyUserConfigPath). It will load all found references, and expects -// at least one configuration to be present. -func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { +// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all +// found references, and expects at least one configuration to be present. +func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) { + identities, unusedLocations, errs := loadAgeSSHIdentities() + var readers = make(map[string]io.Reader, 0) if ageKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok { readers[SopsAgeKeyEnv] = strings.NewReader(ageKey) + } else { + unusedLocations = append(unusedLocations, SopsAgeKeyEnv) } if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok { f, err := os.Open(ageKeyFile) if err != nil { - return nil, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err) + errs = append(errs, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err)) + } else { + defer f.Close() + readers[SopsAgeKeyFileEnv] = f } - defer f.Close() - readers[SopsAgeKeyFileEnv] = f + } else { + unusedLocations = append(unusedLocations, SopsAgeKeyFileEnv) } - userConfigDir, err := getUserConfigDir() - if err != nil && len(readers) == 0 { - return nil, fmt.Errorf("user config directory could not be determined: %w", err) + if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok { + args, err := shlex.Split(ageKeyCmd) + if err != nil { + errs = append(errs, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)) + } else { + out, err := exec.Command(args[0], args[1:]...).Output() + if err != nil { + errs = append(errs, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)) + } else { + readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out) + } + } + } else { + unusedLocations = append(unusedLocations, SopsAgeKeyCmdEnv) } - if userConfigDir != "" { + + userConfigDir, err := getUserConfigDir() + if err != nil && len(readers) == 0 && len(identities) == 0 { + errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err)) + } else if userConfigDir != "" { ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath)) f, err := os.Open(ageKeyFilePath) if err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("failed to open file: %w", err) - } - if errors.Is(err, os.ErrNotExist) && len(readers) == 0 { - // If we have no other readers, presence of the file is required. - return nil, fmt.Errorf("failed to open file: %w", err) - } - if err == nil { + errs = append(errs, fmt.Errorf("failed to open file: %w", err)) + } else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 { + unusedLocations = append(unusedLocations, ageKeyFilePath) + } else if err == nil { defer f.Close() readers[ageKeyFilePath] = f } } - var identities ParsedIdentities - for n, r := range readers { - ids, err := age.ParseIdentities(r) + for location, r := range readers { + ids, err := unwrapIdentities(location, r) if err != nil { - return nil, fmt.Errorf("failed to parse '%s' age identities: %w", n, err) + errs = append(errs, err) + } else { + identities = append(identities, ids...) + if len(ids) == 0 { + unusedLocations = append(unusedLocations, location) + } } - identities = append(identities, ids...) } - return identities, nil + return identities, unusedLocations, errs } // parseRecipient attempts to parse a string containing an encoded age public -// key. -func parseRecipient(recipient string) (*age.X25519Recipient, error) { - parsedRecipient, err := age.ParseX25519Recipient(recipient) - if err != nil { - return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) +// key or a public ssh key. +func parseRecipient(recipient string) (age.Recipient, error) { + switch { + case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1: + parsedRecipient, err := plugin.NewRecipient(recipient, pluginTerminalUI) + if err != nil { + return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err) + } + return parsedRecipient, nil + case strings.HasPrefix(recipient, "age1"): + parsedRecipient, err := age.ParseX25519Recipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) + } + + return parsedRecipient, nil + case strings.HasPrefix(recipient, "ssh-"): + parsedRecipient, err := agessh.ParseRecipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as age-ssh public key: %w", err) + } + return parsedRecipient, nil } - return parsedRecipient, nil + + return nil, fmt.Errorf("failed to parse input, unknown recipient type: %q", recipient) } -// parseIdentities attempts to parse the string set of encoded age identities. -// A single identity argument is allowed to be a multiline string containing -// multiple identities. Empty lines and lines starting with "#" are ignored. -func parseIdentities(identity ...string) (ParsedIdentities, error) { - var identities []age.Identity - for _, i := range identity { - parsed, err := age.ParseIdentities(strings.NewReader(i)) +// parseIdentities attempts to parse one or more age identities from the provided reader. +// One identity per line. +// Empty lines and lines starting with "#" are ignored. +func parseIdentities(r io.Reader) (ParsedIdentities, error) { + var identities ParsedIdentities + + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parsed, err := parseIdentity(line) if err != nil { return nil, err } - identities = append(identities, parsed...) + + identities = append(identities, parsed) } + return identities, nil } + +func parseIdentity(s string) (age.Identity, error) { + switch { + case strings.HasPrefix(s, "AGE-PLUGIN-"): + return plugin.NewIdentity(s, pluginTerminalUI) + case strings.HasPrefix(s, "AGE-SECRET-KEY-1"): + return age.ParseX25519Identity(s) + default: + return nil, fmt.Errorf("unknown identity type") + } +} diff --git a/age/keysource_test.go b/age/keysource_test.go index 1a07058a6a..94b44cdc9f 100644 --- a/age/keysource_test.go +++ b/age/keysource_test.go @@ -28,6 +28,35 @@ EylloI7MNGbadPGb -----END AGE ENCRYPTED FILE-----` // mockEncryptedKeyPlain is the plain value of mockEncryptedKey. mockEncryptedKeyPlain string = "data" + // passphrase used to encrypt age identity. + mockIdentityPassphrase string = "passphrase" + mockEncryptedIdentity string = `-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCBMN2FXZW9xSFViYjdNeW5D +dy9iSHFnIDE4Ck9zV0ZoNldmci9rL3VXd3BtZmQvK3VZWEpBQjdhZ0UrcmhqR2lF +YThFMzAKLS0tIGVEQ0xwODI1TlNYeHNHaHZKWHoyLzYwMTMvTGhaZG1oa203cSs0 +VUpBL1kKsaTnt+H/z8mkL21UYKIt3YMpWSV/oYqTm1cSSUnF9InZEYU9HndK9rc8 +ni+MTJCmYf4mgvvGPMf7oIQvs6ijaTdlQb+zeQsL4eif20w+CWgvPNrS6iXUIs8W +w5/fHsxwmrkG96nDkMErJKhmjmLpC+YdbiMe6P/KIpas09m08RTIqcz7ua0Xm3ey +ndU+8ILJOhcnWV55W43nTw/UUFse7f+qY61n7kcd1sGd7ZfSEdEIqS3K2vEtA3ER +fn0s3cyXVEBxL9OZqcAk45bCFVOl13Fp/DBfquHEjvAyeg0= +-----END AGE ENCRYPTED FILE-----` + // mockSshRecipient is a mock age ssh recipient, it matches mockSshIdentity + mockSshRecipient string = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID+Wi8WZw2bXfBpcs/WECttCzP39OkenS6pHWHWGFJvN Test" + // mockSshIdentity is a mock age identity based on an OpenSSH private key (ed25519) + mockSshIdentity string = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQAAAIgCXDMIAlwz +CAAAAAtzc2gtZWQyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQ +AAAEBJdWTJ8dC0OnMcwy4gQ96sp6KG8GE9EiyhFGhKldKiST+Wi8WZw2bXfBpcs/WECttC +zP39OkenS6pHWHWGFJvNAAAABFRlc3QB +-----END OPENSSH PRIVATE KEY-----` + mockEncryptedSshKey string = `-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDJjd0R4dyB2R3Ns +VUNHaXBiTEJaNU5BMFFQZUpCYWJqODFyTTZ4WWZoRVpUd2M2aTBFCkduUFJHb1U2 +K3RqWVQrLzE4anZKZ3h2T3c2MFpZTHlGaHprcElXenByWTAKLS0tIG56MHFSZERl +em9PWmRMMTY4aytYTnVZN04yeER5Z2E3TWxWT3JTZWR2ekUKp/HZLy4MzQqoszGk ++P0hSPPNhOhvFwv4AqCw1+A+WyeHGQPq +-----END AGE ENCRYPTED FILE-----` ) func TestMasterKeysFromRecipients(t *testing.T) { @@ -41,22 +70,32 @@ func TestMasterKeysFromRecipients(t *testing.T) { assert.Equal(t, got[0].Recipient, mockRecipient) }) + t.Run("recipient-ssh", func(t *testing.T) { + got, err := MasterKeysFromRecipients(mockSshRecipient) + assert.NoError(t, err) + + assert.Len(t, got, 1) + assert.Equal(t, got[0].Recipient, mockSshRecipient) + }) + t.Run("recipients", func(t *testing.T) { - got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient) + got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient + "," + mockSshRecipient) assert.NoError(t, err) - assert.Len(t, got, 2) + assert.Len(t, got, 3) assert.Equal(t, got[0].Recipient, mockRecipient) assert.Equal(t, got[1].Recipient, otherRecipient) + assert.Equal(t, got[2].Recipient, mockSshRecipient) }) t.Run("leading and trailing spaces", func(t *testing.T) { - got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " ") + got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " , " + mockSshRecipient + " ") assert.NoError(t, err) - assert.Len(t, got, 2) + assert.Len(t, got, 3) assert.Equal(t, got[0].Recipient, mockRecipient) assert.Equal(t, got[1].Recipient, otherRecipient) + assert.Equal(t, got[2].Recipient, mockSshRecipient) }) t.Run("empty", func(t *testing.T) { @@ -75,6 +114,14 @@ func TestMasterKeyFromRecipient(t *testing.T) { assert.Nil(t, got.parsedIdentities) }) + t.Run("recipient-ssh", func(t *testing.T) { + got, err := MasterKeyFromRecipient(mockSshRecipient) + assert.NoError(t, err) + assert.EqualValues(t, mockSshRecipient, got.Recipient) + assert.NotNil(t, got.parsedRecipient) + assert.Nil(t, got.parsedIdentities) + }) + t.Run("leading and trailing spaces", func(t *testing.T) { got, err := MasterKeyFromRecipient(" " + mockRecipient + " ") assert.NoError(t, err) @@ -83,6 +130,14 @@ func TestMasterKeyFromRecipient(t *testing.T) { assert.Nil(t, got.parsedIdentities) }) + t.Run("leading and trailing spaces - ssh", func(t *testing.T) { + got, err := MasterKeyFromRecipient(" " + mockSshRecipient + " ") + assert.NoError(t, err) + assert.EqualValues(t, mockSshRecipient, got.Recipient) + assert.NotNil(t, got.parsedRecipient) + assert.Nil(t, got.parsedIdentities) + }) + t.Run("invalid recipient", func(t *testing.T) { got, err := MasterKeyFromRecipient("invalid") assert.Error(t, err) @@ -111,6 +166,8 @@ func TestParsedIdentities_ApplyToMasterKey(t *testing.T) { func TestMasterKey_Encrypt(t *testing.T) { mockParsedRecipient, err := parseRecipient(mockRecipient) assert.NoError(t, err) + mockSshParsedRecipient, err := parseRecipient(mockSshRecipient) + assert.NoError(t, err) t.Run("recipient", func(t *testing.T) { key := &MasterKey{ @@ -120,6 +177,14 @@ func TestMasterKey_Encrypt(t *testing.T) { assert.NotEmpty(t, key.EncryptedKey) }) + t.Run("recipient ssh", func(t *testing.T) { + key := &MasterKey{ + Recipient: mockSshRecipient, + } + assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain))) + assert.NotEmpty(t, key.EncryptedKey) + }) + t.Run("parsed recipient", func(t *testing.T) { key := &MasterKey{ parsedRecipient: mockParsedRecipient, @@ -128,13 +193,21 @@ func TestMasterKey_Encrypt(t *testing.T) { assert.NotEmpty(t, key.EncryptedKey) }) + t.Run("parsed recipient ssh", func(t *testing.T) { + key := &MasterKey{ + parsedRecipient: mockSshParsedRecipient, + } + assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain))) + assert.NotEmpty(t, key.EncryptedKey) + }) + t.Run("invalid recipient", func(t *testing.T) { key := &MasterKey{ Recipient: "invalid", } err := key.Encrypt([]byte(mockEncryptedKeyPlain)) assert.Error(t, err) - assert.ErrorContains(t, err, "failed to parse input as Bech32-encoded age public key") + assert.ErrorContains(t, err, "failed to parse input, unknown recipient type:") assert.Empty(t, key.EncryptedKey) }) @@ -180,6 +253,7 @@ func TestMasterKey_Decrypt(t *testing.T) { }) t.Run("loaded identities", func(t *testing.T) { + overwriteUserConfigDir(t, t.TempDir()) key := &MasterKey{EncryptedKey: mockEncryptedKey} t.Setenv(SopsAgeKeyEnv, mockIdentity) @@ -188,6 +262,25 @@ func TestMasterKey_Decrypt(t *testing.T) { assert.EqualValues(t, mockEncryptedKeyPlain, got) }) + t.Run("loaded identities ssh", func(t *testing.T) { + key := &MasterKey{EncryptedKey: mockEncryptedSshKey} + tmp := t.TempDir() + overwriteUserConfigDir(t, tmp) + + homeDir, err := os.UserHomeDir() + assert.NoError(t, err) + keyPath := filepath.Join(homeDir, ".ssh/id_25519") + assert.True(t, strings.HasPrefix(keyPath, homeDir)) + + assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700)) + assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644)) + t.Setenv(SopsAgeSshPrivateKeyFileEnv, keyPath) + + got, err := key.Decrypt() + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + t.Run("no identities", func(t *testing.T) { tmpDir := t.TempDir() overwriteUserConfigDir(t, tmpDir) @@ -215,6 +308,7 @@ func TestMasterKey_Decrypt(t *testing.T) { }) t.Run("invalid encrypted key", func(t *testing.T) { + overwriteUserConfigDir(t, t.TempDir()) key := &MasterKey{EncryptedKey: "invalid"} t.Setenv(SopsAgeKeyEnv, mockIdentity) @@ -275,9 +369,10 @@ func TestMasterKey_loadIdentities(t *testing.T) { t.Setenv(SopsAgeKeyEnv, mockIdentity) key := &MasterKey{} - got, err := key.loadIdentities() - assert.NoError(t, err) + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 0) assert.Len(t, got, 1) + assert.Len(t, unusedLocations, 5) }) t.Run(SopsAgeKeyEnv+" multiple", func(t *testing.T) { @@ -288,9 +383,10 @@ func TestMasterKey_loadIdentities(t *testing.T) { t.Setenv(SopsAgeKeyEnv, mockIdentity+"\n"+mockOtherIdentity) key := &MasterKey{} - got, err := key.loadIdentities() - assert.NoError(t, err) + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 0) assert.Len(t, got, 2) + assert.Len(t, unusedLocations, 5) }) t.Run(SopsAgeKeyFileEnv, func(t *testing.T) { @@ -304,9 +400,10 @@ func TestMasterKey_loadIdentities(t *testing.T) { t.Setenv(SopsAgeKeyFileEnv, keyPath) key := &MasterKey{} - got, err := key.loadIdentities() - assert.NoError(t, err) + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 0) assert.Len(t, got, 1) + assert.Len(t, unusedLocations, 5) }) t.Run(SopsAgeKeyUserConfigPath, func(t *testing.T) { @@ -322,19 +419,40 @@ func TestMasterKey_loadIdentities(t *testing.T) { assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700)) assert.NoError(t, os.WriteFile(keyPath, []byte(mockIdentity), 0o644)) - got, err := (&MasterKey{}).loadIdentities() + got, unusedLocations, errs := (&MasterKey{}).loadIdentities() + assert.Len(t, errs, 0) + assert.Len(t, got, 1) + assert.Len(t, unusedLocations, 6) + }) + + t.Run(SopsAgeSshPrivateKeyFileEnv, func(t *testing.T) { + tmpDir := t.TempDir() + overwriteUserConfigDir(t, tmpDir) + + homeDir, err := os.UserHomeDir() assert.NoError(t, err) + keyPath := filepath.Join(homeDir, ".ssh/id_25519") + assert.True(t, strings.HasPrefix(keyPath, homeDir)) + + assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700)) + assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644)) + t.Setenv(SopsAgeSshPrivateKeyFileEnv, keyPath) + + key := &MasterKey{} + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 0) assert.Len(t, got, 1) + assert.Len(t, unusedLocations, 5) }) t.Run("no identity", func(t *testing.T) { tmpDir := t.TempDir() overwriteUserConfigDir(t, tmpDir) - got, err := (&MasterKey{}).loadIdentities() - assert.Error(t, err) - assert.ErrorContains(t, err, "failed to open file") + got, unusedLocations, errs := (&MasterKey{}).loadIdentities() + assert.Len(t, errs, 0) assert.Nil(t, got) + assert.Len(t, unusedLocations, 7) }) t.Run("multiple identities", func(t *testing.T) { @@ -354,9 +472,10 @@ func TestMasterKey_loadIdentities(t *testing.T) { assert.NoError(t, os.WriteFile(keyPath2, []byte(mockOtherIdentity), 0o644)) t.Setenv(SopsAgeKeyFileEnv, keyPath2) - got, err := (&MasterKey{}).loadIdentities() - assert.NoError(t, err) + got, unusedLocations, errs := (&MasterKey{}).loadIdentities() + assert.Len(t, errs, 0) assert.Len(t, got, 2) + assert.Len(t, unusedLocations, 5) }) t.Run("parsing error", func(t *testing.T) { @@ -367,15 +486,47 @@ func TestMasterKey_loadIdentities(t *testing.T) { t.Setenv(SopsAgeKeyEnv, "invalid") key := &MasterKey{} - got, err := key.loadIdentities() - assert.Error(t, err) - assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv)) + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 1) + assert.Error(t, errs[0]) + assert.ErrorContains(t, errs[0], fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv)) + assert.Nil(t, got) + assert.Len(t, unusedLocations, 5) + }) + + t.Run(SopsAgeKeyCmdEnv, func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "echo '"+mockIdentity+"'") + + key := &MasterKey{} + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 0) + assert.Len(t, got, 1) + assert.Len(t, unusedLocations, 5) + }) + + t.Run("cmd error", func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "meow") + + key := &MasterKey{} + got, unusedLocations, errs := key.loadIdentities() + assert.Len(t, errs, 1) + assert.Error(t, errs[0]) + assert.ErrorContains(t, errs[0], "failed to execute command meow") assert.Nil(t, got) + assert.Len(t, unusedLocations, 6) }) } -// overwriteUserConfigDir sets the user config directory based on the -// os.UserConfigDir logic. +// overwriteUserConfigDir sets the user config directory and the user home directory +// based on the os.UserConfigDir logic. func overwriteUserConfigDir(t *testing.T, path string) { switch runtime.GOOS { case "windows": @@ -384,6 +535,7 @@ func overwriteUserConfigDir(t *testing.T, path string) { t.Setenv("home", path) default: // Unix t.Setenv("XDG_CONFIG_HOME", path) + t.Setenv("HOME", path) } } @@ -400,3 +552,54 @@ func TestUserConfigDir(t *testing.T) { assert.Equal(t, home, dir) } } + +func TestMasterKey_Identities_Passphrase(t *testing.T) { + t.Run(SopsAgeKeyEnv, func(t *testing.T) { + key := &MasterKey{EncryptedKey: mockEncryptedKey} + t.Setenv(SopsAgeKeyEnv, mockEncryptedIdentity) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + + t.Run(SopsAgeKeyFileEnv, func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + keyPath := filepath.Join(tmpDir, "keys.txt") + assert.NoError(t, os.WriteFile(keyPath, []byte(mockEncryptedIdentity), 0o644)) + + key := &MasterKey{EncryptedKey: mockEncryptedKey} + t.Setenv(SopsAgeKeyFileEnv, keyPath) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + + t.Run("invalid encrypted key", func(t *testing.T) { + key := &MasterKey{EncryptedKey: "invalid"} + t.Setenv(SopsAgeKeyEnv, mockEncryptedIdentity) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to create reader for decrypting sops data key with age") + assert.Nil(t, got) + }) +} diff --git a/age/ssh_parse.go b/age/ssh_parse.go new file mode 100644 index 0000000000..467afc278c --- /dev/null +++ b/age/ssh_parse.go @@ -0,0 +1,84 @@ +// These functions are similar to those in the age project +// https://github.com/FiloSottile/age/blob/v1.0.0/cmd/age/parse.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "fmt" + "io" + "os" + + "filippo.io/age" + "filippo.io/age/agessh" + "golang.org/x/crypto/ssh" +) + +// readPublicKeyFile attempts to read a public key based on the given private +// key path. It assumes the public key is in the same directory, with the same +// name, but with a ".pub" extension. If the public key cannot be read, an +// error is returned. +func readPublicKeyFile(privateKeyPath string) (ssh.PublicKey, error) { + publicKeyPath := privateKeyPath + ".pub" + f, err := os.Open(publicKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to obtain public %q key for %q SSH key: %w", publicKeyPath, privateKeyPath, err) + } + defer f.Close() + contents, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read %q: %w", publicKeyPath, err) + } + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents) + if err != nil { + return nil, fmt.Errorf("failed to parse %q: %w", publicKeyPath, err) + } + return pubKey, nil +} + +// parseSSHIdentityFromPrivateKeyFile returns an age.Identity from the given +// private key file. If the private key file is encrypted, it will configure +// the identity to prompt for a passphrase. +func parseSSHIdentityFromPrivateKeyFile(keyPath string) (age.Identity, error) { + keyFile, err := os.Open(keyPath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer keyFile.Close() + contents, err := io.ReadAll(keyFile) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + id, err := agessh.ParseIdentity(contents) + if sshErr, ok := err.(*ssh.PassphraseMissingError); ok { + pubKey := sshErr.PublicKey + if pubKey == nil { + pubKey, err = readPublicKeyFile(keyPath) + if err != nil { + return nil, err + } + } + passphrasePrompt := func() ([]byte, error) { + pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", keyPath)) + if err != nil { + return nil, fmt.Errorf("could not read passphrase for %q: %v", keyPath, err) + } + return pass, nil + } + i, err := agessh.NewEncryptedSSHIdentity(pubKey, contents, passphrasePrompt) + if err != nil { + return nil, fmt.Errorf("could not create encrypted SSH identity: %w", err) + } + return i, nil + } + if err != nil { + return nil, fmt.Errorf("malformed SSH identity in %q: %w", keyPath, err) + } + return id, nil +} diff --git a/age/tui.go b/age/tui.go new file mode 100644 index 0000000000..35f9f3ad74 --- /dev/null +++ b/age/tui.go @@ -0,0 +1,173 @@ +// These functions have been copied from the age project +// https://github.com/FiloSottile/age/blob/3d91014ea095e8d70f7c6c4833f89b53a96e0832/cmd/age/tui.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "errors" + "filippo.io/age/plugin" + "fmt" + "io" + "os" + "runtime" + "testing" + + "golang.org/x/term" +) + +var testOnlyAgePassword string + +func printf(format string, v ...interface{}) { + log.Printf("age: "+format, v...) +} + +func warningf(format string, v ...interface{}) { + log.Printf("age: warning: "+format, v...) +} + +// clearLine clears the current line on the terminal, or opens a new line if +// terminal escape codes don't work. +func clearLine(out io.Writer) { + const ( + CUI = "\033[" // Control Sequence Introducer + CPL = CUI + "F" // Cursor Previous Line + EL = CUI + "K" // Erase in Line + ) + + // First, open a new line, which is guaranteed to work everywhere. Then, try + // to erase the line above with escape codes. + // + // (We use CRLF instead of LF to work around an apparent bug in WSL2's + // handling of CONOUT$. Only when running a Windows binary from WSL2, the + // cursor would not go back to the start of the line with a simple LF. + // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.) + fmt.Fprintf(out, "\r\n"+CPL+EL) +} + +// withTerminal runs f with the terminal input and output files, if available. +// withTerminal does not open a non-terminal stdin, so the caller does not need +// to check stdinInUse. +func withTerminal(f func(in, out *os.File) error) error { + if runtime.GOOS == "windows" { + in, err := os.OpenFile("CONIN$", os.O_RDWR, 0) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0) + if err != nil { + return err + } + defer out.Close() + return f(in, out) + } else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { + defer tty.Close() + return f(tty, tty) + } else if term.IsTerminal(int(os.Stdin.Fd())) { + return f(os.Stdin, os.Stdin) + } else { + return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err) + } +} + +// readSecret reads a value from the terminal with no echo. The prompt is ephemeral. +func readSecret(prompt string) (s []byte, err error) { + if testing.Testing() { + if testOnlyAgePassword != "" { + return []byte(testOnlyAgePassword), nil + } + } + + err = withTerminal(func(in, out *os.File) error { + fmt.Fprintf(out, "%s ", prompt) + defer clearLine(out) + s, err = term.ReadPassword(int(in.Fd())) + return err + }) + return +} + +// readCharacter reads a single character from the terminal with no echo. The +// prompt is ephemeral. +func readCharacter(prompt string) (c byte, err error) { + err = withTerminal(func(in, out *os.File) error { + fmt.Fprintf(out, "%s ", prompt) + defer clearLine(out) + + oldState, err := term.MakeRaw(int(in.Fd())) + if err != nil { + return err + } + defer term.Restore(int(in.Fd()), oldState) + + b := make([]byte, 1) + if _, err := in.Read(b); err != nil { + return err + } + + c = b[0] + return nil + }) + return +} + +var pluginTerminalUI = &plugin.ClientUI{ + DisplayMessage: func(name, message string) error { + printf("%s plugin: %s", name, message) + return nil + }, + RequestValue: func(name, message string, _ bool) (s string, err error) { + defer func() { + if err != nil { + warningf("could not read value for age-plugin-%s: %v", name, err) + } + }() + secret, err := readSecret(message) + if err != nil { + return "", err + } + return string(secret), nil + }, + Confirm: func(name, message, yes, no string) (choseYes bool, err error) { + defer func() { + if err != nil { + warningf("could not read value for age-plugin-%s: %v", name, err) + } + }() + if no == "" { + message += fmt.Sprintf(" (press enter for %q)", yes) + _, err := readSecret(message) + if err != nil { + return false, err + } + return true, nil + } + message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no) + for { + selection, err := readCharacter(message) + if err != nil { + return false, err + } + switch selection { + case '1': + return true, nil + case '2': + return false, nil + case '\x03': // CTRL-C + return false, errors.New("user cancelled prompt") + default: + warningf("reading value for age-plugin-%s: invalid selection %q", name, selection) + } + } + }, + WaitTimer: func(name string) { + printf("waiting on %s plugin...", name) + }, +} diff --git a/audit/audit.go b/audit/audit.go index 1bbfde1cd7..39989fb6b8 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -14,7 +14,7 @@ import ( "github.com/getsops/sops/v3/logging" "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) var log *logrus.Logger diff --git a/azkv/keysource.go b/azkv/keysource.go index 11e7610269..b0ee338145 100644 --- a/azkv/keysource.go +++ b/azkv/keysource.go @@ -60,6 +60,8 @@ type MasterKey struct { // using TokenCredential.ApplyToMasterKey. // If nil, azidentity.NewDefaultAzureCredential is used. tokenCredential azcore.TokenCredential + // clientOptions contains the azkeys.ClientOptions used by the Azure client. + clientOptions *azkeys.ClientOptions } // NewMasterKey creates a new MasterKey from a URL, key name and version, @@ -77,12 +79,21 @@ func NewMasterKey(vaultURL string, keyName string, keyVersion string) *MasterKey // MasterKey. The URL format is {vaultUrl}/keys/{keyName}/{keyVersion}. func NewMasterKeyFromURL(url string) (*MasterKey, error) { url = strings.TrimSpace(url) - re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)/([^/]+)$") + re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)(/[^/]*)?$") parts := re.FindStringSubmatch(url) - if parts == nil || len(parts) < 3 { - return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey", url) + if len(parts) < 3 { + return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey %v", url, parts) } - return NewMasterKey(parts[1], parts[2], parts[3]), nil + // Blank key versions are supported in Azure Key Vault, as they default to the latest + // version of the key. We need to put the actual version in the sops metadata block though + var key *MasterKey + if len(parts[3]) > 1 { + key = NewMasterKey(parts[1], parts[2], parts[3][1:]) + } else { + key = NewMasterKey(parts[1], parts[2], "") + } + err := key.ensureKeyHasVersion(context.Background()) + return key, err } // MasterKeysFromURLs takes a comma separated list of Azure Key Vault URLs, @@ -118,22 +129,77 @@ func (t TokenCredential) ApplyToMasterKey(key *MasterKey) { key.tokenCredential = t.token } +// ClientOptions is a wrapper around azkeys.ClientOptions to allow +// configuration of the Azure Key Vault client. +type ClientOptions struct { + o *azkeys.ClientOptions +} + +// NewClientOptions creates a new ClientOptions with the provided +// azkeys.ClientOptions. +func NewClientOptions(o *azkeys.ClientOptions) *ClientOptions { + return &ClientOptions{o: o} +} + +// ApplyToMasterKey configures the ClientOptions on the provided key. +func (c ClientOptions) ApplyToMasterKey(key *MasterKey) { + key.clientOptions = c.o +} + // Encrypt takes a SOPS data key, encrypts it with Azure Key Vault, and stores // the result in the EncryptedKey field. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { + return key.EncryptContext(context.Background(), dataKey) +} + +func (key *MasterKey) ensureKeyHasVersion(ctx context.Context) error { + if (key.Version != "") { + // Nothing to do + return nil + } + + token, err := key.getTokenCredential() + + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to get Azure token credential to retrieve key version: %w", err) + } + + c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions) + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to construct Azure Key Vault client to retrieve key version: %w", err) + } + + kdetail, err := c.GetKey(ctx, key.Name, key.Version, nil) + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to fetch Azure Key to retrieve key version: %w", err) + } + key.Version = kdetail.Key.KID.Version() + + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Version fetch succeeded") + return nil +} + +// EncryptContext takes a SOPS data key, encrypts it with Azure Key Vault, and stores +// the result in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { token, err := key.getTokenCredential() if err != nil { log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") return fmt.Errorf("failed to get Azure token credential to encrypt data: %w", err) } - c, err := azkeys.NewClient(key.VaultURL, token, nil) + c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions) if err != nil { log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") return fmt.Errorf("failed to construct Azure Key Vault client to encrypt data: %w", err) } - resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{ + resp, err := c.Encrypt(ctx, key.Name, key.Version, azkeys.KeyOperationParameters{ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256), Value: dataKey, }, nil) @@ -169,7 +235,15 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { // Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns // the result. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey field with Azure Key Vault and returns +// the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { token, err := key.getTokenCredential() if err != nil { log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Decryption failed") @@ -182,13 +256,13 @@ func (key *MasterKey) Decrypt() ([]byte, error) { return nil, fmt.Errorf("failed to base64 decode Azure Key Vault encrypted key: %w", err) } - c, err := azkeys.NewClient(key.VaultURL, token, nil) + c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions) if err != nil { log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Decryption failed") return nil, fmt.Errorf("failed to construct Azure Key Vault client to decrypt data: %w", err) } - resp, err := c.Decrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{ + resp, err := c.Decrypt(ctx, key.Name, key.Version, azkeys.KeyOperationParameters{ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256), Value: rawEncryptedKey, }, nil) diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index 074b71c628..6d6fa0751a 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "io" "os" "path/filepath" "time" @@ -130,11 +131,20 @@ func EncryptTree(opts EncryptTreeOpts) error { return nil } -// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree -func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) { - fileBytes, err := os.ReadFile(inputPath) - if err != nil { - return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) +// LoadEncryptedFileEx loads an encrypted SOPS file from a file or stdin, returning a SOPS tree +func LoadEncryptedFileEx(loader sops.EncryptedFileLoader, inputPath string, readFromStdin bool) (*sops.Tree, error) { + var fileBytes []byte + var err error + if readFromStdin { + fileBytes, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + } + } else { + fileBytes, err = os.ReadFile(inputPath) + if err != nil { + return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + } } path, err := filepath.Abs(inputPath) if err != nil { @@ -145,6 +155,11 @@ func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops return &tree, err } +// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree +func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) { + return LoadEncryptedFileEx(loader, inputPath, false) +} + // NewExitError returns a cli.ExitError given an error (wrapped in a generic interface{}) // and an exit code to represent the failure func NewExitError(i interface{}, exitCode int) *cli.ExitError { @@ -207,7 +222,7 @@ func GetKMSKeyWithEncryptionCtx(tree *sops.Tree) (keyGroupIndex int, keyIndex in for n, k := range kg { kmsKey, ok := k.(*kms.MasterKey) if ok { - if kmsKey.EncryptionContext != nil && len(kmsKey.EncryptionContext) >= 2 { + if len(kmsKey.EncryptionContext) >= 2 { duplicateValues := map[string]int{} for _, v := range kmsKey.EncryptionContext { duplicateValues[*v] = duplicateValues[*v] + 1 @@ -227,6 +242,7 @@ type GenericDecryptOpts struct { Cipher sops.Cipher InputStore sops.Store InputPath string + ReadFromStdin bool IgnoreMAC bool KeyServices []keyservice.KeyServiceClient DecryptionOrder []string @@ -235,7 +251,7 @@ type GenericDecryptOpts struct { // LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes // check for the issue described in https://github.com/mozilla/sops/pull/435 func LoadEncryptedFileWithBugFixes(opts GenericDecryptOpts) (*sops.Tree, error) { - tree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := LoadEncryptedFileEx(opts.InputStore, opts.InputPath, opts.ReadFromStdin) if err != nil { return nil, err } diff --git a/cmd/sops/completion.go b/cmd/sops/completion.go new file mode 100644 index 0000000000..4454026527 --- /dev/null +++ b/cmd/sops/completion.go @@ -0,0 +1,59 @@ +package main + +import "fmt" + +// https://github.com/urfave/cli/blob/v1-maint/autocomplete/zsh_autocomplete +var Zshcompletion = ` +#compdef %s + +_cli_zsh_autocomplete() { + + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + else + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi + + return +} + +compdef _cli_zsh_autocomplete %s +` + +// https://github.com/urfave/cli/blob/v1-maint/autocomplete/bash_autocomplete +var Bashcompletion = ` +#! /bin/bash + +_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == "-"* ]]; then + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) + else + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + fi + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete %s +` + +func GenBashCompletion(name string) string { + return fmt.Sprintf(Bashcompletion, name) +} + +func GenZshCompletion(name string) string { + return fmt.Sprintf(Zshcompletion, name, name) +} diff --git a/cmd/sops/decrypt.go b/cmd/sops/decrypt.go index db038787b2..d0da0ddf18 100644 --- a/cmd/sops/decrypt.go +++ b/cmd/sops/decrypt.go @@ -19,6 +19,7 @@ type decryptOpts struct { InputStore sops.Store OutputStore sops.Store InputPath string + ReadFromStdin bool IgnoreMAC bool Extract []interface{} KeyServices []keyservice.KeyServiceClient @@ -27,11 +28,12 @@ type decryptOpts struct { func decryptTree(opts decryptOpts) (tree *sops.Tree, err error) { tree, err = common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ - Cipher: opts.Cipher, - InputStore: opts.InputStore, - InputPath: opts.InputPath, - IgnoreMAC: opts.IgnoreMAC, - KeyServices: opts.KeyServices, + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + ReadFromStdin: opts.ReadFromStdin, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, }) if err != nil { return nil, err diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 982cfb967e..3510441533 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -109,6 +109,10 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { } // Ensure that in any case, the temporary file is always closed. defer tmpfile.Close() + // Ensure that the file is read+write for owner only. + if err = tmpfile.Chmod(0600); err != nil { + return nil, common.NewExitError(fmt.Sprintf("Could not change permissions of temporary file to read-write for owner only: %s", err), codes.CouldNotWriteOutputFile) + } tmpfileName := tmpfile.Name() @@ -245,7 +249,12 @@ func hashFile(filePath string) ([]byte, error) { } func runEditor(path string) error { - editor := os.Getenv("EDITOR") + envVar := "SOPS_EDITOR" + editor := os.Getenv(envVar) + if editor == "" { + envVar = "EDITOR" + editor = os.Getenv(envVar) + } var cmd *exec.Cmd if editor == "" { editor, err := lookupAnyEditor("vim", "nano", "vi") @@ -256,7 +265,7 @@ func runEditor(path string) error { } else { parts, err := shlex.Split(editor) if err != nil { - return fmt.Errorf("invalid $EDITOR: %s", editor) + return fmt.Errorf("invalid $%s: %s", envVar, editor) } parts = append(parts, path) cmd = exec.Command(parts[0], parts[1:]...) @@ -275,5 +284,5 @@ func lookupAnyEditor(editorNames ...string) (editorPath string, err error) { return editorPath, nil } } - return "", fmt.Errorf("no editor available: sops attempts to use the editor defined in the EDITOR environment variable, and if that's not set defaults to any of %s, but none of them could be found", strings.Join(editorNames, ", ")) + return "", fmt.Errorf("no editor available: sops attempts to use the editor defined in the SOPS_EDITOR or EDITOR environment variables, and if that's not set defaults to any of %s, but none of them could be found", strings.Join(editorNames, ", ")) } diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index ace7d8c2c1..e5ffc6950e 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" "path/filepath" @@ -27,11 +28,12 @@ type encryptConfig struct { } type encryptOpts struct { - Cipher sops.Cipher - InputStore sops.Store - OutputStore sops.Store - InputPath string - KeyServices []keyservice.KeyServiceClient + Cipher sops.Cipher + InputStore sops.Store + OutputStore sops.Store + InputPath string + ReadFromStdin bool + KeyServices []keyservice.KeyServiceClient encryptConfig } @@ -78,9 +80,17 @@ func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata { func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { // Load the file - fileBytes, err := os.ReadFile(opts.InputPath) - if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + var fileBytes []byte + if opts.ReadFromStdin { + fileBytes, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, common.NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + } + } else { + fileBytes, err = os.ReadFile(opts.InputPath) + if err != nil { + return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + } } branches, err := opts.InputStore.LoadPlainFile(fileBytes) if err != nil { diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 94204fdb2d..d0c85d6d55 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -4,6 +4,7 @@ import ( "context" encodingjson "encoding/json" "fmt" + "io" "net" "net/url" "os" @@ -40,12 +41,19 @@ import ( "github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" + "github.com/getsops/sops/v3/stores" "github.com/getsops/sops/v3/stores/dotenv" "github.com/getsops/sops/v3/stores/json" "github.com/getsops/sops/v3/version" ) -var log *logrus.Logger +var ( + log *logrus.Logger + + // Whether the config file warning was already shown to the user. + // Used and set by findConfigFile(). + showedConfigFileWarning bool +) func init() { log = logging.NewLogger("CMD") @@ -72,12 +80,14 @@ func main() { keyserviceFlags := []cli.Flag{ cli.BoolTFlag{ - Name: "enable-local-keyservice", - Usage: "use local key service", + Name: "enable-local-keyservice", + Usage: "use local key service", + EnvVar: "SOPS_ENABLE_LOCAL_KEYSERVICE", }, cli.StringSliceFlag{ - Name: "keyservice", - Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", + Name: "keyservice", + Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", + EnvVar: "SOPS_KEYSERVICE", }, } app.Name = "sops" @@ -85,9 +95,7 @@ func main() { app.ArgsUsage = "sops [options] file" app.Version = version.Version app.Authors = []cli.Author{ - {Name: "AJ Bahnken", Email: "ajvb@mozilla.com"}, - {Name: "Adrian Utrilla", Email: "adrianutrilla@gmail.com"}, - {Name: "Julien Vehent", Email: "jvehent@mozilla.com"}, + {Name: "CNCF Maintainers"}, } app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, AZKV, PGP, and Age @@ -138,7 +146,8 @@ func main() { To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC. - To select a different editor than the default (vim), set EDITOR. + To select a different editor than the default (vim), set SOPS_EDITOR or + EDITOR. Note that flags must always be provided before the filename to operate on. Otherwise, they will be ignored. @@ -146,6 +155,27 @@ func main() { For more information, see the README at https://github.com/getsops/sops` app.EnableBashCompletion = true app.Commands = []cli.Command{ + { + Name: "completion", + Usage: "Generate shell completion scripts", + Subcommands: []cli.Command{ + { + Name: "bash", + Usage: fmt.Sprintf("Generate bash completions. To load completions: `$ source <(%s completion bash)`", app.Name), + Action: func(c *cli.Context) error { + fmt.Fprint(c.App.Writer, GenBashCompletion(app.Name)) + return nil + }, + }, + { + Name: "zsh", + Usage: fmt.Sprintf("Generate zsh completions. To load completions: `$ source <(%s completion zsh)`", app.Name), + Action: func(c *cli.Context) error { + fmt.Fprint(c.App.Writer, GenZshCompletion(app.Name)) + return nil + }, + }}, + }, { Name: "exec-env", Usage: "execute a command with decrypted values inserted into the environment", @@ -163,6 +193,10 @@ func main() { Name: "user", Usage: "the user to run the command as", }, + cli.BoolFlag{ + Name: "same-process", + Usage: "run command in the current process instead of in a child process", + }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.NArg() != 2 { @@ -195,6 +229,10 @@ func main() { if c.Bool("background") { log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops") + + if c.Bool("same-process") { + return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) + } } tree, err := decryptTree(opts) @@ -219,18 +257,19 @@ func main() { } value, ok := item.Value.(string) if !ok { - return cli.NewExitError(fmt.Errorf("cannot use non-string values in environment, got %T", item.Value), codes.ErrorGeneric) + value = stores.ValToString(item.Value) } env = append(env, fmt.Sprintf("%s=%s", key, value)) } if err := exec.ExecWithEnv(exec.ExecOpts{ - Command: command, - Plaintext: []byte{}, - Background: c.Bool("background"), - Pristine: c.Bool("pristine"), - User: c.String("user"), - Env: env, + Command: command, + Plaintext: []byte{}, + Background: c.Bool("background"), + Pristine: c.Bool("pristine"), + User: c.String("user"), + SameProcess: c.Bool("same-process"), + Env: env, }); err != nil { return toExitError(err) } @@ -350,9 +389,15 @@ func main() { if c.Bool("verbose") || c.GlobalBool("verbose") { logging.SetLevel(logrus.DebugLevel) } - configPath, err := config.FindConfigFile(".") - if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + var configPath string + var err error + if c.GlobalString("config") != "" { + configPath = c.GlobalString("config") + } else { + configPath, err = findConfigFile() + if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } } if c.NArg() < 1 { return common.NewExitError("Error: no file specified", codes.NoFileSpecified) @@ -447,7 +492,12 @@ func main() { Name: "filestatus", Usage: "check the status of the file, returning encryption status", ArgsUsage: `file`, - Flags: []cli.Flag{}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "input-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + }, Action: func(c *cli.Context) error { if c.NArg() < 1 { return common.NewExitError("Error: no file specified", codes.NoFileSpecified) @@ -665,7 +715,7 @@ func main() { if c.GlobalString("config") != "" { configPath = c.GlobalString("config") } else { - configPath, err = config.FindConfigFile(".") + configPath, err = findConfigFile() if err != nil { return common.NewExitError(err, codes.ErrorGeneric) } @@ -676,12 +726,12 @@ func main() { failedCounter := 0 for _, path := range c.Args() { err := updatekeys.UpdateKeys(updatekeys.Opts{ - InputPath: path, - GroupQuorum: c.Int("shamir-secret-sharing-threshold"), - KeyServices: keyservices(c), - Interactive: !c.Bool("yes"), - ConfigPath: configPath, - InputType: c.String("input-type"), + InputPath: path, + ShamirThreshold: c.Int("shamir-secret-sharing-threshold"), + KeyServices: keyservices(c), + Interactive: !c.Bool("yes"), + ConfigPath: configPath, + InputType: c.String("input-type"), }) if c.NArg() == 1 { @@ -708,8 +758,8 @@ func main() { }, { Name: "decrypt", - Usage: "decrypt a file, and output the results to stdout", - ArgsUsage: `file`, + Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, Flags: append([]cli.Flag{ cli.BoolFlag{ Name: "in-place, i", @@ -737,7 +787,7 @@ func main() { }, cli.StringFlag{ Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Should be provided when reading from stdin.", }, cli.StringFlag{ Name: "decryption-order", @@ -749,23 +799,33 @@ func main() { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + readFromStdin := c.NArg() == 0 + if readFromStdin && c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args()[0]) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } inputStore, err := inputStore(c, fileNameOverride) @@ -792,6 +852,7 @@ func main() { OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, + ReadFromStdin: readFromStdin, Cipher: aes.NewCipher(), Extract: extract, KeyServices: svcs, @@ -833,8 +894,8 @@ func main() { }, { Name: "encrypt", - Usage: "encrypt a file, and output the results to stdout", - ArgsUsage: `file`, + Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, Flags: append([]cli.Flag{ cli.BoolFlag{ Name: "in-place, i", @@ -912,30 +973,45 @@ func main() { }, cli.StringFlag{ Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.", }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + readFromStdin := c.NArg() == 0 + if readFromStdin { + if c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + } + if c.String("filename-override") == "" { + return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) + } } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args()[0]) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } inputStore, err := inputStore(c, fileNameOverride) @@ -956,6 +1032,7 @@ func main() { OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, + ReadFromStdin: readFromStdin, Cipher: aes.NewCipher(), KeyServices: svcs, encryptConfig: encConfig, @@ -1101,6 +1178,11 @@ func main() { fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } inputStore, err := inputStore(c, fileNameOverride) @@ -1318,8 +1400,8 @@ func main() { }, { Name: "set", - Usage: `set a specific key or branch in the input document. value must be a json encoded string. eg. '/path/to/file ["somekey"][0] {"somevalue":true}'`, - ArgsUsage: `file index value`, + Usage: `set a specific key or branch in the input document. value must be a JSON encoded string, for example '/path/to/file ["somekey"][0] {"somevalue":true}', or a path if --value-file is used, or omitted if --value-stdin is used`, + ArgsUsage: `file index [ value ]`, Flags: append([]cli.Flag{ cli.StringFlag{ Name: "input-type", @@ -1329,6 +1411,14 @@ func main() { Name: "output-type", Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format", }, + cli.BoolFlag{ + Name: "value-file", + Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-stdin", + }, + cli.BoolFlag{ + Name: "value-stdin", + Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-file", + }, cli.IntFlag{ Name: "shamir-secret-sharing-threshold", Usage: "the number of master keys required to retrieve the data key with shamir", @@ -1342,13 +1432,26 @@ func main() { Usage: "comma separated list of decryption key types", EnvVar: "SOPS_DECRYPTION_ORDER", }, + cli.BoolFlag{ + Name: "idempotent", + Usage: "do nothing if the given index already has the given value", + }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() != 3 { - return common.NewExitError("Error: no file specified, or index and value are missing", codes.NoFileSpecified) + if c.Bool("value-file") && c.Bool("value-stdin") { + return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) + } + if c.Bool("value-stdin") { + if c.NArg() != 2 { + return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) + } + } else { + if c.NArg() != 3 { + return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) + } } fileName, err := filepath.Abs(c.Args()[0]) if err != nil { @@ -1370,7 +1473,24 @@ func main() { return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat) } - value, err := jsonValueToTreeInsertableValue(c.Args()[2]) + var data string + if c.Bool("value-stdin") { + content, err := io.ReadAll(os.Stdin) + if err != nil { + return toExitError(err) + } + data = string(content) + } else if c.Bool("value-file") { + filename := c.Args()[2] + content, err := os.ReadFile(filename) + if err != nil { + return toExitError(err) + } + data = string(content) + } else { + data = c.Args()[2] + } + value, err := jsonValueToTreeInsertableValue(data) if err != nil { return toExitError(err) } @@ -1379,7 +1499,7 @@ func main() { if err != nil { return toExitError(err) } - output, err := set(setOpts{ + output, changed, err := set(setOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, @@ -1394,6 +1514,11 @@ func main() { return toExitError(err) } + if !changed && c.Bool("idempotent") { + log.Info("File not written due to no change") + return nil + } + // We open the file *after* the operations on the tree have been // executed to avoid truncating it when there's errors file, err := os.Create(fileName) @@ -1518,8 +1643,13 @@ func main() { Usage: "generate a new data encryption key and reencrypt all values with the new key", }, cli.BoolFlag{ - Name: "disable-version-check", - Usage: "do not check whether the current version is latest during --version", + Name: "disable-version-check", + Usage: "do not check whether the current version is latest during --version", + EnvVar: "SOPS_DISABLE_VERSION_CHECK", + }, + cli.BoolFlag{ + Name: "check-for-updates", + Usage: "do check whether the current version is latest during --version", }, cli.StringFlag{ Name: "kms, k", @@ -1669,8 +1799,9 @@ func main() { Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.", }, cli.StringFlag{ - Name: "config", - Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", + Name: "config", + Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", + EnvVar: "SOPS_CONFIG", }, cli.StringFlag{ Name: "encryption-context", @@ -1740,6 +1871,11 @@ func main() { fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } commandCount := 0 @@ -1843,7 +1979,7 @@ func main() { if err != nil { return toExitError(err) } - output, err = set(setOpts{ + output, _, err = set(setOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, @@ -2100,22 +2236,26 @@ func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) { continue } addr := url.Host - if url.Scheme == "unix" { - addr = url.Path - } + addrToUse := addr opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer( - func(ctx context.Context, addr string) (net.Conn, error) { - return (&net.Dialer{}).DialContext(ctx, url.Scheme, addr) - }, - ), + } + if url.Scheme == "unix" { + addr = url.Path + addrToUse = uri + } else { + opts = append(opts, + grpc.WithContextDialer( + func(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, url.Scheme, addr) + }, + )) } log.WithField( "address", fmt.Sprintf("%s://%s", url.Scheme, addr), ).Infof("Connecting to key service") - conn, err := grpc.Dial(addr, opts...) + conn, err := grpc.NewClient(addrToUse, opts...) if err != nil { log.Fatalf("failed to listen: %v", err) } @@ -2124,11 +2264,21 @@ func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) { return } +// Wrapper of config.LookupConfigFile that takes care of handling the returned warning. +func findConfigFile() (string, error) { + result, err := config.LookupConfigFile(".") + if len(result.Warning) > 0 && !showedConfigFileWarning { + showedConfigFileWarning = true + log.Warn(result.Warning) + } + return result.Path, err +} + func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, error) { configPath := context.GlobalString("config") if configPath == "" { - // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory - foundPath, err := config.FindConfigFile(".") + // Ignore config not found errors returned from findConfigFile since the config file is not mandatory + foundPath, err := findConfigFile() if err != nil { return config.NewStoresConfig(), nil } @@ -2254,7 +2404,7 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { if err != nil { errMsg = fmt.Sprintf("%s: %s", errMsg, err) } - return nil, fmt.Errorf(errMsg) + return nil, fmt.Errorf("%s", errMsg) } return conf.KeyGroups, err } @@ -2270,14 +2420,14 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { return []sops.KeyGroup{group}, nil } -// loadConfig will look for an existing config file, either provided through the command line, or using config.FindConfigFile. +// loadConfig will look for an existing config file, either provided through the command line, or using findConfigFile // Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) { var err error configPath := c.GlobalString("config") if configPath == "" { - // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory - configPath, err = config.FindConfigFile(".") + // Ignore config not found errors returned from findConfigFile since the config file is not mandatory + configPath, err = findConfigFile() if err != nil { // If we can't find a config file, but we were not explicitly requested to, assume it does not exist return nil, nil diff --git a/cmd/sops/set.go b/cmd/sops/set.go index a6e8ed3571..7a7da298df 100644 --- a/cmd/sops/set.go +++ b/cmd/sops/set.go @@ -21,7 +21,7 @@ type setOpts struct { DecryptionOrder []string } -func set(opts setOpts) ([]byte, error) { +func set(opts setOpts) ([]byte, bool, error) { // Load the file // TODO: Issue #173: if the file does not exist, create it with the contents passed in as opts.Value tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ @@ -32,7 +32,7 @@ func set(opts setOpts) ([]byte, error) { KeyServices: opts.KeyServices, }) if err != nil { - return nil, err + return nil, false, err } // Decrypt the file @@ -44,22 +44,23 @@ func set(opts setOpts) ([]byte, error) { DecryptionOrder: opts.DecryptionOrder, }) if err != nil { - return nil, err + return nil, false, err } // Set the value - tree.Branches[0] = tree.Branches[0].Set(opts.TreePath, opts.Value) + var changed bool + tree.Branches[0], changed = tree.Branches[0].Set(opts.TreePath, opts.Value) err = common.EncryptTree(common.EncryptTreeOpts{ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher, }) if err != nil { - return nil, err + return nil, false, err } encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, false, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } - return encryptedFile, err + return encryptedFile, changed, err } diff --git a/cmd/sops/subcommand/exec/exec.go b/cmd/sops/subcommand/exec/exec.go index be74a31a40..3ac7cfd63d 100644 --- a/cmd/sops/subcommand/exec/exec.go +++ b/cmd/sops/subcommand/exec/exec.go @@ -2,6 +2,7 @@ package exec import ( "bytes" + "fmt" "os" "path/filepath" "runtime" @@ -23,14 +24,15 @@ func init() { } type ExecOpts struct { - Command string - Plaintext []byte - Background bool - Pristine bool - Fifo bool - User string - Filename string - Env []string + Command string + Plaintext []byte + Background bool + SameProcess bool + Pristine bool + Fifo bool + User string + Filename string + Env []string } func GetFile(dir, filename string) *os.File { @@ -115,6 +117,10 @@ func ExecWithEnv(opts ExecOpts) error { SwitchUser(opts.User) } + if runtime.GOOS == "windows" && opts.SameProcess { + return fmt.Errorf("The --same-process flag is not supported on Windows") + } + var env []string if !opts.Pristine { @@ -134,6 +140,15 @@ func ExecWithEnv(opts ExecOpts) error { env = append(env, opts.Env...) + if opts.SameProcess { + if opts.Background { + log.Fatal("background is not supported for same-process") + } + + // Note that the call does NOT return, unless an error happens. + return ExecSyscall(opts.Command, env) + } + cmd := BuildCommand(opts.Command) cmd.Env = env diff --git a/cmd/sops/subcommand/exec/exec_unix.go b/cmd/sops/subcommand/exec/exec_unix.go index cc831e798f..bfc268d864 100644 --- a/cmd/sops/subcommand/exec/exec_unix.go +++ b/cmd/sops/subcommand/exec/exec_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package exec @@ -11,6 +12,10 @@ import ( "syscall" ) +func ExecSyscall(command string, env []string) error { + return syscall.Exec("/bin/sh", []string{"/bin/sh", "-c", command}, env) +} + func BuildCommand(command string) *exec.Cmd { return exec.Command("/bin/sh", "-c", command) } diff --git a/cmd/sops/subcommand/exec/exec_windows.go b/cmd/sops/subcommand/exec/exec_windows.go index 7e0f21d749..a510f2826e 100644 --- a/cmd/sops/subcommand/exec/exec_windows.go +++ b/cmd/sops/subcommand/exec/exec_windows.go @@ -4,6 +4,11 @@ import ( "os/exec" ) +func ExecSyscall(command string, env []string) error { + log.Fatal("same-process not available on windows") + return nil +} + func BuildCommand(command string) *exec.Cmd { return exec.Command("cmd.exe", "/C", command) } diff --git a/cmd/sops/subcommand/updatekeys/updatekeys.go b/cmd/sops/subcommand/updatekeys/updatekeys.go index f1239401bd..9dec066a67 100644 --- a/cmd/sops/subcommand/updatekeys/updatekeys.go +++ b/cmd/sops/subcommand/updatekeys/updatekeys.go @@ -15,7 +15,7 @@ import ( // Opts represents key operation options and config type Opts struct { InputPath string - GroupQuorum int + ShamirThreshold int KeyServices []keyservice.KeyServiceClient DecryptionOrder []string Interactive bool @@ -70,8 +70,8 @@ func updateFile(opts Opts) error { // TODO: use conf.ShamirThreshold instead of tree.Metadata.ShamirThreshold in the next line? // Or make this configurable? var shamirThreshold = tree.Metadata.ShamirThreshold - if opts.GroupQuorum != 0 { - shamirThreshold = opts.GroupQuorum + if opts.ShamirThreshold != 0 { + shamirThreshold = opts.ShamirThreshold } shamirThreshold = min(shamirThreshold, len(conf.KeyGroups)) var shamirThresholdWillChange = tree.Metadata.ShamirThreshold != shamirThreshold diff --git a/config/config.go b/config/config.go index 0a55017204..3c4fd17f85 100644 --- a/config/config.go +++ b/config/config.go @@ -20,7 +20,7 @@ import ( "github.com/getsops/sops/v3/ocikms" "github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/publish" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) type fileSystem interface { @@ -38,22 +38,65 @@ func (fs osFS) Stat(name string) (os.FileInfo, error) { var fs fileSystem = osFS{stat: os.Stat} const ( - maxDepth = 100 - configFileName = ".sops.yaml" + maxDepth = 100 + configFileName = ".sops.yaml" + alternateConfigName = ".sops.yml" ) -// FindConfigFile looks for a sops config file in the current working directory and on parent directories, up to the limit defined by the maxDepth constant. -func FindConfigFile(start string) (string, error) { +// ConfigFileResult contains the path to a config file and any warnings +type ConfigFileResult struct { + Path string + Warning string +} + +// LookupConfigFile looks for a sops config file in the current working directory +// and on parent directories, up to the maxDepth limit. +// It returns a result containing the file path and any warnings. +func LookupConfigFile(start string) (ConfigFileResult, error) { filepath := path.Dir(start) + var foundAlternatePath string + for i := 0; i < maxDepth; i++ { - _, err := fs.Stat(path.Join(filepath, configFileName)) - if err != nil { - filepath = path.Join(filepath, "..") - } else { - return path.Join(filepath, configFileName), nil + configPath := path.Join(filepath, configFileName) + _, err := fs.Stat(configPath) + if err == nil { + result := ConfigFileResult{Path: configPath} + + if foundAlternatePath != "" { + result.Warning = fmt.Sprintf( + "ignoring %q when searching for config file; the config file must be called %q; using %q instead", + foundAlternatePath, configFileName, configPath) + } + return result, nil } + + // Check for alternate filename if we haven't found one yet + if foundAlternatePath == "" { + alternatePath := path.Join(filepath, alternateConfigName) + _, altErr := fs.Stat(alternatePath) + if altErr == nil { + foundAlternatePath = alternatePath + } + } + + filepath = path.Join(filepath, "..") } - return "", fmt.Errorf("Config file not found") + + // No config file found + result := ConfigFileResult{} + if foundAlternatePath != "" { + result.Warning = fmt.Sprintf( + "ignoring %q when searching for config file; the config file must be called %q", + foundAlternatePath, configFileName) + } + + return result, fmt.Errorf("config file not found") +} + +// FindConfigFile looks for a sops config file in the current working directory and on parent directories, up to the limit defined by the maxDepth constant. +func FindConfigFile(start string) (string, error) { + result, err := LookupConfigFile(start) + return result.Path, err } type DotenvStoreConfig struct{} @@ -129,24 +172,87 @@ type destinationRule struct { } type creationRule struct { - PathRegex string `yaml:"path_regex"` - KMS string - AwsProfile string `yaml:"aws_profile"` - Age string `yaml:"age"` - OCIKMS string `yaml:"oci_kms"` - PGP string - GCPKMS string `yaml:"gcp_kms"` - AzureKeyVault string `yaml:"azure_keyvault"` - VaultURI string `yaml:"hc_vault_transit_uri"` - KeyGroups []keyGroup `yaml:"key_groups"` - ShamirThreshold int `yaml:"shamir_threshold"` - UnencryptedSuffix string `yaml:"unencrypted_suffix"` - EncryptedSuffix string `yaml:"encrypted_suffix"` - UnencryptedRegex string `yaml:"unencrypted_regex"` - EncryptedRegex string `yaml:"encrypted_regex"` - UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex"` - EncryptedCommentRegex string `yaml:"encrypted_comment_regex"` - MACOnlyEncrypted bool `yaml:"mac_only_encrypted"` + PathRegex string `yaml:"path_regex"` + KMS interface{} `yaml:"kms"` // string or []string + AwsProfile string `yaml:"aws_profile"` + Age interface{} `yaml:"age"` // string or []string + OCIKMS string `yaml:"oci_kms"` + PGP interface{} `yaml:"pgp"` // string or []string + GCPKMS interface{} `yaml:"gcp_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"` + ShamirThreshold int `yaml:"shamir_threshold"` + UnencryptedSuffix string `yaml:"unencrypted_suffix"` + EncryptedSuffix string `yaml:"encrypted_suffix"` + UnencryptedRegex string `yaml:"unencrypted_regex"` + EncryptedRegex string `yaml:"encrypted_regex"` + UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex"` + EncryptedCommentRegex string `yaml:"encrypted_comment_regex"` + MACOnlyEncrypted bool `yaml:"mac_only_encrypted"` +} + +// Helper methods to safely extract keys as []string +func (c *creationRule) GetKMSKeys() ([]string, error) { + return parseKeyField(c.KMS, "kms") +} + +func (c *creationRule) GetAgeKeys() ([]string, error) { + return parseKeyField(c.Age, "age") +} + +func (c *creationRule) GetPGPKeys() ([]string, error) { + return parseKeyField(c.PGP, "pgp") +} + +func (c *creationRule) GetGCPKMSKeys() ([]string, error) { + return parseKeyField(c.GCPKMS, "gcp_kms") +} + +func (c *creationRule) GetAzureKeyVaultKeys() ([]string, error) { + return parseKeyField(c.AzureKeyVault, "azure_keyvault") +} + +func (c *creationRule) GetVaultURIs() ([]string, error) { + return parseKeyField(c.VaultURI, "hc_vault_transit_uri") +} + +// Utility function to handle both string and []string +func parseKeyField(field interface{}, fieldName string) ([]string, error) { + if field == nil { + return []string{}, nil + } + + switch v := field.(type) { + case string: + if v == "" { + return []string{}, nil + } + // Existing CSV parsing logic + keys := strings.Split(v, ",") + result := make([]string, 0, len(keys)) + for _, key := range keys { + trimmed := strings.TrimSpace(key) + if trimmed != "" { // Skip empty strings (fixes trailing comma issue) + result = append(result, trimmed) + } + } + return result, nil + case []interface{}: + result := make([]string, len(v)) + for i, item := range v { + if str, ok := item.(string); ok { + result[i] = str + } else { + return nil, fmt.Errorf("invalid %s key configuration: expected string in list, got %T", fieldName, item) + } + } + return result, nil + case []string: + return v, nil + default: + return nil, fmt.Errorf("invalid %s key configuration: expected string, []string, or nil, got %T", fieldName, field) + } } func NewStoresConfig() *StoresConfig { @@ -242,6 +348,14 @@ func extractMasterKeys(group keyGroup) (sops.KeyGroup, error) { return deduplicateKeygroup(keyGroup), nil } +func getKeysWithValidation(getKeysFunc func() ([]string, error), keyType string) ([]string, error) { + keys, err := getKeysFunc() + if err != nil { + return nil, fmt.Errorf("invalid %s key configuration: %w", keyType, err) + } + return keys, nil +} + func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) { var groups []sops.KeyGroup if len(cRule.KeyGroups) > 0 { @@ -257,8 +371,13 @@ func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[ } } else { var keyGroup sops.KeyGroup - if cRule.Age != "" { - ageKeys, err := age.MasterKeysFromRecipients(cRule.Age) + ageKeys, err := getKeysWithValidation(cRule.GetAgeKeys, "age") + if err != nil { + return nil, err + } + + if len(ageKeys) > 0 { + ageKeys, err := age.MasterKeysFromRecipients(strings.Join(ageKeys, ",")) if err != nil { return nil, err } else { @@ -267,26 +386,46 @@ func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[ } } } - for _, k := range pgp.MasterKeysFromFingerprintString(cRule.PGP) { + pgpKeys, err := getKeysWithValidation(cRule.GetPGPKeys, "pgp") + if err != nil { + return nil, err + } + for _, k := range pgp.MasterKeysFromFingerprintString(strings.Join(pgpKeys, ",")) { keyGroup = append(keyGroup, k) } - for _, k := range kms.MasterKeysFromArnString(cRule.KMS, kmsEncryptionContext, cRule.AwsProfile) { + kmsKeys, err := getKeysWithValidation(cRule.GetKMSKeys, "kms") + if err != nil { + return nil, err + } + for _, k := range kms.MasterKeysFromArnString(strings.Join(kmsKeys, ","), kmsEncryptionContext, cRule.AwsProfile) { keyGroup = append(keyGroup, k) } - for _, k := range gcpkms.MasterKeysFromResourceIDString(cRule.GCPKMS) { + gcpkmsKeys, err := getKeysWithValidation(cRule.GetGCPKMSKeys, "gcpkms") + if err != nil { + return nil, err + } + for _, k := range gcpkms.MasterKeysFromResourceIDString(strings.Join(gcpkmsKeys, ",")) { keyGroup = append(keyGroup, k) } for _, k := range ocikms.MasterKeysFromOCIDString(cRule.OCIKMS) { keyGroup = append(keyGroup, k) } - azureKeys, err := azkv.MasterKeysFromURLs(cRule.AzureKeyVault) + azKeys, err := getKeysWithValidation(cRule.GetAzureKeyVaultKeys, "azure_keyvault") + if err != nil { + return nil, err + } + azureKeys, err := azkv.MasterKeysFromURLs(strings.Join(azKeys, ",")) if err != nil { return nil, err } for _, k := range azureKeys { keyGroup = append(keyGroup, k) } - vaultKeys, err := hcvault.NewMasterKeysFromURIs(cRule.VaultURI) + vaultKeyUris, err := getKeysWithValidation(cRule.GetVaultURIs, "vault") + if err != nil { + return nil, err + } + vaultKeys, err := hcvault.NewMasterKeysFromURIs(strings.Join(vaultKeyUris, ",")) if err != nil { return nil, err } @@ -381,7 +520,18 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio } var dest publish.Destination - if dRule.S3Bucket != "" && dRule.GCSBucket != "" && dRule.VaultPath != "" { + destinationCount := 0 + if dRule.S3Bucket != "" { + destinationCount++ + } + if dRule.GCSBucket != "" { + destinationCount++ + } + if dRule.VaultPath != "" { + destinationCount++ + } + + if destinationCount > 1 { return nil, fmt.Errorf("error loading config: more than one destinations were found in a single destination rule, you can only use one per rule") } if dRule.S3Bucket != "" { diff --git a/config/config_test.go b/config/config_test.go index 9ac63645a8..753f870b1a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -537,29 +537,29 @@ func TestLoadConfigFileWithMerge(t *testing.T) { "hc_vault: https://foo.vault:8200/v1/foo/keys/foo-key", }, ids(conf.KeyGroups[0])) assert.Equal(t, []string{ - "pgp: foo", // key01 - "kms: foo||foo", //key02 + "pgp: foo", // key01 + "kms: foo||foo", //key02 "kms: foo+123|baz:bam|bar", //key03 - "gcp_kms: foo", //key04 + "gcp_kms: foo", //key04 "azure_kv: https://foo.vault.azure.net/keys/foo-key/fooversion", //key05 - "hc_vault: https://bar.vault:8200/v1/bar/keys/bar-key", //key06 - "pgp: bar", //key07 + "hc_vault: https://bar.vault:8200/v1/bar/keys/bar-key", //key06 + "pgp: bar", //key07 "kms: bar||bar", //key08 - "gcp_kms: bar", //key09 - "gcp_kms: baz", //key10 + "gcp_kms: bar", //key09 + "gcp_kms: baz", //key10 "azure_kv: https://bar.vault.azure.net/keys/bar-key/barversion", //key11 - "hc_vault: https://baz.vault:8200/v1/baz/keys/baz-key", //key12 - "pgp: baz", //key13 + "hc_vault: https://baz.vault:8200/v1/baz/keys/baz-key", //key12 + "pgp: baz", //key13 "kms: baz||baz", //key14 "hc_vault: https://foo.vault:8200/v1/foo/keys/foo-key", //key15 - "pgp: qux", //key16 - "kms: qux||qux", //key17 - "kms: baz||bar", //key18 - "kms: baz+123", //key19 - "gcp_kms: qux", //key20 + "pgp: qux", //key16 + "kms: qux||qux", //key17 + "kms: baz||bar", //key18 + "kms: baz+123", //key19 + "gcp_kms: qux", //key20 "gcp_kms: fnord", //key21 "azure_kv: https://baz.vault.azure.net/keys/baz-key/bazversion", //key22 - "hc_vault: https://qux.vault:8200/v1/qux/keys/qux-key", //key23 + "hc_vault: https://qux.vault:8200/v1/qux/keys/qux-key", //key23 "kms: fnord||fnord", //key24 "hc_vault: https://fnord.vault:8200/v1/fnord/keys/fnord-key", //key25 }, ids(conf.KeyGroups[1])) @@ -718,3 +718,164 @@ func TestLoadConfigFileWithVaultDestinationRules(t *testing.T) { assert.NotNil(t, conf.Destination) assert.Contains(t, conf.Destination.Path("barfoo"), "/v1/kv/barfoo/barfoo") } + +func TestCreationRuleNativeKeyLists(t *testing.T) { + var sampleConfigWithNativeKeyLists = []byte(` +creation_rules: + - path_regex: native_list* + pgp: + - "85D77543B3D624B63CEA9E6DBC17301B491B3F21" # name@email.com + - "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4" # server_XYZ + kms: + - "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + age: + - "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p" + gcp_kms: + - "projects/test-project/locations/global/keyRings/test-ring/cryptoKeys/test-key" + hc_vault_transit_uri: + - "https://vault.example.com:8200/v1/transit/keys/key1" +`) + conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithNativeKeyLists, t), "/conf/path", "native_list_test", nil) + assert.Nil(t, err) + if conf == nil { + t.Fatal("Expected configuration but got nil") + } + + assert.True(t, len(conf.KeyGroups) == 1) + assert.True(t, len(conf.KeyGroups[0]) == 6) + + keyTypeCounts := make(map[string]int) + for _, key := range conf.KeyGroups[0] { + keyTypeCounts[key.TypeToIdentifier()]++ + } + + assert.Equal(t, 2, keyTypeCounts["pgp"]) + assert.Equal(t, 1, keyTypeCounts["kms"]) + assert.Equal(t, 1, keyTypeCounts["age"]) + assert.Equal(t, 1, keyTypeCounts["gcp_kms"]) + assert.Equal(t, 1, keyTypeCounts["hc_vault"]) +} + +// Test configurations with multiple destinations should fail +var sampleConfigWithS3GCSConflict = []byte(` +destination_rules: + - path_regex: '^test/.*' + s3_bucket: 'my-s3-bucket' + s3_prefix: 'sops/' + gcs_bucket: 'my-gcs-bucket' + gcs_prefix: 'sops/' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + +var sampleConfigWithS3VaultConflict = []byte(` +destination_rules: + - path_regex: '^test/.*' + s3_bucket: 'my-s3-bucket' + s3_prefix: 'sops/' + vault_path: 'secret/sops' + vault_address: 'https://vault.example.com' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + +var sampleConfigWithGCSVaultConflict = []byte(` +destination_rules: + - path_regex: '^test/.*' + gcs_bucket: 'my-gcs-bucket' + gcs_prefix: 'sops/' + vault_path: 'secret/sops' + vault_address: 'https://vault.example.com' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + +var sampleConfigWithAllThreeDestinations = []byte(` +destination_rules: + - path_regex: '^test/.*' + s3_bucket: 'my-s3-bucket' + s3_prefix: 'sops/' + gcs_bucket: 'my-gcs-bucket' + gcs_prefix: 'sops/' + vault_path: 'secret/sops' + vault_address: 'https://vault.example.com' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + +func TestDestinationValidationS3GCSConflict(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfigWithS3GCSConflict, t), "test/secrets.yaml", nil) + assert.NotNil(t, err, "Expected error when both S3 and GCS destinations are specified") + if err != nil { + assert.Contains(t, err.Error(), "more than one destinations were found") + } +} + +func TestDestinationValidationS3VaultConflict(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfigWithS3VaultConflict, t), "test/secrets.yaml", nil) + assert.NotNil(t, err, "Expected error when both S3 and Vault destinations are specified") + if err != nil { + assert.Contains(t, err.Error(), "more than one destinations were found") + } +} + +func TestDestinationValidationGCSVaultConflict(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfigWithGCSVaultConflict, t), "test/secrets.yaml", nil) + assert.NotNil(t, err, "Expected error when both GCS and Vault destinations are specified") + if err != nil { + assert.Contains(t, err.Error(), "more than one destinations were found") + } +} + +func TestDestinationValidationAllThreeDestinationsConflict(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfigWithAllThreeDestinations, t), "test/secrets.yaml", nil) + assert.NotNil(t, err, "Expected error when all three destinations are specified") + if err != nil { + assert.Contains(t, err.Error(), "more than one destinations were found") + } +} + +func TestDestinationValidationSingleS3Destination(t *testing.T) { + validS3Config := []byte(` +destination_rules: + - path_regex: '^test/.*' + s3_bucket: 'my-s3-bucket' + s3_prefix: 'sops/' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + conf, err := parseDestinationRuleForFile(parseConfigFile(validS3Config, t), "test/secrets.yaml", nil) + assert.Nil(t, err) + assert.NotNil(t, conf.Destination) + assert.Contains(t, conf.Destination.Path("secrets.yaml"), "s3://my-s3-bucket/sops/secrets.yaml") +} + +func TestDestinationValidationSingleGCSDestination(t *testing.T) { + validGCSConfig := []byte(` +destination_rules: + - path_regex: '^test/.*' + gcs_bucket: 'my-gcs-bucket' + gcs_prefix: 'sops/' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + conf, err := parseDestinationRuleForFile(parseConfigFile(validGCSConfig, t), "test/secrets.yaml", nil) + assert.Nil(t, err) + assert.NotNil(t, conf.Destination) + assert.Contains(t, conf.Destination.Path("secrets.yaml"), "gcs://my-gcs-bucket/sops/secrets.yaml") +} + +func TestDestinationValidationSingleVaultDestination(t *testing.T) { + validVaultConfig := []byte(` +destination_rules: + - path_regex: '^test/.*' + vault_path: 'secret/sops' + vault_address: 'https://vault.example.com' + recreation_rule: + kms: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' +`) + conf, err := parseDestinationRuleForFile(parseConfigFile(validVaultConfig, t), "test/secrets.yaml", nil) + assert.Nil(t, err) + assert.NotNil(t, conf.Destination) + assert.Contains(t, conf.Destination.Path("secrets.yaml"), "https://vault.example.com/v1/secret/data/secret/sops/secrets.yaml") +} diff --git a/example.yaml b/example.yaml index a22d69e64c..38ec7a73d7 100644 --- a/example.yaml +++ b/example.yaml @@ -7,13 +7,13 @@ app2: key: ENC[AES256_GCM,data:xRjmLiX4BCoSUElToUs5twDq1WNWQNvNMi8yitXly43iGQiwltIs0FsY5u+7fzLepS66oLTGdL0NfWwCGxksYYzUKz5OXRLEatacZP40D71861zu+njmGdXepY0q0q5VOG7ObgIAMMMElVKRIFdjpVgmgUa+/h6R77mEbDztk6lqb28r15XyR6GmdubierTE7aialFzNoC+XO/yk7bnMi0XA/aomj1H5RdZ37LBR2k+rhqXmhkPdGTdJ39t4Ou1Q2Oc4RHRGvgs/EeFqQHcq0AilqXsFIv/PE0bQP564LaOcXK33B+PnoV1D+lXZ7mLOjKlq04c+UojwXjpZeXBr6Ip4H3dAGkPAoQKMtyGwHYzuLCNesxwPv2tPqbxkbVev0AKezGhnPjvCNvRN3S1Y0LPf1atfwzOCBQBhdUTpmXtxCdTNG0cUUZjeIKAyJXDWJooYHlstzDti/dSGMEedOnKq6648Yp7tLNDjAg5CGbDEWjTWehtqgvixUoRdYc2/r/ie3t09XB9h70BzLFDnbNdTNHhg0/aivHeDf8LJ2Co8YvIjLTwlm7GV0mSwzJIoY/2YxXFtw+XFqt+BCt1G8wq3R6OdXZK/+6fUKH/D4EVym5nrGkZIuOCiTB+wpzy09QmZ80Fo+ba0x/1g9ZTMKHk=,iv:ZtzvrO7QSHEOCnKCrIYcaesKnyScV8KaHZr22tUMLlU=,tag:2A3nJBIPF2Q3FlwMYLvG2w==,type:str] number: ENC[AES256_GCM,data:DX0qiTOWhQvG/w==,iv:ouWsby8JoFwCRj/mLVCnNcYhP2sdyf4h6nwZuGksE7Q=,tag:lPU6AId2JrlquHnYRw+E8Q==,type:float] an_array: -- ENC[AES256_GCM,data:vyczE8EQr9qHkaM=,iv:sT5jKk3LZ61Zq/neTli5tcnDFxCxY5RuGr2k5oGQWJQ=,tag:1HgaHWfyh6EJLkI1V2kOrw==,type:str] -- ENC[AES256_GCM,data:XtBinnYXR7bx1GY=,iv:KvT9smKVmgMNrab+RzfuWscyvJav2r8j1P08ucNmhgQ=,tag:IllAEvIPPeKOfqi1XbmT8w==,type:str] -- ENC[AES256_GCM,data:gpZ7nwWTGaI+Ti+lk+CPQOoM0ypwK7UMMBUiZAniQHDNJelipqc8hyhNeV+tpJLNaRt74OHs04EX8g==,iv:mKcwVelqLvwVDPjR8NeyMZ7AhsjRgmnYmyEuwPNPrQ8=,tag:vrkoccUfJs105yLCmXYYCw==,type:str] -- ENC[AES256_GCM,data:L9jPh+7+XsdqEpUnFcD4nA==,iv:xyfKjOXVrBDCIQG5786pSu5yvHdl/PK8eVxkIUWoCIw=,tag:Q0wlTV2e2vZeU6eTF5Oacg==,type:str] + - ENC[AES256_GCM,data:vyczE8EQr9qHkaM=,iv:sT5jKk3LZ61Zq/neTli5tcnDFxCxY5RuGr2k5oGQWJQ=,tag:1HgaHWfyh6EJLkI1V2kOrw==,type:str] + - ENC[AES256_GCM,data:XtBinnYXR7bx1GY=,iv:KvT9smKVmgMNrab+RzfuWscyvJav2r8j1P08ucNmhgQ=,tag:IllAEvIPPeKOfqi1XbmT8w==,type:str] + - ENC[AES256_GCM,data:gpZ7nwWTGaI+Ti+lk+CPQOoM0ypwK7UMMBUiZAniQHDNJelipqc8hyhNeV+tpJLNaRt74OHs04EX8g==,iv:mKcwVelqLvwVDPjR8NeyMZ7AhsjRgmnYmyEuwPNPrQ8=,tag:vrkoccUfJs105yLCmXYYCw==,type:str] + - ENC[AES256_GCM,data:L9jPh+7+XsdqEpUnFcD4nA==,iv:xyfKjOXVrBDCIQG5786pSu5yvHdl/PK8eVxkIUWoCIw=,tag:Q0wlTV2e2vZeU6eTF5Oacg==,type:str] somebooleans: -- ENC[AES256_GCM,data:ExiXxg==,iv:K7FUwomqdA7o9lzvNoAMH/wbXs08FextTGGeJKnaatU=,tag:A9UntgvPIcappmeM3jsbdA==,type:bool] -- ENC[AES256_GCM,data:3I0AVdM=,iv:q4YKnRIKufREPmwT4sz8plcsOD6iem/tY3NMUV0STBE=,tag:0w4OMKClWTzjKhqsJZT8JA==,type:bool] + - ENC[AES256_GCM,data:ExiXxg==,iv:K7FUwomqdA7o9lzvNoAMH/wbXs08FextTGGeJKnaatU=,tag:A9UntgvPIcappmeM3jsbdA==,type:bool] + - ENC[AES256_GCM,data:3I0AVdM=,iv:q4YKnRIKufREPmwT4sz8plcsOD6iem/tY3NMUV0STBE=,tag:0w4OMKClWTzjKhqsJZT8JA==,type:bool] this: is: a: @@ -23,22 +23,19 @@ this: #ENC[AES256_GCM,data:eYRaxgs3vGeS96+ZDV8GYrwbvsrMtnWHOtsT2045tD2mlfOD,iv:/RVNEWuBlxhhY8OlJPbS/81QJukXZu1EWnPUQwrcin4=,tag:DybgrXKGWxoRyIQOlc+UMA==,type:comment] #ENC[AES256_GCM,data:JXKEWGBg4eeCdeQ=,iv:K5keuEjyekf7a3q7WBOKwljsHGXRdQteJcXeeKvHo28=,tag:60VtIdsy13qSKPIEWHUUNg==,type:comment] somelist_unencrypted: -- all elements of this list -- remain in clear text -- because of the _unencrypted suffix in the key + - all elements of this list + - remain in clear text + - because of the _unencrypted suffix in the key nested_unencrypted: this: is: all: going to remain in clear text sops: - kms: [] - gcp_kms: [] - azure_kv: [] - lastmodified: '2019-12-10T22:45:53Z' - mac: ENC[AES256_GCM,data:WDjMv0eWcyPQzZlr3MppeAMQavN88xv5LzI/9wOlg+WPhRoTdrvgFpWowyWvTdUC/i0ybRQRg2u/Wam0kaqzMDpl/E806Gp9hgJcSneqydDJqPiMh+HpXkXWpc70xbYg8/gc1l7eIfSG7rS1dC2t2je60OAIfC/5zAXrL9KH4Ho=,iv:h0hWhb+46upix6K7hZfNNQoiX7WCapiMTv5I/keZsm4=,tag:v7mVbTN3HWmekol5iaO8FA==,type:str] + lastmodified: "2025-08-06T18:55:05Z" + mac: ENC[AES256_GCM,data:GCWOyMJo56xAfC4he5zJUuQr+uYXpSPX61twiGf+bi6E9Mb6JNEWGz8GhnxtlXDNinTPrTrmjeep0H1B9EzhSjxkjXuvTGrIUZmO3paGqpkyszU9pm4APyPtoq6ggCo+rqIMG+EDYFN4wMKPU+Nyn2EjmrorAg1uHq2ORKN40vk=,iv:V1kH6xaXDIb8NHYNm4A5ilTInizW7gw8nWhINZh9OJs=,tag:u+JwZ5osXC6FHdytwvRwvw==,type:str] pgp: - - created_at: '2019-12-10T22:45:53Z' - enc: |- + - created_at: "2019-12-10T22:45:53Z" + enc: |- -----BEGIN PGP MESSAGE----- wcBMAyUpShfNkFB/AQgAE0MaWAQGbTKY7Xg3fDNtzlvnVBkkQRHsLt5kUTu2nAy4 @@ -51,9 +48,9 @@ sops: Z5/vXKOseeBk5JWCnIHC/MtjOkuPt53nvGzi3lYvW+F0FQA= =RDyn -----END PGP MESSAGE----- - fp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 - - created_at: '2019-12-10T22:45:53Z' - enc: |- + fp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 + - created_at: "2019-12-10T22:45:53Z" + enc: |- -----BEGIN PGP MESSAGE----- wYwDXFUltYFwV4MBBABpm+tFhFhv3A7A/L/p6nL3HXKKhONrgguYgXA/hhSg4/bD @@ -64,6 +61,6 @@ sops: ZAA= =mqGc -----END PGP MESSAGE----- - fp: D7229043384BCC60326C6FB9D8720D957C3D3074 + fp: D7229043384BCC60326C6FB9D8720D957C3D3074 unencrypted_suffix: _unencrypted - version: 3.5.0 + version: 3.10.2 diff --git a/functional-tests/Cargo.lock b/functional-tests/Cargo.lock index 3d418ad9de..8d3885a5ed 100644 --- a/functional-tests/Cargo.lock +++ b/functional-tests/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "bitflags" @@ -96,9 +96,9 @@ checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "memchr" @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.42" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" dependencies = [ "bitflags", "errno", @@ -151,18 +151,27 @@ checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -171,14 +180,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -207,11 +217,10 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ - "cfg-if", "fastrand", "getrandom", "once_cell", diff --git a/functional-tests/Cargo.toml b/functional-tests/Cargo.toml index c1fe80e622..a624f68c81 100644 --- a/functional-tests/Cargo.toml +++ b/functional-tests/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" authors = ["Adrian Utrilla "] [dependencies] -tempfile = "3.16.0" +tempfile = "3.22.0" serde = "1.0" -serde_json = "1.0.138" +serde_json = "1.0.145" serde_yaml = "0.9.34" serde_derive = "1.0" lazy_static = "1.5.0" diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index ccb7b4b865..6530d16bd1 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -14,9 +14,9 @@ mod tests { use serde_yaml::Value; use std::env; use std::fs::File; - use std::io::{Read, Write}; + use std::io::{BufWriter, Read, Write}; use std::path::Path; - use std::process::Command; + use std::process::{Child, Command, Stdio}; use tempfile::Builder; use tempfile::TempDir; const SOPS_BINARY_PATH: &'static str = "./sops"; @@ -81,6 +81,47 @@ mod tests { } } + fn write_to_stdin(process: &Child, content: &[u8]) { + let mut outstdin = process.stdin.as_ref().unwrap(); + let mut writer = BufWriter::new(&mut outstdin); + writer.write_all(content).expect("Cannot write to stdin"); + } + + #[test] + fn encrypt_from_stdin() { + let process = Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("--filename-override") + .arg("test_encrypt.yaml") + .arg("--output-type") + .arg("json") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin( + &process, + b"foo: 2 +bar: baz +", + ); + let output = process.wait_with_output().expect("Failed to wait on sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + let json = &String::from_utf8_lossy(&output.stdout); + let data: Value = serde_json::from_str(json).expect("Error parsing sops's JSON output"); + match data.into() { + Value::Mapping(m) => { + assert!( + m.get(&Value::String("sops".to_owned())).is_some(), + "sops metadata branch not found" + ); + assert_encrypted!(&m, Value::String("foo".to_owned())); + assert_encrypted!(&m, Value::String("bar".to_owned())); + } + _ => panic!("sops's JSON output is not an object"), + } + } + #[test] #[ignore] fn publish_json_file_s3() { @@ -224,6 +265,57 @@ mod tests { } } + #[test] + fn test_ini_values_as_strings() { + let file_path = prepare_temp_file( + "test_ini_values_as_strings.yaml", + b"the_section: + int: 123 + float: 1.23 + bool: true + date: 2025-01-02 + timestamp: 2025-01-02 03:04:05 + utc_timestamp: 2025-01-02T03:04:05Z + string: this is a string", + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--output-type") + .arg("ini") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let data = &String::from_utf8_lossy(&output.stdout); + assert!( + data == "[the_section] +int = 123 +float = 1.23 +bool = true +date = 2025-01-02T00:00:00Z +timestamp = 2025-01-02T03:04:05Z +utc_timestamp = 2025-01-02T03:04:05Z +string = this is a string +" + ); + } + #[test] fn encrypt_yaml_file() { let file_path = prepare_temp_file( @@ -296,6 +388,115 @@ bar: baz", panic!("Output JSON does not have the expected structure"); } + #[test] + fn set_json_file_update_idempotent_write() { + let file_path = prepare_temp_file( + "test_set_update_idempotent_write.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let mut before = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut before) + .unwrap(); + let output = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--output-type") + .arg("yaml") + .arg(file_path.clone()) + .arg(r#"["b"]"#) + .arg(r#""ba""#) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut after = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut after) + .unwrap(); + assert!(before != after); + assert!(after.starts_with("a: ")); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--input-type") + .arg("yaml") + .arg("--output-type") + .arg("yaml") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let data = &String::from_utf8_lossy(&output.stdout); + assert!(data == "a: 2\nb: ba\n"); + } + + #[test] + fn set_json_file_update_idempotent_nowrite() { + let file_path = prepare_temp_file( + "test_set_update_idempotent_nowrite.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let mut before = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut before) + .unwrap(); + let output = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--output-type") + .arg("yaml") + .arg("--idempotent") + .arg(file_path.clone()) + .arg(r#"["b"]"#) + .arg(r#""ba""#) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut after = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut after) + .unwrap(); + println!("before: {}\nafter: {}", &before, &after,); + assert!(before == after); + } + #[test] fn set_json_file_insert() { let file_path = @@ -340,6 +541,105 @@ bar: baz", panic!("Output JSON does not have the expected structure"); } + #[test] + fn set_json_file_insert_with_value_file() { + let file_path = prepare_temp_file( + "test_set_json_file_insert_with_value_file.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + let value_file = prepare_temp_file("insert_value_file.json", r#"{"cc": "ccc"}"#.as_bytes()); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let output = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--value-file") + .arg(file_path.clone()) + .arg(r#"["c"]"#) + .arg(value_file.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut s = String::new(); + File::open(file_path) + .unwrap() + .read_to_string(&mut s) + .unwrap(); + let data: Value = serde_json::from_str(&s).expect("Error parsing sops's JSON output"); + if let Value::Mapping(data) = data { + let a = data.get(&Value::String("c".to_owned())).unwrap(); + if let &Value::Mapping(ref a) = a { + assert_encrypted!(&a, Value::String("cc".to_owned())); + return; + } + } + panic!("Output JSON does not have the expected structure"); + } + + #[test] + fn set_json_file_insert_with_value_stdin() { + let file_path = prepare_temp_file( + "test_set_json_file_insert_with_value_stdin.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + // let value_file = prepare_temp_file("insert_value_file.json", r#"{"cc": "ccc"}"#.as_bytes()); + let process = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--value-stdin") + .arg(file_path.clone()) + .arg(r#"["c"]"#) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin(&process, b"{\"cc\": \"ccc\"}"); + let output = process.wait_with_output().expect("Failed to wait on sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut s = String::new(); + File::open(file_path) + .unwrap() + .read_to_string(&mut s) + .unwrap(); + let data: Value = serde_json::from_str(&s).expect("Error parsing sops's JSON output"); + if let Value::Mapping(data) = data { + let a = data.get(&Value::String("c".to_owned())).unwrap(); + if let &Value::Mapping(ref a) = a { + assert_encrypted!(&a, Value::String("cc".to_owned())); + return; + } + } + panic!("Output JSON does not have the expected structure"); + } + #[test] fn set_yaml_file_update() { let file_path = prepare_temp_file( @@ -550,6 +850,50 @@ b: ba"# } } + #[test] + fn test_yaml_time() { + let file_path = prepare_temp_file( + "test_time.yaml", + r#"a: 2024-01-01 +b: 2006-01-02T15:04:05+07:06"# + .as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut s = String::new(); + File::open(file_path) + .unwrap() + .read_to_string(&mut s) + .unwrap(); + assert_eq!( + s, + r#"a: 2024-01-01T00:00:00Z +b: 2006-01-02T15:04:05+07:06 +"# + ); + } + #[test] fn unset_json_file() { // Test removal of tree branch @@ -802,6 +1146,36 @@ b: ba"# ); } + #[test] + fn decrypt_from_stdin() { + let process = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--input-type") + .arg("yaml") + .arg("--output-type") + .arg("yaml") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin(&process, include_bytes!("../res/comments.enc.yaml")); + let output = process.wait_with_output().expect("Failed to wait on sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + let yaml = &String::from_utf8_lossy(&output.stdout); + let data: Value = serde_yaml::from_str(&yaml).expect("Error parsing sops's YAML output"); + match data.into() { + Value::Mapping(m) => { + assert!( + m.get(&Value::String("sops".to_owned())).is_none(), + "sops metadata branch found" + ); + assert_eq!(m["lorem"], Value::String("ipsum".to_owned())); + assert_eq!(m["dolor"], Value::String("sit".to_owned())); + } + _ => panic!("sops's JSON output is not an object"), + } + } + #[test] fn encrypt_comments() { let file_path = "res/comments.yaml"; @@ -1229,7 +1603,8 @@ bar: |- r#"{ "foo": "bar", "bar": "baz\nbam" -}"# +} +"# ); } diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 8ff51357d0..1969e8b90e 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -12,6 +12,7 @@ import ( kms "cloud.google.com/go/kms/apiv1" "cloud.google.com/go/kms/apiv1/kmspb" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" "google.golang.org/api/option" "google.golang.org/grpc" @@ -23,6 +24,9 @@ const ( // a path to a credentials file, or directly as the variable's value in JSON // format. SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS" + // SopsGoogleCredentialsOAuthTokenEnv is the environment variable used for the + // GCP OAuth 2.0 Token. + SopsGoogleCredentialsOAuthTokenEnv = "GOOGLE_OAUTH_ACCESS_TOKEN" // KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey. KeyTypeIdentifier = "gcp_kms" ) @@ -50,6 +54,11 @@ type MasterKey struct { // for NeedsRotation. CreationDate time.Time + // tokenSource contains the oauth2.TokenSource used by the GCP client. + // It can be injected by a (local) keyservice.KeyServiceServer using + // TokenSource.ApplyToMasterKey. + // If nil, the remaining authentication methods are attempted. + tokenSource oauth2.TokenSource // credentialJSON is the Service Account credentials JSON used for // authenticating towards the GCP KMS service. credentialJSON []byte @@ -57,6 +66,8 @@ type MasterKey struct { // Mostly useful for testing at present, to wire the client to a mock // server. grpcConn *grpc.ClientConn + // grpcDialOpts are the gRPC dial options used to create the gRPC connection. + grpcDialOpts []grpc.DialOption } // NewMasterKeyFromResourceID creates a new MasterKey with the provided resource @@ -82,6 +93,22 @@ func MasterKeysFromResourceIDString(resourceID string) []*MasterKey { return keys } +// TokenSource is an oauth2.TokenSource used for authenticating towards the +// GCP KMS service. +type TokenSource struct { + source oauth2.TokenSource +} + +// NewTokenSource creates a new TokenSource from the provided oauth2.TokenSource. +func NewTokenSource(source oauth2.TokenSource) TokenSource { + return TokenSource{source: source} +} + +// ApplyToMasterKey configures the TokenSource on the provided key. +func (t TokenSource) ApplyToMasterKey(key *MasterKey) { + key.tokenSource = t.source +} + // CredentialJSON is the Service Account credentials JSON used for authenticating // towards the GCP KMS service. type CredentialJSON []byte @@ -91,10 +118,26 @@ func (c CredentialJSON) ApplyToMasterKey(key *MasterKey) { key.credentialJSON = c } +// DialOptions are the gRPC dial options used to create the gRPC connection. +type DialOptions []grpc.DialOption + +// ApplyToMasterKey configures the DialOptions on the provided key. +func (d DialOptions) ApplyToMasterKey(key *MasterKey) { + key.grpcDialOpts = d +} + // Encrypt takes a SOPS data key, encrypts it with GCP KMS, and stores the // result in the EncryptedKey field. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { - service, err := key.newKMSClient() + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext takes a SOPS data key, encrypts it with GCP KMS, and stores the +// result in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { + service, err := key.newKMSClient(ctx) if err != nil { log.WithField("resourceID", key.ResourceID).Info("Encryption failed") return fmt.Errorf("cannot create GCP KMS service: %w", err) @@ -109,7 +152,6 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { Name: key.ResourceID, Plaintext: dataKey, } - ctx := context.Background() resp, err := service.Encrypt(ctx, req) if err != nil { log.WithField("resourceID", key.ResourceID).Info("Encryption failed") @@ -144,8 +186,16 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { // Decrypt decrypts the EncryptedKey field with GCP KMS and returns // the result. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { - service, err := key.newKMSClient() + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey field with GCP KMS and returns +// the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { + service, err := key.newKMSClient(ctx) if err != nil { log.WithField("resourceID", key.ResourceID).Info("Decryption failed") return nil, fmt.Errorf("cannot create GCP KMS service: %w", err) @@ -168,7 +218,6 @@ func (key *MasterKey) Decrypt() ([]byte, error) { Name: key.ResourceID, Ciphertext: decodedCipher, } - ctx := context.Background() resp, err := service.Decrypt(ctx, req) if err != nil { log.WithField("resourceID", key.ResourceID).Info("Decryption failed") @@ -203,11 +252,11 @@ func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } -// newKMSClient returns a GCP KMS client configured with the credentialJSON -// and/or grpcConn, falling back to environmental defaults. +// newKMSClient returns a GCP KMS client configured with the tokenSource +// or credentialJSON, and/or grpcConn, falling back to environmental defaults. // It returns an error if the ResourceID is invalid, or if the setup of the // client fails. -func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { +func (key *MasterKey) newKMSClient(ctx context.Context) (*kms.KeyManagementClient, error) { re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`) matches := re.FindStringSubmatch(key.ResourceID) if matches == nil { @@ -216,22 +265,35 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { var opts []option.ClientOption switch { + case key.tokenSource != nil: + opts = append(opts, option.WithTokenSource(key.tokenSource)) case key.credentialJSON != nil: opts = append(opts, option.WithCredentialsJSON(key.credentialJSON)) default: credentials, err := getGoogleCredentials() if err != nil { - return nil, err + return nil, fmt.Errorf("credentials: failed to obtain credentials from %q: %w", SopsGoogleCredentialsEnv, err) } if credentials != nil { opts = append(opts, option.WithCredentialsJSON(credentials)) + break + } + + if atCredentials := getGoogleOAuthTokenFromEnv(); atCredentials != nil { + opts = append(opts, option.WithTokenSource(atCredentials)) + break } } - if key.grpcConn != nil { + + switch { + case key.grpcConn != nil: opts = append(opts, option.WithGRPCConn(key.grpcConn)) + case len(key.grpcDialOpts) > 0: + for _, opt := range key.grpcDialOpts { + opts = append(opts, option.WithGRPCDialOption(opt)) + } } - ctx := context.Background() client, err := kms.NewKeyManagementClient(ctx, opts...) if err != nil { return nil, err @@ -242,8 +304,8 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { // getGoogleCredentials returns the SopsGoogleCredentialsEnv variable, as // either the file contents of the path of a credentials file, or as value in -// JSON format. It returns an error if the file cannot be read, and may return -// a nil byte slice if no value is set. +// JSON format. +// It returns an error and a nil byte slice if the file cannot be read. func getGoogleCredentials() ([]byte, error) { if defaultCredentials, ok := os.LookupEnv(SopsGoogleCredentialsEnv); ok && len(defaultCredentials) > 0 { if _, err := os.Stat(defaultCredentials); err == nil { @@ -253,3 +315,16 @@ func getGoogleCredentials() ([]byte, error) { } return nil, nil } + +// getGoogleOAuthTokenFromEnv returns the SopsGoogleCredentialsOauthTokenEnv variable, +// as the OAauth 2.0 token. +// It returns an error and a nil byte slice if the envrionment variable is not set. +func getGoogleOAuthTokenFromEnv() oauth2.TokenSource { + if token, ok := os.LookupEnv(SopsGoogleCredentialsOAuthTokenEnv); ok && len(token) > 0 { + tokenSource := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + return tokenSource + } + return nil +} diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index 153bfb2604..e0365de34f 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -1,6 +1,7 @@ package gcpkms import ( + "context" "encoding/base64" "fmt" "net" @@ -9,6 +10,7 @@ import ( "cloud.google.com/go/kms/apiv1/kmspb" "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -38,6 +40,13 @@ func TestMasterKeysFromResourceIDString(t *testing.T) { } } +func TestTokenSource_ApplyToMasterKey(t *testing.T) { + src := NewTokenSource(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "some-token"})) + key := &MasterKey{} + src.ApplyToMasterKey(key) + assert.Equal(t, src.source, key.tokenSource) +} + func TestCredentialJSON_ApplyToMasterKey(t *testing.T) { key := &MasterKey{} credential := CredentialJSON("mock") @@ -53,8 +62,9 @@ func TestMasterKey_Encrypt(t *testing.T) { }) key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, + grpcConn: newGRPCServer("0"), + ResourceID: testResourceID, + credentialJSON: []byte("arbitrary credentials"), } err := key.Encrypt([]byte("encrypt")) assert.NoError(t, err) @@ -80,9 +90,10 @@ func TestMasterKey_Decrypt(t *testing.T) { Plaintext: []byte(decryptedData), }) key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, - EncryptedKey: "encryptedKey", + grpcConn: newGRPCServer("0"), + ResourceID: testResourceID, + EncryptedKey: "encryptedKey", + credentialJSON: []byte("arbitrary credentials"), } data, err := key.Decrypt() assert.NoError(t, err) @@ -116,7 +127,7 @@ func TestMasterKey_ToMap(t *testing.T) { }, key.ToMap()) } -func TestMasterKey_createCloudKMSService(t *testing.T) { +func TestMasterKey_createCloudKMSService_withCredentialsFile(t *testing.T) { tests := []struct { key MasterKey errString string @@ -136,10 +147,16 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { "type": "authorized_user"}`), }, }, + { + key: MasterKey{ + ResourceID: testResourceID, + }, + errString: `credentials: failed to obtain credentials from "SOPS_GOOGLE_CREDENTIALS"`, + }, } for _, tt := range tests { - _, err := tt.key.newKMSClient() + _, err := tt.key.newKMSClient(context.Background()) if tt.errString != "" { assert.Error(t, err) assert.ErrorContains(t, err, tt.errString) @@ -149,6 +166,29 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { } } +func TestMasterKey_createCloudKMSService_withOauthToken(t *testing.T) { + t.Setenv(SopsGoogleCredentialsOAuthTokenEnv, "token") + + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient(context.Background()) + + assert.NoError(t, err) +} + +func TestMasterKey_createCloudKMSService_withoutCredentials(t *testing.T) { + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient(context.Background()) + + assert.Error(t, err) + assert.ErrorContains(t, err, "credentials: could not find default credentials") +} + func newGRPCServer(port string) *grpc.ClientConn { serv := grpc.NewServer() kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement) @@ -159,7 +199,7 @@ func newGRPCServer(port string) *grpc.ClientConn { } go serv.Serve(lis) - conn, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index f90125b58c..eefd8f8a11 100644 --- a/go.mod +++ b/go.mod @@ -1,152 +1,152 @@ module github.com/getsops/sops/v3 -go 1.22 -toolchain go1.22.9 +go 1.23.0 require ( - cloud.google.com/go/kms v1.20.5 - cloud.google.com/go/storage v1.50.0 + cloud.google.com/go/kms v1.22.0 + cloud.google.com/go/storage v1.56.1 filippo.io/age v1.2.1 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 - github.com/ProtonMail/go-crypto v1.1.5 - github.com/aws/aws-sdk-go-v2 v1.36.0 - github.com/aws/aws-sdk-go-v2/config v1.29.4 - github.com/aws/aws-sdk-go-v2/credentials v1.17.57 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.57 - github.com/aws/aws-sdk-go-v2/service/kms v1.37.16 - github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 + github.com/ProtonMail/go-crypto v1.3.0 + github.com/aws/aws-sdk-go-v2 v1.38.1 + github.com/aws/aws-sdk-go-v2/config v1.31.2 + github.com/aws/aws-sdk-go-v2/credentials v1.18.6 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0 + github.com/aws/aws-sdk-go-v2/service/kms v1.44.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 github.com/blang/semver v3.5.1+incompatible github.com/fatih/color v1.18.0 github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/goware/prefixer v0.0.0-20160118172347-395022866408 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/vault/api v1.15.0 + github.com/hashicorp/vault/api v1.20.0 github.com/lib/pq v1.10.9 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/oracle/oci-go-sdk/v65 v65.81.1 - github.com/ory/dockertest/v3 v3.11.0 + github.com/ory/dockertest/v3 v3.12.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 - github.com/urfave/cli v1.22.16 - golang.org/x/net v0.34.0 - golang.org/x/sys v0.29.0 - golang.org/x/term v0.28.0 - google.golang.org/api v0.219.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 - google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 + github.com/stretchr/testify v1.11.0 + github.com/urfave/cli v1.22.17 + go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/crypto v0.41.0 + golang.org/x/net v0.43.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sys v0.35.0 + golang.org/x/term v0.34.0 + google.golang.org/api v0.248.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c + google.golang.org/grpc v1.75.0 + google.golang.org/protobuf v1.36.8 gopkg.in/ini.v1 v1.67.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( - cel.dev/expr v0.19.1 // indirect - cloud.google.com/go v0.117.0 // indirect - cloud.google.com/go/auth v0.14.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.3.0 // indirect - cloud.google.com/go/longrunning v0.6.3 // indirect - cloud.google.com/go/monitoring v1.22.0 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect dario.cat/mergo v1.0.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect - github.com/aws/smithy-go v1.22.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect + github.com/aws/smithy-go v1.22.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.5.0 // indirect - github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/continuity v0.4.5 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v27.4.1+incompatible // indirect - github.com/docker/docker v27.4.1+incompatible // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v28.0.4+incompatible // indirect + github.com/docker/docker v28.0.4+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/envoyproxy/go-control-plane v0.13.1 // indirect - github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/user v0.3.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.2.3 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runc v1.2.6 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sony/gobreaker v0.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ebfe553260..58adca7d5a 100644 --- a/go.sum +++ b/go.sum @@ -1,175 +1,173 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cloud.google.com/go v0.117.0 h1:Z5TNFfQxj7WG2FgOGX1ekC5RiXrYgms6QscOm32M/4s= -cloud.google.com/go v0.117.0/go.mod h1:ZbwhVTb1DBGt2Iwb3tNO6SEK4q+cplHZmLWH+DelYYc= -cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM= -cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.3.0 h1:4Wo2qTaGKFtajbLpF6I4mywg900u3TLlHDb6mriLDPU= -cloud.google.com/go/iam v1.3.0/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/kms v1.20.5 h1:aQQ8esAIVZ1atdJRxihhdxGQ64/zEbJoJnCz/ydSmKg= -cloud.google.com/go/kms v1.20.5/go.mod h1:C5A8M1sv2YWYy1AE6iSrnddSG9lRGdJq5XEdBy28Lmw= -cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= -cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= -cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= -cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= -cloud.google.com/go/monitoring v1.22.0 h1:mQ0040B7dpuRq1+4YiQD43M2vW9HgoVxY98xhqGT+YI= -cloud.google.com/go/monitoring v1.22.0/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= -cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= -cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= -cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI= -cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.56.1 h1:n6gy+yLnHn0hTwBFzNn8zJ1kqWfR91wzdM8hjRF4wP0= +cloud.google.com/go/storage v1.56.1/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0/go.mod h1:l2fIqmwB+FKSfvn3bAD/0i+AXAxhIZjTK2svT/mgUXs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 h1:GYUJLfvd++4DMuMhCFLgLXvFwofIxh/qOwoGuS/LTew= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= -github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= -github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= -github.com/aws/aws-sdk-go-v2/config v1.29.4 h1:ObNqKsDYFGr2WxnoXKOhCvTlf3HhwtoGgc+KmZ4H5yg= -github.com/aws/aws-sdk-go-v2/config v1.29.4/go.mod h1:j2/AF7j/qxVmsNIChw1tWfsVKOayJoGRDjg1Tgq7NPk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.57 h1:kFQDsbdBAR3GZsB8xA+51ptEnq9TIj3tS4MuP5b+TcQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.57/go.mod h1:2kerxPUUbTagAr/kkaHiqvj/bcYHzi2qiJS/ZinllU0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.57 h1:4hFrvTb32jty/LpKdIwWhMgqITPxNo9l1X1hjUyVCZ4= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.57/go.mod h1:n6n8rfggAVPgDVldL1zk9QUzIWImRb6OWI8t9CfDImM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= -github.com/aws/aws-sdk-go-v2/service/kms v1.37.16 h1:DasFbYMIEAOh0QNbGTsJnTeVPualnAc6UR74Fs0Z9ME= -github.com/aws/aws-sdk-go-v2/service/kms v1.37.16/go.mod h1:rtC85vnVnYbYWQWtm2tPHPnZ/JOPa5+iuUDip9XDa1Q= -github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 h1:dyC+iA2+Yc7iDMDh0R4eT6fi8TgBduc+BOWCy6Br0/o= -github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 h1:fqg6c1KVrc3SYWma/egWue5rKI4G2+M4wMQN2JosNAA= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.12/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= +github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0 h1:2FFgK3oFA8PTNBjprLFfcmkgg7U9YuSimBvR64RUmiA= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0/go.mod h1:xdxj6nC1aU/jAO80RIlIj3fU40MOSqutEA9N2XFct04= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 h1:BE/MNQ86yzTINrfxPPFS86QCBNQeLKY2A0KhDh47+wI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4/go.mod h1:SPBBhkJxjcrzJBc+qY85e83MQ2q3qdra8fghhkkyrJg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 h1:Beh9oVgtQnBgR4sKKzkUBRQpf1GnL4wt0l4s8h2VCJ0= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4/go.mod h1:b17At0o8inygF+c6FOD3rNyYZufPw62o9XJbSfQPgbo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 h1:HVSeukL40rHclNcUqVcBwE1YoZhOkoLeBfhUqR3tjIU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4/go.mod h1:DnbBOv4FlIXHj2/xmrUQYtawRFC9L9ZmQPz+DBc6X5I= +github.com/aws/aws-sdk-go-v2/service/kms v1.44.2 h1:yTtMSIGWk8KzPDX2pS9k7wNCPKiNWpiJ9DdB2mCAMzo= +github.com/aws/aws-sdk-go-v2/service/kms v1.44.2/go.mod h1:zgkQ8ige7qtxldA4cGtiXdbql3dBo4TfsP6uQyHwq0E= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 h1:2n6Pd67eJwAb/5KCX62/8RTU0aFAAW7V5XIGSghiHrw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1/go.mod h1:w5PC+6GHLkvMJKasYGVloB3TduOtROEMqm15HSuIbw4= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= -github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= -github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= -github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= +github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= +github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= -github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= -github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= -github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -178,10 +176,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -197,18 +195,18 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= -github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= +github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -219,9 +217,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -234,28 +231,27 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= -github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.2.6 h1:P7Hqg40bsMvQGCS4S7DJYhUZOISMLJOB2iGX5COWiPk= +github.com/opencontainers/runc v1.2.6/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= github.com/oracle/oci-go-sdk/v65 v65.81.1 h1:JYc47bk8n/MUchA2KHu1ggsCQzlJZQLJ+tTKfOho00E= github.com/oracle/oci-go-sdk/v65 v65.81.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= -github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= -github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -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/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -266,6 +262,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 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= @@ -276,11 +274,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= -github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -290,67 +288,68 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4= -go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= -go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -359,24 +358,25 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0= -google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE= -google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8 h1:e26eS1K69yxjjNNHYqjN49y95kcaQLJ3TL5h68dcA1E= -google.golang.org/genproto v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:i5btTErZyoKCCubju3HS5LVho4nZd3yFnEp6moqeUjE= -google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= -google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y= +google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/hcvault/keysource.go b/hcvault/keysource.go index c120d9c996..67706e71e3 100644 --- a/hcvault/keysource.go +++ b/hcvault/keysource.go @@ -2,10 +2,12 @@ package hcvault import ( "bytes" + "context" "encoding/base64" "errors" "fmt" "io" + "net/http" "net/url" "os" "path" @@ -70,6 +72,8 @@ type MasterKey struct { // Token.ApplyToMasterKey. If empty, the default client configuration // is used, before falling back to the token stored in defaultTokenFile. token string + // httpClient is used to override the default HTTP client used by the Vault client. + httpClient *http.Client } // NewMasterKeysFromURIs creates a list of MasterKeys from a list of Vault @@ -128,18 +132,42 @@ func NewMasterKey(address, enginePath, keyName string) *MasterKey { return key } +// HTTPClient is a wrapper around http.Client used for configuring the +// Vault client. +type HTTPClient struct { + hc *http.Client +} + +// NewHTTPClient creates a new HTTPClient with the provided http.Client. +func NewHTTPClient(hc *http.Client) *HTTPClient { + return &HTTPClient{hc: hc} +} + +// ApplyToMasterKey configures the HTTP client on the provided key. +func (h HTTPClient) ApplyToMasterKey(key *MasterKey) { + key.httpClient = h.hc +} + // Encrypt takes a SOPS data key, encrypts it with Vault Transit, and stores // the result in the EncryptedKey field. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext takes a SOPS data key, encrypts it with Vault Transit, and stores +// the result in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { fullPath := key.encryptPath() - client, err := vaultClient(key.VaultAddress, key.token) + client, err := vaultClient(key.VaultAddress, key.token, key.httpClient) if err != nil { log.WithField("Path", fullPath).Info("Encryption failed") return err } - secret, err := client.Logical().Write(fullPath, encryptPayload(dataKey)) + secret, err := client.Logical().WriteWithContext(ctx, fullPath, encryptPayload(dataKey)) if err != nil { log.WithField("Path", fullPath).Info("Encryption failed") return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err) @@ -175,16 +203,23 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { } // Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey field with Vault Transit and returns the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { fullPath := key.decryptPath() - client, err := vaultClient(key.VaultAddress, key.token) + client, err := vaultClient(key.VaultAddress, key.token, key.httpClient) if err != nil { log.WithField("Path", fullPath).Info("Decryption failed") return nil, err } - secret, err := client.Logical().Write(fullPath, decryptPayload(key.EncryptedKey)) + secret, err := client.Logical().WriteWithContext(ctx, fullPath, decryptPayload(key.EncryptedKey)) if err != nil { log.WithField("Path", fullPath).Info("Decryption failed") return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err) @@ -292,10 +327,14 @@ func dataKeyFromSecret(secret *api.Secret) ([]byte, error) { // vaultClient returns a new Vault client, configured with the given address // and token. -func vaultClient(address, token string) (*api.Client, error) { +func vaultClient(address, token string, hc *http.Client) (*api.Client, error) { cfg := api.DefaultConfig() cfg.Address = address + if hc != nil { + cfg.HttpClient = hc + } + client, err := api.NewClient(cfg) if err != nil { return nil, fmt.Errorf("cannot create Vault client: %w", err) diff --git a/hcvault/keysource_test.go b/hcvault/keysource_test.go index 6468953350..02d5a13e2e 100644 --- a/hcvault/keysource_test.go +++ b/hcvault/keysource_test.go @@ -187,7 +187,7 @@ func TestMasterKey_Encrypt(t *testing.T) { assert.NoError(t, key.Encrypt(dataKey)) assert.NotEmpty(t, key.EncryptedKey) - client, err := vaultClient(key.VaultAddress, key.token) + client, err := vaultClient(key.VaultAddress, key.token, nil) assert.NoError(t, err) payload := decryptPayload(key.EncryptedKey) @@ -230,7 +230,7 @@ func TestMasterKey_Decrypt(t *testing.T) { (Token(testVaultToken)).ApplyToMasterKey(key) assert.NoError(t, createVaultKey(key)) - client, err := vaultClient(key.VaultAddress, key.token) + client, err := vaultClient(key.VaultAddress, key.token, nil) assert.NoError(t, err) dataKey := []byte("the heart of a shrimp is located in its head") @@ -368,7 +368,7 @@ func Test_vaultClient(t *testing.T) { t.Setenv("VAULT_TOKEN", "") t.Setenv("HOME", tmpDir) - got, err := vaultClient(testVaultAddress, "") + got, err := vaultClient(testVaultAddress, "", nil) assert.NoError(t, err) assert.NotNil(t, got) assert.Empty(t, got.Token()) @@ -378,7 +378,7 @@ func Test_vaultClient(t *testing.T) { token := "test-token" t.Setenv("VAULT_TOKEN", token) - got, err := vaultClient(testVaultAddress, "") + got, err := vaultClient(testVaultAddress, "", nil) assert.NoError(t, err) assert.NotNil(t, got) assert.Equal(t, token, got.Token()) @@ -388,7 +388,7 @@ func Test_vaultClient(t *testing.T) { ignored := "test-token" t.Setenv("VAULT_TOKEN", ignored) - got, err := vaultClient(testVaultAddress, testVaultToken) + got, err := vaultClient(testVaultAddress, testVaultToken, nil) assert.NoError(t, err) assert.NotNil(t, got) assert.Equal(t, testVaultToken, got.Token()) @@ -407,7 +407,7 @@ func Test_vaultClient(t *testing.T) { t.Setenv("VAULT_TOKEN", "") t.Setenv("HOME", tmpDir) - got, err := vaultClient(testVaultAddress, "") + got, err := vaultClient(testVaultAddress, "", nil) assert.NoError(t, err) assert.NotNil(t, got) assert.Equal(t, token, got.Token()) @@ -487,7 +487,7 @@ func Test_engineAndKeyFromPath(t *testing.T) { // enableVaultTransit enables the Vault Transit backend on the given enginePath. func enableVaultTransit(address, token, enginePath string) error { - client, err := vaultClient(address, token) + client, err := vaultClient(address, token, nil) if err != nil { return fmt.Errorf("cannot create Vault client: %w", err) } @@ -504,7 +504,7 @@ func enableVaultTransit(address, token, enginePath string) error { // createVaultKey creates a new RSA-4096 Vault key using the data from the // provided MasterKey. func createVaultKey(key *MasterKey) error { - client, err := vaultClient(key.VaultAddress, key.token) + client, err := vaultClient(key.VaultAddress, key.token, nil) if err != nil { return fmt.Errorf("cannot create Vault client: %w", err) } diff --git a/kms/keysource.go b/kms/keysource.go index d3be8d1044..bdb9637220 100644 --- a/kms/keysource.go +++ b/kms/keysource.go @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "fmt" + "net/http" "os" "regexp" "sort" @@ -79,6 +80,8 @@ type MasterKey struct { // injected using e.g. an environment variable. The field is not publicly // exposed, nor configurable. baseEndpoint string + // httpClient is used to override the default HTTP client used by the AWS client. + httpClient *http.Client } // NewMasterKey creates a new MasterKey from an ARN, role and context, setting @@ -233,10 +236,34 @@ func (c CredentialsProvider) ApplyToMasterKey(key *MasterKey) { key.credentialsProvider = c.provider } +// HTTPClient is a wrapper around http.Client used for configuring the +// AWS KMS client. +type HTTPClient struct { + hc *http.Client +} + +// NewHTTPClient creates a new HTTPClient with the provided http.Client. +func NewHTTPClient(hc *http.Client) *HTTPClient { + return &HTTPClient{hc: hc} +} + +// ApplyToMasterKey configures the HTTP client on the provided key. +func (h HTTPClient) ApplyToMasterKey(key *MasterKey) { + key.httpClient = h.hc +} + // Encrypt takes a SOPS data key, encrypts it with KMS and stores the result // in the EncryptedKey field. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { - cfg, err := key.createKMSConfig() + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext takes a SOPS data key, encrypts it with KMS and stores the result +// in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { + cfg, err := key.createKMSConfig(ctx) if err != nil { log.WithField("arn", key.Arn).Info("Encryption failed") return err @@ -247,7 +274,7 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { Plaintext: dataKey, EncryptionContext: stringPointerToStringMap(key.EncryptionContext), } - out, err := client.Encrypt(context.TODO(), input) + out, err := client.Encrypt(ctx, input) if err != nil { log.WithField("arn", key.Arn).Info("Encryption failed") return fmt.Errorf("failed to encrypt sops data key with AWS KMS: %w", err) @@ -278,13 +305,21 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { // Decrypt decrypts the EncryptedKey with a newly created AWS KMS config, and // returns the result. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey with a newly created AWS KMS config, and +// returns the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { k, err := base64.StdEncoding.DecodeString(key.EncryptedKey) if err != nil { log.WithField("arn", key.Arn).Info("Decryption failed") return nil, fmt.Errorf("error base64-decoding encrypted data key: %s", err) } - cfg, err := key.createKMSConfig() + cfg, err := key.createKMSConfig(ctx) if err != nil { log.WithField("arn", key.Arn).Info("Decryption failed") return nil, err @@ -295,7 +330,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { CiphertextBlob: k, EncryptionContext: stringPointerToStringMap(key.EncryptionContext), } - decrypted, err := client.Decrypt(context.TODO(), input) + decrypted, err := client.Decrypt(ctx, input) if err != nil { log.WithField("arn", key.Arn).Info("Decryption failed") return nil, fmt.Errorf("failed to decrypt sops data key with AWS KMS: %w", err) @@ -351,7 +386,7 @@ func (key *MasterKey) TypeToIdentifier() string { // createKMSConfig returns an AWS config with the credentialsProvider of the // MasterKey, or the default configuration sources. -func (key MasterKey) createKMSConfig() (*aws.Config, error) { +func (key MasterKey) createKMSConfig(ctx context.Context) (*aws.Config, error) { re := regexp.MustCompile(arnRegex) matches := re.FindStringSubmatch(key.Arn) if matches == nil { @@ -359,7 +394,7 @@ func (key MasterKey) createKMSConfig() (*aws.Config, error) { } region := matches[1] - cfg, err := config.LoadDefaultConfig(context.TODO(), func(lo *config.LoadOptions) error { + cfg, err := config.LoadDefaultConfig(ctx, func(lo *config.LoadOptions) error { // Use the credentialsProvider if present, otherwise default to reading credentials // from the environment. if key.credentialsProvider != nil { @@ -369,6 +404,9 @@ func (key MasterKey) createKMSConfig() (*aws.Config, error) { lo.SharedConfigProfile = key.AwsProfile } lo.Region = region + if key.httpClient != nil { + lo.HTTPClient = key.httpClient + } return nil }) if err != nil { @@ -376,7 +414,7 @@ func (key MasterKey) createKMSConfig() (*aws.Config, error) { } if key.Role != "" { - return key.createSTSConfig(&cfg) + return key.createSTSConfig(ctx, &cfg) } return &cfg, nil } @@ -393,7 +431,7 @@ func (key MasterKey) createClient(config *aws.Config) *kms.Client { // createSTSConfig uses AWS STS to assume a role and returns a config // configured with that role's credentials. It returns an error if // it fails to construct a session name, or assume the role. -func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { +func (key MasterKey) createSTSConfig(ctx context.Context, config *aws.Config) (*aws.Config, error) { name, err := stsSessionName() if err != nil { return nil, err @@ -404,7 +442,7 @@ func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { } client := sts.NewFromConfig(*config) - out, err := client.AssumeRole(context.TODO(), input) + out, err := client.AssumeRole(ctx, input) if err != nil { return nil, fmt.Errorf("failed to assume role '%s': %w", key.Role, err) } diff --git a/kms/keysource_test.go b/kms/keysource_test.go index da3c6b51e9..e44929cb9d 100644 --- a/kms/keysource_test.go +++ b/kms/keysource_test.go @@ -535,7 +535,7 @@ aws_secret_access_key = test-secret`), 0600)) if tt.envFunc != nil { tt.envFunc(t) } - cfg, err := tt.key.createKMSConfig() + cfg, err := tt.key.createKMSConfig(context.Background()) tt.assertFunc(t, cfg, err) }) } @@ -549,7 +549,7 @@ func TestMasterKey_createSTSConfig(t *testing.T) { return } key := NewMasterKeyFromArn(dummyARN, nil, "") - cfg, err := key.createSTSConfig(nil) + cfg, err := key.createSTSConfig(context.Background(), nil) assert.Error(t, err) assert.ErrorContains(t, err, "failed to construct STS session name") assert.Nil(t, cfg) @@ -558,7 +558,7 @@ func TestMasterKey_createSTSConfig(t *testing.T) { t.Run("role assumption error", func(t *testing.T) { key := NewMasterKeyFromArn(dummyARN, nil, "") key.Role = "role" - got, err := key.createSTSConfig(&aws.Config{}) + got, err := key.createSTSConfig(context.Background(), &aws.Config{}) assert.Error(t, err) assert.ErrorContains(t, err, "failed to assume role 'role'") assert.Nil(t, got) @@ -629,7 +629,7 @@ func createTestMasterKey(arn string) MasterKey { // createTestKMSClient creates a new client with the // aws.EndpointResolverWithOptions set to epResolver. func createTestKMSClient(key MasterKey) (*kms.Client, error) { - cfg, err := key.createKMSConfig() + cfg, err := key.createKMSConfig(context.Background()) if err != nil { return nil, err } diff --git a/pgp/keysource.go b/pgp/keysource.go index 1646aceaa6..63e31027ce 100644 --- a/pgp/keysource.go +++ b/pgp/keysource.go @@ -8,6 +8,7 @@ package pgp // import "github.com/getsops/sops/v3/pgp" import ( "bytes" + "context" "encoding/hex" "errors" "fmt" @@ -129,13 +130,22 @@ func NewGnuPGHome() (GnuPGHome, error) { // Import attempts to import the armored key bytes into the GnuPGHome keyring. // It returns an error if the GnuPGHome does not pass Validate, or if the // import failed. +// +// Consider using ImportContext instead. func (d GnuPGHome) Import(armoredKey []byte) error { + return d.ImportContext(context.Background(), armoredKey) +} + +// ImportContext attempts to import the armored key bytes into the GnuPGHome keyring. +// It returns an error if the GnuPGHome does not pass Validate, or if the +// import failed. +func (d GnuPGHome) ImportContext(ctx context.Context, armoredKey []byte) error { if err := d.Validate(); err != nil { return fmt.Errorf("cannot import armored key data into GnuPG keyring: %w", err) } args := []string{"--batch", "--import"} - _, stderr, err := gpgExec(d.String(), args, bytes.NewReader(armoredKey)) + _, stderr, err := gpgExec(ctx, d.String(), args, bytes.NewReader(armoredKey)) if err != nil { stderrStr := strings.TrimSpace(stderr.String()) errStr := err.Error() @@ -254,7 +264,15 @@ func (e errSet) Error() string { // Encrypt encrypts the data key with the PGP key with the same // fingerprint as the MasterKey. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext encrypts the data key with the PGP key with the same +// fingerprint as the MasterKey. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { var errs errSet if !key.disableOpenPGP { @@ -266,7 +284,7 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { errs = append(errs, fmt.Errorf("github.com/ProtonMail/go-crypto/openpgp error: %w", openpgpErr)) } - binaryErr := key.encryptWithGnuPG(dataKey) + binaryErr := key.encryptWithGnuPG(ctx, dataKey) if binaryErr == nil { log.WithField("fingerprint", key.Fingerprint).Info("Encryption succeeded") return nil @@ -320,7 +338,7 @@ func (key *MasterKey) encryptWithOpenPGP(dataKey []byte) error { // encryptWithOpenPGP attempts to encrypt the data key using GnuPG with the // PGP key that belongs to Fingerprint. It sets EncryptedDataKey, or returns // an error. -func (key *MasterKey) encryptWithGnuPG(dataKey []byte) error { +func (key *MasterKey) encryptWithGnuPG(ctx context.Context, dataKey []byte) error { fingerprint := shortenFingerprint(key.Fingerprint) args := []string{ @@ -334,7 +352,7 @@ func (key *MasterKey) encryptWithGnuPG(dataKey []byte) error { fingerprint, "--no-encrypt-to", } - stdout, stderr, err := gpgExec(key.gnuPGHomeDir, args, bytes.NewReader(dataKey)) + stdout, stderr, err := gpgExec(ctx, key.gnuPGHomeDir, args, bytes.NewReader(dataKey)) if err != nil { return fmt.Errorf("failed to encrypt sops data key with pgp: %s", strings.TrimSpace(stderr.String())) } @@ -365,7 +383,16 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { // Decrypt first attempts to obtain the data key from the EncryptedKey // stored in the MasterKey using OpenPGP, before falling back to GnuPG. // When both attempts fail, an error is returned. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext first attempts to obtain the data key from the EncryptedKey +// stored in the MasterKey using OpenPGP, before falling back to GnuPG. +// When both attempts fail, an error is returned. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { var errs errSet if !key.disableOpenPGP { @@ -377,7 +404,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { errs = append(errs, fmt.Errorf("github.com/ProtonMail/go-crypto/openpgp error: %w", openpgpErr)) } - dataKey, binaryErr := key.decryptWithGnuPG() + dataKey, binaryErr := key.decryptWithGnuPG(ctx) if binaryErr == nil { log.WithField("fingerprint", key.Fingerprint).Info("Decryption succeeded") return dataKey, nil @@ -419,16 +446,26 @@ func (key *MasterKey) decryptWithOpenPGP() ([]byte, error) { // GnuPG and returns the result. If DisableAgent is configured on the MasterKey, // the GnuPG agent is not enabled. When the decryption command fails, it returns // the error from stdout. -func (key *MasterKey) decryptWithGnuPG() ([]byte, error) { +func (key *MasterKey) decryptWithGnuPG(ctx context.Context) ([]byte, error) { args := []string{ "-d", } - stdout, stderr, err := gpgExec(key.gnuPGHomeDir, args, strings.NewReader(key.EncryptedKey)) + stdout, stderr, err := gpgExec(ctx, key.gnuPGHomeDir, args, strings.NewReader(key.EncryptedKey)) if err != nil { return nil, fmt.Errorf("failed to decrypt sops data key with pgp: %s", strings.TrimSpace(stderr.String())) } - return stdout.Bytes(), nil + result := stdout.Bytes() + if len(result) == 0 { + // This can happen if an older GnuPG version is used to decrypt a key encrypted with a + // newer GnuPG version that used an AEAD cipher, which the old version does not support. + // Apparently some GnuPG versions drop the unspuported packets, which results in a decrypted + // data of 0 bytes, and returns nothing with exit code 0. + // + // (See https://github.com/getsops/sops/issues/896#issuecomment-2688079300 for more infos.) + return nil, fmt.Errorf("failed to decrypt sops data key with pgp: zero bytes returned") + } + return result, nil } // NeedsRotation returns whether the data key needs to be rotated @@ -585,12 +622,12 @@ func fingerprintIndex(ring openpgp.EntityList) map[string]openpgp.Entity { // gpgExec runs the provided args with the gpgBinary, while restricting it to // homeDir when provided. Stdout and stderr can be read from the returned // buffers. When the command fails, an error is returned. -func gpgExec(homeDir string, args []string, stdin io.Reader) (stdout bytes.Buffer, stderr bytes.Buffer, err error) { +func gpgExec(ctx context.Context, homeDir string, args []string, stdin io.Reader) (stdout bytes.Buffer, stderr bytes.Buffer, err error) { if homeDir != "" { args = append([]string{"--homedir", homeDir}, args...) } - cmd := exec.Command(gpgBinary(), args...) + cmd := exec.CommandContext(ctx, gpgBinary(), args...) cmd.Stdin = stdin cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/pgp/keysource_test.go b/pgp/keysource_test.go index 28fcfeb8ec..4bed79747d 100644 --- a/pgp/keysource_test.go +++ b/pgp/keysource_test.go @@ -2,6 +2,7 @@ package pgp import ( "bytes" + "context" "io" "os" "os/user" @@ -56,14 +57,14 @@ func TestGnuPGHome_Import(t *testing.T) { assert.NoError(t, err) assert.NoError(t, gnuPGHome.Import(b)) - _, stderr, err := gpgExec(gnuPGHome.String(), []string{"--list-keys", mockFingerprint}, nil) + _, stderr, err := gpgExec(context.Background(), gnuPGHome.String(), []string{"--list-keys", mockFingerprint}, nil) assert.NoErrorf(t, err, stderr.String()) b, err = os.ReadFile(mockPrivateKey) assert.NoError(t, err) assert.NoError(t, gnuPGHome.Import(b)) - _, stderr, err = gpgExec(gnuPGHome.String(), []string{"--list-secret-keys", mockFingerprint}, nil) + _, stderr, err = gpgExec(context.Background(), gnuPGHome.String(), []string{"--list-secret-keys", mockFingerprint}, nil) assert.NoErrorf(t, err, stderr.String()) err = gnuPGHome.Import([]byte("invalid armored data")) @@ -281,7 +282,7 @@ func TestMasterKey_encryptWithGnuPG(t *testing.T) { key := NewMasterKeyFromFingerprint(mockFingerprint) gnuPGHome.ApplyToMasterKey(key) data := []byte("oh no, my darkest secret") - assert.NoError(t, key.encryptWithGnuPG(data)) + assert.NoError(t, key.encryptWithGnuPG(context.Background(), data)) assert.NotEmpty(t, key.EncryptedKey) assert.NotEqual(t, data, key.EncryptedKey) @@ -291,14 +292,14 @@ func TestMasterKey_encryptWithGnuPG(t *testing.T) { args := []string{ "-d", } - stdout, stderr, err := gpgExec(key.gnuPGHomeDir, args, strings.NewReader(key.EncryptedKey)) + stdout, stderr, err := gpgExec(context.Background(), key.gnuPGHomeDir, args, strings.NewReader(key.EncryptedKey)) assert.NoError(t, err, stderr.String()) assert.Equal(t, data, stdout.Bytes()) }) t.Run("invalid fingerprint error", func(t *testing.T) { key := NewMasterKeyFromFingerprint("invalid") - err := key.encryptWithGnuPG([]byte("invalid")) + err := key.encryptWithGnuPG(context.Background(), []byte("invalid")) assert.Error(t, err) assert.ErrorContains(t, err, "failed to encrypt sops data key with pgp: gpg: 'invalid' is not a valid long keyID") }) @@ -341,7 +342,7 @@ func TestMasterKey_Decrypt(t *testing.T) { fingerprint := shortenFingerprint(mockFingerprint) data := []byte("this data is absolutely top secret") - stdout, stderr, err := gpgExec(gnuPGHome.String(), []string{ + stdout, stderr, err := gpgExec(context.Background(), gnuPGHome.String(), []string{ "--no-default-recipient", "--yes", "--encrypt", @@ -424,7 +425,7 @@ func TestMasterKey_decryptWithOpenPGP(t *testing.T) { fingerprint := shortenFingerprint(mockFingerprint) data := []byte("this data is absolutely top secret") - stdout, stderr, err := gpgExec(gnuPGHome.String(), []string{ + stdout, stderr, err := gpgExec(context.Background(), gnuPGHome.String(), []string{ "--no-default-recipient", "--yes", "--encrypt", @@ -473,7 +474,7 @@ func TestMasterKey_decryptWithGnuPG(t *testing.T) { fingerprint := shortenFingerprint(mockFingerprint) data := []byte("this data is absolutely top secret") - stdout, stderr, err := gpgExec(gnuPGHome.String(), []string{ + stdout, stderr, err := gpgExec(context.Background(), gnuPGHome.String(), []string{ "--no-default-recipient", "--yes", "--encrypt", @@ -494,7 +495,7 @@ func TestMasterKey_decryptWithGnuPG(t *testing.T) { gnuPGHome.ApplyToMasterKey(key) key.EncryptedKey = encryptedData - got, err := key.decryptWithGnuPG() + got, err := key.decryptWithGnuPG(context.Background()) assert.NoError(t, err) assert.Equal(t, data, got) }) @@ -502,7 +503,7 @@ func TestMasterKey_decryptWithGnuPG(t *testing.T) { t.Run("invalid data error", func(t *testing.T) { key := NewMasterKeyFromFingerprint(mockFingerprint) key.EncryptedKey = "absolute invalid" - got, err := key.decryptWithGnuPG() + got, err := key.decryptWithGnuPG(context.Background()) assert.Error(t, err) assert.ErrorContains(t, err, "gpg: no valid OpenPGP data found") assert.Nil(t, got) diff --git a/publish/vault.go b/publish/vault.go index 6f857cd019..4167ec8081 100644 --- a/publish/vault.go +++ b/publish/vault.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" + "github.com/getsops/sops/v3/logging" "github.com/google/go-cmp/cmp" vault "github.com/hashicorp/vault/api" - "github.com/getsops/sops/v3/logging" "github.com/sirupsen/logrus" ) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9289042575..3a445918b9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.70.0" +channel = "1.85.0" profile = "minimal" diff --git a/shamir/shamir.go b/shamir/shamir.go index 10d7bc3ba6..e9b9e59f6f 100644 --- a/shamir/shamir.go +++ b/shamir/shamir.go @@ -12,10 +12,7 @@ package shamir import ( "crypto/rand" - "crypto/subtle" "fmt" - mathrand "math/rand" - "time" ) const ( @@ -102,63 +99,90 @@ func div(a, b uint8) uint8 { panic("divide by zero") } - var goodVal, zero uint8 - logA := logTable[a] - logB := logTable[b] - diff := (int(logA) - int(logB)) % 255 - if diff < 0 { - diff += 255 - } - - ret := expTable[diff] - - // Ensure we return zero if a is zero but aren't subject to timing attacks - goodVal = ret - - if subtle.ConstantTimeByteEq(a, 0) == 1 { - ret = zero - } else { - ret = goodVal - } + // a divided by b is the same as a multiplied by the inverse of b: + return mult(a, inverse(b)) +} - return ret +// inverse calculates the inverse of a number in GF(2^8) +// Note that a must be non-zero; otherwise 0 is returned +func inverse(a uint8) uint8 { + // This makes use of Fermat's Little Theorem for finite groups: + // If G is a finite group with n elements, and a any element of G, + // then a raised to the power of n equals the neutral element of G. + // (See https://en.wikipedia.org/wiki/Fermat%27s_little_theorem; + // the generalization to finite groups follows from Lagrange's theorem: + // https://en.wikipedia.org/wiki/Lagrange%27s_theorem_(group_theory)) + // + // Here we use the multiplicative group of GF(2^8), which has + // n = 2^8 - 1 elements (every element but zero). Thus raising a to + // the (n - 1)th = 254th power gives a number x so that a*x = 1. + // + // If a happens to be 0, which is not part of the multiplicative group, + // then a raised to the power of 254 is still 0. + + // (See also https://github.com/openbao/openbao/commit/a209a052024b70bc563d9674cde21a20b5106570) + + // In the comments, we use ^ to denote raising to the power: + b := mult(a, a) // b is now a^2 + c := mult(a, b) // c is now a^3 + b = mult(c, c) // b is now a^6 + b = mult(b, b) // b is now a^12 + c = mult(b, c) // c is now a^15 + b = mult(b, b) // b is now a^24 + b = mult(b, b) // b is now a^48 + b = mult(b, c) // b is now a^63 + b = mult(b, b) // b is now a^126 + b = mult(a, b) // b is now a^127 + return mult(b, b) // result is a^254 } // mult multiplies two numbers in GF(2^8) // GF(2^8) multiplication using log/exp tables func mult(a, b uint8) (out uint8) { - var goodVal, zero uint8 - log_a := logTable[a] - log_b := logTable[b] - sum := (int(log_a) + int(log_b)) % 255 - - ret := expTable[sum] - - // Ensure we return zero if either a or b are zero but aren't subject to - // timing attacks - goodVal = ret - - if subtle.ConstantTimeByteEq(a, 0) == 1 { - ret = zero - } else { - ret = goodVal - } - - if subtle.ConstantTimeByteEq(b, 0) == 1 { - ret = zero - } else { - // This operation does not do anything logically useful. It - // only ensures a constant number of assignments to thwart - // timing attacks. - goodVal = zero + // This computes a * b in GF(2^8), which is defined as GF(2)[X] / . + // This finite field is known as Rijndael's finite field. (Rijndael is the algorithm that + // was standardized as AES.) + // (See https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field) + // + // We identify elements in GF(2^8) with polynomials of degree < 8. The i-th bit of a field + // element is the coefficient of X^i in that polynomial. + // + // To multiply a and b in this finite field, we use something similar to Russian peasant + // multiplication. We iterate over b's bits, starting from the highest to the lowest. + // i denotes the bit we're currently processing (7, 6, 5, 4, 3, 2, 1, 0). + // The accumulator is set to 0; every iteration, we multiply the accumulator + // by X modulo X^8+X^4+X^3+X+1, and then add a to the accumulator in case b's i-th bit is 1. + var accumulator uint8 = 0 + var i uint8 = 8 + + for i > 0 { + i-- + // Get the i-th bit of b; bitOfB is either 0 or 1. + bitOfB := b >> i & 1 + // aOrZero is 0 if the i-th bit of b is 0, and a if the i-th bit of b is 1. This is + // what we later add to the accumulator. + aOrZero := -bitOfB & a + // zeroOr1B is 0 if the 7th bit of the accumulator is 0, and 0x1B = 11011_2 if the + // 7th bit of accumulator is 1 + zeroOr1B := -(accumulator >> 7) & 0x1B + // accumulatorMultipliedByX equals accumulator multiplied by X modulo X^8+X^4+X^3+X+1 + // In the expression, accumulator + accumulator equals accumulator << 1, which would be + // the accumulator multiplied by X modulo X^8. + // By XORing (addition and subtraction in GF(2^8)) with zeroOr1B, we turn this into + // accumulator multiplied by X modulo X^8 + X^4 + X^3 + X + 1. + accumulatorMultipliedByX := zeroOr1B ^ (accumulator + accumulator) + // We can now compute the next value of the accumulator as the sum (in GF(2^8)) of aOrZero + // and accumulatorMultipliedByX. + accumulator = aOrZero ^ accumulatorMultipliedByX } - return ret + return accumulator } // add combines two numbers in GF(2^8) // This can also be used for subtraction since it is symmetric. func add(a, b uint8) uint8 { + // Addition in GF(2^8) equals XOR: return a ^ b } @@ -185,14 +209,6 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { return nil, fmt.Errorf("cannot split an empty secret") } - // Generate random x coordinates for computing points. I don't know - // why random x coordinates are used, and I also don't know why - // a non-cryptographically secure source of randomness is used. - // As far as I know the x coordinates do not need to be random. - - mathrand.Seed(time.Now().UnixNano()) - xCoordinates := mathrand.Perm(255) - // Allocate the output array, initialize the final byte // of the output with the offset. The representation of each // output is {y1, y2, .., yN, x}. @@ -203,7 +219,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { // then the result of evaluating the polynomial at that point // will be our secret out[idx] = make([]byte, len(secret)+1) - out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1 + out[idx][len(secret)] = uint8(idx) + 1 } // Construct a random polynomial for each byte of the secret. @@ -224,7 +240,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { for i := 0; i < parts; i++ { // Add 1 to the xCoordinate because if it's 0, // then the result of p.evaluate(x) will be our secret - x := uint8(xCoordinates[i]) + 1 + x := uint8(i) + 1 // Evaluate the polynomial at x y := p.evaluate(x) out[i][idx] = y diff --git a/shamir/shamir_test.go b/shamir/shamir_test.go index 18727a89d1..f2fa1f9090 100644 --- a/shamir/shamir_test.go +++ b/shamir/shamir_test.go @@ -115,6 +115,22 @@ func TestCombine(t *testing.T) { } } +func TestField_MulDivSmoke(t *testing.T) { + for a := range 256 { + for b := range 256 { + if b == 0 { + if out := mult(uint8(a), uint8(b)); out != 0 { + t.Fatalf("Bad: %v * %v = %v 0", a, b, out) + } + } else { + if out := div(mult(uint8(a), uint8(b)), uint8(b)); out != uint8(a) { + t.Fatalf("Bad: (%v * %v) / %v = %v %v", a, b, b, out, a) + } + } + } + } +} + func TestField_Add(t *testing.T) { if out := add(16, 16); out != 0 { t.Fatalf("Bad: %v 16", out) diff --git a/shamir/tables.go b/shamir/tables.go deleted file mode 100644 index 76c245e79d..0000000000 --- a/shamir/tables.go +++ /dev/null @@ -1,77 +0,0 @@ -package shamir - -// Tables taken from http://www.samiam.org/galois.html -// They use 0xe5 (229) as the generator - -var ( - // logTable provides the log(X)/log(g) at each index X - logTable = [256]uint8{ - 0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36, - 0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18, - 0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f, - 0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e, - 0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53, - 0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3, - 0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21, - 0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74, - 0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4, - 0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1, - 0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13, - 0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80, - 0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12, - 0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5, - 0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56, - 0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba, - 0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3, - 0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47, - 0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf, - 0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05, - 0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67, - 0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd, - 0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34, - 0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec, - 0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7, - 0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e, - 0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a, - 0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d, - 0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c, - 0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d, - 0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0, - 0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38} - - // expTable provides the anti-log or exponentiation value - // for the equivalent index - expTable = [256]uint8{ - 0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12, - 0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36, - 0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a, - 0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee, - 0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29, - 0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b, - 0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d, - 0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c, - 0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f, - 0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a, - 0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85, - 0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94, - 0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7, - 0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2, - 0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d, - 0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17, - 0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39, - 0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b, - 0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd, - 0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c, - 0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84, - 0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97, - 0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2, - 0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd, - 0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c, - 0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24, - 0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c, - 0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4, - 0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7, - 0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52, - 0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6, - 0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01} -) diff --git a/shamir/tables_test.go b/shamir/tables_test.go deleted file mode 100644 index 81aa983b10..0000000000 --- a/shamir/tables_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package shamir - -import "testing" - -func TestTables(t *testing.T) { - for i := 1; i < 256; i++ { - logV := logTable[i] - expV := expTable[logV] - if expV != uint8(i) { - t.Fatalf("bad: %d log: %d exp: %d", i, logV, expV) - } - } -} diff --git a/sops.go b/sops.go index 718f51bf81..a32211f1ab 100644 --- a/sops.go +++ b/sops.go @@ -127,6 +127,48 @@ type TreeBranch []TreeItem // Trees usually have more than one branch type TreeBranches []TreeBranch +func equals(oneBranch interface{}, otherBranch interface{}) bool { + switch oneBranch := oneBranch.(type) { + case TreeBranch: + otherBranch, ok := otherBranch.(TreeBranch) + if !ok || len(oneBranch) != len(otherBranch) { + return false + } + for i, item := range oneBranch { + otherItem := otherBranch[i] + if !equals(item.Key, otherItem.Key) || !equals(item.Value, otherItem.Value) { + return false + } + } + return true + case []interface{}: + otherBranch, ok := otherBranch.([]interface{}) + if !ok || len(oneBranch) != len(otherBranch) { + return false + } + for i, item := range oneBranch { + if !equals(item, otherBranch[i]) { + return false + } + } + return true + case Comment: + otherBranch, ok := otherBranch.(Comment) + if !ok { + return false + } + return oneBranch.Value == otherBranch.Value + default: + // Unexpected type + return oneBranch == otherBranch + } +} + +// Compare a branch with another one +func (branch TreeBranch) Equals(other TreeBranch) bool { + return equals(branch, other) +} + func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} { switch component := path[0].(type) { case int: @@ -156,47 +198,55 @@ func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} { } } -func set(branch interface{}, path []interface{}, value interface{}) interface{} { +func set(branch interface{}, path []interface{}, value interface{}) (interface{}, bool) { switch branch := branch.(type) { case TreeBranch: for i, item := range branch { if item.Key == path[0] { + var changed bool if len(path) == 1 { + changed = !equals(branch[i].Value, value) branch[i].Value = value } else { - branch[i].Value = set(item.Value, path[1:], value) + branch[i].Value, changed = set(item.Value, path[1:], value) } - return branch + return branch, changed } } // Not found, need to add the next path entry to the branch value := valueFromPathAndLeaf(path, value) if newBranch, ok := value.(TreeBranch); ok && len(newBranch) > 0 { - return append(branch, newBranch[0]) + return append(branch, newBranch[0]), true } - return branch + return branch, true case []interface{}: position := path[0].(int) + var changed bool if len(path) == 1 { if position >= len(branch) { - return append(branch, value) + return append(branch, value), true } + changed = !equals(branch[position], value) branch[position] = value } else { if position >= len(branch) { branch = append(branch, valueFromPathAndLeaf(path[1:], value)) + changed = true + } else { + branch[position], changed = set(branch[position], path[1:], value) } - branch[position] = set(branch[position], path[1:], value) } - return branch + return branch, changed default: - return valueFromPathAndLeaf(path, value) + newValue := valueFromPathAndLeaf(path, value) + return newValue, !equals(branch, newValue) } } // Set sets a value on a given tree for the specified path -func (branch TreeBranch) Set(path []interface{}, value interface{}) TreeBranch { - return set(branch, path, value).(TreeBranch) +func (branch TreeBranch) Set(path []interface{}, value interface{}) (TreeBranch, bool) { + v, changed := set(branch, path, value) + return v.(TreeBranch), changed } func unset(branch interface{}, path []interface{}) (interface{}, error) { @@ -297,6 +347,8 @@ func (branch TreeBranch) walkValue(in interface{}, path []string, commentsStack return onLeaves(in, path, commentsStack) case float64: return onLeaves(in, path, commentsStack) + case time.Time: + return onLeaves(in, path, commentsStack) case Comment: return onLeaves(in, path, commentsStack) case TreeBranch: @@ -772,7 +824,7 @@ func (m *Metadata) UpdateMasterKeys(dataKey []byte) (errs []error) { // GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each // MasterKey in the Metadata's KeySources until one of them succeeds. -func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) { +func (m *Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) { if m.DataKey != nil { return m.DataKey, nil } @@ -918,6 +970,8 @@ func ToBytes(in interface{}) ([]byte, error) { return boolB, nil case []byte: return in, nil + case time.Time: + return in.MarshalText() case Comment: return ToBytes(in.Value) default: diff --git a/sops_test.go b/sops_test.go index fe1f44e2ca..e10634eda6 100644 --- a/sops_test.go +++ b/sops_test.go @@ -919,7 +919,7 @@ func TestTruncateTreeNotArray(t *testing.T) { func TestTruncateTreeArrayOutOfBounds(t *testing.T) { tree := TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: []interface{}{ "one", "two", @@ -1023,10 +1023,33 @@ func TestSetNewKey(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", "bar", "foo"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "foo"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[1].Value) } +func TestSetNewKeyUnchanged(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "baz", + Value: "foobar", + }, + }, + }, + }, + }, + } + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "foobar") + assert.Equal(t, false, changed) + assert.Equal(t, "foobar", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) +} + func TestSetNewBranch(t *testing.T) { branch := TreeBranch{ TreeItem{ @@ -1034,7 +1057,8 @@ func TestSetNewBranch(t *testing.T) { Value: "value", }, } - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "key", @@ -1067,7 +1091,8 @@ func TestSetArrayDeepNew(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", 2, "bar"}, "hello") + set, changed := branch.Set([]interface{}{"foo", 2, "bar"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.([]interface{})[2].(TreeBranch)[0].Value) } @@ -1078,13 +1103,15 @@ func TestSetNewKeyDeep(t *testing.T) { Value: "bar", }, } - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) } func TestSetNewKeyOnEmptyBranch(t *testing.T) { branch := TreeBranch{} - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) } @@ -1099,13 +1126,15 @@ func TestSetArray(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", 0}, "uno") + set, changed := branch.Set([]interface{}{"foo", 0}, "uno") + assert.Equal(t, true, changed) assert.Equal(t, "uno", set[0].Value.([]interface{})[0]) } func TestSetArrayNew(t *testing.T) { branch := TreeBranch{} - set := branch.Set([]interface{}{"foo", 0, 0}, "uno") + set, changed := branch.Set([]interface{}{"foo", 0, 0}, "uno") + assert.Equal(t, true, changed) assert.Equal(t, "uno", set[0].Value.([]interface{})[0].([]interface{})[0]) } @@ -1116,7 +1145,8 @@ func TestSetExisting(t *testing.T) { Value: "foobar", }, } - set := branch.Set([]interface{}{"foo"}, "bar") + set, changed := branch.Set([]interface{}{"foo"}, "bar") + assert.Equal(t, true, changed) assert.Equal(t, "bar", set[0].Value) } @@ -1127,7 +1157,8 @@ func TestSetArrayLeafNewItem(t *testing.T) { Value: []interface{}{}, }, } - set := branch.Set([]interface{}{"array", 2}, "hello") + set, changed := branch.Set([]interface{}{"array", 2}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "array", @@ -1147,7 +1178,8 @@ func TestSetArrayNonLeaf(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"array", 0, "hello"}, "hello") + set, changed := branch.Set([]interface{}{"array", 0, "hello"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "array", @@ -1166,11 +1198,11 @@ func TestSetArrayNonLeaf(t *testing.T) { func TestUnsetKeyRootLeaf(t *testing.T) { branch := TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: "foo", }, TreeItem{ - Key: "foofoo", + Key: "foofoo", Value: "foofoo", }, } @@ -1178,7 +1210,7 @@ func TestUnsetKeyRootLeaf(t *testing.T) { assert.NoError(t, err) assert.Equal(t, TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: "foo", }, }, unset) @@ -1190,11 +1222,11 @@ func TestUnsetKeyBranchLeaf(t *testing.T) { Key: "foo", Value: TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, TreeItem{ - Key: "barbar", + Key: "barbar", Value: "barbar", }, }, @@ -1207,7 +1239,7 @@ func TestUnsetKeyBranchLeaf(t *testing.T) { Key: "foo", Value: TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1218,14 +1250,14 @@ func TestUnsetKeyBranchLeaf(t *testing.T) { func TestUnsetKeyBranch(t *testing.T) { branch := TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: "foo", }, TreeItem{ Key: "foofoo", Value: TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1235,7 +1267,7 @@ func TestUnsetKeyBranch(t *testing.T) { assert.NoError(t, err) assert.Equal(t, TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: "foo", }, }, unset) @@ -1244,14 +1276,13 @@ func TestUnsetKeyBranch(t *testing.T) { func TestUnsetKeyRootLastLeaf(t *testing.T) { branch := TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: "foo", }, } unset, err := branch.Unset([]interface{}{"foo"}) assert.NoError(t, err) - assert.Equal(t, TreeBranch{ - }, unset) + assert.Equal(t, TreeBranch{}, unset) } func TestUnsetKeyBranchLastLeaf(t *testing.T) { @@ -1260,7 +1291,7 @@ func TestUnsetKeyBranchLastLeaf(t *testing.T) { Key: "foo", Value: TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1270,9 +1301,8 @@ func TestUnsetKeyBranchLastLeaf(t *testing.T) { assert.NoError(t, err) assert.Equal(t, TreeBranch{ TreeItem{ - Key: "foo", - Value: TreeBranch{ - }, + Key: "foo", + Value: TreeBranch{}, }, }, unset) } @@ -1287,7 +1317,7 @@ func TestUnsetKeyArray(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "baz", + Key: "baz", Value: "baz", }, }, @@ -1300,9 +1330,8 @@ func TestUnsetKeyArray(t *testing.T) { assert.NoError(t, err) assert.Equal(t, TreeBranch{ TreeItem{ - Key: "foo", - Value: TreeBranch{ - }, + Key: "foo", + Value: TreeBranch{}, }, }, unset) } @@ -1314,13 +1343,13 @@ func TestUnsetArrayItem(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, TreeBranch{ TreeItem{ - Key: "barbar", + Key: "barbar", Value: "barbar", }, }, @@ -1335,7 +1364,7 @@ func TestUnsetArrayItem(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1351,11 +1380,11 @@ func TestUnsetKeyInArrayItem(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, TreeItem{ - Key: "barbar", + Key: "barbar", Value: "barbar", }, }, @@ -1370,7 +1399,7 @@ func TestUnsetKeyInArrayItem(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1386,7 +1415,7 @@ func TestUnsetArrayLastItem(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1397,9 +1426,8 @@ func TestUnsetArrayLastItem(t *testing.T) { assert.NoError(t, err) assert.Equal(t, TreeBranch{ TreeItem{ - Key: "foo", - Value: []interface{}{ - }, + Key: "foo", + Value: []interface{}{}, }, }, unset) } @@ -1410,7 +1438,7 @@ func TestUnsetKeyNotFound(t *testing.T) { Key: "foo", Value: TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1429,7 +1457,7 @@ func TestUnsetKeyInArrayNotFound(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1448,7 +1476,7 @@ func TestUnsetArrayItemOutOfBounds(t *testing.T) { Value: []interface{}{ TreeBranch{ TreeItem{ - Key: "bar", + Key: "bar", Value: "bar", }, }, @@ -1463,7 +1491,7 @@ func TestUnsetArrayItemOutOfBounds(t *testing.T) { func TestUnsetKeyNotABranch(t *testing.T) { branch := TreeBranch{ TreeItem{ - Key: "foo", + Key: "foo", Value: 99, }, } diff --git a/stores/dotenv/store.go b/stores/dotenv/store.go index 1e533341ec..c6ec8b5056 100644 --- a/stores/dotenv/store.go +++ b/stores/dotenv/store.go @@ -141,7 +141,13 @@ func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) { if comment, ok := item.Key.(sops.Comment); ok { line = fmt.Sprintf("#%s\n", comment.Value) } else { - value := strings.Replace(item.Value.(string), "\n", "\\n", -1) + value, ok := item.Value.(string) + if !ok { + value = stores.ValToString(item.Value) + } else { + value = strings.ReplaceAll(value, "\n", "\\n") + } + line = fmt.Sprintf("%s=%s\n", item.Key, value) } buffer.WriteString(line) diff --git a/stores/flatten.go b/stores/flatten.go index fe6e981fd1..f961d74d00 100644 --- a/stores/flatten.go +++ b/stores/flatten.go @@ -245,16 +245,23 @@ func DecodeNonStrings(m map[string]interface{}) error { } if v, ok := m["shamir_threshold"]; ok { switch val := v.(type) { - case string: - vInt, err := strconv.Atoi(val) - if err != nil { + case string: + vInt, err := strconv.Atoi(val) + if err != nil { + // Older versions of SOPS stored shamir_threshold as a floating point representation + // of the actual integer. Try to parse a floating point number and see whether it + // can be converted without loss to an integer. + vFloat, floatErr := strconv.ParseFloat(val, 64) + vInt = int(vFloat) + if floatErr != nil || float64(vInt) != vFloat { return fmt.Errorf("shamir_threshold is not an integer: %s", err.Error()) } - m["shamir_threshold"] = vInt - case int: - m["shamir_threshold"] = val - default: - return fmt.Errorf("shamir_threshold is neither a string nor an integer, but %T", val) + } + m["shamir_threshold"] = vInt + case int: + m["shamir_threshold"] = val + default: + return fmt.Errorf("shamir_threshold is neither a string nor an integer, but %T", val) } } return nil @@ -274,5 +281,11 @@ func EncodeNonStrings(m map[string]interface{}) { if vInt, ok := v.(int); ok { m["shamir_threshold"] = fmt.Sprintf("%d", vInt) } + // FlattenMetadata serializes the input as JSON and then deserializes it. + // The JSON unserializer treats every number as a float, so the above 'if' + // never applies in that situation. + if vFloat, ok := v.(float64); ok { + m["shamir_threshold"] = fmt.Sprintf("%.0f", vFloat) + } } } diff --git a/stores/ini/store.go b/stores/ini/store.go index 350142d850..d8e72990d5 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -4,8 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - - "strconv" "strings" "github.com/getsops/sops/v3" @@ -24,7 +22,8 @@ func NewStore(c *config.INIStoreConfig) *Store { } func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { - iniFile := ini.Empty() + iniFile := ini.Empty(ini.LoadOptions{AllowNonUniqueSections: true}) + iniFile.DeleteSection(ini.DefaultSection) for _, branch := range branches { for _, item := range branch { if _, ok := item.Key.(sops.Comment); ok { @@ -55,7 +54,7 @@ func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { lastItem.Comment = comment.Value } } else { - lastItem, err = section.NewKey(keyVal.Key.(string), store.valToString(keyVal.Value)) + lastItem, err = section.NewKey(keyVal.Key.(string), stores.ValToString(keyVal.Value)) if err != nil { return nil, fmt.Errorf("Error encoding key: %s", err) } @@ -77,25 +76,12 @@ func (store Store) stripCommentChar(comment string) string { return comment } -func (store Store) valToString(v interface{}) string { - switch v := v.(type) { - case fmt.Stringer: - return v.String() - case float64: - return strconv.FormatFloat(v, 'f', 6, 64) - case bool: - return strconv.FormatBool(v) - default: - return fmt.Sprintf("%s", v) - } -} - func (store Store) iniFromTreeBranches(branches sops.TreeBranches) ([]byte, error) { return store.encodeTree(branches) } func (store Store) treeBranchesFromIni(in []byte) (sops.TreeBranches, error) { - iniFile, err := ini.Load(in) + iniFile, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in) if err != nil { return nil, err } @@ -143,7 +129,7 @@ func (store Store) treeItemFromSection(section *ini.Section) (sops.TreeItem, err // LoadEncryptedFile loads encrypted INI file's bytes onto a sops.Tree runtime object func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { - iniFileOuter, err := ini.Load(in) + iniFileOuter, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in) if err != nil { return sops.Tree{}, err } diff --git a/stores/ini/store_test.go b/stores/ini/store_test.go index 3e833b54cb..b1aff6cf9a 100644 --- a/stores/ini/store_test.go +++ b/stores/ini/store_test.go @@ -3,8 +3,8 @@ package ini import ( "testing" - "github.com/stretchr/testify/assert" "github.com/getsops/sops/v3" + "github.com/stretchr/testify/assert" ) func TestDecodeIni(t *testing.T) { @@ -127,6 +127,55 @@ func TestEncodeIniWithEscaping(t *testing.T) { assert.Equal(t, expected, branches) } +func TestEncodeIniWithDuplicateSections(t *testing.T) { + branches := sops.TreeBranches{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "DEFAULT", + Value: interface{}(sops.TreeBranch(nil)), + }, + sops.TreeItem{ + Key: "foo", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: "bar", + }, + sops.TreeItem{ + Key: "baz", + Value: "3.0", + }, + sops.TreeItem{ + Key: "qux", + Value: "false", + }, + }, + }, + sops.TreeItem{ + Key: "foo", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: "bar", + }, + sops.TreeItem{ + Key: "baz", + Value: "3.0", + }, + sops.TreeItem{ + Key: "qux", + Value: "false", + }, + }, + }, + }, + } + out, err := Store{}.iniFromTreeBranches(branches) + assert.Nil(t, err) + expected, _ := Store{}.treeBranchesFromIni(out) + assert.Equal(t, expected, branches) +} + func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) { data := []byte(`hello=2`) store := Store{} diff --git a/stores/json/store.go b/stores/json/store.go index 3212b5b32b..7b8bf3da5a 100644 --- a/stores/json/store.go +++ b/stores/json/store.go @@ -330,6 +330,7 @@ func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { if err != nil { return nil, fmt.Errorf("Error marshaling to json: %s", err) } + out = append(out, '\n') return out, nil } @@ -340,6 +341,7 @@ func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) { if err != nil { return nil, fmt.Errorf("Error marshaling to json: %s", err) } + out = append(out, '\n') return out, nil } diff --git a/stores/json/store_test.go b/stores/json/store_test.go index 38f9882b53..0f447d8691 100644 --- a/stores/json/store_test.go +++ b/stores/json/store_test.go @@ -34,7 +34,8 @@ func TestDecodeJSON(t *testing.T) { } } } -}` +} +` expected := sops.TreeBranch{ sops.TreeItem{ Key: "glossary", @@ -312,7 +313,8 @@ func TestEncodeJSONArrayOfObjects(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: -1, @@ -446,7 +448,8 @@ func TestIndentTwoSpaces(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: 2, @@ -488,7 +491,8 @@ func TestIndentDefault(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: -1, @@ -530,7 +534,8 @@ func TestNoIndent(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: 0, @@ -539,6 +544,28 @@ func TestNoIndent(t *testing.T) { out, err := store.EmitPlainFile(tree.Branches) assert.Nil(t, err) assert.Equal(t, expected, string(out)) + +} + +func TestConflictingAttributes(t *testing.T) { + // See https://stackoverflow.com/a/23195243 + // Duplicate keys in json is technically valid, but discouraged. + // Implementations may handle them differently. ECMA-262 says + // + // > In the case where there are duplicate name Strings within an object, + // > lexically preceding values for the same key shall be overwritten. + + data := ` +{ + "hello": "Sops config file", + "hello": "Doubles are ok", + "hello": ["repeatedly"], + "hello": 3.14 +} +` + s := new(Store) + _, err := s.LoadPlainFile([]byte(data)) + assert.Nil(t, err) } func TestComments(t *testing.T) { @@ -597,7 +624,8 @@ func TestComments(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: 2, diff --git a/stores/stores.go b/stores/stores.go index 12b0e784fd..3b2fd192e9 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -10,9 +10,10 @@ of the purpose of this package is to make it easy to change the SOPS file format package stores import ( - "time" - "fmt" + "strconv" + "strings" + "time" "github.com/getsops/sops/v3" "github.com/getsops/sops/v3/age" @@ -45,15 +46,15 @@ type SopsFile struct { type Metadata struct { ShamirThreshold int `yaml:"shamir_threshold,omitempty" json:"shamir_threshold,omitempty"` KeyGroups []keygroup `yaml:"key_groups,omitempty" json:"key_groups,omitempty"` - KMSKeys []kmskey `yaml:"kms" json:"kms"` - GCPKMSKeys []gcpkmskey `yaml:"gcp_kms" json:"gcp_kms"` - AzureKeyVaultKeys []azkvkey `yaml:"azure_kv" json:"azure_kv"` - VaultKeys []vaultkey `yaml:"hc_vault" json:"hc_vault"` - AgeKeys []agekey `yaml:"age" json:"age"` + KMSKeys []kmskey `yaml:"kms,omitempty" json:"kms,omitempty"` + GCPKMSKeys []gcpkmskey `yaml:"gcp_kms,omitempty" json:"gcp_kms,omitempty"` + AzureKeyVaultKeys []azkvkey `yaml:"azure_kv,omitempty" json:"azure_kv,omitempty"` + VaultKeys []vaultkey `yaml:"hc_vault,omitempty" json:"hc_vault,omitempty"` + AgeKeys []agekey `yaml:"age,omitempty" json:"age,omitempty"` OCIKMSKeys []ocikmskey `yaml:"oci_kms" json:"oci_kms"` LastModified string `yaml:"lastmodified" json:"lastmodified"` MessageAuthenticationCode string `yaml:"mac" json:"mac"` - PGPKeys []pgpkey `yaml:"pgp" json:"pgp"` + PGPKeys []pgpkey `yaml:"pgp,omitempty" json:"pgp,omitempty"` UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"` EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"` @@ -577,3 +578,25 @@ func HasSopsTopLevelKey(branch sops.TreeBranch) bool { } return false } + +// ValToString converts a simple value to a string. +// It does not handle complex values (arrays and mappings). +func ValToString(v interface{}) string { + switch v := v.(type) { + case float64: + result := strconv.FormatFloat(v, 'G', -1, 64) + // If the result can be confused with an integer, make sure we have at least one decimal digit + if !strings.ContainsRune(result, '.') && !strings.ContainsRune(result, 'E') { + result = strconv.FormatFloat(v, 'f', 1, 64) + } + return result + case bool: + return strconv.FormatBool(v) + case time.Time: + return v.Format(time.RFC3339) + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/stores/stores_test.go b/stores/stores_test.go new file mode 100644 index 0000000000..31ced210aa --- /dev/null +++ b/stores/stores_test.go @@ -0,0 +1,27 @@ +package stores + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + + +func TestValToString(t *testing.T) { + assert.Equal(t, "1", ValToString(1)) + assert.Equal(t, "1.0", ValToString(1.0)) + assert.Equal(t, "1.1", ValToString(1.10)) + assert.Equal(t, "1.23", ValToString(1.23)) + assert.Equal(t, "1.2345678901234567", ValToString(1.234567890123456789)) + assert.Equal(t, "200000.0", ValToString(2E5)) + assert.Equal(t, "-2E+10", ValToString(-2E10)) + assert.Equal(t, "2E-10", ValToString(2E-10)) + assert.Equal(t, "1.2345E+100", ValToString(1.2345E100)) + assert.Equal(t, "1.2345E-100", ValToString(1.2345E-100)) + assert.Equal(t, "true", ValToString(true)) + assert.Equal(t, "false", ValToString(false)) + ts, _ := time.Parse(time.RFC3339, "2025-01-02T03:04:05Z") + assert.Equal(t, "2025-01-02T03:04:05Z", ValToString(ts)) + assert.Equal(t, "a string", ValToString("a string")) +} diff --git a/stores/yaml/store.go b/stores/yaml/store.go index 697ff10e9e..3b15ae1a72 100644 --- a/stores/yaml/store.go +++ b/stores/yaml/store.go @@ -10,7 +10,7 @@ import ( "github.com/getsops/sops/v3" "github.com/getsops/sops/v3/config" "github.com/getsops/sops/v3/stores" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) const IndentDefault = 4 @@ -307,6 +307,13 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { // sops.Tree runtime object func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { var branches sops.TreeBranches + if len(in) > 0 { + // This is needed to make the yaml-decoder check for uniqueness of keys + // Can probably be removed when https://github.com/go-yaml/yaml/issues/814 is merged. + if err := yaml.NewDecoder(bytes.NewReader(in)).Decode(make(map[string]interface{})); err != nil { + return nil, err + } + } d := yaml.NewDecoder(bytes.NewReader(in)) for { var data yaml.Node diff --git a/stores/yaml/store_test.go b/stores/yaml/store_test.go index fb53d9ed32..13db5cbfe7 100644 --- a/stores/yaml/store_test.go +++ b/stores/yaml/store_test.go @@ -62,19 +62,19 @@ key4: *bar var ALIASES_BRANCHES = sops.TreeBranches{ sops.TreeBranch{ sops.TreeItem{ - Key: "key1", + Key: "key1", Value: []interface{}{ "foo", }, }, sops.TreeItem{ - Key: "key2", + Key: "key2", Value: []interface{}{ "foo", }, }, sops.TreeItem{ - Key: "key3", + Key: "key3", Value: sops.TreeBranch{ sops.TreeItem{ Key: "foo", @@ -87,7 +87,7 @@ var ALIASES_BRANCHES = sops.TreeBranches{ }, }, sops.TreeItem{ - Key: "key4", + Key: "key4", Value: sops.TreeBranch{ sops.TreeItem{ Key: "foo", @@ -237,7 +237,6 @@ prometheus-node-exporter: - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$ `) - func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) { data := []byte(`hello: 2`) _, err := (&Store{}).LoadEncryptedFile(data) @@ -398,3 +397,24 @@ func TestHasSopsTopLevelKey(t *testing.T) { }) assert.Equal(t, ok, false) } + +func TestDuplicateAttributes(t *testing.T) { + // Duplicate keys are _not_ valid yaml. + // + // See https://yaml.org/spec/1.2.2/#mapping + // > The content of a mapping node is an unordered set of key/value node pairs, + // > with the restriction that each of the keys is unique. + // + data := ` +hello: Sops config file +hello: Duplicates are not ok +rootunique: + key2: "value" + key2: "foo" +` + s := new(Store) + _, err := s.LoadPlainFile([]byte(data)) + assert.NotNil(t, err) + assert.Equal(t, `yaml: unmarshal errors: + line 3: mapping key "hello" already defined at line 2`, err.Error()) +} diff --git a/version/version.go b/version/version.go index 3b21a6cb03..3d8dcb79df 100644 --- a/version/version.go +++ b/version/version.go @@ -12,11 +12,13 @@ import ( ) // Version represents the value of the current semantic version. -var Version = "3.9.4" +var Version = "3.10.2" // PrintVersion prints the current version of sops. If the flag -// `--disable-version-check` is set, the function will not attempt -// to retrieve the latest version from the GitHub API. +// `--disable-version-check` is set or if the environment variable +// SOPS_DISABLE_VERSION_CHECK is set to a value that is considered +// true by https://pkg.go.dev/strconv#ParseBool, the function will +// not attempt to retrieve the latest version from the GitHub API. // // If the flag is not set, the function will attempt to retrieve // the latest version from the GitHub API and compare it to the @@ -27,7 +29,7 @@ func PrintVersion(c *cli.Context) { out.WriteString(fmt.Sprintf("%s %s", c.App.Name, c.App.Version)) - if c.Bool("disable-version-check") { + if c.Bool("disable-version-check") && !c.Bool("check-for-updates") { out.WriteString("\n") } else { upstreamVersion, upstreamURL, err := RetrieveLatestReleaseVersion() @@ -45,6 +47,12 @@ func PrintVersion(c *cli.Context) { } } } + if !c.Bool("check-for-updates") { + out.WriteString( + "\n[warning] Note that in a future version, sops will no longer check whether the current version is the latest when asking for sops' version." + + " If you want to explicitly check for the latest version, add the `--check-for-updates` option to `sops --version`." + + " This will hide this deprecation warning and will always check, even if the default behavior changes in the future.\n") + } } fmt.Fprintf(c.App.Writer, "%s", out.String()) } From cbb86a1767488e85b2762f51eafb2c699fc2f0b2 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 26 Sep 2025 03:12:34 +0200 Subject: [PATCH 05/12] build(deps): Update oci-go-sdk to v65.101.0 Signed-off-by: Alessandro De Blasis --- go.mod | 3 ++- go.sum | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index eefd8f8a11..e738980bec 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/lib/pq v1.10.9 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 - github.com/oracle/oci-go-sdk/v65 v65.81.1 + github.com/oracle/oci-go-sdk/v65 v65.101.0 github.com/ory/dockertest/v3 v3.12.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 @@ -133,6 +133,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect diff --git a/go.sum b/go.sum index 58adca7d5a..f9f02a9354 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,7 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= @@ -239,8 +240,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.6 h1:P7Hqg40bsMvQGCS4S7DJYhUZOISMLJOB2iGX5COWiPk= github.com/opencontainers/runc v1.2.6/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= -github.com/oracle/oci-go-sdk/v65 v65.81.1 h1:JYc47bk8n/MUchA2KHu1ggsCQzlJZQLJ+tTKfOho00E= -github.com/oracle/oci-go-sdk/v65 v65.81.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= +github.com/oracle/oci-go-sdk/v65 v65.101.0 h1:EErMOuw98JXi0P7DgPg5zjouCA5s61iWD5tFWNCVLHk= +github.com/oracle/oci-go-sdk/v65 v65.101.0/go.mod h1:RGiXfpDDmRRlLtqlStTzeBjjdUNXyqm3KXKyLCm3A/Q= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -286,8 +287,11 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -315,14 +319,32 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -330,22 +352,55 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -354,6 +409,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 47aa92e89ec197dc00f22e5fc52579a95c147b26 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 26 Sep 2025 05:47:11 +0200 Subject: [PATCH 06/12] feat(oci): Add OCI CLI environment provider and related tests - Introduced `oci-cli-env-provider` for enhanced OCI configuration management. - Implemented `configurationProvider` to prioritize CLI environment variables. - Added tests for configuration provider functionality, ensuring correct behavior with OCI CLI and environment variables. Signed-off-by: Alessandro De Blasis --- go.mod | 17 +- go.sum | 50 ++++-- ocikms/config_provider.go | 52 ++++++ ocikms/config_provider_test.go | 303 +++++++++++++++++++++++++++++++++ ocikms/consts.go | 53 ++++++ ocikms/keysource.go | 20 +-- 6 files changed, 459 insertions(+), 36 deletions(-) create mode 100644 ocikms/config_provider.go create mode 100644 ocikms/config_provider_test.go create mode 100644 ocikms/consts.go diff --git a/go.mod b/go.mod index e738980bec..c0572e4659 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/lib/pq v1.10.9 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 + github.com/ontariosystems/oci-cli-env-provider v0.1.0 github.com/oracle/oci-go-sdk/v65 v65.101.0 github.com/ory/dockertest/v3 v3.12.0 github.com/pkg/errors v0.9.1 @@ -35,11 +36,11 @@ require ( github.com/stretchr/testify v1.11.0 github.com/urfave/cli v1.22.17 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.41.0 - golang.org/x/net v0.43.0 + golang.org/x/crypto v0.42.0 + golang.org/x/net v0.44.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sys v0.35.0 - golang.org/x/term v0.34.0 + golang.org/x/sys v0.36.0 + golang.org/x/term v0.35.0 google.golang.org/api v0.248.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c google.golang.org/grpc v1.75.0 @@ -98,7 +99,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect @@ -128,7 +129,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/sony/gobreaker v0.5.0 // indirect + github.com/sony/gobreaker v1.0.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -144,8 +145,8 @@ require ( go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect diff --git a/go.sum b/go.sum index f9f02a9354..1073f1db14 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= @@ -154,12 +156,15 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -171,6 +176,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -234,6 +241,12 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= +github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/ontariosystems/oci-cli-env-provider v0.1.0 h1:xDEhUOXQrskVdyKPymw0tIvvhs8H/piKBCXNgOZ+Agc= +github.com/ontariosystems/oci-cli-env-provider v0.1.0/go.mod h1:7DFGsibH1hm9k4A31Ggf6Lg83ItgeC31I9G/xsVoiIQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -253,16 +266,17 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= +github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -314,6 +328,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -325,8 +341,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -345,8 +361,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -358,8 +374,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -378,8 +394,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -390,8 +406,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -401,8 +417,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -413,6 +429,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ocikms/config_provider.go b/ocikms/config_provider.go new file mode 100644 index 0000000000..bc5658803f --- /dev/null +++ b/ocikms/config_provider.go @@ -0,0 +1,52 @@ +package ocikms + +import ( + "os" + + ocep "github.com/ontariosystems/oci-cli-env-provider" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/common/auth" +) + +// newIPProvider is a variable to allow tests to stub the Instance Principal provider factory +var newIPProvider = auth.InstancePrincipalConfigurationProvider + +// configurationProvider composes multiple OCI configuration providers to make +// authentication work seamlessly across environments. +// Order of precedence: +// 1) OCI_CLI_* environment variables (via ontariosystems/oci-cli-env-provider) +// 2) OCI_* environment variables (native SDK env provider) +// 3) Config file providers (OCI_CLI_CONFIG_FILE/PROFILE if set) +// 4) Instance Principals (when running on OCI compute) +// 5) Default config provider (~/.oci/config, TF_VAR_*), as a last resort +func configurationProvider() (common.ConfigurationProvider, error) { + var providers []common.ConfigurationProvider + + // 1) Prefer the CLI-compatible envs used widely in CI/containers (envs only; no implicit fallbacks) + providers = append(providers, ocep.OciCliEnvironmentConfigurationProvider()) + + // 2) Native SDK envs (OCI_tenancy_ocid, OCI_user_ocid, OCI_fingerprint, OCI_private_key_path, OCI_region) + providers = append(providers, common.ConfigurationProviderEnvironmentVariables("OCI", "")) + + // 3) File-based fallbacks + if cfg := os.Getenv(OCICLIConfigFile); cfg != "" { + if prof := os.Getenv(OCICLIProfile); prof != "" { + if p, err := common.ConfigurationProviderFromFileWithProfile(cfg, prof, ""); err == nil { + providers = append(providers, p) + } + } else { + if p, err := common.ConfigurationProviderFromFile(cfg, ""); err == nil { + providers = append(providers, p) + } + } + } + // 4) Instance principals for compute instances (ignore error here; composition will try next) + if ip, err := newIPProvider(); err == nil { + providers = append(providers, ip) + } + + // 5) Always keep a last-resort default config provider at the end + providers = append(providers, common.DefaultConfigProvider()) + + return common.ComposingConfigurationProvider(providers) +} diff --git a/ocikms/config_provider_test.go b/ocikms/config_provider_test.go new file mode 100644 index 0000000000..128bf25baf --- /dev/null +++ b/ocikms/config_provider_test.go @@ -0,0 +1,303 @@ +package ocikms + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/stretchr/testify/require" +) + +// writeTempRSAKey writes an unencrypted PKCS#1 RSA private key to a temp file. +func writeTempRSAKey(t *testing.T, dir string) string { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate key: %v", err) + } + keyBytes := x509.MarshalPKCS1PrivateKey(key) + pemBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes} + pemData := pem.EncodeToMemory(pemBlock) + path := filepath.Join(dir, "oci-test-private-key.pem") + if err := os.WriteFile(path, pemData, 0600); err != nil { + t.Fatalf("write key: %v", err) + } + return path +} + +// writeOCIConfig writes a minimal ~/.oci/config style file. +func writeOCIConfig(t *testing.T, path string, profile string, user string, tenancy string, region string, fingerprint string, keyFile string) { + t.Helper() + content := strings.Join([]string{ + "[" + profile + "]", + "user=" + user, + "fingerprint=" + fingerprint, + "key_file=" + keyFile, + "tenancy=" + tenancy, + "region=" + region, + "", + }, "\n") + if err := os.WriteFile(path, []byte(content), 0600); err != nil { + t.Fatalf("write config: %v", err) + } +} + +// clearOCIEnv clears OCI SDK environment variables to prevent interference +func clearOCIEnv(t *testing.T) { + t.Helper() + envVars := []string{ + "OCI_tenancy_ocid", + "OCI_user_ocid", + "OCI_region", + "OCI_fingerprint", + "OCI_private_key_path", + } + for _, env := range envVars { + t.Setenv(env, "") + } +} + +// clearCLIOCIEnv clears OCI CLI environment variables to prevent interference +func clearCLIOCIEnv() { + envVars := []string{ + OCICLITenancy, + OCICLIUser, + OCICLIRegion, + OCICLIFingerprint, + OCICLIKeyFile, + } + for _, env := range envVars { + os.Unsetenv(env) + } +} + +// disableIPProvider disables Instance Principal provider in tests +func disableIPProvider(t *testing.T) { + old := newIPProvider + t.Cleanup(func() { newIPProvider = old }) + newIPProvider = func() (common.ConfigurationProvider, error) { + return nil, fmt.Errorf("ip disabled in tests") + } +} + +func TestConfigurationProvider_OCI_CLI_Env(t *testing.T) { + // Disable IP network path in tests by overriding factory + disableIPProvider(t) + // Isolate HOME to avoid default file provider interference + t.Setenv(HomeEnv, t.TempDir()) + + // Generate key + keyDir := t.TempDir() + keyPath := writeTempRSAKey(t, keyDir) + + // Set OCI_CLI_* envs + t.Setenv(OCICLITenancy, "ocid1.tenancy.oc1..exampletenancy") + t.Setenv(OCICLIUser, "ocid1.user.oc1..exampleuser") + t.Setenv(OCICLIRegion, "us-ashburn-1") + t.Setenv(OCICLIFingerprint, "aa:bb:cc:dd") + t.Setenv(OCICLIKeyFile, keyPath) + + // Ensure other providers are not set by accident + // Native SDK env provider uses lower-case suffixes with prefix OCI_ + clearOCIEnv(t) + + prov, err := configurationProvider() + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..exampletenancy", tenancy) + + user, err := prov.UserOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.user.oc1..exampleuser", user) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "us-ashburn-1", region) + + fp, err := prov.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "aa:bb:cc:dd", fp) +} + +func TestConfigurationProvider_OCI_Env(t *testing.T) { + disableIPProvider(t) + // Isolate HOME + t.Setenv(HomeEnv, t.TempDir()) + + keyDir := t.TempDir() + keyPath := writeTempRSAKey(t, keyDir) + + // SDK env provider expects lower-case suffixes + t.Setenv(OCITenancyOCID, "ocid1.tenancy.oc1..ten") + t.Setenv(OCIUserOCID, "ocid1.user.oc1..usr") + t.Setenv(OCIRegion, "eu-frankfurt-1") + t.Setenv(OCIFingerprint, "11:22:33:44") + t.Setenv(OCIPrivateKeyPath, keyPath) + + // Ensure CLI envs are not set (unset, not empty strings) + clearCLIOCIEnv() + + prov, err := configurationProvider() + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..ten", tenancy) + + user, err := prov.UserOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.user.oc1..usr", user) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "eu-frankfurt-1", region) + + fp, err := prov.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "11:22:33:44", fp) +} + +func TestConfigurationProvider_FileViaEnv(t *testing.T) { + disableIPProvider(t) + // Isolate HOME + t.Setenv(HomeEnv, t.TempDir()) + + d := t.TempDir() + keyPath := writeTempRSAKey(t, d) + cfgPath := filepath.Join(d, "config") + writeOCIConfig(t, cfgPath, "DEFAULT", "ocid1.user.oc1..fileusr", "ocid1.tenancy.oc1..fileten", "uk-london-1", "ff:ee:dd:cc", keyPath) + + // Point to config via env + t.Setenv(OCICLIConfigFile, cfgPath) + // Explicit profile not required; default is DEFAULT + + // Ensure env-based providers are not set + clearCLIOCIEnv() + + clearOCIEnv(t) + + prov, err := configurationProvider() + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..fileten", tenancy) + + user, err := prov.UserOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.user.oc1..fileusr", user) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "uk-london-1", region) + + fp, err := prov.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "ff:ee:dd:cc", fp) +} + +func TestConfigurationProvider_DefaultFileFallback(t *testing.T) { + disableIPProvider(t) + // Set HOME to a temp dir and create ~/.oci/config + home := t.TempDir() + if runtime.GOOS == "windows" { + // USERPROFILE is also consulted on Windows + t.Setenv(UserProfileEnv, home) + } + t.Setenv(HomeEnv, home) + + ociDir := filepath.Join(home, ".oci") + if err := os.MkdirAll(ociDir, 0700); err != nil { + t.Fatalf("mkdir: %v", err) + } + keyPath := writeTempRSAKey(t, ociDir) + cfgPath := filepath.Join(ociDir, "config") + writeOCIConfig(t, cfgPath, "DEFAULT", "ocid1.user.oc1..defusr", "ocid1.tenancy.oc1..deften", "ap-tokyo-1", "00:aa:bb:cc", keyPath) + + // Ensure no env points to explicit file and env providers are empty + os.Unsetenv(OCICLIConfigFile) + + clearCLIOCIEnv() + + clearOCIEnv(t) + + prov, err := common.ConfigurationProviderFromFile(cfgPath, "") + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..deften", tenancy) + + user, err := prov.UserOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.user.oc1..defusr", user) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "ap-tokyo-1", region) + + fp, err := prov.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "00:aa:bb:cc", fp) +} + +// ipStubProvider implements common.ConfigurationProvider to stub Instance Principal in tests +type ipStubProvider struct{} + +func (ipStubProvider) TenancyOCID() (string, error) { return "ocid1.tenancy.oc1..ipstub", nil } +func (ipStubProvider) UserOCID() (string, error) { return "", nil } +func (ipStubProvider) KeyFingerprint() (string, error) { return "ip:stub:fp", nil } +func (ipStubProvider) Region() (string, error) { return "me-dubai-1", nil } +func (ipStubProvider) KeyID() (string, error) { return "ST$ipstub", nil } +func (ipStubProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { + // generate a small key for completeness + k, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + return k, nil +} +func (ipStubProvider) AuthType() (common.AuthConfig, error) { return common.AuthConfig{}, nil } + +func TestConfigurationProvider_InstancePrincipal_Stubbed(t *testing.T) { + // Override IP factory to return stub, no network + old := newIPProvider + t.Cleanup(func() { newIPProvider = old }) + newIPProvider = func() (common.ConfigurationProvider, error) { return ipStubProvider{}, nil } + + // Isolate environment so that only IP path is viable + t.Setenv(HomeEnv, t.TempDir()) + os.Unsetenv(OCICLIConfigFile) + os.Unsetenv(OCICLIProfile) + + // Clear CLI envs + clearCLIOCIEnv() + + // Clear native SDK envs + clearOCIEnv(t) + + prov, err := configurationProvider() + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..ipstub", tenancy) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "me-dubai-1", region) + + fp, err := prov.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "ip:stub:fp", fp) +} diff --git a/ocikms/consts.go b/ocikms/consts.go new file mode 100644 index 0000000000..25ad44ace9 --- /dev/null +++ b/ocikms/consts.go @@ -0,0 +1,53 @@ +package ocikms + +// Key type constants +const ( + // KeyTypeIdentifier is the string used to identify an OCI KMS MasterKey in configuration + KeyTypeIdentifier = "oci_kms" +) + +// OCI CLI environment variables (used by oci-cli-env-provider) +const ( + // OCICLIConfigFile is the environment variable for OCI CLI config file path + OCICLIConfigFile = "OCI_CLI_CONFIG_FILE" + // OCICLIProfile is the environment variable for OCI CLI profile name + OCICLIProfile = "OCI_CLI_PROFILE" + // OCICLITenancy is the environment variable for OCI CLI tenancy OCID + OCICLITenancy = "OCI_CLI_TENANCY" + // OCICLIUser is the environment variable for OCI CLI user OCID + OCICLIUser = "OCI_CLI_USER" + // OCICLIRegion is the environment variable for OCI CLI region + OCICLIRegion = "OCI_CLI_REGION" + // OCICLIFingerprint is the environment variable for OCI CLI key fingerprint + OCICLIFingerprint = "OCI_CLI_FINGERPRINT" + // OCICLIKeyFile is the environment variable for OCI CLI private key file path + OCICLIKeyFile = "OCI_CLI_KEY_FILE" +) + +// OCI native SDK environment variables (lowercase after OCI_ prefix) +const ( + // OCITenancyOCID is the environment variable for OCI tenancy OCID (OCI_tenancy_ocid) + OCITenancyOCID = "OCI_tenancy_ocid" + // OCIUserOCID is the environment variable for OCI user OCID (OCI_user_ocid) + OCIUserOCID = "OCI_user_ocid" + // OCIRegion is the environment variable for OCI region (OCI_region) + OCIRegion = "OCI_region" + // OCIFingerprint is the environment variable for OCI key fingerprint (OCI_fingerprint) + OCIFingerprint = "OCI_fingerprint" + // OCIPrivateKeyPath is the environment variable for OCI private key path (OCI_private_key_path) + OCIPrivateKeyPath = "OCI_private_key_path" +) + +// Other environment variables +const ( + // HomeEnv is the HOME environment variable + HomeEnv = "HOME" + // UserProfileEnv is the USERPROFILE environment variable (Windows) + UserProfileEnv = "USERPROFILE" +) + +// Logger constants +const ( + // LoggerName is the name used for the OCI KMS logger + LoggerName = "OCIKMS" +) diff --git a/ocikms/keysource.go b/ocikms/keysource.go index c8d5de00fb..4d3d902a6a 100644 --- a/ocikms/keysource.go +++ b/ocikms/keysource.go @@ -21,12 +21,10 @@ const ( cryptoEndpointTemplate = "https://%s-crypto.kms.%s.oraclecloud.com" // ocidParts is the number of parts in an OCID, separated by ".", eg: "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" ocidParts = 6 - // KeyTypeIdentifier is the string used to identify an OCI KMS MasterKey. - KeyTypeIdentifier = "oci_kms" ) func init() { - log = logging.NewLogger("OCIKMS") + log = logging.NewLogger(LoggerName) } // MasterKey is an Oracle Cloud KMS key used to encrypt and decrypt sops' data key. @@ -63,18 +61,16 @@ func (key *MasterKey) createCryptoClient() (client keymanagement.KmsCryptoClient endpoint := fmt.Sprintf(cryptoEndpointTemplate, vault_ref, region) log.WithField("endpoint", endpoint).Info("Creating OCI KMS client") - // The client is created using the default OCI config provider, using the default profile in the default config file (~/.oci/config) - // There is currently no straightforward way to pass a custom config provider to the client. - // The oci-go-sdk provides a way to pass a custom config provider to the client, but there's no environment variable to feature-flag it. - // Related: https://github.com/oracle/oci-go-sdk/issues/318 - // In order to use a custom provider, the client would need to be created like this: - // client, err := keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.CustomProfileConfigProvider("/home//.oci/config", ""), endpoint) - // Sticking with the defaults for now. + // Build a flexible configuration provider (12 factor app-ish) + cfg, cfgErr := configurationProvider() + if cfgErr != nil { + return client, fmt.Errorf("cannot build OCI configuration provider: %w", cfgErr) + } - client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(common.DefaultConfigProvider(), endpoint) + client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(cfg, endpoint) if err != nil { - return client, fmt.Errorf("Cannot create OCI KMS client: %w", err) + return client, fmt.Errorf("cannot create OCI KMS client: %w", err) } return client, nil } From 8aabc43af0f8dc5067351f03a61c92ec73c32a46 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 26 Sep 2025 05:56:29 +0200 Subject: [PATCH 07/12] docs: Update README with OCI KMS authentication details and examples - Clarified OCI KMS authentication order and added detailed examples for using OCI CLI and SDK environment variables. - Improved formatting and removed unnecessary whitespace for better readability. Signed-off-by: Alessandro De Blasis --- README.rst | 55 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index a86cc9628b..30c376e747 100644 --- a/README.rst +++ b/README.rst @@ -221,7 +221,7 @@ the ``--age`` option or the **SOPS_AGE_RECIPIENTS** environment variable: When decrypting a file with the corresponding identity, SOPS will look for a text file name ``keys.txt`` located in a ``sops`` subdirectory of your user -configuration directory. +configuration directory. - **Linux** @@ -300,7 +300,7 @@ you can enable application default credentials using the sdk: Using OAauth tokens you can authorize by doing this: .. code:: sh - + $ export GOOGLE_OAUTH_ACCESS_TOKEN= Or if you are logged in you can authorize by generating an access token: @@ -527,14 +527,55 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!) Encrypting using OCI KMS ~~~~~~~~~~~~~~~~~~~~~~~~ -OCI KMS uses the `DefaultConfigProvider `_. -It will look for the `DEFAULT` profile in the `~/.oci/config` file. +OCI KMS authentication is resolved in the following order (env-first, then cloud identity, then local fallbacks): -Make sure to authenticate and to have a valid session via: +1. OCI CLI environment variables (OCI_CLI_*) +2. SDK environment variables (OCI_*), e.g. `OCI_tenancy_ocid` +3. Config file only when explicitly pointed by `OCI_CLI_CONFIG_FILE`/`OCI_CLI_PROFILE` +4. Instance Principals (when running on OCI Compute with appropriate IAM policies) +5. SDK DefaultConfigProvider as a last resort (e.g. `~/.oci/config`, TF_VAR_*) -.. code:: sh +Examples +~~~~~~~~ + +- Using OCI CLI-style env (API key): + +.. code:: bash + + export OCI_CLI_TENANCY=ocid1.tenancy.oc1..xxxx + export OCI_CLI_USER=ocid1.user.oc1..xxxx + export OCI_CLI_REGION=us-ashburn-1 + export OCI_CLI_FINGERPRINT=aa:bb:cc:dd:... + export OCI_CLI_KEY_FILE=$HOME/.oci/oci_api_key.pem + +- Using OCI CLI-style env (SSO/security token): + +.. code:: bash + + # Create a session with the OCI CLI, then point SOPS to the token file + oci session authenticate + export OCI_CLI_AUTH=security_token + export OCI_CLI_SECURITY_TOKEN_FILE="$HOME/.oci/sessions//token" + +- Using SDK env (API key): + +.. code:: bash + + export OCI_tenancy_ocid=ocid1.tenancy.oc1..xxxx + export OCI_user_ocid=ocid1.user.oc1..xxxx + export OCI_region=eu-frankfurt-1 + export OCI_fingerprint=aa:bb:cc:dd:... + export OCI_private_key_path=$HOME/.oci/oci_api_key.pem + +- Using a config file via env: + +.. code:: bash + + export OCI_CLI_CONFIG_FILE=$HOME/.oci/config + export OCI_CLI_PROFILE=DEFAULT - $ oci session authenticate +- Running on OCI Compute (Instance Principals): + No env required; ensure the instance has IAM permissions for KMS operations. Encrypting/decrypting with OCI KMS requires a KMS OCID. You can use the cloud console the get the OCID of an existing key or you can create one using the `oci` From c04cbd9b5bec781409286a3686c84759aeacf634 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 26 Sep 2025 06:31:13 +0200 Subject: [PATCH 08/12] chore(go): Update Go version to 1.24.0 and specify toolchain version - Bumped Go version from 1.23.0 to 1.24.0. - Added toolchain version 1.24.4 for consistency. Signed-off-by: Alessandro De Blasis --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c0572e4659..c1d1c425da 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/getsops/sops/v3 -go 1.23.0 +go 1.24.0 + +toolchain go1.24.4 require ( cloud.google.com/go/kms v1.22.0 From e26b4b72912f64559ed8f378f95a3c6e0ca8d139 Mon Sep 17 00:00:00 2001 From: Ben Dean Date: Tue, 30 Sep 2025 16:03:17 -0400 Subject: [PATCH 09/12] use StringToRegion and EndpointForTemplate to create the crypto endpoint Signed-off-by: Ben Dean --- ocikms/keysource.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ocikms/keysource.go b/ocikms/keysource.go index 4d3d902a6a..644c7e8788 100644 --- a/ocikms/keysource.go +++ b/ocikms/keysource.go @@ -17,8 +17,6 @@ import ( var log *logrus.Logger const ( - // cryptoEndpointTemplate is the template for the OCI KMS crypto endpoint that is constructed using parts of the key OCID - cryptoEndpointTemplate = "https://%s-crypto.kms.%s.oraclecloud.com" // ocidParts is the number of parts in an OCID, separated by ".", eg: "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" ocidParts = 6 ) @@ -54,13 +52,14 @@ func MasterKeysFromOCIDString(ocids string) []*MasterKey { // createKeyManagementClient creates a new OCI KMS client func (key *MasterKey) createCryptoClient() (client keymanagement.KmsCryptoClient, err error) { - region, vault_ref, err := extractRefs(key) + region, vaultExt, err := extractRefs(key) if err != nil { log.WithField("ocid", key.Ocid).Errorf("Cannot extract region and vault_ref from OCID: %s", err) } - endpoint := fmt.Sprintf(cryptoEndpointTemplate, vault_ref, region) - log.WithField("endpoint", endpoint).Info("Creating OCI KMS client") + cryptoEndpointTemplate := fmt.Sprintf("https://%s-crypto.kms.{region}.{secondLevelDomain}", vaultExt) + cryptoEndpoint := common.StringToRegion(region).EndpointForTemplate("kms", cryptoEndpointTemplate) + log.WithField("endpoint", cryptoEndpoint).Info("Creating OCI KMS client") // Build a flexible configuration provider (12 factor app-ish) cfg, cfgErr := configurationProvider() @@ -68,7 +67,7 @@ func (key *MasterKey) createCryptoClient() (client keymanagement.KmsCryptoClient return client, fmt.Errorf("cannot build OCI configuration provider: %w", cfgErr) } - client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(cfg, endpoint) + client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(cfg, cryptoEndpoint) if err != nil { return client, fmt.Errorf("cannot create OCI KMS client: %w", err) } @@ -81,8 +80,8 @@ func extractRefs(key *MasterKey) (string, string, error) { return "", "", fmt.Errorf("OCID length is %s, expected %d", key.Ocid, ocidParts) } region := parts[3] - vault_ref := parts[4] - return region, vault_ref, nil + vaultExt := parts[4] + return region, vaultExt, nil } // EncryptedDataKey returns the encrypted data key this master key holds From 4e025d86daea2c21ab0ae314cf0cd3950aa91acb Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 3 Oct 2025 07:59:22 +0200 Subject: [PATCH 10/12] feat: add early exit optimization for Instance Principal in configuration provider - Implemented tests to verify that Instance Principal is skipped when valid environment variables are provided. - Added fallback mechanism to ensure Instance Principal is called when environment variables are missing or invalid. - Updated comments for clarity on the configuration provider's behavior regarding Instance Principals. Signed-off-by: Alessandro De Blasis --- ocikms/config_provider.go | 17 +- ocikms/config_provider_test.go | 76 ++++ ocikms/keysource.go | 119 +++++-- ocikms/keysource_integration_test.go | 495 +++++++++++++++++++++++++++ 4 files changed, 685 insertions(+), 22 deletions(-) create mode 100644 ocikms/keysource_integration_test.go diff --git a/ocikms/config_provider.go b/ocikms/config_provider.go index bc5658803f..34c895e5c8 100644 --- a/ocikms/config_provider.go +++ b/ocikms/config_provider.go @@ -17,7 +17,7 @@ var newIPProvider = auth.InstancePrincipalConfigurationProvider // 1) OCI_CLI_* environment variables (via ontariosystems/oci-cli-env-provider) // 2) OCI_* environment variables (native SDK env provider) // 3) Config file providers (OCI_CLI_CONFIG_FILE/PROFILE if set) -// 4) Instance Principals (when running on OCI compute) +// 4) Instance Principals (when running on OCI compute) - only if env vars don't work // 5) Default config provider (~/.oci/config, TF_VAR_*), as a last resort func configurationProvider() (common.ConfigurationProvider, error) { var providers []common.ConfigurationProvider @@ -40,7 +40,20 @@ func configurationProvider() (common.ConfigurationProvider, error) { } } } - // 4) Instance principals for compute instances (ignore error here; composition will try next) + + // EARLY EXIT: If we have working credentials from env vars or config files, use them + // and skip Instance Principal (which can be slow when not on OCI compute). + if len(providers) > 0 { + if p, err := common.ComposingConfigurationProvider(providers); err == nil { + // Test if the provider actually has valid credentials by checking TenancyOCID + if _, err := p.TenancyOCID(); err == nil { + // Valid credentials found, return early without trying Instance Principal + return p, nil + } + } + } + + // 4) Instance principals for compute instances (only if env vars/config didn't work) if ip, err := newIPProvider(); err == nil { providers = append(providers, ip) } diff --git a/ocikms/config_provider_test.go b/ocikms/config_provider_test.go index 128bf25baf..fdc1374a2a 100644 --- a/ocikms/config_provider_test.go +++ b/ocikms/config_provider_test.go @@ -301,3 +301,79 @@ func TestConfigurationProvider_InstancePrincipal_Stubbed(t *testing.T) { require.NoError(t, err) require.Equal(t, "ip:stub:fp", fp) } + +// TestConfigurationProvider_EarlyExit_SkipsInstancePrincipal verifies that +// when environment variables provide valid credentials, Instance Principal +// is NOT attempted (performance optimization). +func TestConfigurationProvider_EarlyExit_SkipsInstancePrincipal(t *testing.T) { + // Track whether Instance Principal provider was called + ipCalled := false + old := newIPProvider + t.Cleanup(func() { newIPProvider = old }) + newIPProvider = func() (common.ConfigurationProvider, error) { + ipCalled = true + // Return an error - if this is called, we want to know + return nil, fmt.Errorf("Instance Principal should not be called when env vars work") + } + + // Isolate HOME + t.Setenv(HomeEnv, t.TempDir()) + + // Generate key for env var auth + keyDir := t.TempDir() + keyPath := writeTempRSAKey(t, keyDir) + + // Set OCI_CLI_* env vars (highest priority) + t.Setenv(OCICLITenancy, "ocid1.tenancy.oc1..envtest") + t.Setenv(OCICLIUser, "ocid1.user.oc1..envtest") + t.Setenv(OCICLIRegion, "us-phoenix-1") + t.Setenv(OCICLIFingerprint, "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99") + t.Setenv(OCICLIKeyFile, keyPath) + + prov, err := configurationProvider() + require.NoError(t, err) + + // Verify we got credentials from env vars + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..envtest", tenancy) + + region, err := prov.Region() + require.NoError(t, err) + require.Equal(t, "us-phoenix-1", region) + + // CRITICAL: Instance Principal should NOT have been called + require.False(t, ipCalled, "Instance Principal provider should NOT be called when env vars provide valid credentials (early exit optimization)") +} + +// TestConfigurationProvider_EarlyExit_FallsBackToInstancePrincipal verifies that +// when environment variables are missing or invalid, Instance Principal IS attempted. +func TestConfigurationProvider_EarlyExit_FallsBackToInstancePrincipal(t *testing.T) { + // Track whether Instance Principal provider was called + ipCalled := false + old := newIPProvider + t.Cleanup(func() { newIPProvider = old }) + newIPProvider = func() (common.ConfigurationProvider, error) { + ipCalled = true + return ipStubProvider{}, nil + } + + // Isolate environment - NO valid env vars or config files + t.Setenv(HomeEnv, t.TempDir()) + os.Unsetenv(OCICLIConfigFile) + os.Unsetenv(OCICLIProfile) + clearCLIOCIEnv() + clearOCIEnv(t) + + prov, err := configurationProvider() + require.NoError(t, err) + + tenancy, err := prov.TenancyOCID() + require.NoError(t, err) + + // Should have gotten Instance Principal credentials + require.Equal(t, "ocid1.tenancy.oc1..ipstub", tenancy) + + // CRITICAL: Instance Principal SHOULD have been called as fallback + require.True(t, ipCalled, "Instance Principal provider SHOULD be called when env vars don't provide credentials") +} diff --git a/ocikms/keysource.go b/ocikms/keysource.go index 644c7e8788..aca08e2409 100644 --- a/ocikms/keysource.go +++ b/ocikms/keysource.go @@ -14,7 +14,12 @@ import ( "github.com/sirupsen/logrus" ) -var log *logrus.Logger +var ( + // log is the global logger for any OCI KMS MasterKey. + log *logrus.Logger + // ocikmsTTL is the duration after which a MasterKey requires rotation. + ocikmsTTL = time.Hour * 24 * 30 * 6 +) const ( // ocidParts is the number of parts in an OCID, separated by ".", eg: "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" @@ -27,9 +32,21 @@ func init() { // MasterKey is an Oracle Cloud KMS key used to encrypt and decrypt sops' data key. type MasterKey struct { - Ocid string + // Ocid is the Oracle Cloud Identifier for the KMS key + Ocid string + // EncryptedKey stores the SOPS data key in its encrypted form EncryptedKey string + // CreationDate is when this MasterKey was created CreationDate time.Time + + // configProvider is used to configure the OCI client with credentials. + // It can be injected by a (local) keyservice.KeyServiceServer using + // ConfigurationProvider.ApplyToMasterKey. If nil, a fresh config + // provider is created on each operation which tries multiple auth methods. + configProvider common.ConfigurationProvider + // httpClient is used to override the default HTTP client used by the OCI client. + // Mostly useful for testing purposes. + httpClient common.HTTPRequestDispatcher } func NewMasterKeyFromOCID(ocid string) *MasterKey { @@ -50,34 +67,46 @@ func MasterKeysFromOCIDString(ocids string) []*MasterKey { return keys } -// createKeyManagementClient creates a new OCI KMS client +// createCryptoClient creates a new OCI KMS client. It uses the injected configProvider +// if available, otherwise creates a new one on each call. If httpClient is set, it uses +// that for HTTP requests (useful for testing). func (key *MasterKey) createCryptoClient() (client keymanagement.KmsCryptoClient, err error) { region, vaultExt, err := extractRefs(key) if err != nil { - log.WithField("ocid", key.Ocid).Errorf("Cannot extract region and vault_ref from OCID: %s", err) + log.WithField("ocid", key.Ocid).Errorf("Failed to extract region and vault from OCID: %s", err) + return client, fmt.Errorf("failed to parse OCI KMS key OCID: %w", err) } cryptoEndpointTemplate := fmt.Sprintf("https://%s-crypto.kms.{region}.{secondLevelDomain}", vaultExt) cryptoEndpoint := common.StringToRegion(region).EndpointForTemplate("kms", cryptoEndpointTemplate) log.WithField("endpoint", cryptoEndpoint).Info("Creating OCI KMS client") - // Build a flexible configuration provider (12 factor app-ish) - cfg, cfgErr := configurationProvider() - if cfgErr != nil { - return client, fmt.Errorf("cannot build OCI configuration provider: %w", cfgErr) + // Use injected config provider if available, otherwise create a fresh one + cfg := key.configProvider + if cfg == nil { + cfg, err = configurationProvider() + if err != nil { + return client, fmt.Errorf("failed to create OCI configuration provider: %w", err) + } } client, err = keymanagement.NewKmsCryptoClientWithConfigurationProvider(cfg, cryptoEndpoint) if err != nil { - return client, fmt.Errorf("cannot create OCI KMS client: %w", err) + return client, fmt.Errorf("failed to create OCI KMS client: %w", err) + } + + // Inject custom HTTP client if provided (for testing) + if key.httpClient != nil { + client.HTTPClient = key.httpClient } + return client, nil } func extractRefs(key *MasterKey) (string, string, error) { parts := strings.Split(key.Ocid, ".") if len(parts) != ocidParts { - return "", "", fmt.Errorf("OCID length is %s, expected %d", key.Ocid, ocidParts) + return "", "", fmt.Errorf("invalid OCID format '%s': expected %d parts, got %d", key.Ocid, ocidParts, len(parts)) } region := parts[3] vaultExt := parts[4] @@ -94,16 +123,26 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { key.EncryptedKey = string(enc) } -// Encrypt takes a sops data key, encrypts it with Key Vault and stores the result in the EncryptedKey field +// Encrypt takes a sops data key, encrypts it with OCI KMS and stores the result +// in the EncryptedKey field. +// +// Consider using EncryptContext instead. func (key *MasterKey) Encrypt(dataKey []byte) error { + return key.EncryptContext(context.Background(), dataKey) +} + +// EncryptContext takes a sops data key, encrypts it with OCI KMS and stores the result +// in the EncryptedKey field. +func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { c, err := key.createCryptoClient() if err != nil { log.WithField("ocid", key.Ocid).Info("Encryption failed") - return fmt.Errorf("cannot create OCI KMS service: %w", err) + return fmt.Errorf("failed to create OCI KMS service: %w", err) } + data := base64.StdEncoding.EncodeToString(dataKey) - res, err := c.Encrypt(context.TODO(), keymanagement.EncryptRequest{ + res, err := c.Encrypt(ctx, keymanagement.EncryptRequest{ EncryptDataDetails: keymanagement.EncryptDataDetails{ KeyId: common.String(key.Ocid), Plaintext: &data, @@ -114,7 +153,7 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { if err != nil { log.WithError(err).WithField("ocid", key.Ocid). Error("Encryption failed") - return fmt.Errorf("failed to encrypt data: %w", err) + return fmt.Errorf("failed to encrypt sops data key with OCI KMS key: %w", err) } key.EncryptedKey = *res.EncryptedData.Ciphertext @@ -131,15 +170,22 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { return nil } -// Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns the result. +// Decrypt decrypts the EncryptedKey field with OCI KMS and returns the result. +// +// Consider using DecryptContext instead. func (key *MasterKey) Decrypt() ([]byte, error) { + return key.DecryptContext(context.Background()) +} + +// DecryptContext decrypts the EncryptedKey field with OCI KMS and returns the result. +func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) { c, err := key.createCryptoClient() if err != nil { log.WithField("ocid", key.Ocid).Info("Decryption failed") - return nil, err + return nil, fmt.Errorf("failed to create OCI KMS service: %w", err) } - res, err := c.Decrypt(context.TODO(), keymanagement.DecryptRequest{ + res, err := c.Decrypt(ctx, keymanagement.DecryptRequest{ DecryptDataDetails: keymanagement.DecryptDataDetails{ Ciphertext: &key.EncryptedKey, KeyId: &key.Ocid, @@ -148,13 +194,13 @@ func (key *MasterKey) Decrypt() ([]byte, error) { if err != nil { log.WithError(err).WithField("ocid", key.Ocid).Error("Decryption failed") - return nil, fmt.Errorf("error decrypting key: %w", err) + return nil, fmt.Errorf("failed to decrypt sops data key with OCI KMS key: %w", err) } plaintext, err := base64.StdEncoding.DecodeString(*res.Plaintext) if err != nil { log.WithError(err).WithField("ocid", key.Ocid).Error("Decryption failed") - return nil, err + return nil, fmt.Errorf("failed to base64 decode OCI KMS decrypted key: %w", err) } log.WithField("ocid", key.Ocid).Info("Decryption succeeded") @@ -163,7 +209,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) { // NeedsRotation returns whether the data key needs to be rotated or not. func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6) + return time.Since(key.CreationDate) > ocikmsTTL } // ToString converts the key to a string representation @@ -184,3 +230,36 @@ func (key MasterKey) ToMap() map[string]interface{} { func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } + +// ConfigurationProvider is a wrapper around common.ConfigurationProvider used for +// authentication towards OCI KMS. +type ConfigurationProvider struct { + provider common.ConfigurationProvider +} + +// NewConfigurationProvider creates a new ConfigurationProvider with the provided +// common.ConfigurationProvider. +func NewConfigurationProvider(cp common.ConfigurationProvider) *ConfigurationProvider { + return &ConfigurationProvider{provider: cp} +} + +// ApplyToMasterKey configures the ConfigurationProvider on the provided key. +func (c ConfigurationProvider) ApplyToMasterKey(key *MasterKey) { + key.configProvider = c.provider +} + +// HTTPClient is a wrapper around common.HTTPRequestDispatcher used for +// configuring the OCI KMS client HTTP requests. +type HTTPClient struct { + client common.HTTPRequestDispatcher +} + +// NewHTTPClient creates a new HTTPClient with the provided common.HTTPRequestDispatcher. +func NewHTTPClient(hc common.HTTPRequestDispatcher) *HTTPClient { + return &HTTPClient{client: hc} +} + +// ApplyToMasterKey configures the HTTP client on the provided key. +func (h HTTPClient) ApplyToMasterKey(key *MasterKey) { + key.httpClient = h.client +} diff --git a/ocikms/keysource_integration_test.go b/ocikms/keysource_integration_test.go new file mode 100644 index 0000000000..d74c137ef9 --- /dev/null +++ b/ocikms/keysource_integration_test.go @@ -0,0 +1,495 @@ +package ocikms + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // testOCID is a valid OCID format for testing + testOCID = "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq" + // testDataKey is a dummy 32-byte data key for testing + testDataKey = "testtesttesttesttesttesttest1234" +) + +// mockHTTPClient implements common.HTTPRequestDispatcher for testing +type mockHTTPClient struct { + // requests stores all requests made for verification + requests []*http.Request + // responses is a queue of responses to return + responses []*http.Response + // errors is a queue of errors to return + errors []error + // currentIndex tracks which response to return next + currentIndex int +} + +func newMockHTTPClient() *mockHTTPClient { + return &mockHTTPClient{ + requests: make([]*http.Request, 0), + responses: make([]*http.Response, 0), + errors: make([]error, 0), + } +} + +// Do implements the common.HTTPRequestDispatcher interface +func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { + // Store the request for verification + m.requests = append(m.requests, req) + + if m.currentIndex >= len(m.responses) && m.currentIndex >= len(m.errors) { + return nil, fmt.Errorf("mock client: no more responses configured") + } + + // Return error if configured + if m.currentIndex < len(m.errors) && m.errors[m.currentIndex] != nil { + err := m.errors[m.currentIndex] + m.currentIndex++ + return nil, err + } + + // Return response if configured + if m.currentIndex < len(m.responses) { + resp := m.responses[m.currentIndex] + m.currentIndex++ + return resp, nil + } + + return nil, fmt.Errorf("mock client: no response or error configured for request %d", m.currentIndex) +} + +// addResponse adds a mock HTTP response to the queue +func (m *mockHTTPClient) addResponse(statusCode int, body string) { + resp := &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(body)), + Header: make(http.Header), + } + resp.Header.Set("Content-Type", "application/json") + m.responses = append(m.responses, resp) +} + +// addError adds an error to the queue +func (m *mockHTTPClient) addError(err error) { + m.errors = append(m.errors, err) +} + +// getLastRequest returns the most recent request made +func (m *mockHTTPClient) getLastRequest() *http.Request { + if len(m.requests) == 0 { + return nil + } + return m.requests[len(m.requests)-1] +} + +// mockConfigProvider implements common.ConfigurationProvider for testing +type mockConfigProvider struct { + privateKey *rsa.PrivateKey +} + +func newMockConfigProvider() mockConfigProvider { + // Generate a test RSA key (required by OCI SDK for request signing) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(fmt.Sprintf("failed to generate test RSA key: %v", err)) + } + return mockConfigProvider{ + privateKey: privateKey, + } +} + +func (m mockConfigProvider) TenancyOCID() (string, error) { + return "ocid1.tenancy.oc1..test", nil +} + +func (m mockConfigProvider) UserOCID() (string, error) { + return "ocid1.user.oc1..test", nil +} + +func (m mockConfigProvider) KeyFingerprint() (string, error) { + return "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", nil +} + +func (m mockConfigProvider) Region() (string, error) { + return "uk-london-1", nil +} + +func (m mockConfigProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { + return m.privateKey, nil +} + +func (m mockConfigProvider) KeyID() (string, error) { + tenancy, _ := m.TenancyOCID() + user, _ := m.UserOCID() + fingerprint, _ := m.KeyFingerprint() + return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil +} + +func (m mockConfigProvider) AuthType() (common.AuthConfig, error) { + return common.AuthConfig{ + AuthType: common.UserPrincipal, + }, nil +} + +// createTestMasterKey creates a MasterKey configured for testing with mock HTTP client +func createTestMasterKey(ocid string, mockHTTP *mockHTTPClient) *MasterKey { + key := NewMasterKeyFromOCID(ocid) + + // Inject mock config provider to avoid real auth + configProvider := NewConfigurationProvider(newMockConfigProvider()) + configProvider.ApplyToMasterKey(key) + + // Inject mock HTTP client + if mockHTTP != nil { + httpClient := NewHTTPClient(mockHTTP) + httpClient.ApplyToMasterKey(key) + } + + return key +} + +// createEncryptResponse creates a mock OCI KMS encrypt response +func createEncryptResponse(ciphertext string) string { + response := map[string]interface{}{ + "ciphertext": ciphertext, + } + data, _ := json.Marshal(response) + return string(data) +} + +// createDecryptResponse creates a mock OCI KMS decrypt response +func createDecryptResponse(plaintext string) string { + response := map[string]interface{}{ + "plaintext": plaintext, + } + data, _ := json.Marshal(response) + return string(data) +} + +func TestEncryptContext(t *testing.T) { + tests := []struct { + name string + dataKey []byte + mockResponse string + mockStatusCode int + mockError error + expectError bool + errorContains string + }{ + { + name: "successful encryption", + dataKey: []byte(testDataKey), + mockResponse: createEncryptResponse("ENCRYPTED_DATA_KEY_BASE64"), + mockStatusCode: 200, + expectError: false, + }, + { + name: "network error", + dataKey: []byte(testDataKey), + mockError: fmt.Errorf("network timeout"), + expectError: true, + errorContains: "failed to encrypt sops data key with OCI KMS key", + }, + { + name: "HTTP 500 error", + dataKey: []byte(testDataKey), + mockResponse: `{"code":"InternalServerError","message":"Internal server error"}`, + mockStatusCode: 500, + expectError: true, + errorContains: "failed to encrypt sops data key with OCI KMS key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHTTP := newMockHTTPClient() + + if tt.mockError != nil { + mockHTTP.addError(tt.mockError) + } else { + mockHTTP.addResponse(tt.mockStatusCode, tt.mockResponse) + } + + key := createTestMasterKey(testOCID, mockHTTP) + + err := key.EncryptContext(context.Background(), tt.dataKey) + + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + assert.NotEmpty(t, key.EncryptedKey) + assert.Equal(t, "ENCRYPTED_DATA_KEY_BASE64", key.EncryptedKey) + } + + // Verify request was made (unless error before request) + if tt.mockError == nil || tt.mockStatusCode > 0 { + assert.Greater(t, len(mockHTTP.requests), 0, "should have made at least one HTTP request") + } + }) + } +} + +func TestDecryptContext(t *testing.T) { + dataKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testDataKey)) + + tests := []struct { + name string + encryptedKey string + mockResponse string + mockStatusCode int + mockError error + expectError bool + errorContains string + expectedPlain []byte + }{ + { + name: "successful decryption", + encryptedKey: "ENCRYPTED_DATA_KEY_BASE64", + mockResponse: createDecryptResponse(dataKeyBase64), + mockStatusCode: 200, + expectError: false, + expectedPlain: []byte(testDataKey), + }, + { + name: "network error", + encryptedKey: "ENCRYPTED_DATA_KEY_BASE64", + mockError: fmt.Errorf("connection refused"), + expectError: true, + errorContains: "failed to decrypt sops data key with OCI KMS key", + }, + { + name: "invalid ciphertext", + encryptedKey: "INVALID_CIPHERTEXT", + mockResponse: `{"code":"InvalidCiphertext","message":"The ciphertext is invalid"}`, + mockStatusCode: 400, + expectError: true, + errorContains: "failed to decrypt sops data key with OCI KMS key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHTTP := newMockHTTPClient() + + if tt.mockError != nil { + mockHTTP.addError(tt.mockError) + } else { + mockHTTP.addResponse(tt.mockStatusCode, tt.mockResponse) + } + + key := createTestMasterKey(testOCID, mockHTTP) + key.EncryptedKey = tt.encryptedKey + + plaintext, err := key.DecryptContext(context.Background()) + + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedPlain, plaintext) + } + }) + } +} + +func TestHTTPClientInjection(t *testing.T) { + mockHTTP := newMockHTTPClient() + mockHTTP.addResponse(200, createEncryptResponse("ENCRYPTED")) + + key := NewMasterKeyFromOCID(testOCID) + + // Inject config provider (required for client creation) + configProvider := NewConfigurationProvider(newMockConfigProvider()) + configProvider.ApplyToMasterKey(key) + + // Inject HTTP client + httpClient := NewHTTPClient(mockHTTP) + httpClient.ApplyToMasterKey(key) + + // Perform encryption + err := key.EncryptContext(context.Background(), []byte("test")) + require.NoError(t, err) + + // Verify our mock client was used + assert.Equal(t, 1, len(mockHTTP.requests), "should have used injected HTTP client") +} + +func TestEncryptDecryptRoundTrip(t *testing.T) { + dataKey := []byte("this-is-a-32-byte-test-key-12345") + dataKeyBase64 := base64.StdEncoding.EncodeToString(dataKey) + ciphertext := "MOCK_ENCRYPTED_CIPHERTEXT_BASE64" + + mockHTTP := newMockHTTPClient() + + // Mock encrypt response + mockHTTP.addResponse(200, createEncryptResponse(ciphertext)) + // Mock decrypt response + mockHTTP.addResponse(200, createDecryptResponse(dataKeyBase64)) + + key := createTestMasterKey(testOCID, mockHTTP) + + // Encrypt + err := key.EncryptContext(context.Background(), dataKey) + require.NoError(t, err) + assert.Equal(t, ciphertext, key.EncryptedKey) + + // Decrypt + decrypted, err := key.DecryptContext(context.Background()) + require.NoError(t, err) + assert.Equal(t, dataKey, decrypted) + + // Verify two requests were made + assert.Equal(t, 2, len(mockHTTP.requests)) +} + +func TestContextCancellation(t *testing.T) { + mockHTTP := newMockHTTPClient() + mockHTTP.addResponse(200, createEncryptResponse("ENCRYPTED")) + + key := createTestMasterKey(testOCID, mockHTTP) + + // Create a context that's already cancelled + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // Attempt encryption with cancelled context + err := key.EncryptContext(ctx, []byte("test")) + + // Should fail due to context cancellation + // Note: actual behavior depends on when OCI SDK checks context + // This test documents the expected behavior + _ = err // May or may not error depending on when context is checked +} + +func TestEncryptIfNeeded(t *testing.T) { + dataKey := []byte("test-data-key-32-bytes-long-1234") + + t.Run("encrypts when EncryptedKey is empty", func(t *testing.T) { + mockHTTP := newMockHTTPClient() + mockHTTP.addResponse(200, createEncryptResponse("ENCRYPTED")) + + key := createTestMasterKey(testOCID, mockHTTP) + key.EncryptedKey = "" // Explicitly empty + + err := key.EncryptIfNeeded(dataKey) + require.NoError(t, err) + assert.Equal(t, "ENCRYPTED", key.EncryptedKey) + assert.Equal(t, 1, len(mockHTTP.requests)) + }) + + t.Run("skips encryption when EncryptedKey exists", func(t *testing.T) { + mockHTTP := newMockHTTPClient() + // Don't add any responses - should not be called + + key := createTestMasterKey(testOCID, mockHTTP) + key.EncryptedKey = "ALREADY_ENCRYPTED" + + err := key.EncryptIfNeeded(dataKey) + require.NoError(t, err) + assert.Equal(t, "ALREADY_ENCRYPTED", key.EncryptedKey) + assert.Equal(t, 0, len(mockHTTP.requests), "should not make HTTP request") + }) +} + +func TestNeedsRotation(t *testing.T) { + t.Run("new key does not need rotation", func(t *testing.T) { + key := NewMasterKeyFromOCID(testOCID) + assert.False(t, key.NeedsRotation()) + }) + + t.Run("old key needs rotation", func(t *testing.T) { + key := NewMasterKeyFromOCID(testOCID) + // Set creation date to 7 months ago (> 6 months) + key.CreationDate = time.Now().UTC().Add(-7 * 30 * 24 * time.Hour) + assert.True(t, key.NeedsRotation()) + }) + + t.Run("6-month-old key does not need rotation", func(t *testing.T) { + key := NewMasterKeyFromOCID(testOCID) + // Set creation date to just under 6 months ago + key.CreationDate = time.Now().UTC().Add(-6*30*24*time.Hour + time.Hour) + // Should not need rotation (> is used, not >=) + assert.False(t, key.NeedsRotation()) + }) +} + +func TestToString(t *testing.T) { + key := NewMasterKeyFromOCID(testOCID) + assert.Equal(t, testOCID, key.ToString()) +} + +func TestTypeToIdentifier(t *testing.T) { + key := NewMasterKeyFromOCID(testOCID) + assert.Equal(t, KeyTypeIdentifier, key.TypeToIdentifier()) + assert.Equal(t, "oci_kms", key.TypeToIdentifier()) +} + +func TestExtractRefs(t *testing.T) { + tests := []struct { + name string + ocid string + expectError bool + expectedRegion string + expectedVault string + }{ + { + name: "valid OCID", + ocid: "ocid1.key.oc1.uk-london-1.aaaalgz5aacmg.aaaailjtjbkbc5ufsorrihgv2agugpfe7wrtngukihgkybqxcoozz7sbh6lq", + expectError: false, + expectedRegion: "uk-london-1", + expectedVault: "aaaalgz5aacmg", + }, + { + name: "valid OCID 2", + ocid: "ocid1.vault.oc1.iad.asdadsasdagz5aacmg.abwgiljtjasdasdasdagugpfe7wrtngukihgkybqxcoozz7sbh6lq", + expectError: false, + expectedRegion: "iad", + expectedVault: "asdadsasdagz5aacmg", + }, + { + name: "invalid OCID - too few parts", + ocid: "ocid1.key.oc1", + expectError: true, + }, + { + name: "invalid OCID - too many parts", + ocid: "ocid1.key.oc1.region.vault.extra.extra", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := NewMasterKeyFromOCID(tt.ocid) + region, vault, err := extractRefs(key) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedRegion, region) + assert.Equal(t, tt.expectedVault, vault) + } + }) + } +} From a6df58702c0d0e044419e8b5e86fb38092ab3e21 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Sat, 4 Oct 2025 04:34:04 +0200 Subject: [PATCH 11/12] feat: implement lazy initialization for ConfigurationProvider - Added lazyConfigurationProvider to defer the creation of the ConfigurationProvider until the first method call, optimizing performance for expensive providers like Instance Principal. - Implemented tests to ensure factory is only called once and correctly propagates errors. - Updated comments for clarity on the lazy initialization behavior. Signed-off-by: Alessandro De Blasis --- ocikms/config_provider.go | 98 +++++++++++++++++++++----- ocikms/config_provider_test.go | 124 +++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 19 deletions(-) diff --git a/ocikms/config_provider.go b/ocikms/config_provider.go index 34c895e5c8..b3e4a6a6c3 100644 --- a/ocikms/config_provider.go +++ b/ocikms/config_provider.go @@ -1,7 +1,9 @@ package ocikms import ( + "crypto/rsa" "os" + "sync" ocep "github.com/ontariosystems/oci-cli-env-provider" "github.com/oracle/oci-go-sdk/v65/common" @@ -13,12 +15,12 @@ var newIPProvider = auth.InstancePrincipalConfigurationProvider // configurationProvider composes multiple OCI configuration providers to make // authentication work seamlessly across environments. -// Order of precedence: +// Order of precedence (composing provider will try each in order until one works): // 1) OCI_CLI_* environment variables (via ontariosystems/oci-cli-env-provider) // 2) OCI_* environment variables (native SDK env provider) // 3) Config file providers (OCI_CLI_CONFIG_FILE/PROFILE if set) -// 4) Instance Principals (when running on OCI compute) - only if env vars don't work -// 5) Default config provider (~/.oci/config, TF_VAR_*), as a last resort +// 4) Default config provider (~/.oci/config, TF_VAR_*) +// 5) Instance Principals (when running on OCI compute) - lazily evaluated as last resort func configurationProvider() (common.ConfigurationProvider, error) { var providers []common.ConfigurationProvider @@ -41,25 +43,83 @@ func configurationProvider() (common.ConfigurationProvider, error) { } } - // EARLY EXIT: If we have working credentials from env vars or config files, use them - // and skip Instance Principal (which can be slow when not on OCI compute). - if len(providers) > 0 { - if p, err := common.ComposingConfigurationProvider(providers); err == nil { - // Test if the provider actually has valid credentials by checking TenancyOCID - if _, err := p.TenancyOCID(); err == nil { - // Valid credentials found, return early without trying Instance Principal - return p, nil - } - } + // 4) Default config provider (~/.oci/config, TF_VAR_*) + providers = append(providers, common.DefaultConfigProvider()) + + // 5) Instance principals for compute instances (lazy, only called if nothing else works) + providers = append(providers, &lazyConfigurationProvider{factory: newIPProvider}) + + return common.ComposingConfigurationProvider(providers) +} + +// lazyConfigurationProvider wraps a ConfigurationProvider factory function and defers its +// creation until the first method call. This is useful for expensive providers +// like Instance Principal that may timeout or fail in non-OCI environments. +type lazyConfigurationProvider struct { + factory func() (common.ConfigurationProvider, error) + provider common.ConfigurationProvider + once sync.Once + err error +} + +var _ common.ConfigurationProvider = (*lazyConfigurationProvider)(nil) + +func (l *lazyConfigurationProvider) init() { + l.provider, l.err = l.factory() +} + +func (l *lazyConfigurationProvider) TenancyOCID() (string, error) { + l.once.Do(l.init) + if l.err != nil { + return "", l.err } + return l.provider.TenancyOCID() +} - // 4) Instance principals for compute instances (only if env vars/config didn't work) - if ip, err := newIPProvider(); err == nil { - providers = append(providers, ip) +func (l *lazyConfigurationProvider) UserOCID() (string, error) { + l.once.Do(l.init) + if l.err != nil { + return "", l.err } + return l.provider.UserOCID() +} - // 5) Always keep a last-resort default config provider at the end - providers = append(providers, common.DefaultConfigProvider()) +func (l *lazyConfigurationProvider) KeyFingerprint() (string, error) { + l.once.Do(l.init) + if l.err != nil { + return "", l.err + } + return l.provider.KeyFingerprint() +} - return common.ComposingConfigurationProvider(providers) +func (l *lazyConfigurationProvider) Region() (string, error) { + l.once.Do(l.init) + if l.err != nil { + return "", l.err + } + return l.provider.Region() +} + +func (l *lazyConfigurationProvider) KeyID() (string, error) { + l.once.Do(l.init) + if l.err != nil { + return "", l.err + } + return l.provider.KeyID() +} + +func (l *lazyConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { + l.once.Do(l.init) + if l.err != nil { + return nil, l.err + } + return l.provider.PrivateRSAKey() +} + +func (l *lazyConfigurationProvider) AuthType() (common.AuthConfig, error) { + l.once.Do(l.init) + if l.err != nil { + return common.AuthConfig{}, l.err + } + return l.provider.AuthType() } diff --git a/ocikms/config_provider_test.go b/ocikms/config_provider_test.go index fdc1374a2a..12b106a88d 100644 --- a/ocikms/config_provider_test.go +++ b/ocikms/config_provider_test.go @@ -377,3 +377,127 @@ func TestConfigurationProvider_EarlyExit_FallsBackToInstancePrincipal(t *testing // CRITICAL: Instance Principal SHOULD have been called as fallback require.True(t, ipCalled, "Instance Principal provider SHOULD be called when env vars don't provide credentials") } + +// Test suite for lazyConfigurationProvider +func TestLazyProvider_FactoryNotCalledUntilFirstUse(t *testing.T) { + factoryCalled := false + factory := func() (common.ConfigurationProvider, error) { + factoryCalled = true + return ipStubProvider{}, nil + } + + lp := &lazyConfigurationProvider{factory: factory} + + // Factory should NOT be called just by creating the lazy provider + require.False(t, factoryCalled, "Factory should not be called on lazy provider creation") + + // Call a method - this should trigger factory + _, err := lp.TenancyOCID() + require.NoError(t, err) + require.True(t, factoryCalled, "Factory should be called on first method invocation") +} + +func TestLazyProvider_FactoryCalledOnlyOnce(t *testing.T) { + callCount := 0 + factory := func() (common.ConfigurationProvider, error) { + callCount++ + return ipStubProvider{}, nil + } + + lp := &lazyConfigurationProvider{factory: factory} + + // Call multiple methods + _, _ = lp.TenancyOCID() + _, _ = lp.Region() + _, _ = lp.KeyFingerprint() + _, _ = lp.UserOCID() + _, _ = lp.KeyID() + _, _ = lp.PrivateRSAKey() + _, _ = lp.AuthType() + + // Factory should only be called once despite 7 method calls + require.Equal(t, 1, callCount, "Factory should only be called once via sync.Once") +} + +func TestLazyProvider_PropagatesFactoryError(t *testing.T) { + expectedErr := fmt.Errorf("factory initialization failed") + factory := func() (common.ConfigurationProvider, error) { + return nil, expectedErr + } + + lp := &lazyConfigurationProvider{factory: factory} + + // All methods should return the factory error + _, err := lp.TenancyOCID() + require.ErrorIs(t, err, expectedErr) + + _, err = lp.Region() + require.ErrorIs(t, err, expectedErr) + + _, err = lp.KeyFingerprint() + require.ErrorIs(t, err, expectedErr) +} + +func TestLazyProvider_AllMethodsWorkAfterInit(t *testing.T) { + factory := func() (common.ConfigurationProvider, error) { + return ipStubProvider{}, nil + } + + lp := &lazyConfigurationProvider{factory: factory} + + // Test all ConfigurationProvider methods work correctly + tenancy, err := lp.TenancyOCID() + require.NoError(t, err) + require.Equal(t, "ocid1.tenancy.oc1..ipstub", tenancy) + + region, err := lp.Region() + require.NoError(t, err) + require.Equal(t, "me-dubai-1", region) + + fp, err := lp.KeyFingerprint() + require.NoError(t, err) + require.Equal(t, "ip:stub:fp", fp) + + keyID, err := lp.KeyID() + require.NoError(t, err) + require.Equal(t, "ST$ipstub", keyID) + + user, err := lp.UserOCID() + require.NoError(t, err) + require.Equal(t, "", user) + + key, err := lp.PrivateRSAKey() + require.NoError(t, err) + require.NotNil(t, key) + + authType, err := lp.AuthType() + require.NoError(t, err) + require.Equal(t, common.AuthConfig{}, authType) +} + +func TestLazyProvider_ConcurrentAccess(t *testing.T) { + callCount := 0 + factory := func() (common.ConfigurationProvider, error) { + callCount++ + return ipStubProvider{}, nil + } + + lp := &lazyConfigurationProvider{factory: factory} + + // Simulate concurrent access from multiple goroutines + done := make(chan bool, 10) + for i := 0; i < 10; i++ { + go func() { + _, _ = lp.TenancyOCID() + done <- true + }() + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } + + // Factory should still only be called once (sync.Once is thread-safe) + require.Equal(t, 1, callCount, "Factory should only be called once even with concurrent access") +} From 98a7bfa64eeff1d983805629e3bb7dbb2acea7a7 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Wed, 17 Jun 2026 10:04:06 +0200 Subject: [PATCH 12/12] docs: add Oracle Cloud KMS to the supported providers list Signed-off-by: Alessandro De Blasis --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bde72ff8d6..1808ebe6c0 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ SOPS: Secrets OPerationS ======================== **SOPS** is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY -formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, age, and PGP. +formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, Oracle Cloud KMS, age, and PGP. (`demo `_) .. image:: https://i.imgur.com/X0TM5NI.gif