Skip to content

Commit 9bc4e8b

Browse files
fix: the logic to accept a user provided PEM formatted SSH private key for each git repo domain (#1094)
* fix: the logic to accept a user provided PEM formatted SSH private key for each git repo domain Signed-off-by: Harikrishnan Balagopal <[email protected]> * fixup! fix: the logic to accept a user provided PEM formatted SSH private key for each git repo domain Signed-off-by: Harikrishnan Balagopal <[email protected]> * fixup! fixup! fix: the logic to accept a user provided PEM formatted SSH private key for each git repo domain Signed-off-by: Harikrishnan Balagopal <[email protected]> --------- Signed-off-by: Harikrishnan Balagopal <[email protected]>
1 parent d4deb13 commit 9bc4e8b

File tree

1 file changed

+61
-68
lines changed

1 file changed

+61
-68
lines changed

common/sshkeys/sshkeys.go

+61-68
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
)
3535

3636
const (
37-
userProvidedKey = "user_provided_key"
37+
shoudlAskUserForSSHKey = "____m2k_ask_user_to_provide_the_ssh_private_key"
3838
)
3939

4040
var (
@@ -107,65 +107,56 @@ func loadSSHKeysOfCurrentUser() {
107107

108108
// Ask whether to load private keys or provide own key
109109
options := []string{
110-
"Load private ssh keys from " + privateKeyDir,
110+
fmt.Sprintf("Load the private SSH keys from the directory '%s'" + privateKeyDir),
111111
"Provide your own key",
112112
"No, I will add them later if necessary.",
113113
}
114114
message := `The CI/CD pipeline needs access to the git repos in order to clone, build and push.
115115
If any of the repos require ssh keys you will need to provide them.
116116
Select an option:`
117117
selectedOption := qaengine.FetchSelectAnswer(common.ConfigRepoLoadPrivKey, message, nil, "", options, nil)
118-
119118
switch selectedOption {
120119
case options[0]:
121-
if err := loadKeysFromDirectory(privateKeyDir); err != nil {
122-
logrus.Warn("Can't load keys from directory. Error:", err)
120+
selectedKeyFilenames, err := loadKeysFromDirectory(privateKeyDir)
121+
if err != nil {
122+
logrus.Warnf("Failed to load the keys from the SSH directory '%s'. Error: %q", privateKeyDir, err)
123123
return
124124
}
125-
125+
// Save the filenames for now. We will decrypt them if and when we need them.
126+
privateKeysToConsider = selectedKeyFilenames
126127
case options[1]:
127-
privateKeysToConsider = []string{userProvidedKey}
128-
128+
privateKeysToConsider = []string{shoudlAskUserForSSHKey}
129129
default:
130130
logrus.Debug("Don't read private keys. They will be added later if necessary.")
131131
return
132132
}
133-
134133
}
135134

136-
func loadKeysFromDirectory(directory string) error {
135+
func loadKeysFromDirectory(directory string) ([]string, error) {
137136
finfos, err := os.ReadDir(directory)
138137
if err != nil {
139-
return fmt.Errorf("failed to read the SSH directory at path %q: %w", directory, err)
138+
return nil, fmt.Errorf("failed to read the directory '%s'. Error: %w", directory, err)
140139
}
141-
142140
if len(finfos) == 0 {
143-
logrus.Warn("No key files were found in", directory)
144-
return nil
141+
return nil, fmt.Errorf("no key files were found in the directory '%s'", directory)
145142
}
146-
147143
filenames := []string{}
148144
for _, finfo := range finfos {
149145
filenames = append(filenames, finfo.Name())
150146
}
151-
152147
selectedFilenames := qaengine.FetchMultiSelectAnswer(
153148
common.ConfigRepoKeyPathsKey,
154-
fmt.Sprintf("These are the files found in %q. Select the keys to consider:", directory),
155-
[]string{"Select all the keys that give access to git repos."},
149+
fmt.Sprintf("These are the files we found in the SSH directory '%s'. Select the keys to consider:", directory),
150+
[]string{"Select all the keys that give access to the git repos."},
156151
filenames,
157152
filenames,
158153
nil,
159154
)
160-
161155
if len(selectedFilenames) == 0 {
162156
logrus.Info("All key files ignored.")
163-
return nil
157+
return nil, nil
164158
}
165-
166-
// Save the filenames for now. We will decrypt them if and when we need them.
167-
privateKeysToConsider = selectedFilenames
168-
return nil
159+
return selectedFilenames, nil
169160
}
170161

171162
func marshalRSAIntoPEM(key *rsa.PrivateKey) string {
@@ -186,29 +177,22 @@ func marshalECDSAIntoPEM(key *ecdsa.PrivateKey) string {
186177
return string(PEMBytes)
187178
}
188179

189-
func loadSSHKey(filename string) (string, error) {
190-
path := filepath.Join(privateKeyDir, filename)
191-
fileBytes, err := os.ReadFile(path)
192-
if err != nil {
193-
logrus.Errorf("Failed to read the private key file at path %q Error: %q", path, err)
194-
return "", err
195-
}
196-
key, err := ssh.ParseRawPrivateKey(fileBytes)
180+
// loadSSHPrivateKeyFromBytes tries to parse the bytes as an SSH private key.
181+
// The keyName is optional (used to ask the user for the password if necessary).
182+
func loadSSHPrivateKeyFromBytes(keyBytes []byte, keyName string) (string, error) {
183+
key, err := ssh.ParseRawPrivateKey(keyBytes)
197184
if err != nil {
198185
// Could be an encrypted private key.
199186
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
200-
logrus.Errorf("Failed to parse the private key file at path %q Error %q", path, err)
201-
return "", err
187+
return "", fmt.Errorf("failed to parse as a SSH private key. Error %w", err)
202188
}
203-
204-
qaKey := common.JoinQASubKeys(common.ConfigRepoPrivKey, `"`+filename+`"`, "password")
205-
desc := fmt.Sprintf("Enter the password to decrypt the private key %q : ", filename)
189+
qaKey := common.JoinQASubKeys(common.ConfigRepoPrivKey, `"`+keyName+`"`, "password")
190+
desc := fmt.Sprintf("Enter the password to decrypt the SSH private key '%s' : ", keyName)
206191
hints := []string{"Password:"}
207192
password := qaengine.FetchPasswordAnswer(qaKey, desc, hints, nil)
208-
key, err = ssh.ParseRawPrivateKeyWithPassphrase(fileBytes, []byte(password))
193+
key, err = ssh.ParseRawPrivateKeyWithPassphrase(keyBytes, []byte(password))
209194
if err != nil {
210-
logrus.Errorf("Failed to parse the encrypted private key file at path %q Error %q", path, err)
211-
return "", err
195+
return "", fmt.Errorf("failed to decrypt and parse the encrypted private SSH key '%s' . Error %w", keyName, err)
212196
}
213197
}
214198
// *ecdsa.PrivateKey
@@ -218,61 +202,70 @@ func loadSSHKey(filename string) (string, error) {
218202
case *ecdsa.PrivateKey:
219203
return marshalECDSAIntoPEM(actualKey), nil
220204
default:
221-
logrus.Errorf("Unknown key type [%T]", key)
222205
return "", fmt.Errorf("unknown key type [%T]", key)
223206
}
224207
}
225208

209+
func loadSSHPrivateKey(filename string) (string, error) {
210+
path := filepath.Join(privateKeyDir, filename)
211+
fileBytes, err := os.ReadFile(path)
212+
if err != nil {
213+
return "", fmt.Errorf("failed to read the SSH private key file '%s' . Error: %w", path, err)
214+
}
215+
return loadSSHPrivateKeyFromBytes(fileBytes, filename)
216+
}
217+
226218
// GetSSHKey returns the private key for the given domain.
227219
func GetSSHKey(domain string) (string, bool) {
228220
loadSSHKeysOfCurrentUser()
229221
if len(privateKeysToConsider) == 0 {
230222
return "", false
231223
}
232-
if privateKeysToConsider[0] == userProvidedKey {
233-
key := qaengine.FetchStringAnswer(common.ConfigRepoPrivKey, "Provide your own PEM-formatted private key:", []string{"Should not be empty"}, "", nil)
224+
225+
if len(privateKeysToConsider) == 1 && privateKeysToConsider[0] == shoudlAskUserForSSHKey {
226+
qaKey := common.JoinQASubKeys(common.ConfigRepoKeysKey, `"`+domain+`"`, "keyData")
227+
validatedKey := ""
228+
key := qaengine.FetchStringAnswer(
229+
qaKey,
230+
fmt.Sprintf("Provide a PEM-formatted SSH private key for the domain '%s':", domain),
231+
[]string{"To skip this question, just leave the answer empty"},
232+
"",
233+
func(ansI interface{}) error {
234+
ans := ansI.(string)
235+
if ans == "" {
236+
return nil
237+
}
238+
t1, err := loadSSHPrivateKeyFromBytes([]byte(ans), domain)
239+
if err == nil {
240+
validatedKey = t1
241+
}
242+
return err
243+
},
244+
)
234245
if key == "" {
235-
logrus.Error("User-provided private key is empty.")
236-
return "", false
237-
}
238-
if err := validatePEMPrivateKey(key); err != nil {
239-
logrus.Error("Can't validate the PEM-formatted private key. Error:", err)
246+
logrus.Debugf("No key was provided for the domain '%s'", domain)
240247
return "", false
241248
}
242-
return key, true
249+
return validatedKey, true
243250
}
244251

245252
filenames := privateKeysToConsider
246253
noAnswer := "none of the above"
247254
filenames = append(filenames, noAnswer)
248255
qaKey := common.JoinQASubKeys(common.ConfigRepoKeysKey, `"`+domain+`"`, "key")
249-
desc := fmt.Sprintf("Select the key to use for the git domain %s :", domain)
250-
hints := []string{fmt.Sprintf("If none of the keys are correct, select %s", noAnswer)}
256+
desc := fmt.Sprintf("Select the key to use for the git domain '%s' :", domain)
257+
hints := []string{fmt.Sprintf("If none of the keys are correct, select '%s'", noAnswer)}
251258
filename := qaengine.FetchSelectAnswer(qaKey, desc, hints, noAnswer, filenames, nil)
252259
if filename == noAnswer {
253-
logrus.Debugf("No key selected for domain %s", domain)
260+
logrus.Debugf("No key was selected for domain '%s'", domain)
254261
return "", false
255262
}
256263

257264
logrus.Debug("Loading the key", filename)
258-
key, err := loadSSHKey(filename)
265+
key, err := loadSSHPrivateKey(filename)
259266
if err != nil {
260-
logrus.Warnf("Failed to load the key %q Error %q", filename, err)
267+
logrus.Warnf("Failed to load the SSH private key file '%s' . Error %q", filename, err)
261268
return "", false
262269
}
263270
return key, true
264271
}
265-
266-
func validatePEMPrivateKey(key string) error {
267-
block, _ := pem.Decode([]byte(key))
268-
if block == nil || block.Type != "RSA PRIVATE KEY" {
269-
return fmt.Errorf("invalid PEM private key format")
270-
}
271-
272-
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
273-
if err != nil {
274-
return err
275-
}
276-
277-
return nil
278-
}

0 commit comments

Comments
 (0)