diff --git a/README.md b/README.md index 937cbf2..39131b9 100644 --- a/README.md +++ b/README.md @@ -357,14 +357,13 @@ Generate an ssh key without passphrase: Encrypt with the "ssh-sign-with" usage only: - $ ro -minUsers 2 -owners alice,bob -usages ssh-sign-with \ - -server localhost:443 -in id_ed25519 -out id_ed25519.encrypted encrypt + $ ro -server localhost:443 -ca server.crt \ + -minUsers 2 -owners alice,bob -usages ssh-sign-with \ + -in id_ed25519 -out id_ed25519.encrypted encrypt -Use the remote server to authenticate to an SSH server +Initiate a SSH agent with connection to the remote RO server: - $ RO_USER=alice RO_PASS=alice \ - ./ro -server localhost:443 -ca server.crt \ - -in id_ed25519.encrypted -pubkey id_ed25519.pub ssh-agent + $ ro -server localhost:443 -ca server.crt ssh-agent 2018/02/05 05:21:13 Starting Red October Secret Shell Agent export SSH_AUTH_SOCK=/tmp/ro_ssh_267631424/roagent.sock @@ -372,7 +371,12 @@ Use the remote server to authenticate to an SSH server On a separate terminal, run: $ export SSH_AUTH_SOCK=/tmp/ro_ssh_267631424/roagent.sock - $ ssh-add -L # list of all public keys available through ro-agent - $ ssh user@hostname + $ ro -in ssh_key.encrypted -pubkey ssh_key.pub ssh-add + $ ssh-add -L # list of all public keys available through ro-ssh-agent -Other commands such as scp, git, etc. will also authenticate through ro. +Now, all commands that utilize ssh-agents, such as scp, git, etc., will +authenticate through the red october server: + + $ ssh user@hostname + $ git -T git@github.com + $ ... diff --git a/cmd/ro/main.go b/cmd/ro/main.go index 58f8fd4..aaf98e9 100644 --- a/cmd/ro/main.go +++ b/cmd/ro/main.go @@ -24,7 +24,6 @@ import ( "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, pubKeyPath string @@ -52,6 +51,7 @@ var commandSet = map[string]command{ "encrypt": command{Run: runEncrypt, Desc: "encrypt a file"}, "decrypt": command{Run: runDecrypt, Desc: "decrypt a file"}, "ssh-agent": command{Run: runSSHAgent, Desc: "act as an SSH agent"}, + "ssh-add": command{Run: runSSHAdd, Desc: "act as ssh-add"}, "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"}, @@ -404,7 +404,7 @@ func runSSHAgent() { // Prepare a socket dir, err := ioutil.TempDir("", "ro_ssh_") - processError("error", err) + processError("error making a temporary directory", err) authSockPath := path.Join(dir, "roagent.sock") os.Setenv("SSH_AUTH_SOCK", authSockPath) @@ -412,29 +412,17 @@ func runSSHAgent() { socket := net.UnixAddr{Net: "unix", Name: authSockPath} ear, err := net.ListenUnix("unix", &socket) - processError("error", err) - - // Process the arguments - inBytes, err := ioutil.ReadFile(inPath) - processError("error", err) - - 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("error", err) - - pubKey, _, _, _, err := ssh.ParseAuthorizedKey(inBytes) - processError("failed to parse SSH public key", err) + processError("error making a unix socket", err) // Make an agent -// sshagent := agent.NewKeyring() - roagent, err := roagent.NewROAgent(roServer, pubKey, encBytes, user, pswd) - processError("failed to start ROAgent", err) + roAgent := roagent.NewROAgent(roServer, user, pswd) + // Process the arguments + if inPath != "" && pubKeyPath != "" { + go runSSHAdd() + } + + // Prepare for signal interception sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func(ear net.Listener, c chan os.Signal) { @@ -445,15 +433,49 @@ func runSSHAgent() { os.Exit(0) }(ear, sigChan) + // Serve the agent for { conn, err := ear.AcceptUnix() processError("error accepting socket connection", err) - - // Serve the agent - go agent.ServeAgent(roagent, conn) + go roagent.ServeAgent(roAgent, conn) } } +func runSSHAdd() { + // Process the arguments + inBytes, err := ioutil.ReadFile(inPath) + processError("error reading encrypted data", err) + + encBytes, err := base64.StdEncoding.DecodeString(string(inBytes)) + if err != nil { + log.Println("error base64-decoding the data, proceeding with raw data") + encBytes = inBytes + } + + inBytes, err = ioutil.ReadFile(pubKeyPath) + processError("error reading public key", err) + + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(inBytes) + processError("error parsing public key", err) + + // Prepare a socket + authSockPath := os.Getenv("SSH_AUTH_SOCK") + + socket := net.UnixAddr{Net: "unix", Name: authSockPath} + mouth, err := net.DialUnix("unix", nil, &socket) + processError("error connecting to unix socket", err) + + // Contact the agent + newROAgent := roagent.NewClient(mouth) + rosigner := roagent.NewROSigner(pubKey, encBytes) + err = newROAgent.Add(roagent.AddedKey{ + PrivateKey: rosigner, + }) + processError("failed to add new signer to the ROAgent", err) + + fmt.Println("New signer was added to the ROAgent") +} + func main() { flag.Usage = func() { fmt.Println("Usage: ro [options] subcommand") diff --git a/cmd/ro/roagent/client.go b/cmd/ro/roagent/client.go new file mode 100644 index 0000000..d47a376 --- /dev/null +++ b/cmd/ro/roagent/client.go @@ -0,0 +1,486 @@ +// 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. + +package roagent + +import ( + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "io" + "sync" + + "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) +} + +// ConstraintExtension describes an optional constraint defined by users. +type ConstraintExtension struct { + // ExtensionName consist of a UTF-8 string suffixed by the + // implementation domain following the naming scheme defined + // in Section 4.2 of [RFC4251], e.g. "foo@example.com". + ExtensionName string + // ExtensionDetails contains the actual content of the extended + // constraint. + ExtensionDetails []byte +} + +// 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 + // ConstraintExtensions are the experimental or private-use constraints + // defined by users. + ConstraintExtensions []ConstraintExtension +} + +// 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 + agentConstrainExtension = 3 +) + +// 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"` +} + +// 3.7 Key constraint identifiers +type constrainLifetimeAgentMsg struct { + LifetimeSecs uint32 `sshtype:"1"` +} + +type constrainExtensionAgentMsg struct { + ExtensionName string `sshtype:"3"` + ExtensionDetails []byte + + // Rest is a field used for parsing, not part of message + 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("ro-ssh-add: 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("ro-ssh-add: 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("ro-ssh-add: communication 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("ro-ssh-add: 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("ro-ssh-add: 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("ro-ssh-add: 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("ro-ssh-add: 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("ro-ssh-add: unknown type tag %d", packet[0]) + } + if err := ssh.Unmarshal(packet, msg); err != nil { + return nil, err + } + return msg, nil +} + +type roSignerMsg struct { + Type string `sshtype:"17|25"` + Pub []byte + EncKey []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 *ROSigner: + req = ssh.Marshal(roSignerMsg{ + Type: SSHROKey, + Pub: k.pub.Marshal(), + EncKey: k.encKey, + Comments: comment, + Constraints: constraints, + }) + default: + return errors.New("roagent can only accept keys encrypted by a Red October server") + } + + // 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("ro-ssh-add: error adding new key") +} + +// 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 + return c.insertKey(key.PrivateKey, key.Comment, constraints) +} + +// 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/cmd/ro/roagent/roagent.go b/cmd/ro/roagent/roagent.go index 5b6f603..98fcd9a 100644 --- a/cmd/ro/roagent/roagent.go +++ b/cmd/ro/roagent/roagent.go @@ -1,54 +1,56 @@ // Package roagent provides ROAgent, which implements the SSH agent interface, // forwarding sign requests to a Red October server + package roagent import ( "bytes" "crypto/rand" + "crypto/subtle" "encoding/json" "errors" "log" "io" - "github.com/cloudflare/redoctober/client" + roclient "github.com/cloudflare/redoctober/client" "github.com/cloudflare/redoctober/core" "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" ) type ROAgent struct { - server *client.RemoteServer - user string - pswd string - keyring []*ROSigner + locked bool + keyring []*ROSigner + + server *roclient.RemoteServer + username string + password string } type ROSigner struct { - agent *ROAgent - pub ssh.PublicKey - rawKey []byte - encryptedKey []byte + pub ssh.PublicKey + encKey []byte + roagent *ROAgent + comment string } -func (signer ROSigner) PublicKey() ssh.PublicKey { - return signer.pub +func (rosigner ROSigner) PublicKey() ssh.PublicKey { + return rosigner.pub } -func (signer ROSigner) Sign(rand io.Reader, msg []byte) (signature *ssh.Signature, err error) { - // TODO encryptedKey vs rawKey +func (rosigner ROSigner) Sign(rand io.Reader, msg []byte) (signature *ssh.Signature, err error) { req := core.SSHSignWithRequest{ - Name: signer.agent.user, - Password: signer.agent.pswd, - Data: signer.encryptedKey, + Name: rosigner.roagent.username, + Password: rosigner.roagent.password, + Data: rosigner.encKey, TBSData: msg, } - resp, err := signer.agent.server.SSHSignWith(req) + resp, err := rosigner.roagent.server.SSHSignWith(req) if err != nil { return nil, err } if resp.Status != "ok" { - return nil, errors.New("response status error: " + resp.Status) + return nil, errors.New("ro-ssh-agent: response status error: " + resp.Status) } var respMsg core.SSHSignatureWithDelegates @@ -65,114 +67,132 @@ func (signer ROSigner) Sign(rand io.Reader, msg []byte) (signature *ssh.Signatur // NewROAgent creates a new SSH agent which forwards signature requests to the // provided remote server -func NewROAgent(server *client.RemoteServer, pubKey ssh.PublicKey, encryptedPrivKey []byte, user, pswd string) (agent.Agent, error) { - // FIXME these arguments are extra - roagent := &ROAgent{ - server, - user, - pswd, - []*ROSigner{}, +func NewROAgent(server *roclient.RemoteServer, username, password string) *ROAgent { + return &ROAgent{ + server: server, + username: username, + password: password, + keyring: []*ROSigner{}, } - - err := roagent.AddROSigner(pubKey, encryptedPrivKey) - if err != nil { - return nil, errors.New("failed to add new signer to the ROAgent") - } - - return roagent, nil } // NewROSigner adds a new SSH identity to the ROAgent -func (r *ROAgent) AddROSigner(pubKey ssh.PublicKey, encryptedPrivKey []byte) error { - rosigner := &ROSigner{ - agent: r, - pub: pubKey, - encryptedKey: encryptedPrivKey, +func NewROSigner(pubKey ssh.PublicKey, encBytes []byte) *ROSigner { + return &ROSigner{ + pub: pubKey, + encKey: encBytes, } - r.keyring = append(r.keyring, rosigner) - return nil } // RemoveAll empties ROAgent's keyring -func (r *ROAgent) RemoveAll() error { - r.keyring = []*ROSigner{} +func (roagent *ROAgent) RemoveAll() error { + if roagent.locked { + return errLocked + } + roagent.keyring = []*ROSigner{} return nil } // Removes the first matching key from ROAgent's keyring -func (r *ROAgent) Remove(key ssh.PublicKey) error { +func (roagent *ROAgent) Remove(key ssh.PublicKey) error { + if roagent.locked { + return errLocked + } wanted := key.Marshal() - for i, signer := range r.keyring { - if bytes.Equal(signer.PublicKey().Marshal(), wanted) { + for i, rosigner := range roagent.keyring { + if bytes.Equal(rosigner.PublicKey().Marshal(), wanted) { // Order is not preserved - r.keyring[i] = r.keyring[0] - r.keyring = r.keyring[1:] - log.Println("signer was removed") + roagent.keyring[i] = roagent.keyring[0] + roagent.keyring = roagent.keyring[1:] + log.Println("ro-ssh-agent: signer removed") return nil } } - return errors.New("could not remove signer") + return errors.New("ro-ssh-agent: could not remove signer") } // Locks the ROAgent by removing the password // TODO should this encrypt the password instead? -func (r *ROAgent) Lock(passphrase []byte) error { - if bytes.Equal(passphrase, []byte(r.pswd)) { - r.pswd = "" +func (roagent *ROAgent) Lock(passphrase []byte) error { + if roagent.locked { + return errLocked + } + if len(passphrase) != len(roagent.password) || 1 != subtle.ConstantTimeCompare(passphrase, []byte(roagent.password)) { + roagent.password = "" + roagent.locked = true return nil } - return errors.New("could not lock the agent") + return errors.New("ro-ssh-agent: could not lock the agent") } -// Unlocks the ROAgent by adding the password +// Unlocks the ROAgent by changing the password // FIXME ask papa RO if the password is correct -func (r *ROAgent) Unlock(passphrase []byte) error { - r.pswd = string(passphrase) +func (roagent *ROAgent) Unlock(passphrase []byte) error { + if !roagent.locked { + return errors.New("ro-ssh-agent: agent is not locked") + } + roagent.locked = false + roagent.password = string(passphrase) return nil } // List returns the identities known to the agent. -func (r *ROAgent) List() ([]*agent.Key, error) { - list := make([]*agent.Key, len(r.keyring)) - for i, signer := range r.keyring { - list[i] = &agent.Key{ - Format: signer.PublicKey().Type(), - Blob: signer.PublicKey().Marshal(), - Comment: r.user, +func (roagent *ROAgent) List() ([]*Key, error) { + if roagent.locked { + // section 2.7: locked agents return empty. + return nil, nil + } + + list := make([]*Key, len(roagent.keyring)) + for i, rosigner := range roagent.keyring { + list[i] = &Key{ + Format: rosigner.PublicKey().Type(), + Blob: rosigner.PublicKey().Marshal(), + Comment: roagent.username, } } return list, nil } -// Add has no effect for the ROAgent -// FIXME -func (r *ROAgent) Add(key agent.AddedKey) error { - signer, _ := ssh.NewSignerFromKey(key.PrivateKey) - rosigner := &ROSigner{ - pub: signer.PublicKey(), - encryptedKey: nil, //[]byte +// Adds a new encrypted key to ROAgent's keyring +func (roagent *ROAgent) Add(key AddedKey) error { + if roagent.locked { + return errLocked } - r.keyring = append(r.keyring, rosigner) - log.Println("new signer was added") + + rosigner := key.PrivateKey.(*ROSigner) + rosigner.roagent = roagent + roagent.keyring = append(roagent.keyring, rosigner) + log.Println("new signer was added. Total:", len(roagent.keyring)) return nil } // Sign returns a signature for the data. -func (r *ROAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { +func (roagent *ROAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + if roagent.locked { + return nil, errLocked + } + wanted := key.Marshal() - for _, signer := range r.keyring { - if bytes.Equal(signer.PublicKey().Marshal(), wanted) { - return signer.Sign(rand.Reader, data) + for _, rosigner := range roagent.keyring { + if bytes.Equal(rosigner.PublicKey().Marshal(), wanted) { + rosigner.roagent = roagent + return rosigner.Sign(rand.Reader, data) } } - return nil, errors.New("requested key was not found on keyring") + return nil, errors.New("key not found on keyring") } // Signers returns signers for all the known keys. -func (r *ROAgent) Signers() ([]ssh.Signer, error) { - list := make([]ssh.Signer, len(r.keyring)) - for i, signer := range r.keyring { - list[i] = ssh.Signer(signer) +func (roagent *ROAgent) Signers() ([]ssh.Signer, error) { + if roagent.locked { + return nil, errLocked + } + + list := make([]ssh.Signer, len(roagent.keyring)) + for i, rosigner := range roagent.keyring { + rosigner.roagent = roagent + list[i] = ssh.Signer(rosigner) } return list, nil } diff --git a/cmd/ro/roagent/server.go b/cmd/ro/roagent/server.go new file mode 100644 index 0000000..6b4da9c --- /dev/null +++ b/cmd/ro/roagent/server.go @@ -0,0 +1,274 @@ +// 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 roagent + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "log" + + "golang.org/x/crypto/ssh" +) + +const SSHROKey string = "ssh-ro" + +var errLocked = errors.New("agent: locked") + +// Server wraps an Agent and uses it to implement the agent side of +// the SSH-agent, wire protocol. +type server struct { + roagent *ROAgent +} + +func parseROSigner(req []byte) (*AddedKey, error) { + var k roSignerMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + pubKey, err := ssh.ParsePublicKey(k.Pub) + if err != nil { + return nil, errors.New("received ill-formatted public key") + } + + return &AddedKey{ + PrivateKey: &ROSigner{ + pub: pubKey, + encKey: k.EncKey, + }, + Comment: k.Comments, + }, nil +} + +func (s *server) insertIdentity(req []byte) error { + var record struct { + Type string `sshtype:"17|25"` + Rest []byte `ssh:"rest"` + } + + // FIXME what is record? + if err := ssh.Unmarshal(req, &record); err != nil { + return err + } + + var addedKey *AddedKey + var err error + + if record.Type == SSHROKey { + addedKey, err = parseROSigner(req) + } else { + return errors.New("roagent can only accept keys encrypted by a Red October server") + } + + if err != nil { + return err + } + return s.roagent.Add(*addedKey) +} + +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.roagent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob}) + + case agentRemoveAllIdentities: + return nil, s.roagent.RemoveAll() + + case agentLock: + var req agentLockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + return nil, s.roagent.Lock(req.Passphrase) + + case agentUnlock: + var req agentUnlockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + return nil, s.roagent.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.roagent.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.roagent.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 parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse bool, extensions []ConstraintExtension, err error) { + for len(constraints) != 0 { + switch constraints[0] { + case agentConstrainLifetime: + lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5]) + constraints = constraints[5:] + case agentConstrainConfirm: + confirmBeforeUse = true + constraints = constraints[1:] + case agentConstrainExtension: + var msg constrainExtensionAgentMsg + if err = ssh.Unmarshal(constraints, &msg); err != nil { + return 0, false, nil, err + } + extensions = append(extensions, ConstraintExtension{ + ExtensionName: msg.ExtensionName, + ExtensionDetails: msg.ExtensionDetails, + }) + constraints = msg.Rest + default: + return 0, false, nil, fmt.Errorf("unknown constraint type: %d", constraints[0]) + } + } + return +} + +func setConstraints(key *AddedKey, constraintBytes []byte) error { + lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes) + if err != nil { + return err + } + + key.LifetimeSecs = lifetimeSecs + key.ConfirmBeforeUse = confirmBeforeUse + key.ConstraintExtensions = constraintExtensions + return nil +} + +// ServeAgent serves the agent protocol on the given connection. It +// returns when an I/O error occurs. +func ServeAgent(roagent *ROAgent, c io.ReadWriter) error { + s := &server{roagent} + + 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 + } + } +}