| // Copyright 2014 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/rand" | |
| "errors" | |
| "fmt" | |
| "net" | |
| "strings" | |
| "testing" | |
| ) | |
| func TestClientVersion(t *testing.T) { | |
| for _, tt := range []struct { | |
| name string | |
| version string | |
| multiLine string | |
| wantErr bool | |
| }{ | |
| { | |
| name: "default version", | |
| version: packageVersion, | |
| }, | |
| { | |
| name: "custom version", | |
| version: "SSH-2.0-CustomClientVersionString", | |
| }, | |
| { | |
| name: "good multi line version", | |
| version: packageVersion, | |
| multiLine: strings.Repeat("ignored\r\n", 20), | |
| }, | |
| { | |
| name: "bad multi line version", | |
| version: packageVersion, | |
| multiLine: "bad multi line version", | |
| wantErr: true, | |
| }, | |
| { | |
| name: "long multi line version", | |
| version: packageVersion, | |
| multiLine: strings.Repeat("long multi line version\r\n", 50)[:256], | |
| wantErr: true, | |
| }, | |
| } { | |
| t.Run(tt.name, func(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| go func() { | |
| if tt.multiLine != "" { | |
| c1.Write([]byte(tt.multiLine)) | |
| } | |
| NewClientConn(c1, "", &ClientConfig{ | |
| ClientVersion: tt.version, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| }) | |
| c1.Close() | |
| }() | |
| conf := &ServerConfig{NoClientAuth: true} | |
| conf.AddHostKey(testSigners["rsa"]) | |
| conn, _, _, err := NewServerConn(c2, conf) | |
| if err == nil == tt.wantErr { | |
| t.Fatalf("got err %v; wantErr %t", err, tt.wantErr) | |
| } | |
| if tt.wantErr { | |
| // Don't verify the version on an expected error. | |
| return | |
| } | |
| if got := string(conn.ClientVersion()); got != tt.version { | |
| t.Fatalf("got %q; want %q", got, tt.version) | |
| } | |
| }) | |
| } | |
| } | |
| func TestHostKeyCheck(t *testing.T) { | |
| for _, tt := range []struct { | |
| name string | |
| wantError string | |
| key PublicKey | |
| }{ | |
| {"no callback", "must specify HostKeyCallback", nil}, | |
| {"correct key", "", testSigners["rsa"].PublicKey()}, | |
| {"mismatch", "mismatch", testSigners["ecdsa"].PublicKey()}, | |
| } { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| NoClientAuth: true, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| go NewServerConn(c1, serverConf) | |
| clientConf := ClientConfig{ | |
| User: "user", | |
| } | |
| if tt.key != nil { | |
| clientConf.HostKeyCallback = FixedHostKey(tt.key) | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) { | |
| t.Errorf("%s: got error %q, missing %q", tt.name, err.Error(), tt.wantError) | |
| } | |
| } else if tt.wantError != "" { | |
| t.Errorf("%s: succeeded, but want error string %q", tt.name, tt.wantError) | |
| } | |
| } | |
| } | |
| func TestVerifyHostKeySignature(t *testing.T) { | |
| for _, tt := range []struct { | |
| key string | |
| signAlgo string | |
| verifyAlgo string | |
| wantError string | |
| }{ | |
| {"rsa", KeyAlgoRSA, KeyAlgoRSA, ""}, | |
| {"rsa", KeyAlgoRSASHA256, KeyAlgoRSASHA256, ""}, | |
| {"rsa", KeyAlgoRSA, KeyAlgoRSASHA512, `ssh: invalid signature algorithm "ssh-rsa", expected "rsa-sha2-512"`}, | |
| {"ed25519", KeyAlgoED25519, KeyAlgoED25519, ""}, | |
| } { | |
| key := testSigners[tt.key].PublicKey() | |
| s, ok := testSigners[tt.key].(AlgorithmSigner) | |
| if !ok { | |
| t.Fatalf("needed an AlgorithmSigner") | |
| } | |
| sig, err := s.SignWithAlgorithm(rand.Reader, []byte("test"), tt.signAlgo) | |
| if err != nil { | |
| t.Fatalf("couldn't sign: %q", err) | |
| } | |
| b := bytes.Buffer{} | |
| writeString(&b, []byte(sig.Format)) | |
| writeString(&b, sig.Blob) | |
| result := kexResult{Signature: b.Bytes(), H: []byte("test")} | |
| err = verifyHostKeySignature(key, tt.verifyAlgo, &result) | |
| if err != nil { | |
| if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) { | |
| t.Errorf("got error %q, expecting %q", err.Error(), tt.wantError) | |
| } | |
| } else if tt.wantError != "" { | |
| t.Errorf("succeeded, but want error string %q", tt.wantError) | |
| } | |
| } | |
| } | |
| func TestBannerCallback(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 &Permissions{}, nil | |
| }, | |
| BannerCallback: func(conn ConnMetadata) string { | |
| return "Hello World" | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| go NewServerConn(c1, serverConf) | |
| var receivedBanner string | |
| var bannerCount int | |
| clientConf := ClientConfig{ | |
| Auth: []AuthMethod{ | |
| Password("123"), | |
| }, | |
| User: "user", | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| BannerCallback: func(message string) error { | |
| bannerCount++ | |
| receivedBanner = message | |
| return nil | |
| }, | |
| } | |
| _, _, _, err = NewClientConn(c2, "", &clientConf) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if bannerCount != 1 { | |
| t.Errorf("got %d banners; want 1", bannerCount) | |
| } | |
| expected := "Hello World" | |
| if receivedBanner != expected { | |
| t.Fatalf("got %s; want %s", receivedBanner, expected) | |
| } | |
| } | |
| func TestNewClientConn(t *testing.T) { | |
| errHostKeyMismatch := errors.New("host key mismatch") | |
| for _, tt := range []struct { | |
| name string | |
| user string | |
| simulateHostKeyMismatch HostKeyCallback | |
| }{ | |
| { | |
| name: "good user field for ConnMetadata", | |
| user: "testuser", | |
| }, | |
| { | |
| name: "empty user field for ConnMetadata", | |
| user: "", | |
| }, | |
| { | |
| name: "host key mismatch", | |
| user: "testuser", | |
| simulateHostKeyMismatch: func(hostname string, remote net.Addr, key PublicKey) error { | |
| return fmt.Errorf("%w: %s", errHostKeyMismatch, bytes.TrimSpace(MarshalAuthorizedKey(key))) | |
| }, | |
| }, | |
| } { | |
| t.Run(tt.name, func(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 &Permissions{}, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| go NewServerConn(c1, serverConf) | |
| clientConf := &ClientConfig{ | |
| User: tt.user, | |
| Auth: []AuthMethod{ | |
| Password("testpw"), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| if tt.simulateHostKeyMismatch != nil { | |
| clientConf.HostKeyCallback = tt.simulateHostKeyMismatch | |
| } | |
| clientConn, _, _, err := NewClientConn(c2, "", clientConf) | |
| if err != nil { | |
| if tt.simulateHostKeyMismatch != nil && errors.Is(err, errHostKeyMismatch) { | |
| return | |
| } | |
| t.Fatal(err) | |
| } | |
| if userGot := clientConn.User(); userGot != tt.user { | |
| t.Errorf("got user %q; want user %q", userGot, tt.user) | |
| } | |
| }) | |
| } | |
| } | |
| func TestUnsupportedAlgorithm(t *testing.T) { | |
| for _, tt := range []struct { | |
| name string | |
| config Config | |
| wantError string | |
| }{ | |
| { | |
| "unsupported KEX", | |
| Config{ | |
| KeyExchanges: []string{"unsupported"}, | |
| }, | |
| "no common algorithm", | |
| }, | |
| { | |
| "unsupported and supported KEXs", | |
| Config{ | |
| KeyExchanges: []string{"unsupported", KeyExchangeCurve25519}, | |
| }, | |
| "", | |
| }, | |
| { | |
| "unsupported cipher", | |
| Config{ | |
| Ciphers: []string{"unsupported"}, | |
| }, | |
| "no common algorithm", | |
| }, | |
| { | |
| "unsupported and supported ciphers", | |
| Config{ | |
| Ciphers: []string{"unsupported", CipherChaCha20Poly1305}, | |
| }, | |
| "", | |
| }, | |
| { | |
| "unsupported MAC", | |
| Config{ | |
| MACs: []string{"unsupported"}, | |
| // MAC is used for non AAED ciphers. | |
| Ciphers: []string{CipherAES256CTR}, | |
| }, | |
| "no common algorithm", | |
| }, | |
| { | |
| "unsupported and supported MACs", | |
| Config{ | |
| MACs: []string{"unsupported", HMACSHA256ETM}, | |
| // MAC is used for non AAED ciphers. | |
| Ciphers: []string{CipherAES256CTR}, | |
| }, | |
| "", | |
| }, | |
| } { | |
| t.Run(tt.name, func(t *testing.T) { | |
| c1, c2, err := netPipe() | |
| if err != nil { | |
| t.Fatalf("netPipe: %v", err) | |
| } | |
| defer c1.Close() | |
| defer c2.Close() | |
| serverConf := &ServerConfig{ | |
| Config: tt.config, | |
| PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { | |
| return &Permissions{}, nil | |
| }, | |
| } | |
| serverConf.AddHostKey(testSigners["rsa"]) | |
| go NewServerConn(c1, serverConf) | |
| clientConf := &ClientConfig{ | |
| User: "testuser", | |
| Config: tt.config, | |
| Auth: []AuthMethod{ | |
| Password("testpw"), | |
| }, | |
| HostKeyCallback: InsecureIgnoreHostKey(), | |
| } | |
| _, _, _, err = NewClientConn(c2, "", clientConf) | |
| if err != nil { | |
| if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) { | |
| t.Errorf("%s: got error %q, missing %q", tt.name, err.Error(), tt.wantError) | |
| } | |
| } else if tt.wantError != "" { | |
| t.Errorf("%s: succeeded, but want error string %q", tt.name, tt.wantError) | |
| } | |
| }) | |
| } | |
| } |