From a45f80f6138e673c512e9dfd4b48decbf10c128f Mon Sep 17 00:00:00 2001 From: Andrew Buss Date: Mon, 30 Nov 2015 06:03:08 -0800 Subject: [PATCH] Working prototype using RO as a remote Signer for SSH authentication --- client/client.go | 16 +++++ cmd/ro/main.go | 102 ++++++++++++++++++++++++++++-- cmd/ro/roagent/roagent.go | 128 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 cmd/ro/roagent/roagent.go diff --git a/client/client.go b/client/client.go index 43e2f26..47b1c13 100644 --- a/client/client.go +++ b/client/client.go @@ -254,6 +254,22 @@ func (c *RemoteServer) Decrypt(req core.DecryptRequest) (*core.ResponseData, err } +// Decrypt issues an decrypt request to the remote server +func (c *RemoteServer) DecryptSign(req core.DecryptSignRequest) (*core.ResponseData, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.doAction("decrypt-sign", reqBytes) + if err != nil { + return nil, err + } + + return unmarshalResponseData(respBytes) + +} + // DecryptIntoData issues an decrypt request to the remote server and extract // the decrypted data from the response func (c *RemoteServer) DecryptIntoData(req core.DecryptRequest) ([]byte, error) { diff --git a/cmd/ro/main.go b/cmd/ro/main.go index e78345c..3a6390d 100644 --- a/cmd/ro/main.go +++ b/cmd/ro/main.go @@ -6,25 +6,32 @@ import ( "encoding/json" "flag" "fmt" + "io" "io/ioutil" "log" + "net" "os" + "os/exec" "os/signal" + "path" "strings" "syscall" "time" "github.com/cloudflare/redoctober/client" "github.com/cloudflare/redoctober/cmd/ro/gopass" + "github.com/cloudflare/redoctober/cmd/ro/roagent" "github.com/cloudflare/redoctober/core" "github.com/cloudflare/redoctober/cryptor" "github.com/cloudflare/redoctober/msp" "github.com/cloudflare/redoctober/order" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) -var action, user, pswd, userEnv, pswdEnv, server, caPath string +var action, user, pswd, userEnv, pswdEnv, server, caPath, pubKeyPath string -var owners, lefters, righters, inPath, labels, outPath, outEnv string +var owners, lefters, righters, inPath, labels, usages, outPath, outEnv string var uses, minUsers int @@ -46,6 +53,8 @@ var commandSet = map[string]command{ "delegate": command{Run: runDelegate, Desc: "do decryption delegation"}, "encrypt": command{Run: runEncrypt, Desc: "encrypt a file"}, "decrypt": command{Run: runDecrypt, Desc: "decrypt a file"}, + "ssh": command{Run: runSSH, Desc: "a wrapper for SSH using an RO-encrypted private key"}, + "ssh-agent": command{Run: runSSHAgent, Desc: "act as an SSH agent"}, "re-encrypt": command{Run: runReEncrypt, Desc: "re-encrypt a file"}, "order": command{Run: runOrder, Desc: "place an order for delegations"}, "owners": command{Run: runOwner, Desc: "show owners list"}, @@ -65,6 +74,7 @@ func registerFlags() { flag.StringVar(&lefters, "left", "", "comma separated left owners") flag.StringVar(&righters, "right", "", "comma separated right owners") flag.StringVar(&labels, "labels", "", "comma separated labels") + flag.StringVar(&usages, "usages", "", "comma separated usages") flag.StringVar(&inPath, "in", "", "input data file") flag.StringVar(&outPath, "out", "", "output data file") flag.StringVar(&outEnv, "outenv", "", "env variable for output data") @@ -75,6 +85,7 @@ func registerFlags() { flag.StringVar(&userEnv, "userenv", "RO_USER", "env variable for user name") flag.StringVar(&pswdEnv, "pswdenv", "RO_PASS", "env variable for user password") flag.DurationVar(&pollInterval, "poll-interval", time.Second, "interval for polling an outstanding order (set 0 to disable polling)") + flag.StringVar(&pubKeyPath, "pubkey", "id_rsa.pub", "path to SSH public key") } func readLine(prompt string) (line string, err error) { @@ -201,6 +212,7 @@ func runEncrypt() { Password: pswd, Minimum: minUsers, Owners: processCSL(owners), + Usages: processCSL(usages), LeftOwners: processCSL(lefters), RightOwners: processCSL(righters), Labels: processCSL(labels), @@ -386,6 +398,86 @@ func runResetPersisted() { fmt.Println(resp) } +func runSSHAgent() { + inBytes, err := ioutil.ReadFile(inPath) + processError(err) + + // base64 decode the input + encBytes, err := base64.StdEncoding.DecodeString(string(inBytes)) + if err != nil { + log.Println("failed to base64 decode the data, proceeding with raw data") + encBytes = inBytes + } + + inBytes, err = ioutil.ReadFile(pubKeyPath) + processError(err) + + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(inBytes) + + if err != nil { + log.Fatal("failed to parse SSH public key", err) + } + + roagent := roagent.NewROAgent(roServer, pubKey, encBytes, user, pswd) + + authSockPath := os.Getenv("SSH_AUTH_SOCK") + + if authSockPath == "" { + log.Fatal("SSH_AUTH_SOCK not set") + } + + listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: authSockPath, Net: "unix"}) + if err != nil { + log.Fatal("error listening on $SSH_AUTH_SOCK", err) + } + defer os.Remove(authSockPath) + + conn, err := listener.AcceptUnix() + if err != nil { + log.Fatal("error accepting socket connection", err) + } + + err = agent.ServeAgent(roagent, conn) + if err != nil && err != io.EOF { + log.Fatal("error serving socket protocol", err) + } +} + +func runSSH() { + // First pick a path for our socket + // TempDir will ensure that the directory is created with the correct permissions + dir, err := ioutil.TempDir("", "ro_ssh_") + if err != nil { + log.Fatal("error getting temporary directory for SSH auth socket ", err) + } + defer os.RemoveAll(dir) + + os.Setenv("SSH_AUTH_SOCK", path.Join(dir, "roagent.sock")) + go runSSHAgent() + + var sshPath string + sshPath, err = exec.LookPath("ssh") + if err != nil { + log.Fatal("error finding path to ssh binary ", err) + } + + var p *os.Process + p, err = os.StartProcess(sshPath, flag.Args(), + &os.ProcAttr{ + Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, + }, + ) + if err != nil { + log.Fatal("error starting ssh ", err) + } + + _, err = p.Wait() + if err != nil { + log.Fatal("error waiting on ssh ", err) + } +} + + func main() { flag.Usage = func() { fmt.Println("Usage: ro [options] subcommand") @@ -401,13 +493,13 @@ func main() { registerFlags() flag.Parse() - if flag.NArg() != 1 { + action := flag.Arg(0) + + if flag.NArg() != 1 && action != "ssh" { flag.Usage() os.Exit(1) } - action := flag.Arg(0) - cmd, found := commandSet[action] if !found { fmt.Println("Unsupported subcommand:", action) diff --git a/cmd/ro/roagent/roagent.go b/cmd/ro/roagent/roagent.go new file mode 100644 index 0000000..50ab1ef --- /dev/null +++ b/cmd/ro/roagent/roagent.go @@ -0,0 +1,128 @@ +package roagent + +import ( + "bytes" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "io" + "log" + + "github.com/cloudflare/redoctober/client" + "github.com/cloudflare/redoctober/core" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +type ROSigner struct { + server *client.RemoteServer + pub ssh.PublicKey + encryptedKey []byte + user string + pswd string +} + +func (signer ROSigner) PublicKey() ssh.PublicKey { + return signer.pub +} + +func (signer ROSigner) Sign(rand io.Reader, msg []byte) (signature *ssh.Signature, err error) { + req := core.DecryptSignRequest{ + Name: signer.user, + Password: signer.pswd, + Data: signer.encryptedKey, + TBSData: msg, + } + + resp, err := signer.server.DecryptSign(req) + if err != nil { + return nil, err + } + if resp.Status != "ok" { + log.Fatal("response status error:", resp.Status) + return nil, errors.New("response status error") + } + fmt.Println("Response Status:", resp.Status) + + var respMsg core.DecryptSignWithDelegates + err = json.Unmarshal(resp.Response, &respMsg) + if err != nil { + return nil, err + } + + var respSignature ssh.Signature + err = json.Unmarshal(resp.Response, &respSignature) + if err != nil { + return nil, err + } + + return &respSignature, nil +} + +type ROAgent struct { + signer ROSigner +} + +func NewROAgent(server *client.RemoteServer, pubKey ssh.PublicKey, encryptedPrivKey []byte, user, pswd string) agent.Agent { + return &ROAgent{ + ROSigner{ + server, + pubKey, + encryptedPrivKey, + user, + pswd, + }, + } +} + +func (r *ROAgent) RemoveAll() error { + return nil +} + +// Remove removes all identities with the given public key. +func (r *ROAgent) Remove(key ssh.PublicKey) error { + return nil +} + +// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list. +func (r *ROAgent) Lock(passphrase []byte) error { + return nil +} + +// Unlock undoes the effect of Lock +func (r *ROAgent) Unlock(passphrase []byte) error { + return nil +} + +// List returns the identities known to the agent. +func (r *ROAgent) List() ([]*agent.Key, error) { + return []*agent.Key{ + { + Format: r.signer.PublicKey().Type(), + Blob: r.signer.PublicKey().Marshal(), + Comment: "", + }, + }, nil +} + +// Insert adds a private key to the ROAgent. If a certificate +// is given, that certificate is added as public key. Note that +// any constraints given are ignored. +func (r *ROAgent) Add(key agent.AddedKey) error { + return nil +} + +// Sign returns a signature for the data. +func (r *ROAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + wanted := key.Marshal() + if bytes.Equal(r.signer.PublicKey().Marshal(), wanted) { + return r.signer.Sign(rand.Reader, data) + } + return nil, errors.New("wrong key requested") +} + +// Signers returns signers for all the known keys. +func (r *ROAgent) Signers() ([]ssh.Signer, error) { + return []ssh.Signer{r.signer}, nil +}