mirror of
https://github.com/cloudflare/redoctober.git
synced 2026-01-07 05:56:56 +00:00
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:
committed by
Kyle Isom
parent
3fc06e3b12
commit
998d924d79
22
README.md
22
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
|
||||
$ ...
|
||||
|
||||
@@ -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
486
cmd/ro/roagent/client.go
Normal 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)
|
||||
}
|
||||
@@ -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
274
cmd/ro/roagent/server.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user