Skip to content

Commit 9ed3c96

Browse files
committed
feat(ssh-key): support passphrases for --ssh-key options
1. Enter passphrase in cli prompt when running in interactive terminal. 2. Pass passhprase using `--ssh-key file://PASSPHRASE@FILEPATH` format (works only when `file://` schema has been specified). Signed-off-by: Timofey Kirillov <timofey.kirillov@flant.com>
1 parent c389259 commit 9ed3c96

File tree

1 file changed

+134
-44
lines changed

1 file changed

+134
-44
lines changed

pkg/ssh_agent/ssh_agent.go

Lines changed: 134 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"net"
99
"os"
1010
"path/filepath"
11+
"strings"
1112

1213
uuid "github.com/satori/go.uuid"
1314
"golang.org/x/crypto/ssh"
1415
"golang.org/x/crypto/ssh/agent"
16+
"golang.org/x/crypto/ssh/terminal"
1517

1618
"github.com/werf/logboek"
19+
secret_common "github.com/werf/werf/cmd/werf/helm/secret/common"
1720
"github.com/werf/werf/pkg/util"
1821
"github.com/werf/werf/pkg/werf"
1922
)
@@ -28,24 +31,93 @@ func setupProcessSSHAgent(sshAuthSock string) error {
2831
return os.Setenv("SSH_AUTH_SOCK", SSHAuthSock)
2932
}
3033

31-
func Init(ctx context.Context, keys []string) error {
32-
for _, key := range keys {
33-
if keyExists, err := util.FileExists(key); !keyExists {
34-
return fmt.Errorf("specified ssh key %s does not exist", key)
35-
} else if err != nil {
36-
return fmt.Errorf("specified ssh key %s does not exist: %v", key, err)
34+
type sshKeyConfig struct {
35+
FilePath string
36+
Passphrase []byte
37+
}
38+
39+
type sshKey struct {
40+
Config sshKeyConfig
41+
PrivateKey interface{}
42+
}
43+
44+
func getSshKeyConfig(path string) (sshKeyConfig, error) {
45+
var filePath string
46+
var passphrase []byte
47+
48+
switch {
49+
case strings.HasPrefix(path, "file://"):
50+
userinfoWithPath := strings.TrimPrefix(path, "file://")
51+
52+
parts := strings.SplitN(userinfoWithPath, "@", 2)
53+
passphrase = []byte(parts[0])
54+
filePath = util.ExpandPath(parts[1])
55+
56+
default:
57+
filePath = util.ExpandPath(path)
58+
}
59+
60+
if keyExists, err := util.FileExists(filePath); !keyExists {
61+
return sshKeyConfig{}, fmt.Errorf("specified ssh key does not exist")
62+
} else if err != nil {
63+
return sshKeyConfig{}, fmt.Errorf("specified ssh key does not exist: %w", err)
64+
}
65+
66+
return sshKeyConfig{FilePath: filePath, Passphrase: passphrase}, nil
67+
}
68+
69+
type loadSshKeysOptions struct {
70+
WarnInvalidKeys bool
71+
}
72+
73+
func loadSshKeys(ctx context.Context, configs []sshKeyConfig, opts loadSshKeysOptions) ([]sshKey, error) {
74+
var res []sshKey
75+
76+
for _, cfg := range configs {
77+
sshKey, err := parsePrivateSSHKey(cfg)
78+
if err != nil {
79+
if opts.WarnInvalidKeys {
80+
logboek.Context(ctx).Warn().LogF("WARNING: unable to parse ssh key %s: %s\n", cfg.FilePath, err)
81+
continue
82+
} else {
83+
return nil, fmt.Errorf("unable to parse ssh key %s: %s", cfg.FilePath, err)
84+
}
3785
}
86+
87+
res = append(res, sshKey)
3888
}
3989

40-
if len(keys) > 0 {
41-
agentSock, err := runSSHAgentWithKeys(ctx, keys)
90+
return res, nil
91+
}
92+
93+
func Init(ctx context.Context, userKeys []string) error {
94+
var configs []sshKeyConfig
95+
96+
for _, key := range userKeys {
97+
cfg, err := getSshKeyConfig(key)
4298
if err != nil {
43-
return fmt.Errorf("unable to run ssh agent with specified keys: %s", err)
99+
return fmt.Errorf("unable to get ssh key %s config: %s", key, err)
44100
}
45-
if err := setupProcessSSHAgent(agentSock); err != nil {
46-
return fmt.Errorf("unable to init ssh auth socket to %q: %s", agentSock, err)
101+
102+
configs = append(configs, cfg)
103+
}
104+
105+
if len(configs) > 0 {
106+
keys, err := loadSshKeys(ctx, configs, loadSshKeysOptions{})
107+
if err != nil {
108+
return fmt.Errorf("unable to load ssh keys: %s", err)
109+
}
110+
111+
if len(keys) > 0 {
112+
agentSock, err := runSSHAgentWithKeys(ctx, keys)
113+
if err != nil {
114+
return fmt.Errorf("unable to run ssh agent with specified keys: %s", err)
115+
}
116+
if err := setupProcessSSHAgent(agentSock); err != nil {
117+
return fmt.Errorf("unable to init ssh auth socket to %q: %s", agentSock, err)
118+
}
119+
return nil
47120
}
48-
return nil
49121
}
50122

51123
systemAgentSock := os.Getenv("SSH_AUTH_SOCK")
@@ -56,35 +128,23 @@ func Init(ctx context.Context, keys []string) error {
56128
return nil
57129
}
58130

59-
var defaultKeys []string
131+
var defaultConfigs []*sshKeyConfig
60132
for _, defaultFileName := range []string{"id_rsa", "id_dsa"} {
61133
path := filepath.Join(os.Getenv("HOME"), ".ssh", defaultFileName)
62134

63135
if keyExists, _ := util.FileExists(path); keyExists {
64-
defaultKeys = append(defaultKeys, path)
136+
defaultConfigs = append(defaultConfigs, &sshKeyConfig{FilePath: path})
65137
}
66138
}
67139

68-
if len(defaultKeys) > 0 {
69-
var validKeys []string
70-
71-
for _, key := range defaultKeys {
72-
keyData, err := ioutil.ReadFile(key)
73-
if err != nil {
74-
logboek.Context(ctx).Warn().LogF("WARNING: cannot read default key %s: %s\n", key, err)
75-
continue
76-
}
77-
_, err = ssh.ParseRawPrivateKey(keyData)
78-
if err != nil {
79-
logboek.Context(ctx).Warn().LogF("WARNING: default key %s validation error: %s\n", key, err)
80-
continue
81-
}
82-
83-
validKeys = append(validKeys, key)
140+
if len(defaultConfigs) > 0 {
141+
keys, err := loadSshKeys(ctx, configs, loadSshKeysOptions{WarnInvalidKeys: true})
142+
if err != nil {
143+
return fmt.Errorf("unable to load ssh keys: %s", err)
84144
}
85145

86-
if len(validKeys) > 0 {
87-
agentSock, err := runSSHAgentWithKeys(ctx, validKeys)
146+
if len(keys) > 0 {
147+
agentSock, err := runSSHAgentWithKeys(ctx, keys)
88148
if err != nil {
89149
return fmt.Errorf("unable to run ssh agent with specified keys: %s", err)
90150
}
@@ -108,7 +168,7 @@ func Terminate() error {
108168
return nil
109169
}
110170

111-
func runSSHAgentWithKeys(ctx context.Context, keys []string) (string, error) {
171+
func runSSHAgentWithKeys(ctx context.Context, keys []sshKey) (string, error) {
112172
agentSock, err := runSSHAgent(ctx)
113173
if err != nil {
114174
return "", fmt.Errorf("error running ssh agent: %s", err)
@@ -117,7 +177,7 @@ func runSSHAgentWithKeys(ctx context.Context, keys []string) (string, error) {
117177
for _, key := range keys {
118178
err := addSSHKey(ctx, agentSock, key)
119179
if err != nil {
120-
return "", fmt.Errorf("error adding ssh key %s: %s", key, err)
180+
return "", fmt.Errorf("error adding ssh key: %s", err)
121181
}
122182
}
123183

@@ -171,7 +231,7 @@ func runSSHAgent(ctx context.Context) (string, error) {
171231
return sockPath, nil
172232
}
173233

174-
func addSSHKey(ctx context.Context, authSock string, key string) error {
234+
func addSSHKey(ctx context.Context, authSock string, key sshKey) error {
175235
conn, err := net.Dial("unix", authSock)
176236
if err != nil {
177237
return fmt.Errorf("error dialing with ssh agent %s: %s", authSock, err)
@@ -180,22 +240,52 @@ func addSSHKey(ctx context.Context, authSock string, key string) error {
180240

181241
agentClient := agent.NewClient(conn)
182242

183-
keyData, err := ioutil.ReadFile(key)
243+
err = agentClient.Add(agent.AddedKey{PrivateKey: key.PrivateKey})
184244
if err != nil {
185-
return fmt.Errorf("error reading key file %s: %s", key, err)
245+
return err
186246
}
187247

188-
privateKey, err := ssh.ParseRawPrivateKey(keyData)
248+
logboek.Context(ctx).Info().LogF("Added private key %s to ssh agent %s\n", key.Config.FilePath, authSock)
249+
250+
return nil
251+
}
252+
253+
func parsePrivateSSHKey(cfg sshKeyConfig) (sshKey, error) {
254+
keyData, err := ioutil.ReadFile(cfg.FilePath)
189255
if err != nil {
190-
return fmt.Errorf("error parsing private key %s: %s", key, err)
256+
return sshKey{}, fmt.Errorf("error reading key file %q: %s", cfg.FilePath, err)
191257
}
192258

193-
err = agentClient.Add(agent.AddedKey{PrivateKey: privateKey})
259+
var privateKey interface{}
260+
261+
privateKey, err = ssh.ParseRawPrivateKey(keyData)
194262
if err != nil {
195-
return err
196-
}
263+
switch err.(type) {
264+
case *ssh.PassphraseMissingError:
265+
var passphrase []byte
266+
if len(cfg.Passphrase) == 0 {
267+
if terminal.IsTerminal(int(os.Stdin.Fd())) {
268+
if data, err := secret_common.InputFromInteractiveStdin(fmt.Sprintf("Enter passphrase for ssh key %s: ", cfg.FilePath)); err != nil {
269+
return sshKey{}, fmt.Errorf("error getting passphrase for ssh key %s: %s", cfg.FilePath, err)
270+
} else {
271+
passphrase = data
272+
}
273+
} else {
274+
return sshKey{}, fmt.Errorf(`%w: please provide passphrase using --ssh-add="file://PASSPHRASE@FILEPATH" format`, err)
275+
}
276+
} else {
277+
passphrase = cfg.Passphrase
278+
}
197279

198-
logboek.Context(ctx).Info().LogF("Added private key %s to ssh agent %s\n", key, authSock)
280+
privateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(keyData, passphrase)
281+
if err != nil {
282+
return sshKey{}, fmt.Errorf("error parsing private key %s: %s", cfg.FilePath, err)
283+
}
199284

200-
return nil
285+
default:
286+
return sshKey{}, fmt.Errorf("error parsing private key %s: %s", cfg.FilePath, err)
287+
}
288+
}
289+
290+
return sshKey{Config: cfg, PrivateKey: privateKey}, nil
201291
}

0 commit comments

Comments
 (0)