Skip to content

Commit

Permalink
feat: OCI KMS provider
Browse files Browse the repository at this point in the history
Signed-off-by: Alessandro De Blasis <[email protected]>
  • Loading branch information
deblasis committed Dec 28, 2024
1 parent 659b7a5 commit 34d2349
Show file tree
Hide file tree
Showing 14 changed files with 602 additions and 110 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
bin/
dist/
functional-tests/sops
functional-tests/target
vendor/
profile.out
71 changes: 63 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 <https://github.com/oracle/oci-go-sdk/blob/master/README.md#configuring>`_.
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=<substitute-value-of-compartment_id>
$ export display_name=<substitute-value-of-display_name>
$ export vault_type=<substitute-value-of-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=<substitute-value-of-compartment_id>
$ export display_name=<substitute-value-of-display_name>
# you can grab the endpoint from the vault page on the portal, it should be something like: https://asdadsasdagz5aacmg-management.kms.<region>.oraclecloud.com
$ OCI_CLI_AUTH=security_token oci kms management key create --compartment-id $compartment_id --display-name $display_name --endpoint <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.<region>.asdadsasdagz5aacmg.abwgiljtjasdasdasdagugpfe7wrtngukihgkybqxcoozz7sbh6lq test.yaml > test.enc.yaml
And decrypt it using::
$ sops --decrypt test.enc.yaml
Adding and removing keys
~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -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
Expand Down
33 changes: 27 additions & 6 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
10 changes: 10 additions & 0 deletions keyservice/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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))
}
Expand Down
Loading

0 comments on commit 34d2349

Please sign in to comment.