ssh: fix support for partial success authentication responses in client

The existing client side authentication does not handle correctly
the partial success flag in SSH_MSG_USERAUTH_FAILURE authentication
responses.

This commit fixes two problems in ssh library:
1) RetryableAuthMethod() now breaks out from the retry loop and
   returns  when underlying auth method fails with partial success
   set to true.
2) Book keeping of tried (and failed) auth methods in
   clientAuthenticate() does not mark an auth method failed if it
   fails with partial success set to true.

Fixes golang/go#23461

Change-Id: Ib2e1a1d54bfe2549496199bb2f66ebbce58d130d
Reviewed-on: https://go-review.googlesource.com/88035
Reviewed-by: Han-Wen Nienhuys <hanwen@google.com>
Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
This commit is contained in:
Sami Pönkänen
2018-02-08 15:07:01 +00:00
committed by Han-Wen Nienhuys
parent d9133f5469
commit 9334d73e5f
5 changed files with 450 additions and 57 deletions
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2017 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.
// Tests for ssh client multi-auth
//
// These tests run a simple go ssh client against OpenSSH server
// over unix domain sockets. The tests use multiple combinations
// of password, keyboard-interactive and publickey authentication
// methods.
//
// A wrapper library for making sshd PAM authentication use test
// passwords is required in ./sshd_test_pw.so. If the library does
// not exist these tests will be skipped. See compile instructions
// (for linux) in file ./sshd_test_pw.c.
package test
import (
"fmt"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
// test cases
type multiAuthTestCase struct {
authMethods []string
expectedPasswordCbs int
expectedKbdIntCbs int
}
// test context
type multiAuthTestCtx struct {
password string
numPasswordCbs int
numKbdIntCbs int
}
// create test context
func newMultiAuthTestCtx(t *testing.T) *multiAuthTestCtx {
password, err := randomPassword()
if err != nil {
t.Fatalf("Failed to generate random test password: %s", err.Error())
}
return &multiAuthTestCtx{
password: password,
}
}
// password callback
func (ctx *multiAuthTestCtx) passwordCb() (secret string, err error) {
ctx.numPasswordCbs++
return ctx.password, nil
}
// keyboard-interactive callback
func (ctx *multiAuthTestCtx) kbdIntCb(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
if len(questions) == 0 {
return nil, nil
}
ctx.numKbdIntCbs++
if len(questions) == 1 {
return []string{ctx.password}, nil
}
return nil, fmt.Errorf("unsupported keyboard-interactive flow")
}
// TestMultiAuth runs several subtests for different combinations of password, keyboard-interactive and publickey authentication methods
func TestMultiAuth(t *testing.T) {
testCases := []multiAuthTestCase{
// Test password,publickey authentication, assert that password callback is called 1 time
multiAuthTestCase{
authMethods: []string{"password", "publickey"},
expectedPasswordCbs: 1,
},
// Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time
multiAuthTestCase{
authMethods: []string{"keyboard-interactive", "publickey"},
expectedKbdIntCbs: 1,
},
// Test publickey,password authentication, assert that password callback is called 1 time
multiAuthTestCase{
authMethods: []string{"publickey", "password"},
expectedPasswordCbs: 1,
},
// Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time
multiAuthTestCase{
authMethods: []string{"publickey", "keyboard-interactive"},
expectedKbdIntCbs: 1,
},
// Test password,password authentication, assert that password callback is called 2 times
multiAuthTestCase{
authMethods: []string{"password", "password"},
expectedPasswordCbs: 2,
},
}
for _, testCase := range testCases {
t.Run(strings.Join(testCase.authMethods, ","), func(t *testing.T) {
ctx := newMultiAuthTestCtx(t)
server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")})
defer server.Shutdown()
clientConfig := clientConfig()
server.setTestPassword(clientConfig.User, ctx.password)
publicKeyAuthMethod := clientConfig.Auth[0]
clientConfig.Auth = nil
for _, authMethod := range testCase.authMethods {
switch authMethod {
case "publickey":
clientConfig.Auth = append(clientConfig.Auth, publicKeyAuthMethod)
case "password":
clientConfig.Auth = append(clientConfig.Auth,
ssh.RetryableAuthMethod(ssh.PasswordCallback(ctx.passwordCb), 5))
case "keyboard-interactive":
clientConfig.Auth = append(clientConfig.Auth,
ssh.RetryableAuthMethod(ssh.KeyboardInteractive(ctx.kbdIntCb), 5))
default:
t.Fatalf("Unknown authentication method %s", authMethod)
}
}
conn := server.Dial(clientConfig)
defer conn.Close()
if ctx.numPasswordCbs != testCase.expectedPasswordCbs {
t.Fatalf("passwordCallback was called %d times, expected %d times", ctx.numPasswordCbs, testCase.expectedPasswordCbs)
}
if ctx.numKbdIntCbs != testCase.expectedKbdIntCbs {
t.Fatalf("keyboardInteractiveCallback was called %d times, expected %d times", ctx.numKbdIntCbs, testCase.expectedKbdIntCbs)
}
})
}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2017 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.
// sshd_test_pw.c
// Wrapper to inject test password data for sshd PAM authentication
//
// This wrapper implements custom versions of getpwnam, getpwnam_r,
// getspnam and getspnam_r. These functions first call their real
// libc versions, then check if the requested user matches test user
// specified in env variable TEST_USER and if so replace the password
// with crypted() value of TEST_PASSWD env variable.
//
// Compile:
// gcc -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
//
// Compile with debug:
// gcc -DVERBOSE -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
//
// Run sshd:
// LD_PRELOAD="sshd_test_pw.so" TEST_USER="..." TEST_PASSWD="..." sshd ...
// +build ignore
#define _GNU_SOURCE
#include <string.h>
#include <pwd.h>
#include <shadow.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#ifdef VERBOSE
#define DEBUG(X...) fprintf(stderr, X)
#else
#define DEBUG(X...) while (0) { }
#endif
/* crypt() password */
static char *
pwhash(char *passwd) {
return strdup(crypt(passwd, "$6$"));
}
/* Pointers to real functions in libc */
static struct passwd * (*real_getpwnam)(const char *) = NULL;
static int (*real_getpwnam_r)(const char *, struct passwd *, char *, size_t, struct passwd **) = NULL;
static struct spwd * (*real_getspnam)(const char *) = NULL;
static int (*real_getspnam_r)(const char *, struct spwd *, char *, size_t, struct spwd **) = NULL;
/* Cached test user and test password */
static char *test_user = NULL;
static char *test_passwd_hash = NULL;
static void
init(void) {
/* Fetch real libc function pointers */
real_getpwnam = dlsym(RTLD_NEXT, "getpwnam");
real_getpwnam_r = dlsym(RTLD_NEXT, "getpwnam_r");
real_getspnam = dlsym(RTLD_NEXT, "getspnam");
real_getspnam_r = dlsym(RTLD_NEXT, "getspnam_r");
/* abort if env variables are not defined */
if (getenv("TEST_USER") == NULL || getenv("TEST_PASSWD") == NULL) {
fprintf(stderr, "env variables TEST_USER and TEST_PASSWD are missing\n");
abort();
}
/* Fetch test user and test password from env */
test_user = strdup(getenv("TEST_USER"));
test_passwd_hash = pwhash(getenv("TEST_PASSWD"));
DEBUG("sshd_test_pw init():\n");
DEBUG("\treal_getpwnam: %p\n", real_getpwnam);
DEBUG("\treal_getpwnam_r: %p\n", real_getpwnam_r);
DEBUG("\treal_getspnam: %p\n", real_getspnam);
DEBUG("\treal_getspnam_r: %p\n", real_getspnam_r);
DEBUG("\tTEST_USER: '%s'\n", test_user);
DEBUG("\tTEST_PASSWD: '%s'\n", getenv("TEST_PASSWD"));
DEBUG("\tTEST_PASSWD_HASH: '%s'\n", test_passwd_hash);
}
static int
is_test_user(const char *name) {
if (test_user != NULL && strcmp(test_user, name) == 0)
return 1;
return 0;
}
/* getpwnam */
struct passwd *
getpwnam(const char *name) {
struct passwd *pw;
DEBUG("sshd_test_pw getpwnam(%s)\n", name);
if (real_getpwnam == NULL)
init();
if ((pw = real_getpwnam(name)) == NULL)
return NULL;
if (is_test_user(name))
pw->pw_passwd = strdup(test_passwd_hash);
return pw;
}
/* getpwnam_r */
int
getpwnam_r(const char *name,
struct passwd *pwd,
char *buf,
size_t buflen,
struct passwd **result) {
int r;
DEBUG("sshd_test_pw getpwnam_r(%s)\n", name);
if (real_getpwnam_r == NULL)
init();
if ((r = real_getpwnam_r(name, pwd, buf, buflen, result)) != 0 || *result == NULL)
return r;
if (is_test_user(name))
pwd->pw_passwd = strdup(test_passwd_hash);
return 0;
}
/* getspnam */
struct spwd *
getspnam(const char *name) {
struct spwd *sp;
DEBUG("sshd_test_pw getspnam(%s)\n", name);
if (real_getspnam == NULL)
init();
if ((sp = real_getspnam(name)) == NULL)
return NULL;
if (is_test_user(name))
sp->sp_pwdp = strdup(test_passwd_hash);
return sp;
}
/* getspnam_r */
int
getspnam_r(const char *name,
struct spwd *spbuf,
char *buf,
size_t buflen,
struct spwd **spbufp) {
int r;
DEBUG("sshd_test_pw getspnam_r(%s)\n", name);
if (real_getspnam_r == NULL)
init();
if ((r = real_getspnam_r(name, spbuf, buf, buflen, spbufp)) != 0)
return r;
if (is_test_user(name))
spbuf->sp_pwdp = strdup(test_passwd_hash);
return r;
}
+68 -5
View File
@@ -10,6 +10,8 @@ package test
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
@@ -25,7 +27,8 @@ import (
"golang.org/x/crypto/ssh/testdata"
)
const sshdConfig = `
const (
defaultSshdConfig = `
Protocol 2
Banner {{.Dir}}/banner
HostKey {{.Dir}}/id_rsa
@@ -50,8 +53,17 @@ RhostsRSAAuthentication no
HostbasedAuthentication no
PubkeyAcceptedKeyTypes=*
`
multiAuthSshdConfigTail = `
UsePAM yes
PasswordAuthentication yes
ChallengeResponseAuthentication yes
AuthenticationMethods {{.AuthMethods}}
`
)
var configTmpl = template.Must(template.New("").Parse(sshdConfig))
var configTmpl = map[string]*template.Template{
"default": template.Must(template.New("").Parse(defaultSshdConfig)),
"MultiAuth": template.Must(template.New("").Parse(defaultSshdConfig + multiAuthSshdConfigTail))}
type server struct {
t *testing.T
@@ -60,6 +72,10 @@ type server struct {
cmd *exec.Cmd
output bytes.Buffer // holds stderr from sshd process
testUser string // test username for sshd
testPasswd string // test password for sshd
sshdTestPwSo string // dynamic library to inject a custom password into sshd
// Client half of the network connection.
clientConn net.Conn
}
@@ -186,6 +202,20 @@ func (s *server) TryDialWithAddr(config *ssh.ClientConfig, addr string) (*ssh.Cl
s.cmd.Stdin = f
s.cmd.Stdout = f
s.cmd.Stderr = &s.output
if s.sshdTestPwSo != "" {
if s.testUser == "" {
s.t.Fatal("user missing from sshd_test_pw.so config")
}
if s.testPasswd == "" {
s.t.Fatal("password missing from sshd_test_pw.so config")
}
s.cmd.Env = append(os.Environ(),
fmt.Sprintf("LD_PRELOAD=%s", s.sshdTestPwSo),
fmt.Sprintf("TEST_USER=%s", s.testUser),
fmt.Sprintf("TEST_PASSWD=%s", s.testPasswd))
}
if err := s.cmd.Start(); err != nil {
s.t.Fail()
s.Shutdown()
@@ -236,8 +266,39 @@ func writeFile(path string, contents []byte) {
}
}
// generate random password
func randomPassword() (string, error) {
b := make([]byte, 12)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// setTestPassword is used for setting user and password data for sshd_test_pw.so
// This function also checks that ./sshd_test_pw.so exists and if not calls s.t.Skip()
func (s *server) setTestPassword(user, passwd string) error {
wd, _ := os.Getwd()
wrapper := filepath.Join(wd, "sshd_test_pw.so")
if _, err := os.Stat(wrapper); err != nil {
s.t.Skip(fmt.Errorf("sshd_test_pw.so is not available"))
return err
}
s.sshdTestPwSo = wrapper
s.testUser = user
s.testPasswd = passwd
return nil
}
// newServer returns a new mock ssh server.
func newServer(t *testing.T) *server {
return newServerForConfig(t, "default", map[string]string{})
}
// newServerForConfig returns a new mock ssh server.
func newServerForConfig(t *testing.T, config string, configVars map[string]string) *server {
if testing.Short() {
t.Skip("skipping test due to -short")
}
@@ -249,9 +310,11 @@ func newServer(t *testing.T) *server {
if err != nil {
t.Fatal(err)
}
err = configTmpl.Execute(f, map[string]string{
"Dir": dir,
})
if _, ok := configTmpl[config]; ok == false {
t.Fatal(fmt.Errorf("Invalid server config '%s'", config))
}
configVars["Dir"] = dir
err = configTmpl[config].Execute(f, configVars)
if err != nil {
t.Fatal(err)
}