| // 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 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 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) |
| } |
| }) |
| } |