Skip to content

x/crypto/ssh: client auth loop calls PasswordCallback after receiving disconnect msg from server #66991

Closed
@samiponkanenssh

Description

@samiponkanenssh

Go version

go version go1.22.2 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/xxxx/.cache/go-build'
GOENV='/home/xxxx/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/xxxx/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/xxxx/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/lib/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/lib/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.22.2'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/xxxx/go/src/github.com/samiponkanenssh/crypto/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1562252138=/tmp/go-build -gno-record-gcc-switches'

What did you do?

When connecting with ssh.ClientConfig having publickey and password authentication enabled, and a OpenSSH server not accepting the client's publickey, and server using MaxAuthTries 1, then the golang ssh client still calls PasswordCallback() unexpectedly.

This happens because client auth loop does not exit immediately (in ssh/client_auth.go:74) when receiving a disconnect msg from server, but instead continues to try the next auth method. If that auth method is PasswordCallback, then the callback is called even though the server has already disconnected. This leads to weird UX because where the end user may get prompted for password followed by immediate failure.

The following code highlights the problem:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"log"
	"net"
	"os"
	"strings"

	"golang.org/x/crypto/ssh"
)

func main() {
	exit := func(v interface{}) {
		l := log.New(os.Stderr, "", 0)
		l.Printf("%v\n", v)
		os.Exit(-1)
	}

	args := os.Args[1:]
	if len(args) != 1 {
		exit("missing destination")
	}
	idx := strings.LastIndex(args[0], "@")
	if idx == -1 {
		exit("destination does not contain username")
	}
	user := args[0][:idx]
	dst := args[0][idx+1:]
	host, port, err := net.SplitHostPort(dst)
	if err != nil || port == "" {
		host = dst
		port = "22"
	}
	dst = net.JoinHostPort(host, port)

	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		exit(err)
	}

	signer, err := ssh.NewSignerFromKey(key)
	if err != nil {
		exit(err)
	}

	cfg := &ssh.ClientConfig{
		User: user,
		Auth: []ssh.AuthMethod{
			//ssh.PublicKeys(signer),
			ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
				log.Printf("PublicKeysCallback()")
				return []ssh.Signer{signer}, nil
			}),
			ssh.PasswordCallback(func() (secret string, err error) {
				log.Printf("PasswordCallback()")
				return "notaverysecretpassword", nil
			}),
			ssh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
				log.Printf("KeyboardInteractive()")
				return []string{"notaverysecretpassword"}, nil
			}),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	log.Printf("connecting to %s@%s", user, dst)
	conn, err := ssh.Dial("tcp", dst, cfg)
	if err != nil {
		exit(err)
	}
	conn.Close()
}

What did you see happen?

$ ./sshclient [email protected]
2024/04/23 14:12:58 connecting to [email protected]:22
2024/04/23 14:12:58 PublicKeysCallback()
2024/04/23 14:12:58 PasswordCallback()
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures

What did you expect to see?

$ ./sshclient [email protected]
2024/04/23 14:13:49 connecting to [email protected]:22
2024/04/23 14:13:49 PublicKeysCallback()
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions