x/crypto/ssh: wrapping ssh.KeyboardInteractive() into ssh.RetryableAuthMethod() fails to handle early auth errors with Windows OpenSSH server · Issue #67855 · golang/go (original) (raw)
Go version
go version go1.22.3 linux/amd64
Output of go env
in your module/workspace:
GO111MODULE='' GOARCH='amd64' GOBIN='' GOCACHE='/home/xxx/.cache/go-build' GOENV='/home/xxx/.config/go/env' GOEXE='' GOEXPERIMENT='' GOFLAGS='' GOHOSTARCH='amd64' GOHOSTOS='linux' GOINSECURE='' GOMODCACHE='/home/xxx/go/pkg/mod' GONOPROXY='' GONOSUMDB='' GOOS='linux' GOPATH='/home/xxx/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.3' GCCGO='gccgo' GOAMD64='v1' AR='ar' CC='gcc' CXX='g++' CGO_ENABLED='1' GOMOD='/home/xxx/go/src/github.com/samiponkanen/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-build2434574384=/tmp/go-build -gno-record-gcc-switches'
What did you do?
It seems that Windows OpenSSH server behaves incorrectly w.r.t keyboard-interactive authentication:
$ ssh -vvv -o "PubkeyAuthentication no" -o "PasswordAuthentication no" user@10.1.102.148
OpenSSH_9.1p1, OpenSSL 3.0.2 15 Mar 2022
...
debug1: Local version string SSH-2.0-OpenSSH_9.1
debug1: Remote protocol version 2.0, remote software version OpenSSH_for_Windows_8.1
debug1: compat_banner: match: OpenSSH_for_Windows_8.1 pat OpenSSH* compat 0x04000000
...
debug3: receive packet: type 6
debug2: service_accept: ssh-userauth
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug3: send packet: type 50
debug3: receive packet: type 51
debug1: Authentications that can continue: publickey,password,keyboard-interactive
debug3: start over, passed a different list publickey,password,keyboard-interactive
debug3: preferred keyboard-interactive
debug3: authmethod_lookup keyboard-interactive
debug3: remaining preferred:
debug3: authmethod_is_enabled keyboard-interactive
debug1: Next authentication method: keyboard-interactive
debug2: userauth_kbdint
debug3: send packet: type 50
debug2: we sent a keyboard-interactive packet, wait for reply
debug3: receive packet: type 51
debug1: Authentications that can continue: publickey,password,keyboard-interactive
debug3: userauth_kbdint: disable: no info_req_seen
debug2: we did not send a packet, disable method
debug1: No more authentication methods to try.
user@10.1.102.148: Permission denied (publickey,password,keyboard-interactive).
When trying to connect to such host using a golang client that uses ssh.KeyboardInteractive() wrapped a into ssh.RetryableAuthMethod(), then ssh.RetryableAuthMethod() will retry ssh.KeyboardInteractive() even if the failure happens so early that password is never prompted from the user.
What did you see happen?
package main
import (
"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)
cfg := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.RetryableAuthMethod(ssh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
log.Printf("KeyboardInteractive()")
return []string{"notaverysecretpassword"}, nil
}), 6),
ssh.RetryableAuthMethod(ssh.PasswordCallback(func() (secret string, err error) {
log.Printf("PasswordCallback()")
return "notaverysecretpassword", nil
}), 6),
},
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()
}
Running this test client against a Windows OpenSSH server (and assuming MaxAuthTries is 6) reveals that neither KeyboardInteractive nor PasswordCallback is called:
$ ./testclient user@10.1.102.148
2024/06/06 12:02:05 connecting to user@10.1.102.148:22
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures
What did you expect to see?
Expected result is that PasswordCallback gets called:
$ ./testclient user@10.1.102.148
2024/06/06 12:02:42 connecting to user@10.1.102.148:22
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:42 PasswordCallback()
2024/06/06 12:02:43 PasswordCallback()
2024/06/06 12:02:43 PasswordCallback()
ssh: handshake failed: ssh: disconnect, reason 2: Too many authentication failures