Skip to content

Commit 6f79b5a

Browse files
drakkanpeterverraedt
authored andcommittedApr 3, 2024·
ssh: add server side multi-step authentication
Add support for sending back partial success to the client while handling authentication in the server. This is implemented by a special error that can be returned by any of the authentication methods, which contains the authentication methods to offer next. This patch is based on CL 399075 with some minor changes and the addition of test cases. Fixes golang/go#17889 Fixes golang/go#61447 Fixes golang/go#64974 Co-authored-by: Peter Verraedt <[email protected]> Change-Id: I05c8f913bb407d22c2e41c4cbe965e36ab4739b0 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/516355 Reviewed-by: Andrew Lytvynov <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]>
1 parent 8d0d405 commit 6f79b5a

File tree

2 files changed

+530
-50
lines changed

2 files changed

+530
-50
lines changed
 

‎ssh/server.go

+118-50
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,35 @@ func (l ServerAuthError) Error() string {
426426
return "[" + strings.Join(errs, ", ") + "]"
427427
}
428428

429+
// ServerAuthCallbacks defines server-side authentication callbacks.
430+
type ServerAuthCallbacks struct {
431+
// PasswordCallback behaves like [ServerConfig.PasswordCallback].
432+
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
433+
434+
// PublicKeyCallback behaves like [ServerConfig.PublicKeyCallback].
435+
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
436+
437+
// KeyboardInteractiveCallback behaves like [ServerConfig.KeyboardInteractiveCallback].
438+
KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)
439+
440+
// GSSAPIWithMICConfig behaves like [ServerConfig.GSSAPIWithMICConfig].
441+
GSSAPIWithMICConfig *GSSAPIWithMICConfig
442+
}
443+
444+
// PartialSuccessError can be returned by any of the [ServerConfig]
445+
// authentication callbacks to indicate to the client that authentication has
446+
// partially succeeded, but further steps are required.
447+
type PartialSuccessError struct {
448+
// Next defines the authentication callbacks to apply to further steps. The
449+
// available methods communicated to the client are based on the non-nil
450+
// ServerAuthCallbacks fields.
451+
Next ServerAuthCallbacks
452+
}
453+
454+
func (p *PartialSuccessError) Error() string {
455+
return "ssh: authenticated with partial success"
456+
}
457+
429458
// ErrNoAuth is the error value returned if no
430459
// authentication method has been passed yet. This happens as a normal
431460
// part of the authentication loop, since the client first tries
@@ -441,6 +470,15 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
441470
authFailures := 0
442471
var authErrs []error
443472
var displayedBanner bool
473+
partialSuccessReturned := false
474+
// Set the initial authentication callbacks from the config. They can be
475+
// changed if a PartialSuccessError is returned.
476+
authConfig := ServerAuthCallbacks{
477+
PasswordCallback: config.PasswordCallback,
478+
PublicKeyCallback: config.PublicKeyCallback,
479+
KeyboardInteractiveCallback: config.KeyboardInteractiveCallback,
480+
GSSAPIWithMICConfig: config.GSSAPIWithMICConfig,
481+
}
444482

445483
userAuthLoop:
446484
for {
@@ -471,6 +509,11 @@ userAuthLoop:
471509
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
472510
}
473511

512+
if s.user != userAuthReq.User && partialSuccessReturned {
513+
return nil, fmt.Errorf("ssh: client changed the user after a partial success authentication, previous user %q, current user %q",
514+
s.user, userAuthReq.User)
515+
}
516+
474517
s.user = userAuthReq.User
475518

476519
if !displayedBanner && config.BannerCallback != nil {
@@ -491,20 +534,17 @@ userAuthLoop:
491534

492535
switch userAuthReq.Method {
493536
case "none":
494-
if config.NoClientAuth {
537+
// We don't allow none authentication after a partial success
538+
// response.
539+
if config.NoClientAuth && !partialSuccessReturned {
495540
if config.NoClientAuthCallback != nil {
496541
perms, authErr = config.NoClientAuthCallback(s)
497542
} else {
498543
authErr = nil
499544
}
500545
}
501-
502-
// allow initial attempt of 'none' without penalty
503-
if authFailures == 0 {
504-
authFailures--
505-
}
506546
case "password":
507-
if config.PasswordCallback == nil {
547+
if authConfig.PasswordCallback == nil {
508548
authErr = errors.New("ssh: password auth not configured")
509549
break
510550
}
@@ -518,17 +558,17 @@ userAuthLoop:
518558
return nil, parseError(msgUserAuthRequest)
519559
}
520560

521-
perms, authErr = config.PasswordCallback(s, password)
561+
perms, authErr = authConfig.PasswordCallback(s, password)
522562
case "keyboard-interactive":
523-
if config.KeyboardInteractiveCallback == nil {
563+
if authConfig.KeyboardInteractiveCallback == nil {
524564
authErr = errors.New("ssh: keyboard-interactive auth not configured")
525565
break
526566
}
527567

528568
prompter := &sshClientKeyboardInteractive{s}
529-
perms, authErr = config.KeyboardInteractiveCallback(s, prompter.Challenge)
569+
perms, authErr = authConfig.KeyboardInteractiveCallback(s, prompter.Challenge)
530570
case "publickey":
531-
if config.PublicKeyCallback == nil {
571+
if authConfig.PublicKeyCallback == nil {
532572
authErr = errors.New("ssh: publickey auth not configured")
533573
break
534574
}
@@ -562,11 +602,18 @@ userAuthLoop:
562602
if !ok {
563603
candidate.user = s.user
564604
candidate.pubKeyData = pubKeyData
565-
candidate.perms, candidate.result = config.PublicKeyCallback(s, pubKey)
566-
if candidate.result == nil && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
567-
candidate.result = checkSourceAddress(
605+
candidate.perms, candidate.result = authConfig.PublicKeyCallback(s, pubKey)
606+
_, isPartialSuccessError := candidate.result.(*PartialSuccessError)
607+
608+
if (candidate.result == nil || isPartialSuccessError) &&
609+
candidate.perms != nil &&
610+
candidate.perms.CriticalOptions != nil &&
611+
candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
612+
if err := checkSourceAddress(
568613
s.RemoteAddr(),
569-
candidate.perms.CriticalOptions[sourceAddressCriticalOption])
614+
candidate.perms.CriticalOptions[sourceAddressCriticalOption]); err != nil {
615+
candidate.result = err
616+
}
570617
}
571618
cache.add(candidate)
572619
}
@@ -578,8 +625,8 @@ userAuthLoop:
578625
if len(payload) > 0 {
579626
return nil, parseError(msgUserAuthRequest)
580627
}
581-
582-
if candidate.result == nil {
628+
_, isPartialSuccessError := candidate.result.(*PartialSuccessError)
629+
if candidate.result == nil || isPartialSuccessError {
583630
okMsg := userAuthPubKeyOkMsg{
584631
Algo: algo,
585632
PubKey: pubKeyData,
@@ -629,11 +676,11 @@ userAuthLoop:
629676
perms = candidate.perms
630677
}
631678
case "gssapi-with-mic":
632-
if config.GSSAPIWithMICConfig == nil {
679+
if authConfig.GSSAPIWithMICConfig == nil {
633680
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
634681
break
635682
}
636-
gssapiConfig := config.GSSAPIWithMICConfig
683+
gssapiConfig := authConfig.GSSAPIWithMICConfig
637684
userAuthRequestGSSAPI, err := parseGSSAPIPayload(userAuthReq.Payload)
638685
if err != nil {
639686
return nil, parseError(msgUserAuthRequest)
@@ -689,49 +736,70 @@ userAuthLoop:
689736
break userAuthLoop
690737
}
691738

692-
authFailures++
693-
if config.MaxAuthTries > 0 && authFailures >= config.MaxAuthTries {
694-
// If we have hit the max attempts, don't bother sending the
695-
// final SSH_MSG_USERAUTH_FAILURE message, since there are
696-
// no more authentication methods which can be attempted,
697-
// and this message may cause the client to re-attempt
698-
// authentication while we send the disconnect message.
699-
// Continue, and trigger the disconnect at the start of
700-
// the loop.
701-
//
702-
// The SSH specification is somewhat confusing about this,
703-
// RFC 4252 Section 5.1 requires each authentication failure
704-
// be responded to with a respective SSH_MSG_USERAUTH_FAILURE
705-
// message, but Section 4 says the server should disconnect
706-
// after some number of attempts, but it isn't explicit which
707-
// message should take precedence (i.e. should there be a failure
708-
// message than a disconnect message, or if we are going to
709-
// disconnect, should we only send that message.)
710-
//
711-
// Either way, OpenSSH disconnects immediately after the last
712-
// failed authnetication attempt, and given they are typically
713-
// considered the golden implementation it seems reasonable
714-
// to match that behavior.
715-
continue
739+
var failureMsg userAuthFailureMsg
740+
741+
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
742+
// After a partial success error we don't allow changing the user
743+
// name and execute the NoClientAuthCallback.
744+
partialSuccessReturned = true
745+
746+
// In case a partial success is returned, the server may send
747+
// a new set of authentication methods.
748+
authConfig = partialSuccess.Next
749+
750+
// Reset pubkey cache, as the new PublicKeyCallback might
751+
// accept a different set of public keys.
752+
cache = pubKeyCache{}
753+
754+
// Send back a partial success message to the user.
755+
failureMsg.PartialSuccess = true
756+
} else {
757+
// Allow initial attempt of 'none' without penalty.
758+
if authFailures > 0 || userAuthReq.Method != "none" {
759+
authFailures++
760+
}
761+
if config.MaxAuthTries > 0 && authFailures >= config.MaxAuthTries {
762+
// If we have hit the max attempts, don't bother sending the
763+
// final SSH_MSG_USERAUTH_FAILURE message, since there are
764+
// no more authentication methods which can be attempted,
765+
// and this message may cause the client to re-attempt
766+
// authentication while we send the disconnect message.
767+
// Continue, and trigger the disconnect at the start of
768+
// the loop.
769+
//
770+
// The SSH specification is somewhat confusing about this,
771+
// RFC 4252 Section 5.1 requires each authentication failure
772+
// be responded to with a respective SSH_MSG_USERAUTH_FAILURE
773+
// message, but Section 4 says the server should disconnect
774+
// after some number of attempts, but it isn't explicit which
775+
// message should take precedence (i.e. should there be a failure
776+
// message than a disconnect message, or if we are going to
777+
// disconnect, should we only send that message.)
778+
//
779+
// Either way, OpenSSH disconnects immediately after the last
780+
// failed authnetication attempt, and given they are typically
781+
// considered the golden implementation it seems reasonable
782+
// to match that behavior.
783+
continue
784+
}
716785
}
717786

718-
var failureMsg userAuthFailureMsg
719-
if config.PasswordCallback != nil {
787+
if authConfig.PasswordCallback != nil {
720788
failureMsg.Methods = append(failureMsg.Methods, "password")
721789
}
722-
if config.PublicKeyCallback != nil {
790+
if authConfig.PublicKeyCallback != nil {
723791
failureMsg.Methods = append(failureMsg.Methods, "publickey")
724792
}
725-
if config.KeyboardInteractiveCallback != nil {
793+
if authConfig.KeyboardInteractiveCallback != nil {
726794
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
727795
}
728-
if config.GSSAPIWithMICConfig != nil && config.GSSAPIWithMICConfig.Server != nil &&
729-
config.GSSAPIWithMICConfig.AllowLogin != nil {
796+
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
797+
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
730798
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
731799
}
732800

733801
if len(failureMsg.Methods) == 0 {
734-
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
802+
return nil, errors.New("ssh: no authentication methods available")
735803
}
736804

737805
if err := s.transport.writePacket(Marshal(&failureMsg)); err != nil {

‎ssh/server_multi_auth_test.go

+412
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,412 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package ssh
6+
7+
import (
8+
"bytes"
9+
"errors"
10+
"fmt"
11+
"strings"
12+
"testing"
13+
)
14+
15+
func doClientServerAuth(t *testing.T, serverConfig *ServerConfig, clientConfig *ClientConfig) ([]error, error) {
16+
c1, c2, err := netPipe()
17+
if err != nil {
18+
t.Fatalf("netPipe: %v", err)
19+
}
20+
defer c1.Close()
21+
defer c2.Close()
22+
23+
var serverAuthErrors []error
24+
25+
serverConfig.AddHostKey(testSigners["rsa"])
26+
serverConfig.AuthLogCallback = func(conn ConnMetadata, method string, err error) {
27+
serverAuthErrors = append(serverAuthErrors, err)
28+
}
29+
go newServer(c1, serverConfig)
30+
c, _, _, err := NewClientConn(c2, "", clientConfig)
31+
if err == nil {
32+
c.Close()
33+
}
34+
return serverAuthErrors, err
35+
}
36+
37+
func TestMultiStepAuth(t *testing.T) {
38+
// This user can login with password, public key or public key + password.
39+
username := "testuser"
40+
// This user can login with public key + password only.
41+
usernameSecondFactor := "testuser_second_factor"
42+
errPwdAuthFailed := errors.New("password auth failed")
43+
errWrongSequence := errors.New("wrong sequence")
44+
45+
serverConfig := &ServerConfig{
46+
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
47+
if conn.User() == usernameSecondFactor {
48+
return nil, errWrongSequence
49+
}
50+
if conn.User() == username && string(password) == clientPassword {
51+
return nil, nil
52+
}
53+
return nil, errPwdAuthFailed
54+
},
55+
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
56+
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
57+
if conn.User() == usernameSecondFactor {
58+
return nil, &PartialSuccessError{
59+
Next: ServerAuthCallbacks{
60+
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
61+
if string(password) == clientPassword {
62+
return nil, nil
63+
}
64+
return nil, errPwdAuthFailed
65+
},
66+
},
67+
}
68+
}
69+
return nil, nil
70+
}
71+
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
72+
},
73+
}
74+
75+
clientConfig := &ClientConfig{
76+
User: usernameSecondFactor,
77+
Auth: []AuthMethod{
78+
PublicKeys(testSigners["rsa"]),
79+
Password(clientPassword),
80+
},
81+
HostKeyCallback: InsecureIgnoreHostKey(),
82+
}
83+
84+
serverAuthErrors, err := doClientServerAuth(t, serverConfig, clientConfig)
85+
if err != nil {
86+
t.Fatalf("client login error: %s", err)
87+
}
88+
89+
// The error sequence is:
90+
// - no auth passed yet
91+
// - partial success
92+
// - nil
93+
if len(serverAuthErrors) != 3 {
94+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
95+
}
96+
if _, ok := serverAuthErrors[1].(*PartialSuccessError); !ok {
97+
t.Fatalf("expected partial success error, got: %v", serverAuthErrors[1])
98+
}
99+
// Now test a wrong sequence.
100+
clientConfig.Auth = []AuthMethod{
101+
Password(clientPassword),
102+
PublicKeys(testSigners["rsa"]),
103+
}
104+
105+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
106+
if err == nil {
107+
t.Fatal("client login with wrong sequence must fail")
108+
}
109+
// The error sequence is:
110+
// - no auth passed yet
111+
// - wrong sequence
112+
// - partial success
113+
if len(serverAuthErrors) != 3 {
114+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
115+
}
116+
if serverAuthErrors[1] != errWrongSequence {
117+
t.Fatal("server not returned wrong sequence")
118+
}
119+
if _, ok := serverAuthErrors[2].(*PartialSuccessError); !ok {
120+
t.Fatalf("expected partial success error, got: %v", serverAuthErrors[2])
121+
}
122+
// Now test using a correct sequence but a wrong password before the right
123+
// one.
124+
n := 0
125+
passwords := []string{"WRONG", "WRONG", clientPassword}
126+
clientConfig.Auth = []AuthMethod{
127+
PublicKeys(testSigners["rsa"]),
128+
RetryableAuthMethod(PasswordCallback(func() (string, error) {
129+
p := passwords[n]
130+
n++
131+
return p, nil
132+
}), 3),
133+
}
134+
135+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
136+
if err != nil {
137+
t.Fatalf("client login error: %s", err)
138+
}
139+
// The error sequence is:
140+
// - no auth passed yet
141+
// - partial success
142+
// - wrong password
143+
// - wrong password
144+
// - nil
145+
if len(serverAuthErrors) != 5 {
146+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
147+
}
148+
if _, ok := serverAuthErrors[1].(*PartialSuccessError); !ok {
149+
t.Fatal("server not returned partial success")
150+
}
151+
if serverAuthErrors[2] != errPwdAuthFailed {
152+
t.Fatal("server not returned password authentication failed")
153+
}
154+
if serverAuthErrors[3] != errPwdAuthFailed {
155+
t.Fatal("server not returned password authentication failed")
156+
}
157+
// Only password authentication should fail.
158+
clientConfig.Auth = []AuthMethod{
159+
Password(clientPassword),
160+
}
161+
162+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
163+
if err == nil {
164+
t.Fatal("client login with password only must fail")
165+
}
166+
// The error sequence is:
167+
// - no auth passed yet
168+
// - wrong sequence
169+
if len(serverAuthErrors) != 2 {
170+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
171+
}
172+
if serverAuthErrors[1] != errWrongSequence {
173+
t.Fatal("server not returned wrong sequence")
174+
}
175+
176+
// Only public key authentication should fail.
177+
clientConfig.Auth = []AuthMethod{
178+
PublicKeys(testSigners["rsa"]),
179+
}
180+
181+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
182+
if err == nil {
183+
t.Fatal("client login with public key only must fail")
184+
}
185+
// The error sequence is:
186+
// - no auth passed yet
187+
// - partial success
188+
if len(serverAuthErrors) != 2 {
189+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
190+
}
191+
if _, ok := serverAuthErrors[1].(*PartialSuccessError); !ok {
192+
t.Fatal("server not returned partial success")
193+
}
194+
195+
// Public key and wrong password.
196+
clientConfig.Auth = []AuthMethod{
197+
PublicKeys(testSigners["rsa"]),
198+
Password("WRONG"),
199+
}
200+
201+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
202+
if err == nil {
203+
t.Fatal("client login with wrong password after public key must fail")
204+
}
205+
// The error sequence is:
206+
// - no auth passed yet
207+
// - partial success
208+
// - password auth failed
209+
if len(serverAuthErrors) != 3 {
210+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
211+
}
212+
if _, ok := serverAuthErrors[1].(*PartialSuccessError); !ok {
213+
t.Fatal("server not returned partial success")
214+
}
215+
if serverAuthErrors[2] != errPwdAuthFailed {
216+
t.Fatal("server not returned password authentication failed")
217+
}
218+
219+
// Public key, public key again and then correct password. Public key
220+
// authentication is attempted only once because the partial success error
221+
// returns only "password" as the allowed authentication method.
222+
clientConfig.Auth = []AuthMethod{
223+
PublicKeys(testSigners["rsa"]),
224+
PublicKeys(testSigners["rsa"]),
225+
Password(clientPassword),
226+
}
227+
228+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
229+
if err != nil {
230+
t.Fatalf("client login error: %s", err)
231+
}
232+
// The error sequence is:
233+
// - no auth passed yet
234+
// - partial success
235+
// - nil
236+
if len(serverAuthErrors) != 3 {
237+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
238+
}
239+
if _, ok := serverAuthErrors[1].(*PartialSuccessError); !ok {
240+
t.Fatal("server not returned partial success")
241+
}
242+
243+
// The unrestricted username can do anything
244+
clientConfig = &ClientConfig{
245+
User: username,
246+
Auth: []AuthMethod{
247+
PublicKeys(testSigners["rsa"]),
248+
Password(clientPassword),
249+
},
250+
HostKeyCallback: InsecureIgnoreHostKey(),
251+
}
252+
253+
_, err = doClientServerAuth(t, serverConfig, clientConfig)
254+
if err != nil {
255+
t.Fatalf("unrestricted client login error: %s", err)
256+
}
257+
258+
clientConfig = &ClientConfig{
259+
User: username,
260+
Auth: []AuthMethod{
261+
PublicKeys(testSigners["rsa"]),
262+
},
263+
HostKeyCallback: InsecureIgnoreHostKey(),
264+
}
265+
266+
_, err = doClientServerAuth(t, serverConfig, clientConfig)
267+
if err != nil {
268+
t.Fatalf("unrestricted client login error: %s", err)
269+
}
270+
271+
clientConfig = &ClientConfig{
272+
User: username,
273+
Auth: []AuthMethod{
274+
Password(clientPassword),
275+
},
276+
HostKeyCallback: InsecureIgnoreHostKey(),
277+
}
278+
279+
_, err = doClientServerAuth(t, serverConfig, clientConfig)
280+
if err != nil {
281+
t.Fatalf("unrestricted client login error: %s", err)
282+
}
283+
}
284+
285+
func TestDynamicAuthCallbacks(t *testing.T) {
286+
user1 := "user1"
287+
user2 := "user2"
288+
errInvalidCredentials := errors.New("invalid credentials")
289+
290+
serverConfig := &ServerConfig{
291+
NoClientAuth: true,
292+
NoClientAuthCallback: func(conn ConnMetadata) (*Permissions, error) {
293+
switch conn.User() {
294+
case user1:
295+
return nil, &PartialSuccessError{
296+
Next: ServerAuthCallbacks{
297+
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
298+
if conn.User() == user1 && string(password) == clientPassword {
299+
return nil, nil
300+
}
301+
return nil, errInvalidCredentials
302+
},
303+
},
304+
}
305+
case user2:
306+
return nil, &PartialSuccessError{
307+
Next: ServerAuthCallbacks{
308+
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
309+
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
310+
if conn.User() == user2 {
311+
return nil, nil
312+
}
313+
}
314+
return nil, errInvalidCredentials
315+
},
316+
},
317+
}
318+
default:
319+
return nil, errInvalidCredentials
320+
}
321+
},
322+
}
323+
324+
clientConfig := &ClientConfig{
325+
User: user1,
326+
Auth: []AuthMethod{
327+
Password(clientPassword),
328+
},
329+
HostKeyCallback: InsecureIgnoreHostKey(),
330+
}
331+
332+
serverAuthErrors, err := doClientServerAuth(t, serverConfig, clientConfig)
333+
if err != nil {
334+
t.Fatalf("client login error: %s", err)
335+
}
336+
// The error sequence is:
337+
// - partial success
338+
// - nil
339+
if len(serverAuthErrors) != 2 {
340+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
341+
}
342+
if _, ok := serverAuthErrors[0].(*PartialSuccessError); !ok {
343+
t.Fatal("server not returned partial success")
344+
}
345+
346+
clientConfig = &ClientConfig{
347+
User: user2,
348+
Auth: []AuthMethod{
349+
PublicKeys(testSigners["rsa"]),
350+
},
351+
HostKeyCallback: InsecureIgnoreHostKey(),
352+
}
353+
354+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
355+
if err != nil {
356+
t.Fatalf("client login error: %s", err)
357+
}
358+
// The error sequence is:
359+
// - partial success
360+
// - nil
361+
if len(serverAuthErrors) != 2 {
362+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
363+
}
364+
if _, ok := serverAuthErrors[0].(*PartialSuccessError); !ok {
365+
t.Fatal("server not returned partial success")
366+
}
367+
368+
// user1 cannot login with public key
369+
clientConfig = &ClientConfig{
370+
User: user1,
371+
Auth: []AuthMethod{
372+
PublicKeys(testSigners["rsa"]),
373+
},
374+
HostKeyCallback: InsecureIgnoreHostKey(),
375+
}
376+
377+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
378+
if err == nil {
379+
t.Fatal("user1 login with public key must fail")
380+
}
381+
if !strings.Contains(err.Error(), "no supported methods remain") {
382+
t.Errorf("got %v, expected 'no supported methods remain'", err)
383+
}
384+
if len(serverAuthErrors) != 1 {
385+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
386+
}
387+
if _, ok := serverAuthErrors[0].(*PartialSuccessError); !ok {
388+
t.Fatal("server not returned partial success")
389+
}
390+
// user2 cannot login with password
391+
clientConfig = &ClientConfig{
392+
User: user2,
393+
Auth: []AuthMethod{
394+
Password(clientPassword),
395+
},
396+
HostKeyCallback: InsecureIgnoreHostKey(),
397+
}
398+
399+
serverAuthErrors, err = doClientServerAuth(t, serverConfig, clientConfig)
400+
if err == nil {
401+
t.Fatal("user2 login with password must fail")
402+
}
403+
if !strings.Contains(err.Error(), "no supported methods remain") {
404+
t.Errorf("got %v, expected 'no supported methods remain'", err)
405+
}
406+
if len(serverAuthErrors) != 1 {
407+
t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors)
408+
}
409+
if _, ok := serverAuthErrors[0].(*PartialSuccessError); !ok {
410+
t.Fatal("server not returned partial success")
411+
}
412+
}

0 commit comments

Comments
 (0)
Please sign in to comment.