Skip to content

Commit

Permalink
Merge pull request #165 from srm09/feature/support-for-ssh-agent
Browse files Browse the repository at this point in the history
Adds support for passphrase protected ssh key
  • Loading branch information
srm09 authored Sep 3, 2020
2 parents 55e39fe + ec0d453 commit bc18d64
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 56 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/compile-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ jobs:
runs-on: ubuntu-latest
steps:

- name: Set up Go 1.13
- name: Set up Go 1.15
uses: actions/setup-go@v1
with:
go-version: 1.13.x
go-version: 1.15
id: go

- name: Check out code into the Go module directory
Expand Down
9 changes: 5 additions & 4 deletions exec/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
package exec

import (
"fmt"
"io"
"os"

"github.com/pkg/errors"
"github.com/vmware-tanzu/crash-diagnostics/starlark"
)

Expand All @@ -25,11 +25,12 @@ func Execute(name string, source io.Reader, args ArgMap) error {
star.AddPredeclared("args", starStruct)
}

if err := star.Exec(name, source); err != nil {
return fmt.Errorf("exec failed: %s", err)
err := star.Exec(name, source)
if err != nil {
err = errors.Wrap(err, "exec failed")
}

return nil
return err
}

func ExecuteFile(file *os.File, args ArgMap) error {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/vmware-tanzu/crash-diagnostics

go 1.12
go 1.15

require (
github.com/imdario/mergo v0.3.7 // indirect
Expand Down
175 changes: 175 additions & 0 deletions ssh/agent.go
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],
}
}
160 changes: 160 additions & 0 deletions ssh/agent_test.go
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)
})
}
}
Loading

0 comments on commit bc18d64

Please sign in to comment.