Add ClientAuthCallback, a hook invoked before each authentication attempt that allows the client to dynamically select an auth method based on server capabilities, partial successes, or previous failures. clientAuthenticate tracks partial successes in a slice and passes them to AuthCallback via ClientAuthContext.PartialSuccessMethods, along with the list of failed methods and currently allowed methods. When AuthCallback returns a non-nil AuthMethod, it is used for the next attempt, bypassing the static findNext selection. When it returns (nil, nil), the static config.Auth selection proceeds as before. If AuthCallback returns (nil, error) the handshake aborts immediately with that error. To bound resource use when AuthCallback keeps supplying methods, the client caps the total number of authentication attempts (failures and partial successes combined) at 64; exceeding the cap aborts the handshake with an error. Fixes golang/go#76146 Change-Id: I0d02bea7b9dd724e95e5d9d49d85306666c0df7a Reviewed-on: https://go-review.googlesource.com/c/crypto/+/717140 Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Filippo Valsorda <filippo@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
343 lines
11 KiB
Go
343 lines
11 KiB
Go
// Copyright 2011 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 ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Client implements a traditional SSH client that supports shells,
|
|
// subprocesses, TCP port/streamlocal forwarding and tunneled dialing.
|
|
type Client struct {
|
|
Conn
|
|
|
|
handleForwardsOnce sync.Once // guards calling (*Client).handleForwards
|
|
|
|
forwards forwardList // forwarded tcpip connections from the remote side
|
|
mu sync.Mutex
|
|
channelHandlers map[string]chan NewChannel
|
|
}
|
|
|
|
// HandleChannelOpen returns a channel on which NewChannel requests
|
|
// for the given type are sent. If the type already is being handled,
|
|
// nil is returned. The channel is closed when the connection is closed.
|
|
func (c *Client) HandleChannelOpen(channelType string) <-chan NewChannel {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.channelHandlers == nil {
|
|
// The SSH channel has been closed.
|
|
c := make(chan NewChannel)
|
|
close(c)
|
|
return c
|
|
}
|
|
|
|
ch := c.channelHandlers[channelType]
|
|
if ch != nil {
|
|
return nil
|
|
}
|
|
|
|
ch = make(chan NewChannel, chanSize)
|
|
c.channelHandlers[channelType] = ch
|
|
return ch
|
|
}
|
|
|
|
// NewClient creates a Client on top of the given connection.
|
|
func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
|
|
conn := &Client{
|
|
Conn: c,
|
|
channelHandlers: make(map[string]chan NewChannel, 1),
|
|
}
|
|
|
|
go conn.handleGlobalRequests(reqs)
|
|
go conn.handleChannelOpens(chans)
|
|
go func() {
|
|
conn.Wait()
|
|
conn.forwards.closeAll()
|
|
}()
|
|
return conn
|
|
}
|
|
|
|
// NewClientConn establishes an authenticated SSH connection using c
|
|
// as the underlying transport. The Request and NewChannel channels
|
|
// must be serviced or the connection will hang.
|
|
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
|
fullConf := *config
|
|
fullConf.SetDefaults()
|
|
if fullConf.HostKeyCallback == nil {
|
|
c.Close()
|
|
return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback")
|
|
}
|
|
|
|
conn := &connection{
|
|
sshConn: sshConn{conn: c, user: fullConf.User},
|
|
}
|
|
|
|
if err := conn.clientHandshake(addr, &fullConf); err != nil {
|
|
c.Close()
|
|
return nil, nil, nil, fmt.Errorf("ssh: handshake failed: %w", err)
|
|
}
|
|
conn.mux = newMux(conn.transport)
|
|
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
|
}
|
|
|
|
// clientHandshake performs the client side key exchange. See RFC 4253 Section
|
|
// 7.
|
|
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
|
|
if config.ClientVersion != "" {
|
|
c.clientVersion = []byte(config.ClientVersion)
|
|
} else {
|
|
c.clientVersion = []byte(packageVersion)
|
|
}
|
|
var err error
|
|
c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.transport = newClientTransport(
|
|
newTransport(c.sshConn.conn, config.Rand, true /* is client */),
|
|
c.clientVersion, c.serverVersion, config, dialAddress, c.sshConn.RemoteAddr())
|
|
if err := c.transport.waitSession(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.sessionID = c.transport.getSessionID()
|
|
c.algorithms = c.transport.getAlgorithms()
|
|
return c.clientAuthenticate(config)
|
|
}
|
|
|
|
// verifyHostKeySignature verifies the host key obtained in the key exchange.
|
|
// algo is the negotiated algorithm, and may be a certificate type.
|
|
func verifyHostKeySignature(hostKey PublicKey, algo string, result *kexResult) error {
|
|
sig, rest, ok := parseSignatureBody(result.Signature)
|
|
if len(rest) > 0 || !ok {
|
|
return errors.New("ssh: signature parse error")
|
|
}
|
|
|
|
if a := underlyingAlgo(algo); sig.Format != a {
|
|
return fmt.Errorf("ssh: invalid signature algorithm %q, expected %q", sig.Format, a)
|
|
}
|
|
|
|
return hostKey.Verify(result.H, sig)
|
|
}
|
|
|
|
// NewSession opens a new Session for this client. (A session is a remote
|
|
// execution of a program.)
|
|
func (c *Client) NewSession() (*Session, error) {
|
|
ch, in, err := c.OpenChannel("session", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newSession(ch, in)
|
|
}
|
|
|
|
func (c *Client) handleGlobalRequests(incoming <-chan *Request) {
|
|
for r := range incoming {
|
|
// This handles keepalive messages and matches
|
|
// the behaviour of OpenSSH.
|
|
r.Reply(false, nil)
|
|
}
|
|
}
|
|
|
|
// handleChannelOpens channel open messages from the remote side.
|
|
func (c *Client) handleChannelOpens(in <-chan NewChannel) {
|
|
for ch := range in {
|
|
c.mu.Lock()
|
|
handler := c.channelHandlers[ch.ChannelType()]
|
|
c.mu.Unlock()
|
|
|
|
if handler != nil {
|
|
handler <- ch
|
|
} else {
|
|
ch.Reject(UnknownChannelType, fmt.Sprintf("unknown channel type: %v", ch.ChannelType()))
|
|
}
|
|
}
|
|
|
|
c.mu.Lock()
|
|
for _, ch := range c.channelHandlers {
|
|
close(ch)
|
|
}
|
|
c.channelHandlers = nil
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Dial starts a client connection to the given SSH server. It is a
|
|
// convenience function that connects to the given network address,
|
|
// initiates the SSH handshake, and then sets up a Client. For access
|
|
// to incoming channels and requests, use net.Dial with NewClientConn
|
|
// instead.
|
|
func Dial(network, addr string, config *ClientConfig) (*Client, error) {
|
|
conn, err := net.DialTimeout(network, addr, config.Timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, chans, reqs, err := NewClientConn(conn, addr, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewClient(c, chans, reqs), nil
|
|
}
|
|
|
|
// HostKeyCallback is the function type used for verifying server
|
|
// keys. A HostKeyCallback must return nil if the host key is OK, or
|
|
// an error to reject it. It receives the hostname as passed to Dial
|
|
// or NewClientConn. The remote address is the RemoteAddr of the
|
|
// net.Conn underlying the SSH connection.
|
|
type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
|
|
|
// BannerCallback is the function type used for treat the banner sent by
|
|
// the server. A BannerCallback receives the message sent by the remote server.
|
|
type BannerCallback func(message string) error
|
|
|
|
// ClientAuthContext contains information about the current state of the
|
|
// authentication process, passed to [ClientAuthCallback].
|
|
type ClientAuthContext struct {
|
|
// Metadata contains the connection metadata.
|
|
Metadata ConnMetadata
|
|
|
|
// Algorithms contains the negotiated algorithms.
|
|
Algorithms NegotiatedAlgorithms
|
|
|
|
// AllowedMethods lists the authentication methods currently accepted
|
|
// by the server. These are the protocol-level names defined in RFC 4252
|
|
// such as "publickey", "password".
|
|
AllowedMethods []string
|
|
|
|
// PartialSuccessMethods lists the authentication methods that have already
|
|
// succeeded, indicating a multi-step authentication flow. This list
|
|
// represents the exact sequence of partial successes and may contain
|
|
// duplicates if the same method succeeded multiple times.
|
|
PartialSuccessMethods []string
|
|
|
|
// TriedMethods lists the methods that have already been attempted and
|
|
// failed during this session. This list represents the exact sequence of
|
|
// failures and may contain duplicates. This allows the callback to also
|
|
// track the number of failed attempts for a specific method.
|
|
TriedMethods []string
|
|
}
|
|
|
|
// ClientAuthCallback is a hook invoked before each authentication attempt. It
|
|
// allows the client to dynamically select an authentication method based on the
|
|
// current context, server capabilities, or previous failures.
|
|
//
|
|
// The callback is invoked after the initial "none" authentication method, once
|
|
// the server's supported authentication methods are known.
|
|
//
|
|
// Return values:
|
|
// - (AuthMethod, nil): The client will attempt this specific method next.
|
|
// The returned method does NOT need to be present in [ClientConfig.Auth].
|
|
// This allows for dynamic authentication strategies (e.g., prompting
|
|
// for a password only if public key auth fails). Callers should inspect
|
|
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
|
|
// same failing method.
|
|
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
|
|
// instance of a method that has not been tried yet, or aborts if none
|
|
// are left. If authentication is not successful, the callback is invoked
|
|
// again before the following attempt.
|
|
// - (nil, error): The authentication process is aborted immediately,
|
|
// causing the ongoing SSH handshake to fail with the provided error.
|
|
//
|
|
// To bound resource use, the client caps the total number of authentication
|
|
// attempts (failures and partial successes combined) at 64. If the cap is
|
|
// exceeded the handshake aborts with an error.
|
|
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
|
|
|
|
// A ClientConfig structure is used to configure a Client. It must not be
|
|
// modified after having been passed to an SSH function.
|
|
type ClientConfig struct {
|
|
// Config contains configuration that is shared between clients and
|
|
// servers.
|
|
Config
|
|
|
|
// User contains the username to authenticate as.
|
|
User string
|
|
|
|
// Auth contains possible authentication methods to use with the
|
|
// server. Only the first instance of a particular RFC 4252 method will
|
|
// be used during authentication.
|
|
//
|
|
// If AuthCallback is set, these AuthMethod are only used if the
|
|
// callback returns nil.
|
|
Auth []AuthMethod
|
|
|
|
// HostKeyCallback is called during the cryptographic
|
|
// handshake to validate the server's host key. The client
|
|
// configuration must supply this callback for the connection
|
|
// to succeed. The functions InsecureIgnoreHostKey or
|
|
// FixedHostKey can be used for simplistic host key checks.
|
|
HostKeyCallback HostKeyCallback
|
|
|
|
// BannerCallback is called during the SSH dance to display a custom
|
|
// server's message. The client configuration can supply this callback to
|
|
// handle it as wished. The function BannerDisplayStderr can be used for
|
|
// simplistic display on Stderr.
|
|
BannerCallback BannerCallback
|
|
|
|
// ClientVersion contains the version identification string that will
|
|
// be used for the connection. If empty, a reasonable default is used.
|
|
ClientVersion string
|
|
|
|
// HostKeyAlgorithms lists the public key algorithms that the client will
|
|
// accept from the server for host key authentication, in order of
|
|
// preference. If empty, a reasonable default is used. Any
|
|
// string returned from a PublicKey.Type method may be used, or
|
|
// any of the CertAlgo and KeyAlgo constants.
|
|
HostKeyAlgorithms []string
|
|
|
|
// Timeout is the maximum amount of time for the TCP connection to establish.
|
|
//
|
|
// A Timeout of zero means no timeout.
|
|
Timeout time.Duration
|
|
|
|
// AuthCallback, if non-nil, is invoked before each authentication attempt.
|
|
AuthCallback ClientAuthCallback
|
|
}
|
|
|
|
// InsecureIgnoreHostKey returns a function that can be used for
|
|
// ClientConfig.HostKeyCallback to accept any host key. It should
|
|
// not be used for production code.
|
|
func InsecureIgnoreHostKey() HostKeyCallback {
|
|
return func(hostname string, remote net.Addr, key PublicKey) error {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type fixedHostKey struct {
|
|
key PublicKey
|
|
}
|
|
|
|
func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error {
|
|
if f.key == nil {
|
|
return fmt.Errorf("ssh: required host key was nil")
|
|
}
|
|
if !bytes.Equal(key.Marshal(), f.key.Marshal()) {
|
|
return fmt.Errorf("ssh: host key mismatch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FixedHostKey returns a function for use in
|
|
// ClientConfig.HostKeyCallback to accept only a specific host key.
|
|
func FixedHostKey(key PublicKey) HostKeyCallback {
|
|
hk := &fixedHostKey{key}
|
|
return hk.check
|
|
}
|
|
|
|
// BannerDisplayStderr returns a function that can be used for
|
|
// ClientConfig.BannerCallback to display banners on os.Stderr.
|
|
func BannerDisplayStderr() BannerCallback {
|
|
return func(banner string) error {
|
|
_, err := os.Stderr.WriteString(banner)
|
|
|
|
return err
|
|
}
|
|
}
|