616 lines
15 KiB
Go
Executable File
616 lines
15 KiB
Go
Executable File
// 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 a client to an ssh-agent daemon.
|
|
|
|
References:
|
|
[PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD
|
|
*/
|
|
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/dsa"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/gogits/gogs/modules/crypto/ssh"
|
|
)
|
|
|
|
// Agent represents the capabilities of an ssh-agent.
|
|
type Agent interface {
|
|
// List returns the identities known to the agent.
|
|
List() ([]*Key, error)
|
|
|
|
// Sign has the agent sign the data using a protocol 2 key as defined
|
|
// in [PROTOCOL.agent] section 2.6.2.
|
|
Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
|
|
|
|
// Add adds a private key to the agent.
|
|
Add(key AddedKey) error
|
|
|
|
// Remove removes all identities with the given public key.
|
|
Remove(key ssh.PublicKey) error
|
|
|
|
// RemoveAll removes all identities.
|
|
RemoveAll() error
|
|
|
|
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
|
|
Lock(passphrase []byte) error
|
|
|
|
// Unlock undoes the effect of Lock
|
|
Unlock(passphrase []byte) error
|
|
|
|
// Signers returns signers for all the known keys.
|
|
Signers() ([]ssh.Signer, error)
|
|
}
|
|
|
|
// AddedKey describes an SSH key to be added to an Agent.
|
|
type AddedKey struct {
|
|
// PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or
|
|
// *ecdsa.PrivateKey, which will be inserted into the agent.
|
|
PrivateKey interface{}
|
|
// Certificate, if not nil, is communicated to the agent and will be
|
|
// stored with the key.
|
|
Certificate *ssh.Certificate
|
|
// Comment is an optional, free-form string.
|
|
Comment string
|
|
// LifetimeSecs, if not zero, is the number of seconds that the
|
|
// agent will store the key for.
|
|
LifetimeSecs uint32
|
|
// ConfirmBeforeUse, if true, requests that the agent confirm with the
|
|
// user before each use of this key.
|
|
ConfirmBeforeUse bool
|
|
}
|
|
|
|
// See [PROTOCOL.agent], section 3.
|
|
const (
|
|
agentRequestV1Identities = 1
|
|
|
|
// 3.2 Requests from client to agent for protocol 2 key operations
|
|
agentAddIdentity = 17
|
|
agentRemoveIdentity = 18
|
|
agentRemoveAllIdentities = 19
|
|
agentAddIdConstrained = 25
|
|
|
|
// 3.3 Key-type independent requests from client to agent
|
|
agentAddSmartcardKey = 20
|
|
agentRemoveSmartcardKey = 21
|
|
agentLock = 22
|
|
agentUnlock = 23
|
|
agentAddSmartcardKeyConstrained = 26
|
|
|
|
// 3.7 Key constraint identifiers
|
|
agentConstrainLifetime = 1
|
|
agentConstrainConfirm = 2
|
|
)
|
|
|
|
// maxAgentResponseBytes is the maximum agent reply size that is accepted. This
|
|
// is a sanity check, not a limit in the spec.
|
|
const maxAgentResponseBytes = 16 << 20
|
|
|
|
// Agent messages:
|
|
// These structures mirror the wire format of the corresponding ssh agent
|
|
// messages found in [PROTOCOL.agent].
|
|
|
|
// 3.4 Generic replies from agent to client
|
|
const agentFailure = 5
|
|
|
|
type failureAgentMsg struct{}
|
|
|
|
const agentSuccess = 6
|
|
|
|
type successAgentMsg struct{}
|
|
|
|
// See [PROTOCOL.agent], section 2.5.2.
|
|
const agentRequestIdentities = 11
|
|
|
|
type requestIdentitiesAgentMsg struct{}
|
|
|
|
// See [PROTOCOL.agent], section 2.5.2.
|
|
const agentIdentitiesAnswer = 12
|
|
|
|
type identitiesAnswerAgentMsg struct {
|
|
NumKeys uint32 `sshtype:"12"`
|
|
Keys []byte `ssh:"rest"`
|
|
}
|
|
|
|
// See [PROTOCOL.agent], section 2.6.2.
|
|
const agentSignRequest = 13
|
|
|
|
type signRequestAgentMsg struct {
|
|
KeyBlob []byte `sshtype:"13"`
|
|
Data []byte
|
|
Flags uint32
|
|
}
|
|
|
|
// See [PROTOCOL.agent], section 2.6.2.
|
|
|
|
// 3.6 Replies from agent to client for protocol 2 key operations
|
|
const agentSignResponse = 14
|
|
|
|
type signResponseAgentMsg struct {
|
|
SigBlob []byte `sshtype:"14"`
|
|
}
|
|
|
|
type publicKey struct {
|
|
Format string
|
|
Rest []byte `ssh:"rest"`
|
|
}
|
|
|
|
// Key represents a protocol 2 public key as defined in
|
|
// [PROTOCOL.agent], section 2.5.2.
|
|
type Key struct {
|
|
Format string
|
|
Blob []byte
|
|
Comment string
|
|
}
|
|
|
|
func clientErr(err error) error {
|
|
return fmt.Errorf("agent: client error: %v", err)
|
|
}
|
|
|
|
// String returns the storage form of an agent key with the format, base64
|
|
// encoded serialized key, and the comment if it is not empty.
|
|
func (k *Key) String() string {
|
|
s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob)
|
|
|
|
if k.Comment != "" {
|
|
s += " " + k.Comment
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Type returns the public key type.
|
|
func (k *Key) Type() string {
|
|
return k.Format
|
|
}
|
|
|
|
// Marshal returns key blob to satisfy the ssh.PublicKey interface.
|
|
func (k *Key) Marshal() []byte {
|
|
return k.Blob
|
|
}
|
|
|
|
// Verify satisfies the ssh.PublicKey interface, but is not
|
|
// implemented for agent keys.
|
|
func (k *Key) Verify(data []byte, sig *ssh.Signature) error {
|
|
return errors.New("agent: agent key does not know how to verify")
|
|
}
|
|
|
|
type wireKey struct {
|
|
Format string
|
|
Rest []byte `ssh:"rest"`
|
|
}
|
|
|
|
func parseKey(in []byte) (out *Key, rest []byte, err error) {
|
|
var record struct {
|
|
Blob []byte
|
|
Comment string
|
|
Rest []byte `ssh:"rest"`
|
|
}
|
|
|
|
if err := ssh.Unmarshal(in, &record); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var wk wireKey
|
|
if err := ssh.Unmarshal(record.Blob, &wk); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &Key{
|
|
Format: wk.Format,
|
|
Blob: record.Blob,
|
|
Comment: record.Comment,
|
|
}, record.Rest, nil
|
|
}
|
|
|
|
// client is a client for an ssh-agent process.
|
|
type client struct {
|
|
// conn is typically a *net.UnixConn
|
|
conn io.ReadWriter
|
|
// mu is used to prevent concurrent access to the agent
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewClient returns an Agent that talks to an ssh-agent process over
|
|
// the given connection.
|
|
func NewClient(rw io.ReadWriter) Agent {
|
|
return &client{conn: rw}
|
|
}
|
|
|
|
// call sends an RPC to the agent. On success, the reply is
|
|
// unmarshaled into reply and replyType is set to the first byte of
|
|
// the reply, which contains the type of the message.
|
|
func (c *client) call(req []byte) (reply interface{}, err error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
msg := make([]byte, 4+len(req))
|
|
binary.BigEndian.PutUint32(msg, uint32(len(req)))
|
|
copy(msg[4:], req)
|
|
if _, err = c.conn.Write(msg); err != nil {
|
|
return nil, clientErr(err)
|
|
}
|
|
|
|
var respSizeBuf [4]byte
|
|
if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil {
|
|
return nil, clientErr(err)
|
|
}
|
|
respSize := binary.BigEndian.Uint32(respSizeBuf[:])
|
|
if respSize > maxAgentResponseBytes {
|
|
return nil, clientErr(err)
|
|
}
|
|
|
|
buf := make([]byte, respSize)
|
|
if _, err = io.ReadFull(c.conn, buf); err != nil {
|
|
return nil, clientErr(err)
|
|
}
|
|
reply, err = unmarshal(buf)
|
|
if err != nil {
|
|
return nil, clientErr(err)
|
|
}
|
|
return reply, err
|
|
}
|
|
|
|
func (c *client) simpleCall(req []byte) error {
|
|
resp, err := c.call(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := resp.(*successAgentMsg); ok {
|
|
return nil
|
|
}
|
|
return errors.New("agent: failure")
|
|
}
|
|
|
|
func (c *client) RemoveAll() error {
|
|
return c.simpleCall([]byte{agentRemoveAllIdentities})
|
|
}
|
|
|
|
func (c *client) Remove(key ssh.PublicKey) error {
|
|
req := ssh.Marshal(&agentRemoveIdentityMsg{
|
|
KeyBlob: key.Marshal(),
|
|
})
|
|
return c.simpleCall(req)
|
|
}
|
|
|
|
func (c *client) Lock(passphrase []byte) error {
|
|
req := ssh.Marshal(&agentLockMsg{
|
|
Passphrase: passphrase,
|
|
})
|
|
return c.simpleCall(req)
|
|
}
|
|
|
|
func (c *client) Unlock(passphrase []byte) error {
|
|
req := ssh.Marshal(&agentUnlockMsg{
|
|
Passphrase: passphrase,
|
|
})
|
|
return c.simpleCall(req)
|
|
}
|
|
|
|
// List returns the identities known to the agent.
|
|
func (c *client) List() ([]*Key, error) {
|
|
// see [PROTOCOL.agent] section 2.5.2.
|
|
req := []byte{agentRequestIdentities}
|
|
|
|
msg, err := c.call(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case *identitiesAnswerAgentMsg:
|
|
if msg.NumKeys > maxAgentResponseBytes/8 {
|
|
return nil, errors.New("agent: too many keys in agent reply")
|
|
}
|
|
keys := make([]*Key, msg.NumKeys)
|
|
data := msg.Keys
|
|
for i := uint32(0); i < msg.NumKeys; i++ {
|
|
var key *Key
|
|
var err error
|
|
if key, data, err = parseKey(data); err != nil {
|
|
return nil, err
|
|
}
|
|
keys[i] = key
|
|
}
|
|
return keys, nil
|
|
case *failureAgentMsg:
|
|
return nil, errors.New("agent: failed to list keys")
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
// Sign has the agent sign the data using a protocol 2 key as defined
|
|
// in [PROTOCOL.agent] section 2.6.2.
|
|
func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
|
|
req := ssh.Marshal(signRequestAgentMsg{
|
|
KeyBlob: key.Marshal(),
|
|
Data: data,
|
|
})
|
|
|
|
msg, err := c.call(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case *signResponseAgentMsg:
|
|
var sig ssh.Signature
|
|
if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &sig, nil
|
|
case *failureAgentMsg:
|
|
return nil, errors.New("agent: failed to sign challenge")
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
// unmarshal parses an agent message in packet, returning the parsed
|
|
// form and the message type of packet.
|
|
func unmarshal(packet []byte) (interface{}, error) {
|
|
if len(packet) < 1 {
|
|
return nil, errors.New("agent: empty packet")
|
|
}
|
|
var msg interface{}
|
|
switch packet[0] {
|
|
case agentFailure:
|
|
return new(failureAgentMsg), nil
|
|
case agentSuccess:
|
|
return new(successAgentMsg), nil
|
|
case agentIdentitiesAnswer:
|
|
msg = new(identitiesAnswerAgentMsg)
|
|
case agentSignResponse:
|
|
msg = new(signResponseAgentMsg)
|
|
default:
|
|
return nil, fmt.Errorf("agent: unknown type tag %d", packet[0])
|
|
}
|
|
if err := ssh.Unmarshal(packet, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
type rsaKeyMsg struct {
|
|
Type string `sshtype:"17"`
|
|
N *big.Int
|
|
E *big.Int
|
|
D *big.Int
|
|
Iqmp *big.Int // IQMP = Inverse Q Mod P
|
|
P *big.Int
|
|
Q *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
type dsaKeyMsg struct {
|
|
Type string `sshtype:"17"`
|
|
P *big.Int
|
|
Q *big.Int
|
|
G *big.Int
|
|
Y *big.Int
|
|
X *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
type ecdsaKeyMsg struct {
|
|
Type string `sshtype:"17"`
|
|
Curve string
|
|
KeyBytes []byte
|
|
D *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
// Insert adds a private key to the agent.
|
|
func (c *client) insertKey(s interface{}, comment string, constraints []byte) error {
|
|
var req []byte
|
|
switch k := s.(type) {
|
|
case *rsa.PrivateKey:
|
|
if len(k.Primes) != 2 {
|
|
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
|
|
}
|
|
k.Precompute()
|
|
req = ssh.Marshal(rsaKeyMsg{
|
|
Type: ssh.KeyAlgoRSA,
|
|
N: k.N,
|
|
E: big.NewInt(int64(k.E)),
|
|
D: k.D,
|
|
Iqmp: k.Precomputed.Qinv,
|
|
P: k.Primes[0],
|
|
Q: k.Primes[1],
|
|
Comments: comment,
|
|
Constraints: constraints,
|
|
})
|
|
case *dsa.PrivateKey:
|
|
req = ssh.Marshal(dsaKeyMsg{
|
|
Type: ssh.KeyAlgoDSA,
|
|
P: k.P,
|
|
Q: k.Q,
|
|
G: k.G,
|
|
Y: k.Y,
|
|
X: k.X,
|
|
Comments: comment,
|
|
Constraints: constraints,
|
|
})
|
|
case *ecdsa.PrivateKey:
|
|
nistID := fmt.Sprintf("nistp%d", k.Params().BitSize)
|
|
req = ssh.Marshal(ecdsaKeyMsg{
|
|
Type: "ecdsa-sha2-" + nistID,
|
|
Curve: nistID,
|
|
KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y),
|
|
D: k.D,
|
|
Comments: comment,
|
|
Constraints: constraints,
|
|
})
|
|
default:
|
|
return fmt.Errorf("agent: unsupported key type %T", s)
|
|
}
|
|
|
|
// if constraints are present then the message type needs to be changed.
|
|
if len(constraints) != 0 {
|
|
req[0] = agentAddIdConstrained
|
|
}
|
|
|
|
resp, err := c.call(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := resp.(*successAgentMsg); ok {
|
|
return nil
|
|
}
|
|
return errors.New("agent: failure")
|
|
}
|
|
|
|
type rsaCertMsg struct {
|
|
Type string `sshtype:"17"`
|
|
CertBytes []byte
|
|
D *big.Int
|
|
Iqmp *big.Int // IQMP = Inverse Q Mod P
|
|
P *big.Int
|
|
Q *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
type dsaCertMsg struct {
|
|
Type string `sshtype:"17"`
|
|
CertBytes []byte
|
|
X *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
type ecdsaCertMsg struct {
|
|
Type string `sshtype:"17"`
|
|
CertBytes []byte
|
|
D *big.Int
|
|
Comments string
|
|
Constraints []byte `ssh:"rest"`
|
|
}
|
|
|
|
// Insert adds a private key to the agent. If a certificate is given,
|
|
// that certificate is added instead as public key.
|
|
func (c *client) Add(key AddedKey) error {
|
|
var constraints []byte
|
|
|
|
if secs := key.LifetimeSecs; secs != 0 {
|
|
constraints = append(constraints, agentConstrainLifetime)
|
|
|
|
var secsBytes [4]byte
|
|
binary.BigEndian.PutUint32(secsBytes[:], secs)
|
|
constraints = append(constraints, secsBytes[:]...)
|
|
}
|
|
|
|
if key.ConfirmBeforeUse {
|
|
constraints = append(constraints, agentConstrainConfirm)
|
|
}
|
|
|
|
if cert := key.Certificate; cert == nil {
|
|
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
|
} else {
|
|
return c.insertCert(key.PrivateKey, cert, key.Comment, constraints)
|
|
}
|
|
}
|
|
|
|
func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error {
|
|
var req []byte
|
|
switch k := s.(type) {
|
|
case *rsa.PrivateKey:
|
|
if len(k.Primes) != 2 {
|
|
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
|
|
}
|
|
k.Precompute()
|
|
req = ssh.Marshal(rsaCertMsg{
|
|
Type: cert.Type(),
|
|
CertBytes: cert.Marshal(),
|
|
D: k.D,
|
|
Iqmp: k.Precomputed.Qinv,
|
|
P: k.Primes[0],
|
|
Q: k.Primes[1],
|
|
Comments: comment,
|
|
Constraints: constraints,
|
|
})
|
|
case *dsa.PrivateKey:
|
|
req = ssh.Marshal(dsaCertMsg{
|
|
Type: cert.Type(),
|
|
CertBytes: cert.Marshal(),
|
|
X: k.X,
|
|
Comments: comment,
|
|
})
|
|
case *ecdsa.PrivateKey:
|
|
req = ssh.Marshal(ecdsaCertMsg{
|
|
Type: cert.Type(),
|
|
CertBytes: cert.Marshal(),
|
|
D: k.D,
|
|
Comments: comment,
|
|
})
|
|
default:
|
|
return fmt.Errorf("agent: unsupported key type %T", s)
|
|
}
|
|
|
|
// if constraints are present then the message type needs to be changed.
|
|
if len(constraints) != 0 {
|
|
req[0] = agentAddIdConstrained
|
|
}
|
|
|
|
signer, err := ssh.NewSignerFromKey(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
|
|
return errors.New("agent: signer and cert have different public key")
|
|
}
|
|
|
|
resp, err := c.call(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := resp.(*successAgentMsg); ok {
|
|
return nil
|
|
}
|
|
return errors.New("agent: failure")
|
|
}
|
|
|
|
// Signers provides a callback for client authentication.
|
|
func (c *client) Signers() ([]ssh.Signer, error) {
|
|
keys, err := c.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result []ssh.Signer
|
|
for _, k := range keys {
|
|
result = append(result, &agentKeyringSigner{c, k})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type agentKeyringSigner struct {
|
|
agent *client
|
|
pub ssh.PublicKey
|
|
}
|
|
|
|
func (s *agentKeyringSigner) PublicKey() ssh.PublicKey {
|
|
return s.pub
|
|
}
|
|
|
|
func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
|
|
// The agent has its own entropy source, so the rand argument is ignored.
|
|
return s.agent.Sign(s.pub, data)
|
|
}
|