// 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. // // References: // // [PROTOCOL.agent]: https://tools.ietf.org/html/draft-miller-ssh-agent-00 package agent // import "github.com/Neur0toxine/sshpoke/pkg/proto/ssh/agent" import ( "bytes" "crypto/dsa" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "encoding/base64" "encoding/binary" "errors" "fmt" "io" "math/big" "sync" "github.com/Neur0toxine/sshpoke/pkg/proto/ssh" ) // SignatureFlags represent additional flags that can be passed to the signature // requests an defined in [PROTOCOL.agent] section 4.5.1. type SignatureFlags uint32 // SignatureFlag values as defined in [PROTOCOL.agent] section 5.3. const ( SignatureFlagReserved SignatureFlags = 1 << iota SignatureFlagRsaSha256 SignatureFlagRsaSha512 ) // 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) } type ExtendedAgent interface { Agent // SignWithFlags signs like Sign, but allows for additional flags to be sent/received SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error) // Extension processes a custom extension request. Standard-compliant agents are not // required to support any extensions, but this method allows agents to implement // vendor-specific methods or add experimental features. See [PROTOCOL.agent] section 4.7. // If agent extensions are unsupported entirely this method MUST return an // ErrExtensionUnsupported error. Similarly, if just the specific extensionType in // the request is unsupported by the agent then ErrExtensionUnsupported MUST be // returned. // // In the case of success, since [PROTOCOL.agent] section 4.7 specifies that the contents // of the response are unspecified (including the type of the message), the complete // response will be returned as a []byte slice, including the "type" byte of the message. Extension(extensionType string, contents []byte) ([]byte, 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 RFC 4251, 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, // ed25519.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 // Constraint extension identifier up to version 2 of the protocol. A // backward incompatible change will be required if we want to add support // for SSH_AGENT_CONSTRAIN_MAXSIGN which uses the same ID. agentConstrainExtensionV00 = 3 // Constraint extension identifier in version 3 and later of the protocol. agentConstrainExtension = 255 ) // 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:"255|3"` ExtensionDetails []byte // Rest is a field used for parsing, not part of message Rest []byte `ssh:"rest"` } // See [PROTOCOL.agent], section 4.7 const agentExtension = 27 const agentExtensionFailure = 28 // ErrExtensionUnsupported indicates that an extension defined in // [PROTOCOL.agent] section 4.7 is unsupported by the agent. Specifically this // error indicates that the agent returned a standard SSH_AGENT_FAILURE message // as the result of a SSH_AGENTC_EXTENSION request. Note that the protocol // specification (and therefore this error) does not distinguish between a // specific extension being unsupported and extensions being unsupported entirely. var ErrExtensionUnsupported = errors.New("agent: extension unsupported") type extensionAgentMsg struct { ExtensionType string `sshtype:"27"` // NOTE: this matches OpenSSH's PROTOCOL.agent, not the IETF draft [PROTOCOL.agent], // so that it matches what OpenSSH actually implements in the wild. Contents []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. func (k *Key) Verify(data []byte, sig *ssh.Signature) error { pubKey, err := ssh.ParsePublicKey(k.Blob) if err != nil { return fmt.Errorf("agent: 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) ExtendedAgent { 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) { buf, err := c.callRaw(req) if err != nil { return nil, err } reply, err = unmarshal(buf) if err != nil { return nil, clientErr(err) } return reply, nil } // callRaw sends an RPC to the agent. On success, the raw // bytes of the response are returned; no unmarshalling is // performed on the response. func (c *client) callRaw(req []byte) (reply []byte, 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(errors.New("response too large")) } buf := make([]byte, respSize) if _, err = io.ReadFull(c.conn, buf); err != nil { return nil, clientErr(err) } return buf, nil } 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) { return c.SignWithFlags(key, data, 0) } func (c *client) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error) { req := ssh.Marshal(signRequestAgentMsg{ KeyBlob: key.Marshal(), Data: data, Flags: uint32(flags), }) 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) case agentV1IdentitiesAnswer: msg = new(agentV1IdentityMsg) 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|25"` 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|25"` 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|25"` Curve string KeyBytes []byte D *big.Int Comments string Constraints []byte `ssh:"rest"` } type ed25519KeyMsg struct { Type string `sshtype:"17|25"` Pub []byte Priv []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 *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, }) case ed25519.PrivateKey: req = ssh.Marshal(ed25519KeyMsg{ Type: ssh.KeyAlgoED25519, Pub: []byte(k)[32:], Priv: []byte(k), Comments: comment, Constraints: constraints, }) // This function originally supported only *ed25519.PrivateKey, however the // general idiom is to pass ed25519.PrivateKey by value, not by pointer. // We still support the pointer variant for backwards compatibility. case *ed25519.PrivateKey: req = ssh.Marshal(ed25519KeyMsg{ Type: ssh.KeyAlgoED25519, Pub: []byte(*k)[32:], Priv: []byte(*k), 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|25"` 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|25"` CertBytes []byte X *big.Int Comments string Constraints []byte `ssh:"rest"` } type ecdsaCertMsg struct { Type string `sshtype:"17|25"` CertBytes []byte D *big.Int Comments string Constraints []byte `ssh:"rest"` } type ed25519CertMsg struct { Type string `sshtype:"17|25"` CertBytes []byte Pub []byte Priv []byte Comments string Constraints []byte `ssh:"rest"` } // 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 if secs := key.LifetimeSecs; secs != 0 { constraints = append(constraints, ssh.Marshal(constrainLifetimeAgentMsg{secs})...) } if key.ConfirmBeforeUse { constraints = append(constraints, agentConstrainConfirm) } cert := key.Certificate if cert == nil { return c.insertKey(key.PrivateKey, key.Comment, constraints) } 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, Constraints: constraints, }) case *ecdsa.PrivateKey: req = ssh.Marshal(ecdsaCertMsg{ Type: cert.Type(), CertBytes: cert.Marshal(), D: k.D, Comments: comment, Constraints: constraints, }) case ed25519.PrivateKey: req = ssh.Marshal(ed25519CertMsg{ Type: cert.Type(), CertBytes: cert.Marshal(), Pub: []byte(k)[32:], Priv: []byte(k), Comments: comment, Constraints: constraints, }) // This function originally supported only *ed25519.PrivateKey, however the // general idiom is to pass ed25519.PrivateKey by value, not by pointer. // We still support the pointer variant for backwards compatibility. case *ed25519.PrivateKey: req = ssh.Marshal(ed25519CertMsg{ Type: cert.Type(), CertBytes: cert.Marshal(), Pub: []byte(*k)[32:], Priv: []byte(*k), 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 } signer, err := ssh.NewSignerFromKey(s) if err != nil { return err } if !bytes.Equal(cert.Key.Marshal(), signer.PublicKey().Marshal()) { 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) } func (s *agentKeyringSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) { if algorithm == "" || algorithm == underlyingAlgo(s.pub.Type()) { return s.Sign(rand, data) } var flags SignatureFlags switch algorithm { case ssh.KeyAlgoRSASHA256: flags = SignatureFlagRsaSha256 case ssh.KeyAlgoRSASHA512: flags = SignatureFlagRsaSha512 default: return nil, fmt.Errorf("agent: unsupported algorithm %q", algorithm) } return s.agent.SignWithFlags(s.pub, data, flags) } var _ ssh.AlgorithmSigner = &agentKeyringSigner{} // certKeyAlgoNames is a mapping from known certificate algorithm names to the // corresponding public key signature algorithm. // // This map must be kept in sync with the one in certs.go. var certKeyAlgoNames = map[string]string{ ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA, ssh.CertAlgoRSASHA256v01: ssh.KeyAlgoRSASHA256, ssh.CertAlgoRSASHA512v01: ssh.KeyAlgoRSASHA512, ssh.CertAlgoDSAv01: ssh.KeyAlgoDSA, ssh.CertAlgoECDSA256v01: ssh.KeyAlgoECDSA256, ssh.CertAlgoECDSA384v01: ssh.KeyAlgoECDSA384, ssh.CertAlgoECDSA521v01: ssh.KeyAlgoECDSA521, ssh.CertAlgoSKECDSA256v01: ssh.KeyAlgoSKECDSA256, ssh.CertAlgoED25519v01: ssh.KeyAlgoED25519, ssh.CertAlgoSKED25519v01: ssh.KeyAlgoSKED25519, } // underlyingAlgo returns the signature algorithm associated with algo (which is // an advertised or negotiated public key or host key algorithm). These are // usually the same, except for certificate algorithms. func underlyingAlgo(algo string) string { if a, ok := certKeyAlgoNames[algo]; ok { return a } return algo } // Calls an extension method. It is up to the agent implementation as to whether or not // any particular extension is supported and may always return an error. Because the // type of the response is up to the implementation, this returns the bytes of the // response and does not attempt any type of unmarshalling. func (c *client) Extension(extensionType string, contents []byte) ([]byte, error) { req := ssh.Marshal(extensionAgentMsg{ ExtensionType: extensionType, Contents: contents, }) buf, err := c.callRaw(req) if err != nil { return nil, err } if len(buf) == 0 { return nil, errors.New("agent: failure; empty response") } // [PROTOCOL.agent] section 4.7 indicates that an SSH_AGENT_FAILURE message // represents an agent that does not support the extension if buf[0] == agentFailure { return nil, ErrExtensionUnsupported } if buf[0] == agentExtensionFailure { return nil, errors.New("agent: generic extension failure") } return buf, nil }