| // Copyright 2023 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. | |
| package ssh | |
| import ( | |
| "bytes" | |
| "crypto/ecdsa" | |
| "crypto/elliptic" | |
| "crypto/rand" | |
| "crypto/sha256" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "math/big" | |
| "net" | |
| "reflect" | |
| "slices" | |
| "strings" | |
| "sync/atomic" | |
| "testing" | |
| "time" | |
| ) | |
| func TestClientAuthRestrictedPublicKeyAlgos(t *testing.T) { | |
| for _, tt := range []struct { | |
| name string | |
| key Signer | |
| wantError bool | |
| }{ | |
| {"rsa", testSigners["rsa"], false}, | |
| {"dsa", testSigners["dsa"], true}, | |
| {"ed25519", testSigners["ed25519"], true}, | |
| } { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyAuthAlgorithms: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512}, | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["ecdsap256"]) | |
| done := make(chan struct{}) | |
| go func() { | |
| defer close(done) | |
| NewServerConn(c1, serverConf) | |
| }() | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(tt.key), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| if !tt.wantError { | |
| t.Errorf("%s: got unexpected error %q", tt.name, err.Error()) | |
| } | |
| } else if tt.wantError { | |
| t.Errorf("%s: succeeded, but want error", tt.name) | |
| } | |
| <-done | |
| } | |
| } | |
| func TestMaxAuthTriesNoneMethod(t *testing.T) { | |
| username := "testuser" | |
| serverConfig := &ServerConfig{ | |
| MaxAuthTries: 2, | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if conn.User() == username && string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| var serverAuthErrors []error | |
| serverConfig.AddHostKey(testSigners["rsa"]) | |
| serverConfig.AuthLogCallback = func(conn ConnMetadata, method string, err error) { | |
| serverAuthErrors = append(serverAuthErrors, err) | |
| } | |
| go newServer(c1, serverConfig) | |
| clientConfig := ClientConfig{ | |
| User: username, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| clientConfig.SetDefaults() | |
| // Our client will send 'none' auth only once, so we need to send the | |
| // requests manually. | |
| c := &connection{ | |
| sshConn: sshConn{ | |
| conn: c2, | |
| user: username, | |
| clientVersion: []byte(packageVersion), | |
| }, | |
| } | |
| c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion) | |
| if err != nil { | |
| t.Fatalf("unable to exchange version: %v", err) | |
| } | |
| c.transport = newClientTransport( | |
| newTransport(c.sshConn.conn, clientConfig.Rand, true /* is client */), | |
| c.clientVersion, c.serverVersion, &clientConfig, "", c.sshConn.RemoteAddr()) | |
| if err := c.transport.waitSession(); err != nil { | |
| t.Fatalf("unable to wait session: %v", err) | |
| } | |
| c.sessionID = c.transport.getSessionID() | |
| if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil { | |
| t.Fatalf("unable to send ssh-userauth message: %v", err) | |
| } | |
| packet, err := c.transport.readPacket() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(packet) > 0 && packet[0] == msgExtInfo { | |
| packet, err = c.transport.readPacket() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| var serviceAccept serviceAcceptMsg | |
| if err := Unmarshal(packet, &serviceAccept); err != nil { | |
| t.Fatal(err) | |
| } | |
| for i := 0; i <= serverConfig.MaxAuthTries; i++ { | |
| auth := new(noneAuth) | |
| _, _, err := auth.auth(c.sessionID, clientConfig.User, c.transport, clientConfig.Rand, nil) | |
| if i < serverConfig.MaxAuthTries { | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| continue | |
| } | |
| if err == nil { | |
| t.Fatal("client: got no error") | |
| } else if !strings.Contains(err.Error(), "too many authentication failures") { | |
| t.Fatalf("client: got unexpected error: %v", err) | |
| } | |
| } | |
| if len(serverAuthErrors) != 3 { | |
| t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors) | |
| } | |
| for _, err := range serverAuthErrors { | |
| if !errors.Is(err, ErrNoAuth) { | |
| t.Errorf("go error: %v; want: %v", err, ErrNoAuth) | |
| } | |
| } | |
| } | |
| func TestMaxAuthServerAttempts(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| invocations := 0 | |
| clientConf := &ClientConfig{ | |
| User: "user", | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) { | |
| invocations++ | |
| return PublicKeys(testSigners["rsa"]), nil | |
| }, | |
| } | |
| go NewServerConn(c1, alwaysPartialPubKeyServer()) | |
| _, _, _, err = NewClientConn(c2, "", clientConf) | |
| if err == nil { | |
| t.Fatal("expected the server to disconnect after exceeding the attempts cap") | |
| } | |
| // The error must be the server's disconnect, not the client-side | |
| // "too many authentication attempts (N), aborting" bound. The | |
| // disconnect format is set by disconnectMsg.Error(). | |
| if !strings.Contains(err.Error(), "ssh: disconnect") || | |
| !strings.Contains(err.Error(), "too many authentication attempts") { | |
| t.Fatalf("expected server disconnect, got: %v", err) | |
| } | |
| // Server-side request sequence per AuthCallback iteration: | |
| // 1 "none" (initial, before any AuthCallback) | |
| // 2 pubkey query (AuthCallback #k, server returns OK) | |
| // 3 pubkey signed (AuthCallback #k, server returns partial) | |
| // So after k complete callbacks the server has processed 1+2k requests. | |
| // The (k+1)-th callback's query is processed as request number 2(k+1); | |
| // the subsequent signed request is rejected by the cap check | |
| // (authAttempts >= maxAuthServerAttempts), so 2(k+1) = maxAuthServerAttempts | |
| // and invocations = maxAuthServerAttempts / 2. | |
| expected := maxAuthServerAttempts / 2 | |
| if invocations != expected { | |
| t.Errorf("AuthCallback invoked %d times; want %d", invocations, expected) | |
| } | |
| } | |
| func TestMaxAuthTriesFirstNoneAuthErrorIgnored(t *testing.T) { | |
| username := "testuser" | |
| serverConfig := &ServerConfig{ | |
| MaxAuthTries: 1, | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if conn.User() == username && string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| clientConfig := &ClientConfig{ | |
| User: username, | |
| Auth: []AuthMethod{ | |
| Password(clientPassword), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| serverAuthErrors, err := doClientServerAuth(t, serverConfig, clientConfig) | |
| if err != nil { | |
| t.Fatalf("client login error: %s", err) | |
| } | |
| if len(serverAuthErrors) != 2 { | |
| t.Fatalf("unexpected number of server auth errors: %v, errors: %+v", len(serverAuthErrors), serverAuthErrors) | |
| } | |
| if !errors.Is(serverAuthErrors[0], ErrNoAuth) { | |
| t.Errorf("go error: %v; want: %v", serverAuthErrors[0], ErrNoAuth) | |
| } | |
| if serverAuthErrors[1] != nil { | |
| t.Errorf("unexpected error: %v", serverAuthErrors[1]) | |
| } | |
| } | |
| func TestNewServerConnValidationErrors(t *testing.T) { | |
| serverConf := &ServerConfig{ | |
| PublicKeyAuthAlgorithms: []string{CertAlgoRSAv01}, | |
| } | |
| c := &markerConn{} | |
| _, _, _, err := NewServerConn(c, serverConf) | |
| if err == nil { | |
| t.Fatal("NewServerConn with invalid public key auth algorithms succeeded") | |
| } | |
| if !c.isClosed() { | |
| t.Fatal("NewServerConn with invalid public key auth algorithms left connection open") | |
| } | |
| if c.isUsed() { | |
| t.Fatal("NewServerConn with invalid public key auth algorithms used connection") | |
| } | |
| serverConf = &ServerConfig{ | |
| Config: Config{ | |
| KeyExchanges: []string{KeyExchangeDHGEXSHA256}, | |
| }, | |
| } | |
| c = &markerConn{} | |
| _, _, _, err = NewServerConn(c, serverConf) | |
| if err == nil { | |
| t.Fatal("NewServerConn with unsupported key exchange succeeded") | |
| } | |
| if !c.isClosed() { | |
| t.Fatal("NewServerConn with unsupported key exchange left connection open") | |
| } | |
| if c.isUsed() { | |
| t.Fatal("NewServerConn with unsupported key exchange used connection") | |
| } | |
| } | |
| func TestBannerError(t *testing.T) { | |
| serverConfig := &ServerConfig{ | |
| BannerCallback: func(ConnMetadata) string { | |
| return "banner from BannerCallback" | |
| }, | |
| NoClientAuth: true, | |
| NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) { | |
| err := &BannerError{ | |
| Err: errors.New("error from NoClientAuthCallback"), | |
| Message: "banner from NoClientAuthCallback", | |
| } | |
| return nil, fmt.Errorf("wrapped: %w", err) | |
| }, | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| return &Permissions{}, nil | |
| }, | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, &BannerError{ | |
| Err: errors.New("error from PublicKeyCallback"), | |
| Message: "banner from PublicKeyCallback", | |
| } | |
| }, | |
| KeyboardInteractiveCallback: func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) { | |
| return nil, &BannerError{ | |
| Err: nil, // make sure that a nil inner error is allowed | |
| Message: "banner from KeyboardInteractiveCallback", | |
| } | |
| }, | |
| } | |
| serverConfig.AddHostKey(testSigners["rsa"]) | |
| var banners []string | |
| clientConfig := &ClientConfig{ | |
| User: "test", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) { | |
| return []string{"letmein"}, nil | |
| }), | |
| Password(clientPassword), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| BannerCallback: func(msg string) error { | |
| banners = append(banners, msg) | |
| return nil | |
| }, | |
| } | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| go newServer(c1, serverConfig) | |
| c, _, _, err := NewClientConn(c2, "", clientConfig) | |
| if err != nil { | |
| t.Fatalf("client connection failed: %v", err) | |
| } | |
| defer c.Close() | |
| wantBanners := []string{ | |
| "banner from BannerCallback", | |
| "banner from NoClientAuthCallback", | |
| "banner from PublicKeyCallback", | |
| "banner from KeyboardInteractiveCallback", | |
| } | |
| if !reflect.DeepEqual(banners, wantBanners) { | |
| t.Errorf("got banners:\n%q\nwant banners:\n%q", banners, wantBanners) | |
| } | |
| } | |
| func TestPublicKeyCallbackLastSeen(t *testing.T) { | |
| var lastSeenKey PublicKey | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| lastSeenKey = key | |
| fmt.Printf("seen %#v\n", key) | |
| if _, ok := key.(*dsaPublicKey); !ok { | |
| return nil, errors.New("nope") | |
| } | |
| return nil, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["ecdsap256"]) | |
| done := make(chan struct{}) | |
| go func() { | |
| defer close(done) | |
| NewServerConn(c1, serverConf) | |
| }() | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"], testSigners["dsa"], testSigners["ed25519"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| <-done | |
| expectedPublicKey := testSigners["dsa"].PublicKey().Marshal() | |
| lastSeenMarshalled := lastSeenKey.Marshal() | |
| if !bytes.Equal(lastSeenMarshalled, expectedPublicKey) { | |
| t.Errorf("unexpected key: got %#v, want %#v", lastSeenKey, testSigners["dsa"].PublicKey()) | |
| } | |
| } | |
| func TestPreAuthConnAndBanners(t *testing.T) { | |
| testDone := make(chan struct{}) | |
| defer close(testDone) | |
| authConnc := make(chan ServerPreAuthConn, 1) | |
| serverConfig := &ServerConfig{ | |
| PreAuthConnCallback: func(c ServerPreAuthConn) { | |
| t.Logf("got ServerPreAuthConn: %v", c) | |
| authConnc <- c // for use later in the test | |
| for _, s := range []string{"hello1", "hello2"} { | |
| if err := c.SendAuthBanner(s); err != nil { | |
| t.Errorf("failed to send banner %q: %v", s, err) | |
| } | |
| } | |
| // Now start a goroutine to spam SendAuthBanner in hopes | |
| // of hitting a race. | |
| go func() { | |
| for { | |
| select { | |
| case <-testDone: | |
| return | |
| default: | |
| if err := c.SendAuthBanner("attempted-race"); err != nil && err != errSendBannerPhase { | |
| t.Errorf("unexpected error from SendAuthBanner: %v", err) | |
| } | |
| time.Sleep(5 * time.Millisecond) | |
| } | |
| } | |
| }() | |
| }, | |
| NoClientAuth: true, | |
| NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) { | |
| t.Logf("got NoClientAuthCallback") | |
| return &Permissions{}, nil | |
| }, | |
| } | |
| serverConfig.AddHostKey(testSigners["rsa"]) | |
| var banners []string | |
| clientConfig := &ClientConfig{ | |
| User: "test", | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| BannerCallback: func(msg string) error { | |
| if msg != "attempted-race" { | |
| banners = append(banners, msg) | |
| } | |
| return nil | |
| }, | |
| } | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| go newServer(c1, serverConfig) | |
| c, _, _, err := NewClientConn(c2, "", clientConfig) | |
| if err != nil { | |
| t.Fatalf("client connection failed: %v", err) | |
| } | |
| defer c.Close() | |
| wantBanners := []string{ | |
| "hello1", | |
| "hello2", | |
| } | |
| if !reflect.DeepEqual(banners, wantBanners) { | |
| t.Errorf("got banners:\n%q\nwant banners:\n%q", banners, wantBanners) | |
| } | |
| // Now that we're authenticated, verify that use of SendBanner | |
| // is an error. | |
| var bc ServerPreAuthConn | |
| select { | |
| case bc = <-authConnc: | |
| default: | |
| t.Fatal("expected ServerPreAuthConn") | |
| } | |
| if err := bc.SendAuthBanner("wrong-phase"); err == nil { | |
| t.Error("unexpected success of SendAuthBanner after authentication") | |
| } else if err != errSendBannerPhase { | |
| t.Errorf("unexpected error: %v; want %v", err, errSendBannerPhase) | |
| } | |
| } | |
| func TestVerifiedPublicKeyCallback(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| extraKey := "extra" | |
| extraDataString := "just a string" | |
| serverConf := &ServerConfig{ | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if permissions != nil && permissions.ExtraData != nil { | |
| if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, permissions.ExtraData) { | |
| t.Errorf("expected extra data: %v; got: %v", extraDataString, permissions.ExtraData) | |
| } | |
| } else { | |
| t.Error("expected extra data is missing") | |
| } | |
| if signatureAlgorithm != KeyAlgoRSASHA256 { | |
| t.Errorf("expected signature algorithm: %q; got: %q", KeyAlgoRSASHA256, signatureAlgorithm) | |
| } | |
| return permissions, nil | |
| }, | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return &Permissions{ExtraData: map[any]any{extraKey: extraDataString}}, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| done := make(chan struct{}) | |
| go func() { | |
| defer close(done) | |
| conn, _, _, err := NewServerConn(c1, serverConf) | |
| if err != nil { | |
| t.Errorf("unexpected server error: %v", err) | |
| } | |
| if !reflect.DeepEqual(map[any]any{extraKey: extraDataString}, conn.Permissions.ExtraData) { | |
| t.Errorf("expected extra data: %v; got: %v", extraDataString, conn.Permissions.ExtraData) | |
| } | |
| }() | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| <-done | |
| } | |
| func TestVerifiedPublicKeyCallbackSignatureFormat(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| var gotSignatureAlgorithm string | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| gotSignatureAlgorithm = signatureAlgorithm | |
| return permissions, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| serverDone := make(chan error, 1) | |
| go func() { | |
| conn, _, _, err := NewServerConn(c1, serverConf) | |
| if err == nil { | |
| conn.Close() | |
| } | |
| serverDone <- err | |
| }() | |
| clientConf := &ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| // The selected algorithm and signature format differ for | |
| // compatibility with old RSA clients. | |
| configurablePublicKeyCallback{ | |
| signer: testSigners["rsa"].(AlgorithmSigner), | |
| signatureAlgo: KeyAlgoRSASHA256, | |
| signatureFormat: KeyAlgoRSA, | |
| }, | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| clientConn, _, _, clientErr := NewClientConn(c2, "", clientConf) | |
| if clientErr == nil { | |
| clientConn.Close() | |
| } | |
| serverErr := <-serverDone | |
| if clientErr != nil || serverErr != nil { | |
| t.Fatalf("connection failed: client error: %v; server error: %v", clientErr, serverErr) | |
| } | |
| if gotSignatureAlgorithm != KeyAlgoRSA { | |
| t.Errorf("signature algorithm = %q; want verified signature format %q", gotSignatureAlgorithm, KeyAlgoRSA) | |
| } | |
| } | |
| func TestVerifiedPublicCallbackPartialSuccess(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, &PartialSuccessError{ | |
| Next: ServerAuthCallbacks{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, nil | |
| }, | |
| }, | |
| } | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: nil, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| AuthCallback: func(ctx *ClientAuthContext) (AuthMethod, error) { | |
| if slices.Contains(ctx.AllowedMethods, "publickey") && !slices.Contains(ctx.PartialSuccessMethods, "publickey") { | |
| return PublicKeys(testSigners["rsa"]), nil | |
| } | |
| if slices.Contains(ctx.AllowedMethods, "password") && slices.Contains(ctx.PartialSuccessMethods, "publickey") { | |
| return Password(clientPassword), nil | |
| } | |
| return nil, nil | |
| }, | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| t.Fatalf("client login error: %s", err) | |
| } | |
| } | |
| func TestVerifiedPublicKeyCallbackPwdAndKey(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if string(password) == clientPassword { | |
| return nil, &PartialSuccessError{ | |
| Next: ServerAuthCallbacks{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| }, | |
| } | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| Password(clientPassword), | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| t.Fatalf("client login error: %s", err) | |
| } | |
| } | |
| func TestVerifiedPubKeyCallbackAuthMethods(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("client login succeed with only VerifiedPublicKeyCallback defined") | |
| } | |
| } | |
| func TestVerifiedPubKeyCallbackError(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("client login succeed with VerifiedPublicKeyCallback returning an error") | |
| } | |
| } | |
| func TestVerifiedPubKeyCallbackSourceAddress(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| return &Permissions{ | |
| CriticalOptions: map[string]string{ | |
| sourceAddressCriticalOption: "192.168.99.99", | |
| }, | |
| }, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("client login succeed with VerifiedPublicKeyCallback returning mismatching source-address") | |
| } | |
| } | |
| func TestAuthCallbacksSourceAddress(t *testing.T) { | |
| permsWithSourceAddress := func(sourceAddress string) *Permissions { | |
| return &Permissions{ | |
| CriticalOptions: map[string]string{ | |
| sourceAddressCriticalOption: sourceAddress, | |
| }, | |
| } | |
| } | |
| methods := []struct { | |
| name string | |
| serverConfig func(sourceAddress string) *ServerConfig | |
| clientAuth []AuthMethod | |
| }{ | |
| { | |
| name: "password", | |
| serverConfig: func(sourceAddress string) *ServerConfig { | |
| return &ServerConfig{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| return permsWithSourceAddress(sourceAddress), nil | |
| }, | |
| } | |
| }, | |
| clientAuth: []AuthMethod{Password(clientPassword)}, | |
| }, | |
| { | |
| name: "keyboard-interactive", | |
| serverConfig: func(sourceAddress string) *ServerConfig { | |
| return &ServerConfig{ | |
| KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) { | |
| return permsWithSourceAddress(sourceAddress), nil | |
| }, | |
| } | |
| }, | |
| clientAuth: []AuthMethod{ | |
| KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) { | |
| return nil, nil | |
| }), | |
| }, | |
| }, | |
| { | |
| name: "none", | |
| serverConfig: func(sourceAddress string) *ServerConfig { | |
| return &ServerConfig{ | |
| NoClientAuth: true, | |
| NoClientAuthCallback: func(conn ConnMetadata) (*Permissions, error) { | |
| return permsWithSourceAddress(sourceAddress), nil | |
| }, | |
| } | |
| }, | |
| }, | |
| } | |
| for _, method := range methods { | |
| for _, tc := range []struct { | |
| name string | |
| sourceAddress string | |
| wantErr bool | |
| }{ | |
| {"mismatching", "192.168.99.99", true}, | |
| {"matching", "127.0.0.0/8,::1/128", false}, | |
| {"present but empty", "", true}, | |
| } { | |
| t.Run(method.name+"/"+tc.name, func(t *testing.T) { | |
| clientConf := &ClientConfig{ | |
| User: "user", | |
| Auth: method.clientAuth, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| serverAuthErrors, err := doClientServerAuth(t, method.serverConfig(tc.sourceAddress), clientConf) | |
| if tc.wantErr { | |
| if err == nil { | |
| t.Fatalf("client login succeeded with %s callback returning mismatching source-address", method.name) | |
| } | |
| var sourceAddressErrors int | |
| for _, err := range serverAuthErrors { | |
| if err != nil && strings.Contains(err.Error(), "source-address restriction") { | |
| sourceAddressErrors++ | |
| } | |
| } | |
| if sourceAddressErrors == 0 { | |
| t.Fatalf("no server auth error mentions the source-address restriction, got %v", serverAuthErrors) | |
| } | |
| } else if err != nil { | |
| t.Fatalf("client login failed with matching source-address: %v", err) | |
| } | |
| }) | |
| } | |
| } | |
| } | |
| func TestCheckSourceAddressNonIP(t *testing.T) { | |
| for _, addr := range []net.Addr{ | |
| &net.UnixAddr{Name: "/run/test.sock", Net: "unix"}, | |
| &net.UnixAddr{}, | |
| nil, | |
| } { | |
| if err := checkSourceAddress(addr, "127.0.0.0/8,::1/128"); err == nil { | |
| t.Errorf("source-address check succeeded for non-IP remote address %v", addr) | |
| } | |
| } | |
| } | |
| func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| // Returning PartialSuccessError is not permitted when | |
| // VerifiedPublicKeyCallback is defined. This callback is | |
| // invoked for both query requests and real authentications, | |
| // while VerifiedPublicKeyCallback is only triggered if the | |
| // client has proven control of the key. | |
| return nil, &PartialSuccessError{ | |
| Next: ServerAuthCallbacks{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, nil | |
| }, | |
| }, | |
| } | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, &PartialSuccessError{ | |
| Next: ServerAuthCallbacks{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, nil | |
| }, | |
| }, | |
| } | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| Password(clientPassword), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("authentication succeeded with PartialSuccess returned from PublicKeyCallback and VerifiedPublicKeyCallback defined") | |
| } | |
| } | |
| func TestVerifiedPublicKeyCallbackOnError(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| var verifiedCallbackCalled bool | |
| serverConf := &ServerConfig{ | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| verifiedCallbackCalled = true | |
| return nil, nil | |
| }, | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| return nil, errors.New("invalid key") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| done := make(chan struct{}) | |
| go func() { | |
| defer close(done) | |
| NewServerConn(c1, serverConf) | |
| }() | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("authentication should fail") | |
| } | |
| <-done | |
| if verifiedCallbackCalled { | |
| t.Error("VerifiedPublicKeyCallback called after PublicKeyCallback returned an error") | |
| } | |
| } | |
| func TestVerifiedPublicKeyCallbackOnly(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| return nil, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| done := make(chan struct{}) | |
| go func() { | |
| defer close(done) | |
| NewServerConn(c1, serverConf) | |
| }() | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("authentication succeeded with only VerifiedPublicKeyCallback defined") | |
| } | |
| <-done | |
| } | |
| func TestPartialSuccessWithNonNilPerms(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| return nil, nil | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| VerifiedPublicKeyCallback: func(conn ConnMetadata, key PublicKey, permissions *Permissions, signatureAlgorithm string) (*Permissions, error) { | |
| if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) { | |
| // Intentionally return non-nil Permissions along with a | |
| // PartialSuccessError. Since permissions are reset between | |
| // authentication steps, this constitutes invalid library usage | |
| // and the server is expected to reject the connection. | |
| return &Permissions{Extensions: map[string]string{"permit-port-forwarding": ""}}, &PartialSuccessError{ | |
| Next: ServerAuthCallbacks{ | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| if string(password) == clientPassword { | |
| return nil, nil | |
| } | |
| return nil, nil | |
| }, | |
| }, | |
| } | |
| } | |
| return nil, errors.New("invalid credentials") | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{ | |
| PublicKeys(testSigners["rsa"]), | |
| Password(clientPassword), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| go NewServerConn(c1, serverConf) | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err == nil { | |
| t.Fatal("authentication succeeded unexpectedly; server should have rejected non-nil Permissions combined with PartialSuccessError") | |
| } | |
| } | |
| type markerConn struct { | |
| closed uint32 | |
| used uint32 | |
| } | |
| func (c *markerConn) isClosed() bool { | |
| return atomic.LoadUint32(&c.closed) != 0 | |
| } | |
| func (c *markerConn) isUsed() bool { | |
| return atomic.LoadUint32(&c.used) != 0 | |
| } | |
| func (c *markerConn) Close() error { | |
| atomic.StoreUint32(&c.closed, 1) | |
| return nil | |
| } | |
| func (c *markerConn) Read(b []byte) (n int, err error) { | |
| atomic.StoreUint32(&c.used, 1) | |
| if atomic.LoadUint32(&c.closed) != 0 { | |
| return 0, net.ErrClosed | |
| } else { | |
| return 0, io.EOF | |
| } | |
| } | |
| func (c *markerConn) Write(b []byte) (n int, err error) { | |
| atomic.StoreUint32(&c.used, 1) | |
| if atomic.LoadUint32(&c.closed) != 0 { | |
| return 0, net.ErrClosed | |
| } else { | |
| return 0, io.ErrClosedPipe | |
| } | |
| } | |
| func (*markerConn) LocalAddr() net.Addr { return nil } | |
| func (*markerConn) RemoteAddr() net.Addr { return nil } | |
| func (*markerConn) SetDeadline(t time.Time) error { return nil } | |
| func (*markerConn) SetReadDeadline(t time.Time) error { return nil } | |
| func (*markerConn) SetWriteDeadline(t time.Time) error { return nil } | |
| // skTestSigner is a Signer that produces SK-ECDSA signatures over | |
| // user-auth data, simulating a FIDO/U2F authenticator. The flags | |
| // byte (UP and other bits) is caller-controlled so tests can exercise | |
| // the server's user-presence enforcement and its opt-out paths. This | |
| // is test-only: the real Signer is hardware-backed. | |
| type skTestSigner struct { | |
| priv *ecdsa.PrivateKey | |
| pub PublicKey | |
| flags byte | |
| application string | |
| } | |
| func (s *skTestSigner) PublicKey() PublicKey { return s.pub } | |
| func (s *skTestSigner) Sign(r io.Reader, data []byte) (*Signature, error) { | |
| h := sha256.New() | |
| h.Write([]byte(s.application)) | |
| appDigest := h.Sum(nil) | |
| h.Reset() | |
| h.Write(data) | |
| dataDigest := h.Sum(nil) | |
| var counter uint32 = 1 | |
| blob := struct { | |
| ApplicationDigest []byte `ssh:"rest"` | |
| Flags byte | |
| Counter uint32 | |
| MessageDigest []byte `ssh:"rest"` | |
| }{appDigest, s.flags, counter, dataDigest} | |
| h.Reset() | |
| h.Write(Marshal(blob)) | |
| digest := h.Sum(nil) | |
| x, y, err := ecdsa.Sign(r, s.priv, digest) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &Signature{ | |
| Format: KeyAlgoSKECDSA256, | |
| Blob: Marshal(struct{ R, S *big.Int }{x, y}), | |
| Rest: Marshal(struct { | |
| Flags byte | |
| Counter uint32 | |
| }{s.flags, counter}), | |
| }, nil | |
| } | |
| // TestServerAuthSKUserPresence drives the full userAuthLoop with an SK | |
| // public-key client and verifies the server's wiring of the UP check | |
| // and its two opt-out paths: the per-key Permissions.Extensions route | |
| // and (via cert) the cert-level route. It also confirms that non-SK | |
| // clients are unaffected by the new code path. | |
| func TestServerAuthSKUserPresence(t *testing.T) { | |
| userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| skPub := &skECDSAPublicKey{application: "ssh:", PublicKey: userKey.PublicKey} | |
| runAuth := func(t *testing.T, signer Signer, perms *Permissions) error { | |
| t.Helper() | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| PublicKeyCallback: func(ConnMetadata, PublicKey) (*Permissions, error) { | |
| return perms, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["ecdsa"]) | |
| serverErr := make(chan error, 1) | |
| go func() { | |
| _, _, _, err := NewServerConn(c1, serverConf) | |
| serverErr <- err | |
| }() | |
| clientConf := &ClientConfig{ | |
| User: "user", | |
| Auth: []AuthMethod{PublicKeys(signer)}, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, clientErr := NewClientConn(c2, "", clientConf) | |
| <-serverErr | |
| return clientErr | |
| } | |
| optOut := &Permissions{Extensions: map[string]string{noTouchRequiredExtension: ""}} | |
| t.Run("UP=1, default perms accepts", func(t *testing.T) { | |
| s := &skTestSigner{priv: userKey, pub: skPub, flags: flagUserPresence, application: "ssh:"} | |
| if err := runAuth(t, s, nil); err != nil { | |
| t.Errorf("expected auth to succeed: %v", err) | |
| } | |
| }) | |
| t.Run("UP=0, default perms rejects", func(t *testing.T) { | |
| s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} | |
| if err := runAuth(t, s, nil); err == nil { | |
| t.Error("expected auth to fail with UP=0") | |
| } | |
| }) | |
| t.Run("UP=0, perms opt-out accepts", func(t *testing.T) { | |
| s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} | |
| if err := runAuth(t, s, optOut); err != nil { | |
| t.Errorf("expected auth to succeed with opt-out: %v", err) | |
| } | |
| }) | |
| t.Run("UP=0, perms CriticalOptions does NOT opt out", func(t *testing.T) { | |
| s := &skTestSigner{priv: userKey, pub: skPub, flags: 0, application: "ssh:"} | |
| critOnly := &Permissions{CriticalOptions: map[string]string{noTouchRequiredExtension: ""}} | |
| if err := runAuth(t, s, critOnly); err == nil { | |
| t.Error("no-touch-required in CriticalOptions must not waive UP") | |
| } | |
| }) | |
| t.Run("non-SK RSA signer unaffected", func(t *testing.T) { | |
| if err := runAuth(t, testSigners["rsa"], nil); err != nil { | |
| t.Errorf("plain RSA auth must still work: %v", err) | |
| } | |
| }) | |
| } |