mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-05 15:55:49 +00:00

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>
368 lines
8.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|