tailscale/tempfork/sshtest/ssh/client_test.go
Brad Fitzpatrick 2f98197857 tempfork/sshtest/ssh: add fork of golang.org/x/crypto/ssh for testing only
This fork golang.org/x/crypto/ssh (at upstream x/crypto git rev e47973b1c1)
into tailscale.com/tempfork/sshtest/ssh so we can hack up the client in weird
ways to simulate other SSH clients seen in the wild.

Two changes were made to the files when they were copied from x/crypto:

* internal/poly1305 imports were replaced by the non-internal version;
  no code changes otherwise. It didn't need the internal one.
* all decode-with-passphrase funcs were deleted, to avoid
  using the internal package x/crypto/ssh/internal/bcrypt_pbkdf

Then the tests passed.

Updates #14969

Change-Id: Ibf1abebfe608c75fef4da0255314f65e54ce5077
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 07:45:06 -08:00

368 lines
8.6 KiB
Go

// 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", kexAlgoCurve25519SHA256},
},
"",
},
{
"unsupported cipher",
Config{
Ciphers: []string{"unsupported"},
},
"no common algorithm",
},
{
"unsupported and supported ciphers",
Config{
Ciphers: []string{"unsupported", chacha20Poly1305ID},
},
"",
},
{
"unsupported MAC",
Config{
MACs: []string{"unsupported"},
// MAC is used for non AAED ciphers.
Ciphers: []string{"aes256-ctr"},
},
"no common algorithm",
},
{
"unsupported and supported MACs",
Config{
MACs: []string{"unsupported", "hmac-sha2-256-etm@openssh.com"},
// MAC is used for non AAED ciphers.
Ciphers: []string{"aes256-ctr"},
},
"",
},
} {
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)
}
})
}
}