mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
615 lines
15 KiB
615 lines
15 KiB
// 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) |
|
}
|
|
|