-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #165 from srm09/feature/support-for-ssh-agent
Adds support for passphrase protected ssh key
- Loading branch information
Showing
20 changed files
with
498 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Copyright (c) 2020 VMware, Inc. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package ssh | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/sirupsen/logrus" | ||
"github.com/vladimirvivien/echo" | ||
) | ||
|
||
// ssh-agent constant identifiers | ||
const ( | ||
AgentPidIdentifier = "SSH_AGENT_PID" | ||
AuthSockIdentifier = "SSH_AUTH_SOCK" | ||
) | ||
|
||
type Agent interface { | ||
AddKey(keyPath string) error | ||
RemoveKey(keyPath string) error | ||
Stop() error | ||
GetEnvVariables() string | ||
} | ||
|
||
// agentInfo captures the connection information of the ssh-agent | ||
type agentInfo map[string]string | ||
|
||
// agent represents an instance of the ssh-agent | ||
type agent struct { | ||
// Pid of the ssh-agent | ||
Pid string | ||
|
||
// Authentication socket to communicate with the ssh-agent | ||
AuthSockPath string | ||
|
||
// File paths of the keys added to the ssh-agent | ||
KeyPaths []string | ||
} | ||
|
||
// AddKey adds a key to the ssh-agent process | ||
func (agent *agent) AddKey(keyPath string) error { | ||
e := echo.New() | ||
sshAddCmd := e.Prog.Avail("ssh-add") | ||
if len(sshAddCmd) == 0 { | ||
return errors.New("ssh-add not found") | ||
} | ||
|
||
p := e.Env(agent.GetEnvVariables()). | ||
RunProc(fmt.Sprintf("%s %s", sshAddCmd, keyPath)) | ||
if err := p.Err(); err != nil { | ||
return errors.Wrapf(err, "could not add key %s to ssh-agent", keyPath) | ||
} | ||
agent.KeyPaths = append(agent.KeyPaths, keyPath) | ||
return nil | ||
} | ||
|
||
// RemoveKey removes a key from the ssh-agent process | ||
func (agent *agent) RemoveKey(keyPath string) error { | ||
e := echo.New() | ||
sshAddCmd := e.Prog.Avail("ssh-add") | ||
if len(sshAddCmd) == 0 { | ||
return errors.New("ssh-add not found") | ||
} | ||
|
||
p := e.Env(agent.GetEnvVariables()). | ||
RunProc(fmt.Sprintf("%s -d %s", sshAddCmd, keyPath)) | ||
if err := p.Err(); err != nil { | ||
return errors.Wrapf(err, "could not add key %s to ssh-agent", keyPath) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Stop kills the ssh-agent process. | ||
// It also tries to remove the added keys from the agent | ||
func (agent *agent) Stop() error { | ||
for _, path := range agent.KeyPaths { | ||
logrus.Debugf("removing key from ssh-agent: %s", path) | ||
err := agent.RemoveKey(path) | ||
if err != nil { | ||
logrus.Warnf("failed to remove SSH key from agent: %s", err) | ||
} | ||
} | ||
|
||
logrus.Debugf("stopping the ssh-agent with Pid: %s", agent.Pid) | ||
p := echo.New().Env(agent.GetEnvVariables()).RunProc("ssh-agent -k") | ||
|
||
return p.Err() | ||
} | ||
|
||
// GetEnvVariables returns the space separated key=value information used to communicate with the ssh-agent | ||
func (agent *agent) GetEnvVariables() string { | ||
return fmt.Sprintf("%s=%s %s=%s", AgentPidIdentifier, agent.Pid, AuthSockIdentifier, agent.AuthSockPath) | ||
} | ||
|
||
// StartAgent starts the ssh-agent process and returns the SSH authentication parameters. | ||
func StartAgent() (Agent, error) { | ||
e := echo.New() | ||
sshAgentCmd := e.Prog.Avail("ssh-agent") | ||
if len(sshAgentCmd) == 0 { | ||
return nil, fmt.Errorf("ssh-agent not found") | ||
} | ||
|
||
p := e.RunProc(fmt.Sprintf("%s -s", sshAgentCmd)) | ||
if p.Err() != nil { | ||
return nil, errors.Wrap(p.Err(), "failed to start ssh agent") | ||
} | ||
|
||
agentInfo, err := parseAgentInfo(p.Out()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if err := validateAgentInfo(agentInfo); err != nil { | ||
return nil, err | ||
} | ||
|
||
return agentFromInfo(agentInfo), nil | ||
} | ||
|
||
// parseAgentInfo parses the output of ssh-agent -s to determine the information | ||
// for the ssh authentication agent. | ||
// example output: | ||
// SSH_AUTH_SOCK=/foo/bar.1234; export SSH_AUTH_SOCK; | ||
// SSH_AGENT_PID=4567; export SSH_AGENT_PID; | ||
// echo Agent pid 4567; | ||
func parseAgentInfo(info io.Reader) (agentInfo, error) { | ||
agentInfo := map[string]string{} | ||
|
||
scanner := bufio.NewScanner(info) | ||
if err := scanner.Err(); err != nil { | ||
return agentInfo, err | ||
} | ||
|
||
for scanner.Scan() { | ||
line := scanner.Text() | ||
// separate the line using the semi-colon as a separator | ||
if equal := strings.Index(line, ";"); equal >= 0 { | ||
s := strings.Split(line, ";")[0] | ||
// check if any key=value pair is present | ||
if equal := strings.Index(s, "="); equal >= 0 { | ||
kv := strings.Split(s, "=") | ||
// store the key-value pair in the map | ||
agentInfo[kv[0]] = kv[1] | ||
} | ||
} | ||
} | ||
|
||
return agentInfo, nil | ||
} | ||
|
||
// validateAgentInfo checks whether the ssh-agent information is valid | ||
func validateAgentInfo(info agentInfo) error { | ||
if len(info) != 2 { | ||
return errors.New("faulty ssh-agent identifier info") | ||
} | ||
for k, v := range info { | ||
if !strings.Contains(strings.Join([]string{AgentPidIdentifier, AuthSockIdentifier}, ""), k) || len(v) == 0 { | ||
return errors.New("faulty ssh-agent identifier info") | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// agentFromInfo parses the information map and returns an instance of agent | ||
func agentFromInfo(agentInfo agentInfo) *agent { | ||
return &agent{ | ||
Pid: agentInfo[AgentPidIdentifier], | ||
AuthSockPath: agentInfo[AuthSockIdentifier], | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Copyright (c) 2019 VMware, Inc. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package ssh | ||
|
||
import ( | ||
"bufio" | ||
"regexp" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/vladimirvivien/echo" | ||
) | ||
|
||
func TestParseAndValidateAgentInfo(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
info string | ||
shouldErr bool | ||
}{ | ||
{ | ||
name: "valid info", | ||
shouldErr: false, | ||
info: `SSH_AUTH_SOCK=/foo/bar.1234; export SSH_AUTH_SOCK; | ||
SSH_AGENT_PID=4567; export SSH_AGENT_PID; | ||
echo Agent pid 4567;`, | ||
}, | ||
{ | ||
name: "invalid info", | ||
shouldErr: true, | ||
info: `FOO=/foo/bar.1234; export BAR; | ||
BLAH=4567; export BLOOP; | ||
echo lorem ipsum 4567;`, | ||
}, | ||
{ | ||
name: "invalid info", | ||
shouldErr: true, | ||
info: `SSH_AUTH_SOCK=/foo/bar.1234; export SSH_AUTH_SOCK; | ||
BLAH=4567; export BLOOP; | ||
echo lorem ipsum 4567;`, | ||
}, | ||
{ | ||
name: "invalid info", | ||
shouldErr: true, | ||
info: `FOO=/foo/bar.1234; export BAR; | ||
SSH_AGENT_PID=4567; export SSH_AGENT_PID; | ||
echo lorem ipsum 4567;`, | ||
}, | ||
{ | ||
name: "invalid info", | ||
shouldErr: true, | ||
info: `lorem ipsum 1; | ||
lorem ipsum 2.`, | ||
}, | ||
{ | ||
name: "invalid info", | ||
shouldErr: true, | ||
info: "", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
agentInfo, err := parseAgentInfo(strings.NewReader(test.info)) | ||
if err != nil { | ||
t.Fail() | ||
} | ||
err = validateAgentInfo(agentInfo) | ||
if err != nil && !test.shouldErr { | ||
// unexpected failures | ||
t.Fail() | ||
} else if !test.shouldErr { | ||
if _, ok := agentInfo[AgentPidIdentifier]; !ok { | ||
t.Fail() | ||
} | ||
if _, ok := agentInfo[AuthSockIdentifier]; !ok { | ||
t.Fail() | ||
} | ||
} else { | ||
// asserting error scenarios | ||
if err == nil { | ||
t.Fail() | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestStartAgent(t *testing.T) { | ||
a, err := StartAgent() | ||
if err != nil || a == nil { | ||
t.Fatalf("error should be nil and agent should not be nil: %v", err) | ||
} | ||
out := echo.New().Run("ps -ax") | ||
if !strings.Contains(out, "ssh-agent") { | ||
t.Fatal("no ssh-agent process found") | ||
} | ||
|
||
failed := true | ||
scanner := bufio.NewScanner(strings.NewReader(out)) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if strings.Contains(line, "ssh-agent") { | ||
pid := strings.Split(strings.TrimSpace(line), " ")[0] | ||
// set failed to false if correct ssh-agent process is found | ||
agentStruct, _ := a.(*agent) | ||
if pid == agentStruct.Pid { | ||
failed = false | ||
} | ||
} | ||
} | ||
if failed { | ||
t.Fatal("could not find agent with correct Pid") | ||
} | ||
|
||
t.Cleanup(func() { | ||
_ = a.Stop() | ||
}) | ||
} | ||
|
||
func TestAgent(t *testing.T) { | ||
a, err := StartAgent() | ||
if err != nil { | ||
t.Fatalf("failed to start agent: %v", err) | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
assert func(*testing.T, Agent) | ||
}{ | ||
{ | ||
name: "GetEnvVariables", | ||
assert: func(t *testing.T, agent Agent) { | ||
vars := agent.GetEnvVariables() | ||
if len(strings.Split(vars, " ")) != 2 { | ||
t.Fatalf("not enough variables") | ||
} | ||
|
||
match, err := regexp.MatchString(`SSH_AGENT_PID=[0-9]+ SSH_AUTH_SOCK=\S*`, vars) | ||
if err != nil || !match { | ||
t.Fatalf("format does not match") | ||
} | ||
}, | ||
}, | ||
{ | ||
name: "Stop", | ||
assert: func(t *testing.T, agent Agent) { | ||
if err := agent.Stop(); err != nil { | ||
t.Errorf("failed to stop agent: %s", err) | ||
} | ||
}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
test.assert(t, a) | ||
}) | ||
} | ||
} |
Oops, something went wrong.