ssh-add mode is introduced, ssh-agent mode is polished

See the last paragraph in README.md for an example.
This commit is contained in:
Mahrud Sayrafi
2018-02-06 06:01:20 -08:00
committed by Kyle Isom
parent 3fc06e3b12
commit 998d924d79
5 changed files with 921 additions and 115 deletions

View File

@@ -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
$ ...

View File

@@ -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")

486
cmd/ro/roagent/client.go Normal file
View File

@@ -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)
}

View File

@@ -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
}

274
cmd/ro/roagent/server.go Normal file
View File

@@ -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
}
}
}