From 8b10b8c3e925df5bd0b1d83006f06af4245b34a5 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 4 Mar 2017 17:22:33 +0800 Subject: [PATCH] feat: Add time out flag. --- DOCS.md | 19 + easyssh/easyssh.go | 146 ---- main.go | 23 +- plugin.go | 27 +- plugin_test.go | 32 + .../github.com/appleboy/easyssh-proxy/LICENSE | 21 + .../appleboy/easyssh-proxy/Makefile | 60 ++ .../appleboy/easyssh-proxy/README.md | 5 + .../appleboy/easyssh-proxy/easyssh.go | 250 +++++++ .../golang.org/x/crypto/ssh/agent/client.go | 659 ++++++++++++++++++ .../golang.org/x/crypto/ssh/agent/forward.go | 103 +++ .../golang.org/x/crypto/ssh/agent/keyring.go | 215 ++++++ .../golang.org/x/crypto/ssh/agent/server.go | 451 ++++++++++++ vendor/vendor.json | 18 +- 14 files changed, 1862 insertions(+), 167 deletions(-) delete mode 100644 easyssh/easyssh.go create mode 100644 vendor/github.com/appleboy/easyssh-proxy/LICENSE create mode 100644 vendor/github.com/appleboy/easyssh-proxy/Makefile create mode 100644 vendor/github.com/appleboy/easyssh-proxy/README.md create mode 100644 vendor/github.com/appleboy/easyssh-proxy/easyssh.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/client.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/forward.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/keyring.go create mode 100644 vendor/golang.org/x/crypto/ssh/agent/server.go diff --git a/DOCS.md b/DOCS.md index aafb5a3..6df97b9 100644 --- a/DOCS.md +++ b/DOCS.md @@ -72,6 +72,22 @@ pipeline: - echo world ``` +Example configuration for command timeout (unit: second), default value is 60 seconds: + +```diff +pipeline: + ssh: + image: appleboy/drone-ssh + host: foo.com + username: root + password: 1234 + port: 22 ++ command_timeout: 10 + script: + - echo hello + - echo world +``` + Example configuration for success build: ```diff @@ -132,3 +148,6 @@ script timeout : Timeout is the maximum amount of time for the TCP connection to establish. + +command_timeout +: Command timeout is the maximum amount of time for the execute commands, default is 60 secs. diff --git a/easyssh/easyssh.go b/easyssh/easyssh.go deleted file mode 100644 index aba83e0..0000000 --- a/easyssh/easyssh.go +++ /dev/null @@ -1,146 +0,0 @@ -// Package easyssh provides a simple implementation of some SSH protocol -// features in Go. You can simply run a command on a remote server or get a file -// even simpler than native console SSH client. You don't need to think about -// Dials, sessions, defers, or public keys... Let easyssh think about it! -package easyssh - -import ( - "bufio" - "io" - "io/ioutil" - "net" - "time" - - "golang.org/x/crypto/ssh" -) - -// MakeConfig Contains main authority information. -// User field should be a name of user on remote server (ex. john in ssh john@example.com). -// Server field should be a remote machine address (ex. example.com in ssh john@example.com) -// Key is a path to private key on your local machine. -// Port is SSH server port on remote machine. -// Note: easyssh looking for private key in user's home directory (ex. /home/john + Key). -// Then ensure your Key begins from '/' (ex. /.ssh/id_rsa) -type MakeConfig struct { - User string - Server string - Key string - KeyPath string - Port string - Password string - Timeout time.Duration -} - -// returns ssh.Signer from user you running app home path + cutted key path. -// (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") ) -func getKeyFile(keypath string) (ssh.Signer, error) { - buf, err := ioutil.ReadFile(keypath) - if err != nil { - return nil, err - } - - pubkey, err := ssh.ParsePrivateKey(buf) - if err != nil { - return nil, err - } - - return pubkey, nil -} - -// connects to remote server using MakeConfig struct and returns *ssh.Session -func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) { - // auths holds the detected ssh auth methods - auths := []ssh.AuthMethod{} - - // figure out what auths are requested, what is supported - if ssh_conf.Password != "" { - auths = append(auths, ssh.Password(ssh_conf.Password)) - } - - if ssh_conf.KeyPath != "" { - if pubkey, err := getKeyFile(ssh_conf.KeyPath); err == nil { - auths = append(auths, ssh.PublicKeys(pubkey)) - } - } - - if ssh_conf.Key != "" { - signer, _ := ssh.ParsePrivateKey([]byte(ssh_conf.Key)) - auths = append(auths, ssh.PublicKeys(signer)) - } - - config := &ssh.ClientConfig{ - Timeout: ssh_conf.Timeout, - User: ssh_conf.User, - Auth: auths, - } - - client, err := ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), config) - if err != nil { - return nil, err - } - - session, err := client.NewSession() - if err != nil { - return nil, err - } - - return session, nil -} - -// Stream returns one channel that combines the stdout and stderr of the command -// as it is run on the remote machine, and another that sends true when the -// command is done. The sessions and channels will then be closed. -func (ssh_conf *MakeConfig) Stream(command string) (output chan string, done chan bool, err error) { - // connect to remote host - session, err := ssh_conf.connect() - if err != nil { - return output, done, err - } - // connect to both outputs (they are of type io.Reader) - outReader, err := session.StdoutPipe() - if err != nil { - return output, done, err - } - errReader, err := session.StderrPipe() - if err != nil { - return output, done, err - } - // combine outputs, create a line-by-line scanner - outputReader := io.MultiReader(outReader, errReader) - err = session.Start(command) - scanner := bufio.NewScanner(outputReader) - // continuously send the command's output over the channel - outputChan := make(chan string) - done = make(chan bool) - go func(scanner *bufio.Scanner, out chan string, done chan bool) { - defer close(outputChan) - defer close(done) - for scanner.Scan() { - outputChan <- scanner.Text() - } - // close all of our open resources - done <- true - session.Close() - }(scanner, outputChan, done) - return outputChan, done, err -} - -// Run command on remote machine and returns its stdout as a string -func (ssh_conf *MakeConfig) Run(command string) (outStr string, err error) { - outChan, doneChan, err := ssh_conf.Stream(command) - if err != nil { - return outStr, err - } - // read from the output channel until the done signal is passed - stillGoing := true - for stillGoing { - select { - case <-doneChan: - stillGoing = false - case line := <-outChan: - outStr += line + "\n" - } - } - // return the concatenation of all signals from the output channel - return outStr, err -} diff --git a/main.go b/main.go index fb2d134..3711e3b 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,12 @@ func main() { Usage: "connection timeout", EnvVar: "PLUGIN_TIMEOUT,SSH_TIMEOUT", }, + cli.IntFlag{ + Name: "command.timeout,T", + Usage: "command timeout", + EnvVar: "PLUGIN_COMMAND_TIMEOUT,SSH_COMMAND_TIMEOUT", + Value: 60, + }, cli.StringSliceFlag{ Name: "script,s", Usage: "execute commands", @@ -116,14 +122,15 @@ func run(c *cli.Context) error { plugin := Plugin{ Config: Config{ - Key: c.String("ssh-key"), - KeyPath: c.String("key-path"), - UserName: c.String("user"), - Password: c.String("password"), - Host: c.StringSlice("host"), - Port: c.Int("port"), - Timeout: c.Duration("timeout"), - Script: c.StringSlice("script"), + Key: c.String("ssh-key"), + KeyPath: c.String("key-path"), + UserName: c.String("user"), + Password: c.String("password"), + Host: c.StringSlice("host"), + Port: c.Int("port"), + Timeout: c.Duration("timeout"), + CommandTimeout: c.Int("command.timeout"), + Script: c.StringSlice("script"), }, } diff --git a/plugin.go b/plugin.go index 3dae5e9..2079b05 100644 --- a/plugin.go +++ b/plugin.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/appleboy/drone-ssh/easyssh" + "github.com/appleboy/easyssh-proxy" ) var wg sync.WaitGroup @@ -21,14 +21,15 @@ const ( type ( // Config for the plugin. Config struct { - Key string - KeyPath string - UserName string - Password string - Host []string - Port int - Timeout time.Duration - Script []string + Key string + KeyPath string + UserName string + Password string + Host []string + Port int + Timeout time.Duration + CommandTimeout int + Script []string } // Plugin structure @@ -68,8 +69,12 @@ func (p Plugin) Exec() error { } p.log(host, "commands: ", strings.Join(p.Config.Script, "\n")) - response, err := ssh.Run(strings.Join(p.Config.Script, "\n")) - p.log(host, "outputs:", response) + outStr, errStr, isTimeout, err := ssh.Run(strings.Join(p.Config.Script, "\n"), p.Config.CommandTimeout) + p.log(host, "outputs:", outStr) + + if !isTimeout || len(errStr) != 0 { + errChannel <- fmt.Errorf(errStr) + } if err != nil { errChannel <- err diff --git a/plugin_test.go b/plugin_test.go index a2d50ae..7083c49 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -100,3 +100,35 @@ func TestSSHScriptFromKeyFile(t *testing.T) { err := plugin.Exec() assert.Nil(t, err) } + +func TestSSHCommandTimeOut(t *testing.T) { + plugin := Plugin{ + Config: Config{ + Host: []string{"localhost"}, + UserName: "drone-scp", + Port: 22, + KeyPath: "./tests/.ssh/id_rsa", + Script: []string{"sleep 5"}, + CommandTimeout: 1, + }, + } + + err := plugin.Exec() + assert.NotNil(t, err) +} + +func TestSSHCommandNotFound(t *testing.T) { + plugin := Plugin{ + Config: Config{ + Host: []string{"localhost"}, + UserName: "drone-scp", + Port: 22, + KeyPath: "./tests/.ssh/id_rsa", + Script: []string{"whoami1234"}, + CommandTimeout: 1, + }, + } + + err := plugin.Exec() + assert.NotNil(t, err) +} diff --git a/vendor/github.com/appleboy/easyssh-proxy/LICENSE b/vendor/github.com/appleboy/easyssh-proxy/LICENSE new file mode 100644 index 0000000..b6df010 --- /dev/null +++ b/vendor/github.com/appleboy/easyssh-proxy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Bo-Yi Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/appleboy/easyssh-proxy/Makefile b/vendor/github.com/appleboy/easyssh-proxy/Makefile new file mode 100644 index 0000000..f29113d --- /dev/null +++ b/vendor/github.com/appleboy/easyssh-proxy/Makefile @@ -0,0 +1,60 @@ +.PHONY: test drone-ssh build fmt vet errcheck lint install update release-dirs release-build release-copy release-check release coverage + +PACKAGES ?= $(shell go list ./... | grep -v /vendor/) + +all: build + +fmt: + find . -name "*.go" -type f -not -path "./vendor/*" | xargs gofmt -s -w + +vet: + go vet $(PACKAGES) + +errcheck: + @hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/kisielk/errcheck; \ + fi + errcheck $(PACKAGES) + +lint: + @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/golang/lint/golint; \ + fi + for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; + +unconvert: + @hash unconvert > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/mdempsky/unconvert; \ + fi + for PKG in $(PACKAGES); do unconvert -v $$PKG || exit 1; done; + +test: + for PKG in $(PACKAGES); do go test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done; + +html: + go tool cover -html=coverage.txt + +coverage: + sed -i '/main.go/d' .cover/coverage.txt + curl -s https://codecov.io/bash > .codecov && \ + chmod +x .codecov && \ + ./.codecov -f .cover/coverage.txt + +clean: + go clean -x -i ./... + rm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor + +ssh-server: + adduser -h /home/drone-scp -s /bin/bash -D -S drone-scp + echo drone-scp:1234 | chpasswd + mkdir -p /home/drone-scp/.ssh + chmod 700 /home/drone-scp/.ssh + cp tests/.ssh/id_rsa.pub /home/drone-scp/.ssh/authorized_keys + chown -R drone-scp /home/drone-scp/.ssh + # install ssh and start server + apk add --update openssh openrc + rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key + ./tests/entrypoint.sh /usr/sbin/sshd -D & + +version: + @echo $(VERSION) diff --git a/vendor/github.com/appleboy/easyssh-proxy/README.md b/vendor/github.com/appleboy/easyssh-proxy/README.md new file mode 100644 index 0000000..134bd52 --- /dev/null +++ b/vendor/github.com/appleboy/easyssh-proxy/README.md @@ -0,0 +1,5 @@ +# easyssh-proxy + +[![GoDoc](https://godoc.org/github.com/appleboy/easyssh-proxy?status.svg)](https://godoc.org/github.com/appleboy/easyssh-proxy) [![Build Status](http://drone.wu-boy.com/api/badges/appleboy/easyssh-proxy/status.svg)](http://drone.wu-boy.com/appleboy/easyssh-proxy) [![codecov](https://codecov.io/gh/appleboy/easyssh-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/easyssh-proxy) [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/easyssh-proxy)](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy) + +easyssh-proxy provides a simple implementation of some SSH protocol features in Go diff --git a/vendor/github.com/appleboy/easyssh-proxy/easyssh.go b/vendor/github.com/appleboy/easyssh-proxy/easyssh.go new file mode 100644 index 0000000..27a074d --- /dev/null +++ b/vendor/github.com/appleboy/easyssh-proxy/easyssh.go @@ -0,0 +1,250 @@ +// Package easyssh provides a simple implementation of some SSH protocol +// features in Go. You can simply run a command on a remote server or get a file +// even simpler than native console SSH client. You don't need to think about +// Dials, sessions, defers, or public keys... Let easyssh think about it! +package easyssh + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "path/filepath" + "time" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +// MakeConfig Contains main authority information. +// User field should be a name of user on remote server (ex. john in ssh john@example.com). +// Server field should be a remote machine address (ex. example.com in ssh john@example.com) +// Key is a path to private key on your local machine. +// Port is SSH server port on remote machine. +// Note: easyssh looking for private key in user's home directory (ex. /home/john + Key). +// Then ensure your Key begins from '/' (ex. /.ssh/id_rsa) +type MakeConfig struct { + User string + Server string + Key string + KeyPath string + Port string + Password string + Timeout time.Duration +} + +type sshConfig struct { + User string + Key string + KeyPath string + Password string + Timeout time.Duration +} + +// returns ssh.Signer from user you running app home path + cutted key path. +// (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") ) +func getKeyFile(keypath string) (ssh.Signer, error) { + buf, err := ioutil.ReadFile(keypath) + if err != nil { + return nil, err + } + + pubkey, err := ssh.ParsePrivateKey(buf) + if err != nil { + return nil, err + } + + return pubkey, nil +} + +func getSSHConfig(config sshConfig) *ssh.ClientConfig { + // auths holds the detected ssh auth methods + auths := []ssh.AuthMethod{} + + // figure out what auths are requested, what is supported + if config.Password != "" { + auths = append(auths, ssh.Password(config.Password)) + } + + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)) + defer sshAgent.Close() + } + + if config.KeyPath != "" { + if pubkey, err := getKeyFile(config.KeyPath); err == nil { + auths = append(auths, ssh.PublicKeys(pubkey)) + } + } + + if config.Key != "" { + signer, _ := ssh.ParsePrivateKey([]byte(config.Key)) + auths = append(auths, ssh.PublicKeys(signer)) + } + + return &ssh.ClientConfig{ + Timeout: config.Timeout, + User: config.User, + Auth: auths, + } +} + +// connect to remote server using MakeConfig struct and returns *ssh.Session +func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) { + config := getSSHConfig(sshConfig{ + User: ssh_conf.User, + Key: ssh_conf.Key, + KeyPath: ssh_conf.KeyPath, + Password: ssh_conf.Password, + Timeout: ssh_conf.Timeout, + }) + + client, err := ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), config) + if err != nil { + return nil, err + } + + session, err := client.NewSession() + if err != nil { + return nil, err + } + + return session, nil +} + +// Stream returns one channel that combines the stdout and stderr of the command +// as it is run on the remote machine, and another that sends true when the +// command is done. The sessions and channels will then be closed. +func (ssh_conf *MakeConfig) Stream(command string, timeout int) (stdout chan string, stderr chan string, done chan bool, err error) { + // connect to remote host + session, err := ssh_conf.connect() + if err != nil { + return stdout, stderr, done, err + } + // connect to both outputs (they are of type io.Reader) + outReader, err := session.StdoutPipe() + if err != nil { + return stdout, stderr, done, err + } + errReader, err := session.StderrPipe() + if err != nil { + return stdout, stderr, done, err + } + // combine outputs, create a line-by-line scanner + stdoutReader := io.MultiReader(outReader) + stderrReader := io.MultiReader(errReader) + err = session.Start(command) + stdoutScanner := bufio.NewScanner(stdoutReader) + stderrScanner := bufio.NewScanner(stderrReader) + // continuously send the command's output over the channel + stdoutChan := make(chan string) + stderrChan := make(chan string) + done = make(chan bool) + + go func(stdoutScanner, stderrScanner *bufio.Scanner, stdoutChan, stderrChan chan string, done chan bool) { + defer close(stdoutChan) + defer close(stderrChan) + defer close(done) + + timeoutChan := time.After(time.Duration(timeout) * time.Second) + res := make(chan bool, 1) + + go func() { + for stdoutScanner.Scan() { + stdoutChan <- stdoutScanner.Text() + } + for stderrScanner.Scan() { + stderrChan <- stderrScanner.Text() + } + // close all of our open resources + res <- true + }() + + select { + case <-res: + stdoutChan <- "" + stderrChan <- "" + done <- true + case <-timeoutChan: + stdoutChan <- "" + stderrChan <- "Run Command Timeout!" + done <- false + } + + session.Close() + }(stdoutScanner, stderrScanner, stdoutChan, stderrChan, done) + return stdoutChan, stderrChan, done, err +} + +// Run command on remote machine and returns its stdout as a string +func (ssh_conf *MakeConfig) Run(command string, timeout int) (outStr string, errStr string, isTimeout bool, err error) { + stdoutChan, stderrChan, doneChan, err := ssh_conf.Stream(command, timeout) + if err != nil { + return outStr, errStr, isTimeout, err + } + // read from the output channel until the done signal is passed + stillGoing := true + for stillGoing { + select { + case isTimeout = <-doneChan: + stillGoing = false + case outline := <-stdoutChan: + if outline != "" { + outStr += outline + "\n" + } + case errline := <-stderrChan: + if errline != "" { + errStr += errline + "\n" + } + } + } + // return the concatenation of all signals from the output channel + return outStr, errStr, isTimeout, err +} + +// Scp uploads sourceFile to remote machine like native scp console app. +func (ssh_conf *MakeConfig) Scp(sourceFile string, etargetFile string) error { + session, err := ssh_conf.connect() + + if err != nil { + return err + } + defer session.Close() + + targetFile := filepath.Base(etargetFile) + + src, srcErr := os.Open(sourceFile) + + if srcErr != nil { + return srcErr + } + + srcStat, statErr := src.Stat() + + if statErr != nil { + return statErr + } + + go func() { + w, _ := session.StdinPipe() + + fmt.Fprintln(w, "C0644", srcStat.Size(), targetFile) + + if srcStat.Size() > 0 { + io.Copy(w, src) + fmt.Fprint(w, "\x00") + w.Close() + } else { + fmt.Fprint(w, "\x00") + w.Close() + } + }() + + if err := session.Run(fmt.Sprintf("scp -tr %s", etargetFile)); err != nil { + return err + } + + return nil +} diff --git a/vendor/golang.org/x/crypto/ssh/agent/client.go b/vendor/golang.org/x/crypto/ssh/agent/client.go new file mode 100644 index 0000000..ecfd7c5 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/agent/client.go @@ -0,0 +1,659 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package agent implements the ssh-agent protocol, and provides both +// a client and a server. The client can talk to a standard ssh-agent +// that uses UNIX sockets, and one could implement an alternative +// ssh-agent process using the sample server. +// +// References: +// [PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD +package agent // import "golang.org/x/crypto/ssh/agent" + +import ( + "bytes" + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "io" + "math/big" + "sync" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +// Agent represents the capabilities of an ssh-agent. +type Agent interface { + // List returns the identities known to the agent. + List() ([]*Key, error) + + // Sign has the agent sign the data using a protocol 2 key as defined + // in [PROTOCOL.agent] section 2.6.2. + Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) + + // Add adds a private key to the agent. + Add(key AddedKey) error + + // Remove removes all identities with the given public key. + Remove(key ssh.PublicKey) error + + // RemoveAll removes all identities. + RemoveAll() error + + // Lock locks the agent. Sign and Remove will fail, and List will empty an empty list. + Lock(passphrase []byte) error + + // Unlock undoes the effect of Lock + Unlock(passphrase []byte) error + + // Signers returns signers for all the known keys. + Signers() ([]ssh.Signer, error) +} + +// AddedKey describes an SSH key to be added to an Agent. +type AddedKey struct { + // PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or + // *ecdsa.PrivateKey, which will be inserted into the agent. + PrivateKey interface{} + // Certificate, if not nil, is communicated to the agent and will be + // stored with the key. + Certificate *ssh.Certificate + // Comment is an optional, free-form string. + Comment string + // LifetimeSecs, if not zero, is the number of seconds that the + // agent will store the key for. + LifetimeSecs uint32 + // ConfirmBeforeUse, if true, requests that the agent confirm with the + // user before each use of this key. + ConfirmBeforeUse bool +} + +// See [PROTOCOL.agent], section 3. +const ( + agentRequestV1Identities = 1 + agentRemoveAllV1Identities = 9 + + // 3.2 Requests from client to agent for protocol 2 key operations + agentAddIdentity = 17 + agentRemoveIdentity = 18 + agentRemoveAllIdentities = 19 + agentAddIdConstrained = 25 + + // 3.3 Key-type independent requests from client to agent + agentAddSmartcardKey = 20 + agentRemoveSmartcardKey = 21 + agentLock = 22 + agentUnlock = 23 + agentAddSmartcardKeyConstrained = 26 + + // 3.7 Key constraint identifiers + agentConstrainLifetime = 1 + agentConstrainConfirm = 2 +) + +// maxAgentResponseBytes is the maximum agent reply size that is accepted. This +// is a sanity check, not a limit in the spec. +const maxAgentResponseBytes = 16 << 20 + +// Agent messages: +// These structures mirror the wire format of the corresponding ssh agent +// messages found in [PROTOCOL.agent]. + +// 3.4 Generic replies from agent to client +const agentFailure = 5 + +type failureAgentMsg struct{} + +const agentSuccess = 6 + +type successAgentMsg struct{} + +// See [PROTOCOL.agent], section 2.5.2. +const agentRequestIdentities = 11 + +type requestIdentitiesAgentMsg struct{} + +// See [PROTOCOL.agent], section 2.5.2. +const agentIdentitiesAnswer = 12 + +type identitiesAnswerAgentMsg struct { + NumKeys uint32 `sshtype:"12"` + Keys []byte `ssh:"rest"` +} + +// See [PROTOCOL.agent], section 2.6.2. +const agentSignRequest = 13 + +type signRequestAgentMsg struct { + KeyBlob []byte `sshtype:"13"` + Data []byte + Flags uint32 +} + +// See [PROTOCOL.agent], section 2.6.2. + +// 3.6 Replies from agent to client for protocol 2 key operations +const agentSignResponse = 14 + +type signResponseAgentMsg struct { + SigBlob []byte `sshtype:"14"` +} + +type publicKey struct { + Format string + Rest []byte `ssh:"rest"` +} + +// Key represents a protocol 2 public key as defined in +// [PROTOCOL.agent], section 2.5.2. +type Key struct { + Format string + Blob []byte + Comment string +} + +func clientErr(err error) error { + return fmt.Errorf("agent: client error: %v", err) +} + +// String returns the storage form of an agent key with the format, base64 +// encoded serialized key, and the comment if it is not empty. +func (k *Key) String() string { + s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob) + + if k.Comment != "" { + s += " " + k.Comment + } + + return s +} + +// Type returns the public key type. +func (k *Key) Type() string { + return k.Format +} + +// Marshal returns key blob to satisfy the ssh.PublicKey interface. +func (k *Key) Marshal() []byte { + return k.Blob +} + +// Verify satisfies the ssh.PublicKey interface. +func (k *Key) Verify(data []byte, sig *ssh.Signature) error { + pubKey, err := ssh.ParsePublicKey(k.Blob) + if err != nil { + return fmt.Errorf("agent: bad public key: %v", err) + } + return pubKey.Verify(data, sig) +} + +type wireKey struct { + Format string + Rest []byte `ssh:"rest"` +} + +func parseKey(in []byte) (out *Key, rest []byte, err error) { + var record struct { + Blob []byte + Comment string + Rest []byte `ssh:"rest"` + } + + if err := ssh.Unmarshal(in, &record); err != nil { + return nil, nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(record.Blob, &wk); err != nil { + return nil, nil, err + } + + return &Key{ + Format: wk.Format, + Blob: record.Blob, + Comment: record.Comment, + }, record.Rest, nil +} + +// client is a client for an ssh-agent process. +type client struct { + // conn is typically a *net.UnixConn + conn io.ReadWriter + // mu is used to prevent concurrent access to the agent + mu sync.Mutex +} + +// NewClient returns an Agent that talks to an ssh-agent process over +// the given connection. +func NewClient(rw io.ReadWriter) Agent { + return &client{conn: rw} +} + +// call sends an RPC to the agent. On success, the reply is +// unmarshaled into reply and replyType is set to the first byte of +// the reply, which contains the type of the message. +func (c *client) call(req []byte) (reply interface{}, err error) { + c.mu.Lock() + defer c.mu.Unlock() + + msg := make([]byte, 4+len(req)) + binary.BigEndian.PutUint32(msg, uint32(len(req))) + copy(msg[4:], req) + if _, err = c.conn.Write(msg); err != nil { + return nil, clientErr(err) + } + + var respSizeBuf [4]byte + if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil { + return nil, clientErr(err) + } + respSize := binary.BigEndian.Uint32(respSizeBuf[:]) + if respSize > maxAgentResponseBytes { + return nil, clientErr(err) + } + + buf := make([]byte, respSize) + if _, err = io.ReadFull(c.conn, buf); err != nil { + return nil, clientErr(err) + } + reply, err = unmarshal(buf) + if err != nil { + return nil, clientErr(err) + } + return reply, err +} + +func (c *client) simpleCall(req []byte) error { + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +func (c *client) RemoveAll() error { + return c.simpleCall([]byte{agentRemoveAllIdentities}) +} + +func (c *client) Remove(key ssh.PublicKey) error { + req := ssh.Marshal(&agentRemoveIdentityMsg{ + KeyBlob: key.Marshal(), + }) + return c.simpleCall(req) +} + +func (c *client) Lock(passphrase []byte) error { + req := ssh.Marshal(&agentLockMsg{ + Passphrase: passphrase, + }) + return c.simpleCall(req) +} + +func (c *client) Unlock(passphrase []byte) error { + req := ssh.Marshal(&agentUnlockMsg{ + Passphrase: passphrase, + }) + return c.simpleCall(req) +} + +// List returns the identities known to the agent. +func (c *client) List() ([]*Key, error) { + // see [PROTOCOL.agent] section 2.5.2. + req := []byte{agentRequestIdentities} + + msg, err := c.call(req) + if err != nil { + return nil, err + } + + switch msg := msg.(type) { + case *identitiesAnswerAgentMsg: + if msg.NumKeys > maxAgentResponseBytes/8 { + return nil, errors.New("agent: too many keys in agent reply") + } + keys := make([]*Key, msg.NumKeys) + data := msg.Keys + for i := uint32(0); i < msg.NumKeys; i++ { + var key *Key + var err error + if key, data, err = parseKey(data); err != nil { + return nil, err + } + keys[i] = key + } + return keys, nil + case *failureAgentMsg: + return nil, errors.New("agent: failed to list keys") + } + panic("unreachable") +} + +// Sign has the agent sign the data using a protocol 2 key as defined +// in [PROTOCOL.agent] section 2.6.2. +func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + req := ssh.Marshal(signRequestAgentMsg{ + KeyBlob: key.Marshal(), + Data: data, + }) + + msg, err := c.call(req) + if err != nil { + return nil, err + } + + switch msg := msg.(type) { + case *signResponseAgentMsg: + var sig ssh.Signature + if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil { + return nil, err + } + + return &sig, nil + case *failureAgentMsg: + return nil, errors.New("agent: failed to sign challenge") + } + panic("unreachable") +} + +// unmarshal parses an agent message in packet, returning the parsed +// form and the message type of packet. +func unmarshal(packet []byte) (interface{}, error) { + if len(packet) < 1 { + return nil, errors.New("agent: empty packet") + } + var msg interface{} + switch packet[0] { + case agentFailure: + return new(failureAgentMsg), nil + case agentSuccess: + return new(successAgentMsg), nil + case agentIdentitiesAnswer: + msg = new(identitiesAnswerAgentMsg) + case agentSignResponse: + msg = new(signResponseAgentMsg) + case agentV1IdentitiesAnswer: + msg = new(agentV1IdentityMsg) + default: + return nil, fmt.Errorf("agent: unknown type tag %d", packet[0]) + } + if err := ssh.Unmarshal(packet, msg); err != nil { + return nil, err + } + return msg, nil +} + +type rsaKeyMsg struct { + Type string `sshtype:"17|25"` + N *big.Int + E *big.Int + D *big.Int + Iqmp *big.Int // IQMP = Inverse Q Mod P + P *big.Int + Q *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type dsaKeyMsg struct { + Type string `sshtype:"17|25"` + P *big.Int + Q *big.Int + G *big.Int + Y *big.Int + X *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ecdsaKeyMsg struct { + Type string `sshtype:"17|25"` + Curve string + KeyBytes []byte + D *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ed25519KeyMsg struct { + Type string `sshtype:"17|25"` + Pub []byte + Priv []byte + Comments string + Constraints []byte `ssh:"rest"` +} + +// Insert adds a private key to the agent. +func (c *client) insertKey(s interface{}, comment string, constraints []byte) error { + var req []byte + switch k := s.(type) { + case *rsa.PrivateKey: + if len(k.Primes) != 2 { + return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes)) + } + k.Precompute() + req = ssh.Marshal(rsaKeyMsg{ + Type: ssh.KeyAlgoRSA, + N: k.N, + E: big.NewInt(int64(k.E)), + D: k.D, + Iqmp: k.Precomputed.Qinv, + P: k.Primes[0], + Q: k.Primes[1], + Comments: comment, + Constraints: constraints, + }) + case *dsa.PrivateKey: + req = ssh.Marshal(dsaKeyMsg{ + Type: ssh.KeyAlgoDSA, + P: k.P, + Q: k.Q, + G: k.G, + Y: k.Y, + X: k.X, + Comments: comment, + Constraints: constraints, + }) + case *ecdsa.PrivateKey: + nistID := fmt.Sprintf("nistp%d", k.Params().BitSize) + req = ssh.Marshal(ecdsaKeyMsg{ + Type: "ecdsa-sha2-" + nistID, + Curve: nistID, + KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y), + D: k.D, + Comments: comment, + Constraints: constraints, + }) + case *ed25519.PrivateKey: + req = ssh.Marshal(ed25519KeyMsg{ + Type: ssh.KeyAlgoED25519, + Pub: []byte(*k)[32:], + Priv: []byte(*k), + Comments: comment, + Constraints: constraints, + }) + default: + return fmt.Errorf("agent: unsupported key type %T", s) + } + + // if constraints are present then the message type needs to be changed. + if len(constraints) != 0 { + req[0] = agentAddIdConstrained + } + + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +type rsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + D *big.Int + Iqmp *big.Int // IQMP = Inverse Q Mod P + P *big.Int + Q *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type dsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + X *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ecdsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + D *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ed25519CertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + Pub []byte + Priv []byte + Comments string + Constraints []byte `ssh:"rest"` +} + +// Add adds a private key to the agent. If a certificate is given, +// that certificate is added instead as public key. +func (c *client) Add(key AddedKey) error { + var constraints []byte + + if secs := key.LifetimeSecs; secs != 0 { + constraints = append(constraints, agentConstrainLifetime) + + var secsBytes [4]byte + binary.BigEndian.PutUint32(secsBytes[:], secs) + constraints = append(constraints, secsBytes[:]...) + } + + if key.ConfirmBeforeUse { + constraints = append(constraints, agentConstrainConfirm) + } + + if cert := key.Certificate; cert == nil { + return c.insertKey(key.PrivateKey, key.Comment, constraints) + } else { + return c.insertCert(key.PrivateKey, cert, key.Comment, constraints) + } +} + +func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error { + var req []byte + switch k := s.(type) { + case *rsa.PrivateKey: + if len(k.Primes) != 2 { + return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes)) + } + k.Precompute() + req = ssh.Marshal(rsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + D: k.D, + Iqmp: k.Precomputed.Qinv, + P: k.Primes[0], + Q: k.Primes[1], + Comments: comment, + Constraints: constraints, + }) + case *dsa.PrivateKey: + req = ssh.Marshal(dsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + X: k.X, + Comments: comment, + Constraints: constraints, + }) + case *ecdsa.PrivateKey: + req = ssh.Marshal(ecdsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + D: k.D, + Comments: comment, + Constraints: constraints, + }) + case *ed25519.PrivateKey: + req = ssh.Marshal(ed25519CertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + Pub: []byte(*k)[32:], + Priv: []byte(*k), + Comments: comment, + Constraints: constraints, + }) + default: + return fmt.Errorf("agent: unsupported key type %T", s) + } + + // if constraints are present then the message type needs to be changed. + if len(constraints) != 0 { + req[0] = agentAddIdConstrained + } + + signer, err := ssh.NewSignerFromKey(s) + if err != nil { + return err + } + if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 { + return errors.New("agent: signer and cert have different public key") + } + + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +// Signers provides a callback for client authentication. +func (c *client) Signers() ([]ssh.Signer, error) { + keys, err := c.List() + if err != nil { + return nil, err + } + + var result []ssh.Signer + for _, k := range keys { + result = append(result, &agentKeyringSigner{c, k}) + } + return result, nil +} + +type agentKeyringSigner struct { + agent *client + pub ssh.PublicKey +} + +func (s *agentKeyringSigner) PublicKey() ssh.PublicKey { + return s.pub +} + +func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { + // The agent has its own entropy source, so the rand argument is ignored. + return s.agent.Sign(s.pub, data) +} diff --git a/vendor/golang.org/x/crypto/ssh/agent/forward.go b/vendor/golang.org/x/crypto/ssh/agent/forward.go new file mode 100644 index 0000000..fd24ba9 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/agent/forward.go @@ -0,0 +1,103 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "errors" + "io" + "net" + "sync" + + "golang.org/x/crypto/ssh" +) + +// RequestAgentForwarding sets up agent forwarding for the session. +// ForwardToAgent or ForwardToRemote should be called to route +// the authentication requests. +func RequestAgentForwarding(session *ssh.Session) error { + ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil) + if err != nil { + return err + } + if !ok { + return errors.New("forwarding request denied") + } + return nil +} + +// ForwardToAgent routes authentication requests to the given keyring. +func ForwardToAgent(client *ssh.Client, keyring Agent) error { + channels := client.HandleChannelOpen(channelType) + if channels == nil { + return errors.New("agent: already have handler for " + channelType) + } + + go func() { + for ch := range channels { + channel, reqs, err := ch.Accept() + if err != nil { + continue + } + go ssh.DiscardRequests(reqs) + go func() { + ServeAgent(keyring, channel) + channel.Close() + }() + } + }() + return nil +} + +const channelType = "auth-agent@openssh.com" + +// ForwardToRemote routes authentication requests to the ssh-agent +// process serving on the given unix socket. +func ForwardToRemote(client *ssh.Client, addr string) error { + channels := client.HandleChannelOpen(channelType) + if channels == nil { + return errors.New("agent: already have handler for " + channelType) + } + conn, err := net.Dial("unix", addr) + if err != nil { + return err + } + conn.Close() + + go func() { + for ch := range channels { + channel, reqs, err := ch.Accept() + if err != nil { + continue + } + go ssh.DiscardRequests(reqs) + go forwardUnixSocket(channel, addr) + } + }() + return nil +} + +func forwardUnixSocket(channel ssh.Channel, addr string) { + conn, err := net.Dial("unix", addr) + if err != nil { + return + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { + io.Copy(conn, channel) + conn.(*net.UnixConn).CloseWrite() + wg.Done() + }() + go func() { + io.Copy(channel, conn) + channel.CloseWrite() + wg.Done() + }() + + wg.Wait() + conn.Close() + channel.Close() +} diff --git a/vendor/golang.org/x/crypto/ssh/agent/keyring.go b/vendor/golang.org/x/crypto/ssh/agent/keyring.go new file mode 100644 index 0000000..a6ba06a --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/agent/keyring.go @@ -0,0 +1,215 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "bytes" + "crypto/rand" + "crypto/subtle" + "errors" + "fmt" + "sync" + "time" + + "golang.org/x/crypto/ssh" +) + +type privKey struct { + signer ssh.Signer + comment string + expire *time.Time +} + +type keyring struct { + mu sync.Mutex + keys []privKey + + locked bool + passphrase []byte +} + +var errLocked = errors.New("agent: locked") + +// NewKeyring returns an Agent that holds keys in memory. It is safe +// for concurrent use by multiple goroutines. +func NewKeyring() Agent { + return &keyring{} +} + +// RemoveAll removes all identities. +func (r *keyring) RemoveAll() error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + r.keys = nil + return nil +} + +// removeLocked does the actual key removal. The caller must already be holding the +// keyring mutex. +func (r *keyring) removeLocked(want []byte) error { + found := false + for i := 0; i < len(r.keys); { + if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) { + found = true + r.keys[i] = r.keys[len(r.keys)-1] + r.keys = r.keys[:len(r.keys)-1] + continue + } else { + i++ + } + } + + if !found { + return errors.New("agent: key not found") + } + return nil +} + +// Remove removes all identities with the given public key. +func (r *keyring) Remove(key ssh.PublicKey) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + return r.removeLocked(key.Marshal()) +} + +// Lock locks the agent. Sign and Remove will fail, and List will return an empty list. +func (r *keyring) Lock(passphrase []byte) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + r.locked = true + r.passphrase = passphrase + return nil +} + +// Unlock undoes the effect of Lock +func (r *keyring) Unlock(passphrase []byte) error { + r.mu.Lock() + defer r.mu.Unlock() + if !r.locked { + return errors.New("agent: not locked") + } + if len(passphrase) != len(r.passphrase) || 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) { + return fmt.Errorf("agent: incorrect passphrase") + } + + r.locked = false + r.passphrase = nil + return nil +} + +// expireKeysLocked removes expired keys from the keyring. If a key was added +// with a lifetimesecs contraint and seconds >= lifetimesecs seconds have +// ellapsed, it is removed. The caller *must* be holding the keyring mutex. +func (r *keyring) expireKeysLocked() { + for _, k := range r.keys { + if k.expire != nil && time.Now().After(*k.expire) { + r.removeLocked(k.signer.PublicKey().Marshal()) + } + } +} + +// List returns the identities known to the agent. +func (r *keyring) List() ([]*Key, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + // section 2.7: locked agents return empty. + return nil, nil + } + + r.expireKeysLocked() + var ids []*Key + for _, k := range r.keys { + pub := k.signer.PublicKey() + ids = append(ids, &Key{ + Format: pub.Type(), + Blob: pub.Marshal(), + Comment: k.comment}) + } + return ids, nil +} + +// Insert adds a private key to the keyring. If a certificate +// is given, that certificate is added as public key. Note that +// any constraints given are ignored. +func (r *keyring) Add(key AddedKey) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + signer, err := ssh.NewSignerFromKey(key.PrivateKey) + + if err != nil { + return err + } + + if cert := key.Certificate; cert != nil { + signer, err = ssh.NewCertSigner(cert, signer) + if err != nil { + return err + } + } + + p := privKey{ + signer: signer, + comment: key.Comment, + } + + if key.LifetimeSecs > 0 { + t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second) + p.expire = &t + } + + r.keys = append(r.keys, p) + + return nil +} + +// Sign returns a signature for the data. +func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return nil, errLocked + } + + r.expireKeysLocked() + wanted := key.Marshal() + for _, k := range r.keys { + if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) { + return k.signer.Sign(rand.Reader, data) + } + } + return nil, errors.New("not found") +} + +// Signers returns signers for all the known keys. +func (r *keyring) Signers() ([]ssh.Signer, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return nil, errLocked + } + + r.expireKeysLocked() + s := make([]ssh.Signer, 0, len(r.keys)) + for _, k := range r.keys { + s = append(s, k.signer) + } + return s, nil +} diff --git a/vendor/golang.org/x/crypto/ssh/agent/server.go b/vendor/golang.org/x/crypto/ssh/agent/server.go new file mode 100644 index 0000000..68a333f --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/agent/server.go @@ -0,0 +1,451 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "math/big" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +// Server wraps an Agent and uses it to implement the agent side of +// the SSH-agent, wire protocol. +type server struct { + agent Agent +} + +func (s *server) processRequestBytes(reqData []byte) []byte { + rep, err := s.processRequest(reqData) + if err != nil { + if err != errLocked { + // TODO(hanwen): provide better logging interface? + log.Printf("agent %d: %v", reqData[0], err) + } + return []byte{agentFailure} + } + + if err == nil && rep == nil { + return []byte{agentSuccess} + } + + return ssh.Marshal(rep) +} + +func marshalKey(k *Key) []byte { + var record struct { + Blob []byte + Comment string + } + record.Blob = k.Marshal() + record.Comment = k.Comment + + return ssh.Marshal(&record) +} + +// See [PROTOCOL.agent], section 2.5.1. +const agentV1IdentitiesAnswer = 2 + +type agentV1IdentityMsg struct { + Numkeys uint32 `sshtype:"2"` +} + +type agentRemoveIdentityMsg struct { + KeyBlob []byte `sshtype:"18"` +} + +type agentLockMsg struct { + Passphrase []byte `sshtype:"22"` +} + +type agentUnlockMsg struct { + Passphrase []byte `sshtype:"23"` +} + +func (s *server) processRequest(data []byte) (interface{}, error) { + switch data[0] { + case agentRequestV1Identities: + return &agentV1IdentityMsg{0}, nil + + case agentRemoveAllV1Identities: + return nil, nil + + case agentRemoveIdentity: + var req agentRemoveIdentityMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil { + return nil, err + } + + return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob}) + + case agentRemoveAllIdentities: + return nil, s.agent.RemoveAll() + + case agentLock: + var req agentLockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + return nil, s.agent.Lock(req.Passphrase) + + case agentUnlock: + var req agentLockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + return nil, s.agent.Unlock(req.Passphrase) + + case agentSignRequest: + var req signRequestAgentMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil { + return nil, err + } + + k := &Key{ + Format: wk.Format, + Blob: req.KeyBlob, + } + + sig, err := s.agent.Sign(k, req.Data) // TODO(hanwen): flags. + if err != nil { + return nil, err + } + return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil + + case agentRequestIdentities: + keys, err := s.agent.List() + if err != nil { + return nil, err + } + + rep := identitiesAnswerAgentMsg{ + NumKeys: uint32(len(keys)), + } + for _, k := range keys { + rep.Keys = append(rep.Keys, marshalKey(k)...) + } + return rep, nil + + case agentAddIdConstrained, agentAddIdentity: + return nil, s.insertIdentity(data) + } + + return nil, fmt.Errorf("unknown opcode %d", data[0]) +} + +func parseRSAKey(req []byte) (*AddedKey, error) { + var k rsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + if k.E.BitLen() > 30 { + return nil, errors.New("agent: RSA public exponent too large") + } + priv := &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + E: int(k.E.Int64()), + N: k.N, + }, + D: k.D, + Primes: []*big.Int{k.P, k.Q}, + } + priv.Precompute() + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func parseEd25519Key(req []byte) (*AddedKey, error) { + var k ed25519KeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + priv := ed25519.PrivateKey(k.Priv) + return &AddedKey{PrivateKey: &priv, Comment: k.Comments}, nil +} + +func parseDSAKey(req []byte) (*AddedKey, error) { + var k dsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + priv := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: k.P, + Q: k.Q, + G: k.G, + }, + Y: k.Y, + }, + X: k.X, + } + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func unmarshalECDSA(curveName string, keyBytes []byte, privScalar *big.Int) (priv *ecdsa.PrivateKey, err error) { + priv = &ecdsa.PrivateKey{ + D: privScalar, + } + + switch curveName { + case "nistp256": + priv.Curve = elliptic.P256() + case "nistp384": + priv.Curve = elliptic.P384() + case "nistp521": + priv.Curve = elliptic.P521() + default: + return nil, fmt.Errorf("agent: unknown curve %q", curveName) + } + + priv.X, priv.Y = elliptic.Unmarshal(priv.Curve, keyBytes) + if priv.X == nil || priv.Y == nil { + return nil, errors.New("agent: point not on curve") + } + + return priv, nil +} + +func parseEd25519Cert(req []byte) (*AddedKey, error) { + var k ed25519CertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + priv := ed25519.PrivateKey(k.Priv) + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad ED25519 certificate") + } + return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseECDSAKey(req []byte) (*AddedKey, error) { + var k ecdsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + priv, err := unmarshalECDSA(k.Curve, k.KeyBytes, k.D) + if err != nil { + return nil, err + } + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func parseRSACert(req []byte) (*AddedKey, error) { + var k rsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad RSA certificate") + } + + // An RSA publickey as marshaled by rsaPublicKey.Marshal() in keys.go + var rsaPub struct { + Name string + E *big.Int + N *big.Int + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &rsaPub); err != nil { + return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err) + } + + if rsaPub.E.BitLen() > 30 { + return nil, errors.New("agent: RSA public exponent too large") + } + + priv := rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + E: int(rsaPub.E.Int64()), + N: rsaPub.N, + }, + D: k.D, + Primes: []*big.Int{k.Q, k.P}, + } + priv.Precompute() + + return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseDSACert(req []byte) (*AddedKey, error) { + var k dsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad DSA certificate") + } + + // A DSA publickey as marshaled by dsaPublicKey.Marshal() in keys.go + var w struct { + Name string + P, Q, G, Y *big.Int + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &w); err != nil { + return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err) + } + + priv := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: w.P, + Q: w.Q, + G: w.G, + }, + Y: w.Y, + }, + X: k.X, + } + + return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseECDSACert(req []byte) (*AddedKey, error) { + var k ecdsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad ECDSA certificate") + } + + // An ECDSA publickey as marshaled by ecdsaPublicKey.Marshal() in keys.go + var ecdsaPub struct { + Name string + ID string + Key []byte + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &ecdsaPub); err != nil { + return nil, err + } + + priv, err := unmarshalECDSA(ecdsaPub.ID, ecdsaPub.Key, k.D) + if err != nil { + return nil, err + } + + return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil +} + +func (s *server) insertIdentity(req []byte) error { + var record struct { + Type string `sshtype:"17|25"` + Rest []byte `ssh:"rest"` + } + + if err := ssh.Unmarshal(req, &record); err != nil { + return err + } + + var addedKey *AddedKey + var err error + + switch record.Type { + case ssh.KeyAlgoRSA: + addedKey, err = parseRSAKey(req) + case ssh.KeyAlgoDSA: + addedKey, err = parseDSAKey(req) + case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521: + addedKey, err = parseECDSAKey(req) + case ssh.KeyAlgoED25519: + addedKey, err = parseEd25519Key(req) + case ssh.CertAlgoRSAv01: + addedKey, err = parseRSACert(req) + case ssh.CertAlgoDSAv01: + addedKey, err = parseDSACert(req) + case ssh.CertAlgoECDSA256v01, ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01: + addedKey, err = parseECDSACert(req) + case ssh.CertAlgoED25519v01: + addedKey, err = parseEd25519Cert(req) + default: + return fmt.Errorf("agent: not implemented: %q", record.Type) + } + + if err != nil { + return err + } + return s.agent.Add(*addedKey) +} + +// ServeAgent serves the agent protocol on the given connection. It +// returns when an I/O error occurs. +func ServeAgent(agent Agent, c io.ReadWriter) error { + s := &server{agent} + + var length [4]byte + for { + if _, err := io.ReadFull(c, length[:]); err != nil { + return err + } + l := binary.BigEndian.Uint32(length[:]) + if l > maxAgentResponseBytes { + // We also cap requests. + return fmt.Errorf("agent: request too large: %d", l) + } + + req := make([]byte, l) + if _, err := io.ReadFull(c, req); err != nil { + return err + } + + repData := s.processRequestBytes(req) + if len(repData) > maxAgentResponseBytes { + return fmt.Errorf("agent: reply too large: %d bytes", len(repData)) + } + + binary.BigEndian.PutUint32(length[:], uint32(len(repData))) + if _, err := c.Write(length[:]); err != nil { + return err + } + if _, err := c.Write(repData); err != nil { + return err + } + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 20c261f..e3abe61 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -2,6 +2,14 @@ "comment": "", "ignore": "test", "package": [ + { + "checksumSHA1": "NCdmzR+clcl2/1jUPn0vFeqjwjk=", + "path": "github.com/appleboy/easyssh-proxy", + "revision": "89c61a4555c1578454f75ae406f4e3cdded275d2", + "revisionTime": "2017-03-04T06:27:13Z", + "version": "=1.0.0", + "versionExact": "1.0.0" + }, { "checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=", "path": "github.com/davecgh/go-spew/spew", @@ -59,8 +67,14 @@ { "checksumSHA1": "fsrFs762jlaILyqqQImS1GfvIvw=", "path": "golang.org/x/crypto/ssh", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "40541ccb1c6e64c947ed6f606b8a6cb4b67d7436", + "revisionTime": "2017-02-12T21:20:41Z" + }, + { + "checksumSHA1": "SJ3Ma3Ozavxpbh1usZWBCnzMKIc=", + "path": "golang.org/x/crypto/ssh/agent", + "revision": "40541ccb1c6e64c947ed6f606b8a6cb4b67d7436", + "revisionTime": "2017-02-12T21:20:41Z" } ], "rootPath": "github.com/appleboy/drone-ssh"