Closed
Description
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