| // 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. | |
| //go:build linux | |
| 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 | |
| { | |
| authMethods: []string{"password", "publickey"}, | |
| expectedPasswordCbs: 1, | |
| }, | |
| // Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time | |
| { | |
| authMethods: []string{"keyboard-interactive", "publickey"}, | |
| expectedKbdIntCbs: 1, | |
| }, | |
| // Test publickey,password authentication, assert that password callback is called 1 time | |
| { | |
| authMethods: []string{"publickey", "password"}, | |
| expectedPasswordCbs: 1, | |
| }, | |
| // Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time | |
| { | |
| authMethods: []string{"publickey", "keyboard-interactive"}, | |
| expectedKbdIntCbs: 1, | |
| }, | |
| // Test password,password authentication, assert that password callback is called 2 times | |
| { | |
| 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, ",")}) | |
| 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) | |
| } | |
| }) | |
| } | |
| } |