grim/convey

8467e23c44f2
Parents 1b9d4a44ccbd
Children d4945adeea3b
A ton of work on the ssh-agent stuff. refs #88
--- a/config/config.go Mon May 01 21:36:29 2017 -0500
+++ b/config/config.go Tue May 02 02:13:31 2017 -0500
@@ -23,8 +23,9 @@
)
type Config struct {
- Tasks map[string]tasks.Task
- Plans map[string]plans.Plan
- MetaPlans map[string]plans.MetaPlan
- Environment []string
+ Tasks map[string]tasks.Task
+ Plans map[string]plans.Plan
+ MetaPlans map[string]plans.MetaPlan
+ SSHIdentities []string
+ Environment []string
}
--- a/config/convey.go Mon May 01 21:36:29 2017 -0500
+++ b/config/convey.go Tue May 02 02:13:31 2017 -0500
@@ -28,11 +28,12 @@
)
type conveyConfig struct {
- DefaultPlan string `yaml:"default-plan"`
- Tasks map[string]yaml.MapSlice `yaml:"tasks"`
- Plans map[string]plans.Plan `yaml:"plans"`
- MetaPlans map[string]plans.MetaPlan `yaml:"meta-plans"`
- Environment []string `yaml:"environment"`
+ DefaultPlan string `yaml:"default-plan"`
+ Tasks map[string]yaml.MapSlice `yaml:"tasks"`
+ Plans map[string]plans.Plan `yaml:"plans"`
+ MetaPlans map[string]plans.MetaPlan `yaml:"meta-plans"`
+ SSHIdentities []string `yaml:"ssh-identities"`
+ Environment []string `yaml:"environment"`
}
type conveyOverride struct {
--- a/convey.yml Mon May 01 21:36:29 2017 -0500
+++ b/convey.yml Tue May 02 02:13:31 2017 -0500
@@ -1,6 +1,8 @@
environment:
- CONVEY_VERSION=0.5.0
- GO_PACKAGE=bitbucket.org/rw_grim/convey
+ssh-identities:
+ - SHA256:Efrocgd+rvwjDAnHt2jZAcwDqeka0s8Vv7N3m08cVnA
tasks:
# tasks for the default plan
import:
--- a/main.go Mon May 01 21:36:29 2017 -0500
+++ b/main.go Tue May 02 02:13:31 2017 -0500
@@ -54,7 +54,8 @@
listPlans = app.Flag("list-plans", "List the plans that are available").Short('P').Default("false").Bool()
memory = app.Flag("memory", "The ammount of memor to give the run task").Short('m').String()
showConfig = app.Flag("show-config", "Show a dump of the config file").Short('C').Hidden().Default("false").Bool()
- sshAgent = app.Flag("ssh-agent", "Enabled ssh-agent support for tasks").Default("false").Bool()
+ sshAgent = app.Flag("ssh-agent", "A shortcut for --ssh-identity=*").Default("false").Bool()
+ sshIdentities = app.Flag("ssh-identity", "Enable ssh-agent for the given identities").Strings()
taskTimeout = app.Flag("timeout", "The maximum amount of time a task can run in seconds. 0 to disable").Default("15m").Duration()
verbose = app.Flag("verbose", "Be more verbose").Short('v').Default("False").Bool()
@@ -114,7 +115,11 @@
defEnv := environment.Initialize()
if *sshAgent {
- haveKeys, err := ssh.KeysAvailable()
+ *sshIdentities = append(*sshIdentities, "*")
+ }
+
+ if len(*sshIdentities) > 0 {
+ haveKeys, err := ssh.KeysAvailable(*sshIdentities)
if err != nil {
fmt.Printf("error talking to ssh-agent: %s\n", err)
os.Exit(1)
--- a/ssh/agent.go Mon May 01 21:36:29 2017 -0500
+++ b/ssh/agent.go Tue May 02 02:13:31 2017 -0500
@@ -18,25 +18,71 @@
package ssh
import (
+ "fmt"
+ "io"
"net"
"os"
+ "strings"
+ "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// KeysAvailable returns True if there is at least one key that the ssh agent
// knows about, otherwise false. err will be set if there is an error along
// the way.
-func KeysAvailable() (bool, error) {
+func KeysAvailable(identites []string) (bool, error) {
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return false, err
}
- keys, err := agent.NewClient(sshAgent).List()
+ return keysAvailable(sshAgent, identites)
+}
+
+func keysAvailable(rw io.ReadWriter, identities []string) (bool, error) {
+ keys, err := agent.NewClient(rw).List()
if err != nil {
return false, err
}
- return len(keys) != 0, nil
+ // this runs in O(m * n) but thankful the number of keys should be low.
+ for _, identity := range identities {
+ if identity == "*" {
+ // if the user doesn't care which key and we have a key, return true
+ if len(keys) > 0 {
+ return true, nil
+ } else {
+ // if the user doesn't care which key but we have no keys, return an error
+ return false, fmt.Errorf("no ssh keys available")
+ }
+ }
+
+ uIdentity := strings.ToUpper(identity)
+ for _, pubKey := range keys {
+ fpSHA256 := strings.ToUpper(ssh.FingerprintSHA256(pubKey))
+ fpMD5 := strings.ToUpper(ssh.FingerprintLegacyMD5(pubKey))
+
+ if strings.HasPrefix(uIdentity, "SHA256:") {
+ if uIdentity == fpSHA256 {
+ return true, nil
+ }
+ } else if strings.HasPrefix(uIdentity, "MD5:") {
+ if uIdentity[4:] == fpMD5 {
+ return true, nil
+ }
+ } else {
+ // no known prefix check sha256 then md5, then give up
+ if uIdentity == fpSHA256[7:] || uIdentity == fpMD5 {
+ return true, nil
+ }
+ }
+ }
+ }
+
+ if err == nil {
+ err = fmt.Errorf("no usable ssh identities found")
+ }
+
+ return false, err
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ssh/agent_test.go Tue May 02 02:13:31 2017 -0500
@@ -0,0 +1,189 @@
+/*
+ * Convey
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package ssh
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "net"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/agent"
+)
+
+func (s *sshSuite) setupAgent(keys []agent.AddedKey) net.Conn {
+ a := agent.NewKeyring()
+
+ for _, key := range keys {
+ err := a.Add(key)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ c1, c2 := net.Pipe()
+
+ go func() {
+ defer c2.Close()
+
+ err := agent.ServeAgent(a, c2)
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ return c1
+}
+
+func (s *sshSuite) generateKey() *ecdsa.PrivateKey {
+ priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+
+ return priv
+}
+
+func (s *sshSuite) TestKeysAvailableEmpty(t *testing.T) {
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{}),
+ []string{},
+ )
+
+ Expect(avail).To(BeFalse())
+ Expect(err).To(Not(BeNil()))
+}
+
+func (s *sshSuite) TestKeysAvailableWildcardNoKeys(t *testing.T) {
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{}),
+ []string{"*"},
+ )
+
+ Expect(avail).To(BeFalse())
+ Expect(err).To(Not(BeNil()))
+}
+
+func (s *sshSuite) TestKeysAvailableWildcardWithKeys(t *testing.T) {
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: s.generateKey()},
+ }),
+ []string{"*"},
+ )
+
+ Expect(avail).To(BeTrue())
+ Expect(err).To(BeNil())
+}
+
+func (s *sshSuite) TestKeysAvailableUnknownFingerprint(t *testing.T) {
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: s.generateKey()},
+ }),
+ []string{"SHA256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"},
+ )
+
+ Expect(avail).To(BeFalse())
+ Expect(err).To(Not(BeNil()))
+}
+
+func (s *sshSuite) TestKeysAvailableFingerprintMD5NoPrefix(t *testing.T) {
+ key := s.generateKey()
+ pub := key.Public()
+
+ sshPub, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ panic(err)
+ }
+
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: key},
+ }),
+ []string{ssh.FingerprintLegacyMD5(sshPub)},
+ )
+
+ Expect(err).To(BeNil())
+ Expect(avail).To(BeTrue())
+}
+
+func (s *sshSuite) TestKeysAvailableFingerprintMD5Prefix(t *testing.T) {
+ key := s.generateKey()
+ pub := key.Public()
+
+ sshPub, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ panic(err)
+ }
+
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: key},
+ }),
+ []string{"MD5:" + ssh.FingerprintLegacyMD5(sshPub)},
+ )
+
+ Expect(err).To(BeNil())
+ Expect(avail).To(BeTrue())
+}
+
+func (s *sshSuite) TestKeysAvailableFingerprintSHA256Prefix(t *testing.T) {
+ key := s.generateKey()
+ pub := key.Public()
+
+ sshPub, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ panic(err)
+ }
+
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: key},
+ }),
+ []string{ssh.FingerprintSHA256(sshPub)},
+ )
+
+ Expect(err).To(BeNil())
+ Expect(avail).To(BeTrue())
+}
+
+func (s *sshSuite) TestKeysAvailableFingerprintSHA256NoPrefix(t *testing.T) {
+ key := s.generateKey()
+ pub := key.Public()
+
+ sshPub, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ panic(err)
+ }
+
+ // FingerprintSHA256 always has the prefix so we strip it off for testing
+ avail, err := keysAvailable(
+ s.setupAgent([]agent.AddedKey{
+ agent.AddedKey{PrivateKey: key},
+ }),
+ []string{ssh.FingerprintSHA256(sshPub)[7:]},
+ )
+
+ Expect(err).To(BeNil())
+ Expect(avail).To(BeTrue())
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ssh/ssh_test.go Tue May 02 02:13:31 2017 -0500
@@ -0,0 +1,35 @@
+/*
+ * Convey
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package ssh
+
+import (
+ "testing"
+
+ "github.com/aphistic/sweet"
+ . "github.com/onsi/gomega"
+)
+
+type sshSuite struct{}
+
+func Test(t *testing.T) {
+ sweet.T(func(s *sweet.S) {
+ RegisterFailHandler(sweet.GomegaFail)
+
+ s.RunSuite(t, &sshSuite{})
+ })
+}