diff --git a/cmd/cmd/secret/init.go b/cmd/cmd/secret/init.go index d8510b4e6..348088ee2 100644 --- a/cmd/cmd/secret/init.go +++ b/cmd/cmd/secret/init.go @@ -23,7 +23,6 @@ import ( "encoding/base64" "fmt" "os" - "path/filepath" "strings" "syscall" @@ -39,7 +38,7 @@ const secretInitCmdLongDesc = "Initialize the Key Store information required for var secretInitCmdExamples = "To initialize a Key Store information\n" + " " + utils.ProjectName + " " + utils.MiCmdLiteral + " " + secretCmdLiteral + " " + secretInitCmdLiteral + "\n" + - "NOTE: Secret encryption supports only JKS Key Stores" + "NOTE: Secret encryption supports JKS and PKCS12 Key Stores" var secretInitCmd = &cobra.Command{ Use: secretInitCmdLiteral, @@ -62,10 +61,12 @@ func startConsoleForKeyStore() { fmt.Printf("Enter Key Store location: ") path, _ := reader.ReadString('\n') - if !isJKSKeyStore(path) { - utils.HandleErrorAndExit("Invalid Key Store Type. Supports only JKS Key Stores", nil) + keyStoreType, err := utils.GetKeyStoreType(path) + if err != nil { + utils.HandleErrorAndExit(err.Error(), nil) } keyStoreConfig.KeyStorePath = strings.TrimSpace(path) + keyStoreConfig.KeyStoreType = keyStoreType fmt.Printf("Enter Key Store password: ") byteStorePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) @@ -77,11 +78,13 @@ func startConsoleForKeyStore() { alias, _ := reader.ReadString('\n') keyStoreConfig.KeyAlias = strings.TrimSpace(alias) - fmt.Printf("Enter Key password: ") - bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) - keyPassword := string(bytePassword) - fmt.Println() - keyStoreConfig.KeyPassword = base64.StdEncoding.EncodeToString([]byte(strings.TrimSpace(keyPassword))) + if !utils.IsPKCS12KeyStore(keyStoreType) { + fmt.Printf("Enter Key password: ") + bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) + keyPassword := string(bytePassword) + fmt.Println() + keyStoreConfig.KeyPassword = base64.StdEncoding.EncodeToString([]byte(strings.TrimSpace(keyPassword))) + } if utils.IsValidKeyStoreConfig(keyStoreConfig) { utils.CreateDirIfNotExist(utils.GetKeyStoreDirectoryPath()) @@ -96,7 +99,3 @@ func startConsoleForKeyStore() { func updateMap(params map[string]string, key, value string) { params[key] = strings.TrimSpace(value) } - -func isJKSKeyStore(path string) bool { - return filepath.Ext(strings.TrimSpace(path)) == ".jks" -} diff --git a/cmd/docs/mi_secret_init.md b/cmd/docs/mi_secret_init.md index 9daf00274..5ecaa9201 100644 --- a/cmd/docs/mi_secret_init.md +++ b/cmd/docs/mi_secret_init.md @@ -15,7 +15,7 @@ mi secret init [flags] ``` To initialize a Key Store information mi mi secret init -NOTE: Secret encryption supports only JKS Key Stores +NOTE: Secret encryption supports JKS and PKCS12 Key Stores ``` ### Options diff --git a/cmd/go.mod b/cmd/go.mod index 73935a418..7e575a5f6 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -12,6 +12,7 @@ require ( golang.org/x/crypto v0.45.0 gopkg.in/resty.v1 v1.12.0 gopkg.in/yaml.v2 v2.2.2 + software.sslmate.com/src/go-pkcs12 v0.7.2 ) require ( diff --git a/cmd/go.sum b/cmd/go.sum index c32d8a463..e23b798bd 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -151,3 +151,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +software.sslmate.com/src/go-pkcs12 v0.7.2 h1:Rh9FoMaI5k7Oo6EOS+2/BnoZ+JFIS+XHjM0VGkSPXLM= +software.sslmate.com/src/go-pkcs12 v0.7.2/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/cmd/utils/encryptionUtils.go b/cmd/utils/encryptionUtils.go index eb84b976a..b4ea62b71 100644 --- a/cmd/utils/encryptionUtils.go +++ b/cmd/utils/encryptionUtils.go @@ -34,10 +34,15 @@ import ( "github.com/magiconair/properties" "github.com/pavlo-v-chernykh/keystore-go/v4" "gopkg.in/yaml.v2" + pkcs12 "software.sslmate.com/src/go-pkcs12" ) const keystoreDirName = "keystore" const keyStoreConfigFileName = "keystore_info.yaml" + +// JKSKeyStoreType and PKCS12KeyStoreType are the supported keystore types +const JKSKeyStoreType = "jks" +const PKCS12KeyStoreType = "pkcs12" const encryptedSecretsPropertiesFileName = "wso2-secrets.properties" const encryptedSecretsYamlFileName = "wso2-secrets.yaml" @@ -65,9 +70,10 @@ type SecretConfig struct { type KeyStoreConfig struct { KeyStorePath string `yaml:"keyStorePath"` + KeyStoreType string `yaml:"keyStoreType,omitempty"` KeyStorePassword string `yaml:"keyStorePassword"` KeyAlias string `yaml:"keyAlias"` - KeyPassword string `yaml:"keyPassword"` + KeyPassword string `yaml:"keyPassword,omitempty"` } type encryptFunc func(key *rsa.PublicKey, plainText string) (string, error) @@ -75,12 +81,30 @@ type encryptFunc func(key *rsa.PublicKey, plainText string) (string, error) // IsValidKeyStoreConfig return true if the KeyStoreConfig is valid func IsValidKeyStoreConfig(config *KeyStoreConfig) bool { if IsNonEmptyString(config.KeyStorePath) && IsNonEmptyString(config.KeyStorePassword) && - IsNonEmptyString(config.KeyAlias) && IsNonEmptyString(config.KeyPassword) { - return true + IsNonEmptyString(config.KeyAlias) { + // PKCS12 keystores use the keystore password for the key as well + return IsPKCS12KeyStore(config.KeyStoreType) || IsNonEmptyString(config.KeyPassword) } return false } +// IsPKCS12KeyStore return true if the keystore type is pkcs12 +func IsPKCS12KeyStore(keyStoreType string) bool { + return strings.EqualFold(keyStoreType, PKCS12KeyStoreType) +} + +// GetKeyStoreType resolves the keystore type from the file extension of the keystore path +func GetKeyStoreType(keyStorePath string) (string, error) { + switch strings.ToLower(filepath.Ext(strings.TrimSpace(keyStorePath))) { + case ".jks": + return JKSKeyStoreType, nil + case ".p12", ".pfx", ".pkcs12": + return PKCS12KeyStoreType, nil + default: + return "", errors.New("Invalid Key Store Type. Supports only JKS and PKCS12 Key Stores") + } +} + // EncryptSecrets encrypts the secrets using the keystore and write them to a file or console depending on the config map argument func EncryptSecrets(keyStoreConfig *KeyStoreConfig, secretConfig SecretConfig) error { encryptionKey, err := getEncryptionKey(keyStoreConfig) @@ -147,6 +171,10 @@ func GetKeyStoreConfigFromFile(filePath string) (*KeyStoreConfig, error) { if err := yaml.Unmarshal(data, config); err != nil { return nil, errors.New("Parsing error.\nExecute 'mi secret init --help' for more information") } + if !IsNonEmptyString(config.KeyStoreType) { + // keystore configs created before PKCS12 support have no type field + config.KeyStoreType = JKSKeyStoreType + } if !IsValidKeyStoreConfig(config) { return nil, errors.New("Missing required fields.\nExecute 'mi secret init --help' for more information") } @@ -154,9 +182,15 @@ func GetKeyStoreConfigFromFile(filePath string) (*KeyStoreConfig, error) { } func getEncryptionKey(keyStoreConfig *KeyStoreConfig) (*rsa.PublicKey, error) { - keyStorePath := keyStoreConfig.KeyStorePath keyStorePassword, _ := base64.StdEncoding.DecodeString(keyStoreConfig.KeyStorePassword) - keyStore, err := readKeyStore(keyStorePath, keyStorePassword) + if IsPKCS12KeyStore(keyStoreConfig.KeyStoreType) { + return getEncryptionKeyFromPKCS12(keyStoreConfig.KeyStorePath, string(keyStorePassword)) + } + return getEncryptionKeyFromJKS(keyStoreConfig, keyStorePassword) +} + +func getEncryptionKeyFromJKS(keyStoreConfig *KeyStoreConfig, keyStorePassword []byte) (*rsa.PublicKey, error) { + keyStore, err := readKeyStore(keyStoreConfig.KeyStorePath, keyStorePassword) if err != nil { return nil, errors.New("Reading Key Store: " + err.Error()) } @@ -167,13 +201,32 @@ func getEncryptionKey(keyStoreConfig *KeyStoreConfig) (*rsa.PublicKey, error) { return nil, errors.New("Reading Key Entry: " + err.Error()) } key, err := x509.ParsePKCS8PrivateKey(pke.PrivateKey) - rsaKey := key.(*rsa.PrivateKey) if err != nil { return nil, errors.New("Parsing Key Entry: " + err.Error()) } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("Parsing Key Entry: key entry is not an RSA key") + } return &rsaKey.PublicKey, nil } +func getEncryptionKeyFromPKCS12(keyStorePath string, keyStorePassword string) (*rsa.PublicKey, error) { + data, err := ioutil.ReadFile(keyStorePath) + if err != nil { + return nil, errors.New("Reading Key Store: " + err.Error()) + } + _, cert, _, err := pkcs12.DecodeChain(data, keyStorePassword) + if err != nil { + return nil, errors.New("Reading Key Store: " + err.Error()) + } + rsaKey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, errors.New("Reading Key Entry: certificate does not hold an RSA public key") + } + return rsaKey, nil +} + func encrypt(encryptionKey *rsa.PublicKey, plainTextSecrets map[string]string, encryptFunction encryptFunc) (map[string]string, error) { var encryptedSecrets = make(map[string]string) for alias, plainText := range plainTextSecrets { diff --git a/cmd/utils/encryptionUtils_test.go b/cmd/utils/encryptionUtils_test.go new file mode 100644 index 000000000..decfc8b86 --- /dev/null +++ b/cmd/utils/encryptionUtils_test.go @@ -0,0 +1,75 @@ +/* +* Copyright (c) WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 LLC. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. + */ + +package utils + +import "testing" + +func TestGetKeyStoreType(t *testing.T) { + validCases := map[string]string{ + "wso2carbon.jks": JKSKeyStoreType, + "wso2carbon.JKS": JKSKeyStoreType, + "wso2carbon.p12": PKCS12KeyStoreType, + "wso2carbon.pfx": PKCS12KeyStoreType, + "wso2carbon.pkcs12": PKCS12KeyStoreType, + " wso2carbon.p12\n": PKCS12KeyStoreType, + "/a/b/c/wso2carbon.jks": JKSKeyStoreType, + } + for path, expected := range validCases { + actual, err := GetKeyStoreType(path) + if err != nil { + t.Errorf("GetKeyStoreType(%q) returned error: %v", path, err) + } + if actual != expected { + t.Errorf("GetKeyStoreType(%q) = %q, expected %q", path, actual, expected) + } + } + + invalidCases := []string{"wso2carbon.txt", "wso2carbon", ""} + for _, path := range invalidCases { + if _, err := GetKeyStoreType(path); err == nil { + t.Errorf("GetKeyStoreType(%q) expected error, got nil", path) + } + } +} + +func TestIsValidKeyStoreConfig(t *testing.T) { + jksConfig := &KeyStoreConfig{ + KeyStorePath: "wso2carbon.jks", + KeyStoreType: JKSKeyStoreType, + KeyStorePassword: "cGFzcw==", + KeyAlias: "wso2carbon", + } + if IsValidKeyStoreConfig(jksConfig) { + t.Error("JKS config without key password should be invalid") + } + jksConfig.KeyPassword = "cGFzcw==" + if !IsValidKeyStoreConfig(jksConfig) { + t.Error("JKS config with all fields should be valid") + } + + pkcs12Config := &KeyStoreConfig{ + KeyStorePath: "wso2carbon.p12", + KeyStoreType: PKCS12KeyStoreType, + KeyStorePassword: "cGFzcw==", + KeyAlias: "wso2carbon", + } + if !IsValidKeyStoreConfig(pkcs12Config) { + t.Error("PKCS12 config without key password should be valid") + } +}