ssh: add integration tests against a reference implementation
As in crypto/tls, we record the connection against a reference implementation, OpenSSH in our case, and run part of our integration tests by replaying these recorded SSH connections. Change-Id: If042b5f650b267bd3ede34a05ec3a6fa6d1a86b3 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/644436 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Filippo Valsorda <filippo@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
This commit is contained in:
committed by
Gopher Robot
parent
959f8f3db0
commit
6b13eef40b
@@ -1,31 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBannerCallbackAgainstOpenSSH(t *testing.T) {
|
||||
server := newServer(t)
|
||||
|
||||
clientConf := clientConfig()
|
||||
|
||||
var receivedBanner string
|
||||
clientConf.BannerCallback = func(message string) error {
|
||||
receivedBanner = message
|
||||
return nil
|
||||
}
|
||||
|
||||
conn := server.Dial(clientConf)
|
||||
defer conn.Close()
|
||||
|
||||
expected := "Server Banner"
|
||||
if receivedBanner != expected {
|
||||
t.Fatalf("got %v; want %v", receivedBanner, expected)
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func testPortForward(t *testing.T, n, listenAddr string) {
|
||||
if len(sent) != len(read) {
|
||||
t.Fatalf("got %d bytes, want %d", len(read), len(sent))
|
||||
}
|
||||
if bytes.Compare(sent, read) != 0 {
|
||||
if !bytes.Equal(sent, read) {
|
||||
t.Fatalf("read back data does not match")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
// Copyright 2025 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 test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/internal/testenv"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/testdata"
|
||||
)
|
||||
|
||||
// serverPort contains the port that OpenSSH will listen on. OpenSSH can't take
|
||||
// "0" as an argument here so we have to pick a number and hope that it's not in
|
||||
// use on the machine. Since this only occurs when -update is given and thus
|
||||
// when there's a human watching the test, this isn't too bad.
|
||||
const serverPort = 24222
|
||||
|
||||
var (
|
||||
storeUsernameOnce sync.Once
|
||||
)
|
||||
|
||||
type clientTest struct {
|
||||
// name is a freeform string identifying the test and the file in which
|
||||
// the expected results will be stored.
|
||||
name string
|
||||
// config contains the client configuration to use for this test.
|
||||
config *ssh.ClientConfig
|
||||
// expectError defines the error string to check if the connection is
|
||||
// expected to fail.
|
||||
expectError string
|
||||
// successCallback defines a callback to execute after the client connection
|
||||
// is established.
|
||||
successCallback func(t *testing.T, client *ssh.Client)
|
||||
}
|
||||
|
||||
// connFromCommand starts the reference server process, connects to it and
|
||||
// returns a recordingConn for the connection. It must be closed before Waiting
|
||||
// for child.
|
||||
func (test *clientTest) connFromCommand(t *testing.T, config string) *recordingConn {
|
||||
sshd, err := exec.LookPath("sshd")
|
||||
if err != nil {
|
||||
t.Skipf("sshd not found, skipping test: %v", err)
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "sshtest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := os.Create(filepath.Join(dir, "sshd_config"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := configTmpl[config]; ok == false {
|
||||
t.Fatal(fmt.Errorf("Invalid server config '%s'", config))
|
||||
}
|
||||
configVars := map[string]string{
|
||||
"Dir": dir,
|
||||
}
|
||||
err = configTmpl[config].Execute(f, configVars)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
writeFile(filepath.Join(dir, "banner"), []byte("Server Banner"))
|
||||
|
||||
for k, v := range testdata.PEMBytes {
|
||||
filename := "id_" + k
|
||||
writeFile(filepath.Join(dir, filename), v)
|
||||
writeFile(filepath.Join(dir, filename+".pub"), ssh.MarshalAuthorizedKey(testPublicKeys[k]))
|
||||
}
|
||||
|
||||
var authkeys bytes.Buffer
|
||||
for k := range testdata.PEMBytes {
|
||||
authkeys.Write(ssh.MarshalAuthorizedKey(testPublicKeys[k]))
|
||||
}
|
||||
writeFile(filepath.Join(dir, "authorized_keys"), authkeys.Bytes())
|
||||
cmd := testenv.Command(t, sshd, "-D", "-e", "-f", f.Name(), "-p", strconv.Itoa(serverPort))
|
||||
cmd.Stdin = nil
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Don't check for errors; if it fails it's most
|
||||
// likely "os: process already finished", and we don't
|
||||
// care about that. Use os.Interrupt, so child
|
||||
// processes are killed too.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
cmd.Wait()
|
||||
if t.Failed() {
|
||||
t.Logf("OpenSSH output:\n\n%s", cmd.Stdout)
|
||||
}
|
||||
})
|
||||
var tcpConn net.Conn
|
||||
for i := uint(0); i < 5; i++ {
|
||||
tcpConn, err = net.DialTCP("tcp", nil, &net.TCPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Port: serverPort,
|
||||
})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep((1 << i) * 5 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("error connecting to the OpenSSH server: %v (%v)\n\n%s", err, cmd.Wait(), output.Bytes())
|
||||
}
|
||||
|
||||
record := &recordingConn{
|
||||
Conn: tcpConn,
|
||||
clientToServer: true,
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func (test *clientTest) dataPath() string {
|
||||
return filepath.Join("..", "testdata", "Client-"+test.name)
|
||||
}
|
||||
|
||||
func (test *clientTest) usernameDataPath() string {
|
||||
return filepath.Join("..", "testdata", "Client-username")
|
||||
}
|
||||
|
||||
func (test *clientTest) loadData() (flows [][]byte, err error) {
|
||||
in, err := os.Open(test.dataPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer in.Close()
|
||||
return parseTestData(in)
|
||||
}
|
||||
|
||||
func (test *clientTest) storeUsername() (err error) {
|
||||
storeUsernameOnce.Do(func() {
|
||||
err = os.WriteFile(test.usernameDataPath(), []byte(username()), 0666)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (test *clientTest) loadUsername() (string, error) {
|
||||
data, err := os.ReadFile(test.usernameDataPath())
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func (test *clientTest) run(t *testing.T, write bool) {
|
||||
var clientConn net.Conn
|
||||
var recordingConn *recordingConn
|
||||
|
||||
if write {
|
||||
// We store the username used when we record the connection so we can
|
||||
// reuse the same username when running tests.
|
||||
if err := test.storeUsername(); err != nil {
|
||||
t.Fatalf("failed to store username to %q: %v", test.usernameDataPath(), err)
|
||||
}
|
||||
recordingConn = test.connFromCommand(t, "default")
|
||||
clientConn = recordingConn
|
||||
} else {
|
||||
username, err := test.loadUsername()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load username from %q: %v", test.usernameDataPath(), err)
|
||||
}
|
||||
test.config.User = username
|
||||
timer := time.AfterFunc(10*time.Second, func() {
|
||||
fmt.Println("This test may be stuck, try running using -timeout 10s")
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
timer.Stop()
|
||||
})
|
||||
flows, err := test.loadData()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load data from %s: %v", test.dataPath(), err)
|
||||
}
|
||||
clientConn = newReplayingConn(t, flows)
|
||||
}
|
||||
c, chans, reqs, err := ssh.NewClientConn(clientConn, "", test.config)
|
||||
if err != nil {
|
||||
if test.expectError == "" {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), test.expectError) {
|
||||
t.Fatalf("%q not found in %v", test.expectError, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if test.expectError != "" {
|
||||
t.Error("dial should have failed.")
|
||||
}
|
||||
client := ssh.NewClient(c, chans, reqs)
|
||||
if test.successCallback != nil {
|
||||
test.successCallback(t, client)
|
||||
}
|
||||
if err := client.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if write {
|
||||
path := test.dataPath()
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create output file: %v", err)
|
||||
}
|
||||
defer out.Close()
|
||||
recordingConn.Close()
|
||||
|
||||
recordingConn.WriteTo(out)
|
||||
t.Logf("Wrote %s\n", path)
|
||||
}
|
||||
}
|
||||
|
||||
func recordingsClientConfig() *ssh.ClientConfig {
|
||||
config := clientConfig()
|
||||
config.Rand = sha3.NewShake128()
|
||||
config.Auth = []ssh.AuthMethod{
|
||||
ssh.PublicKeys(testSigners["rsa"]),
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func TestClientKeyExchanges(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
var keyExchanges []string
|
||||
for _, kex := range config.KeyExchanges {
|
||||
// Exclude ecdh for now, to make them determistic we should use see a
|
||||
// stream of fixed bytes as the random source.
|
||||
if !strings.HasPrefix(kex, "ecdh-") {
|
||||
keyExchanges = append(keyExchanges, kex)
|
||||
}
|
||||
}
|
||||
// Add diffie-hellman-group-exchange-sha256 and
|
||||
// diffie-hellman-group16-sha512 as they are not enabled by default.
|
||||
keyExchanges = append(keyExchanges, "diffie-hellman-group-exchange-sha256", "diffie-hellman-group16-sha512")
|
||||
|
||||
for _, kex := range keyExchanges {
|
||||
c := recordingsClientConfig()
|
||||
c.KeyExchanges = []string{kex}
|
||||
test := clientTest{
|
||||
name: "KEX-" + kex,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCiphers(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
for _, ciph := range config.Ciphers {
|
||||
c := recordingsClientConfig()
|
||||
c.Ciphers = []string{ciph}
|
||||
test := clientTest{
|
||||
name: "Cipher-" + ciph,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientMACs(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
for _, mac := range config.MACs {
|
||||
c := recordingsClientConfig()
|
||||
c.MACs = []string{mac}
|
||||
test := clientTest{
|
||||
name: "MAC-" + mac,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBannerCallback(t *testing.T) {
|
||||
var receivedBanner string
|
||||
config := recordingsClientConfig()
|
||||
config.BannerCallback = func(message string) error {
|
||||
receivedBanner = message
|
||||
return nil
|
||||
}
|
||||
test := clientTest{
|
||||
name: "BannerCallback",
|
||||
config: config,
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
expected := "Server Banner"
|
||||
if receivedBanner != expected {
|
||||
t.Fatalf("got %v; want %v", receivedBanner, expected)
|
||||
}
|
||||
},
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestRunCommandSuccess(t *testing.T) {
|
||||
test := clientTest{
|
||||
name: "RunCommandSuccess",
|
||||
config: recordingsClientConfig(),
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
err = session.Run("true")
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestHostKeyCheck(t *testing.T) {
|
||||
config := recordingsClientConfig()
|
||||
hostDB := hostKeyDB()
|
||||
config.HostKeyCallback = hostDB.Check
|
||||
|
||||
// change the keys.
|
||||
hostDB.keys[ssh.KeyAlgoRSA][25]++
|
||||
hostDB.keys[ssh.KeyAlgoDSA][25]++
|
||||
hostDB.keys[ssh.KeyAlgoECDSA256][25]++
|
||||
|
||||
test := clientTest{
|
||||
name: "HostKeyCheck",
|
||||
config: config,
|
||||
expectError: "host key mismatch",
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestRunCommandStdin(t *testing.T) {
|
||||
test := clientTest{
|
||||
name: "RunCommandStdin",
|
||||
config: recordingsClientConfig(),
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
defer w.Close()
|
||||
session.Stdin = r
|
||||
|
||||
err = session.Run("true")
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestRunCommandStdinError(t *testing.T) {
|
||||
test := clientTest{
|
||||
name: "RunCommandStdinError",
|
||||
config: recordingsClientConfig(),
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
session.Stdin = r
|
||||
pipeErr := errors.New("closing write end of pipe")
|
||||
w.CloseWithError(pipeErr)
|
||||
|
||||
err = session.Run("true")
|
||||
if err != pipeErr {
|
||||
t.Fatalf("expected %v, found %v", pipeErr, err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestRunCommandFailed(t *testing.T) {
|
||||
test := clientTest{
|
||||
name: "RunCommandFailed",
|
||||
config: recordingsClientConfig(),
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Trigger a failure by attempting to execute a non-existent
|
||||
// command.
|
||||
err = session.Run(`non-existent command`)
|
||||
if err == nil {
|
||||
t.Fatalf("session succeeded: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
|
||||
func TestWindowChange(t *testing.T) {
|
||||
test := clientTest{
|
||||
name: "WindowChange",
|
||||
config: recordingsClientConfig(),
|
||||
successCallback: func(t *testing.T, client *ssh.Client) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to acquire stdout pipe: %s", err)
|
||||
}
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to acquire stdin pipe: %s", err)
|
||||
}
|
||||
|
||||
tm := ssh.TerminalModes{ssh.ECHO: 0}
|
||||
if err = session.RequestPty("xterm", 80, 40, tm); err != nil {
|
||||
t.Fatalf("req-pty failed: %s", err)
|
||||
}
|
||||
|
||||
if err := session.WindowChange(100, 100); err != nil {
|
||||
t.Fatalf("window-change failed: %s", err)
|
||||
}
|
||||
|
||||
err = session.Shell()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %s", err)
|
||||
}
|
||||
|
||||
stdin.Write([]byte("stty size && exit\n"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, stdout); err != nil {
|
||||
t.Fatalf("reading failed: %s", err)
|
||||
}
|
||||
|
||||
if sttyOutput := buf.String(); !strings.Contains(sttyOutput, "100 100") {
|
||||
t.Fatalf("terminal WindowChange failure: expected \"100 100\" stty output, got %s", sttyOutput)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright 2025 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 test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/internal/testenv"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/testdata"
|
||||
)
|
||||
|
||||
type serverTest struct {
|
||||
// name is a freeform string identifying the test and the file in which
|
||||
// the expected results will be stored.
|
||||
name string
|
||||
// config contains the server configuration to use for this test.
|
||||
config *ssh.ServerConfig
|
||||
}
|
||||
|
||||
// connFromCommand starts opens a listening socket and starts the reference
|
||||
// client to connect to it. It returns a recordingConn that wraps the resulting
|
||||
// connection.
|
||||
func (test *serverTest) connFromCommand(t *testing.T) (conn *recordingConn, err error) {
|
||||
sshCLI, err := exec.LookPath("ssh")
|
||||
if err != nil {
|
||||
t.Skipf("skipping test: %v", err)
|
||||
}
|
||||
l, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Port: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
dir, err := os.MkdirTemp("", "sshtest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filename := "id_ed25519"
|
||||
writeFile(filepath.Join(dir, filename), testdata.PEMBytes["ed25519"])
|
||||
writeFile(filepath.Join(dir, filename+".pub"), ssh.MarshalAuthorizedKey(testPublicKeys["ed25519"]))
|
||||
var args []string
|
||||
args = append(args, "-v", "-i", filepath.Join(dir, filename), "-o", "StrictHostKeyChecking=no")
|
||||
args = append(args, "-oKexAlgorithms=+diffie-hellman-group14-sha1")
|
||||
args = append(args, "-p", strconv.Itoa(port))
|
||||
args = append(args, "testuser@127.0.0.1")
|
||||
args = append(args, "true")
|
||||
cmd := testenv.Command(t, sshCLI, args...)
|
||||
cmd.Stdin = nil
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Don't check for errors; if it fails it's most
|
||||
// likely "os: process already finished", and we don't
|
||||
// care about that.
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
if t.Failed() {
|
||||
t.Logf("OpenSSH output:\n\n%s", cmd.Stdout)
|
||||
}
|
||||
})
|
||||
|
||||
connChan := make(chan any, 1)
|
||||
go func() {
|
||||
tcpConn, err := l.Accept()
|
||||
if err != nil {
|
||||
connChan <- err
|
||||
return
|
||||
}
|
||||
connChan <- tcpConn
|
||||
}()
|
||||
|
||||
var tcpConn net.Conn
|
||||
select {
|
||||
case connOrError := <-connChan:
|
||||
if err, ok := connOrError.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
tcpConn = connOrError.(net.Conn)
|
||||
case <-time.After(2 * time.Second):
|
||||
return nil, errors.New("timed out waiting for connection from child process")
|
||||
}
|
||||
|
||||
record := &recordingConn{
|
||||
Conn: tcpConn,
|
||||
clientToServer: false,
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (test *serverTest) dataPath() string {
|
||||
return filepath.Join("..", "testdata", "Server-"+test.name)
|
||||
}
|
||||
|
||||
func (test *serverTest) loadData() (flows [][]byte, err error) {
|
||||
in, err := os.Open(test.dataPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer in.Close()
|
||||
return parseTestData(in)
|
||||
}
|
||||
|
||||
func (test *serverTest) run(t *testing.T, write bool) {
|
||||
var serverConn net.Conn
|
||||
var recordingConn *recordingConn
|
||||
|
||||
if write {
|
||||
var err error
|
||||
recordingConn, err = test.connFromCommand(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start subcommand: %v", err)
|
||||
}
|
||||
serverConn = recordingConn
|
||||
} else {
|
||||
timer := time.AfterFunc(10*time.Second, func() {
|
||||
fmt.Println("This test may be stuck, try running using -timeout 10s")
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
timer.Stop()
|
||||
})
|
||||
flows, err := test.loadData()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load data from %s", test.dataPath())
|
||||
}
|
||||
serverConn = newReplayingConn(t, flows)
|
||||
}
|
||||
|
||||
server, chans, reqs, err := ssh.NewServerConn(serverConn, test.config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server conn: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
go ssh.DiscardRequests(reqs)
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
for newChannel := range chans {
|
||||
if newChannel.ChannelType() != "session" {
|
||||
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||
continue
|
||||
}
|
||||
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go func(in <-chan *ssh.Request) {
|
||||
for req := range in {
|
||||
switch req.Type {
|
||||
case "exec":
|
||||
if req.WantReply {
|
||||
req.Reply(true, nil)
|
||||
}
|
||||
channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatusMsg{Status: 0}))
|
||||
channel.Close()
|
||||
done <- true
|
||||
default:
|
||||
if req.WantReply {
|
||||
req.Reply(false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(requests)
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
if write {
|
||||
path := test.dataPath()
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create output file: %v", err)
|
||||
}
|
||||
defer out.Close()
|
||||
recordingConn.Close()
|
||||
|
||||
recordingConn.WriteTo(out)
|
||||
t.Logf("Wrote %s\n", path)
|
||||
}
|
||||
}
|
||||
|
||||
func recordingsServerConfig() *ssh.ServerConfig {
|
||||
config := &ssh.ServerConfig{
|
||||
Config: ssh.Config{
|
||||
Rand: sha3.NewShake128(),
|
||||
},
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
config.AddHostKey(testSigners["rsa"])
|
||||
return config
|
||||
}
|
||||
|
||||
func TestServerKeyExchanges(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
var keyExchanges []string
|
||||
for _, kex := range config.KeyExchanges {
|
||||
// Exclude ecdh for now, to make them determistic we should use see a
|
||||
// stream of fixed bytes as the random source.
|
||||
if !strings.HasPrefix(kex, "ecdh-") {
|
||||
keyExchanges = append(keyExchanges, kex)
|
||||
}
|
||||
}
|
||||
// Add diffie-hellman-group16-sha512 as it is not enabled by default.
|
||||
keyExchanges = append(keyExchanges, "diffie-hellman-group16-sha512")
|
||||
|
||||
for _, kex := range keyExchanges {
|
||||
c := recordingsServerConfig()
|
||||
c.KeyExchanges = []string{kex}
|
||||
test := serverTest{
|
||||
name: "KEX-" + kex,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerCiphers(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
for _, ciph := range config.Ciphers {
|
||||
c := recordingsServerConfig()
|
||||
c.Ciphers = []string{ciph}
|
||||
test := serverTest{
|
||||
name: "Cipher-" + ciph,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMACs(t *testing.T) {
|
||||
config := ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
|
||||
for _, mac := range config.MACs {
|
||||
c := recordingsServerConfig()
|
||||
c.MACs = []string{mac}
|
||||
test := serverTest{
|
||||
name: "MAC-" + mac,
|
||||
config: c,
|
||||
}
|
||||
runTestAndUpdateIfNeeded(t, test.name, test.run)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// Copyright 2025 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 test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSSHDConfig = `
|
||||
Protocol 2
|
||||
Banner {{.Dir}}/banner
|
||||
HostKey {{.Dir}}/id_rsa
|
||||
HostKey {{.Dir}}/id_dsa
|
||||
HostKey {{.Dir}}/id_ecdsa
|
||||
HostCertificate {{.Dir}}/id_rsa-sha2-512-cert.pub
|
||||
Pidfile {{.Dir}}/sshd.pid
|
||||
KeyRegenerationInterval 3600
|
||||
ServerKeyBits 768
|
||||
SyslogFacility AUTH
|
||||
LogLevel DEBUG2
|
||||
LoginGraceTime 120
|
||||
PermitRootLogin no
|
||||
StrictModes no
|
||||
RSAAuthentication yes
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile {{.Dir}}/authorized_keys
|
||||
TrustedUserCAKeys {{.Dir}}/id_ecdsa.pub
|
||||
IgnoreRhosts yes
|
||||
RhostsRSAAuthentication no
|
||||
HostbasedAuthentication no
|
||||
PubkeyAcceptedKeyTypes=*
|
||||
# In recent OpenSSH versions, the 'diffie-hellman-group14-sha1'
|
||||
# algorithm is disabled by default. However, it remains part of
|
||||
# our default Key Exchange (KEX) configuration, so we explicitly
|
||||
# enable it to ensure compatibility for testing.
|
||||
KexAlgorithms +diffie-hellman-group14-sha1
|
||||
`
|
||||
multiAuthSshdConfigTail = `
|
||||
UsePAM yes
|
||||
PasswordAuthentication yes
|
||||
ChallengeResponseAuthentication yes
|
||||
AuthenticationMethods {{.AuthMethods}}
|
||||
`
|
||||
maxAuthTriesSshdConfigTail = `
|
||||
PasswordAuthentication yes
|
||||
MaxAuthTries 1
|
||||
`
|
||||
)
|
||||
|
||||
var configTmpl = map[string]*template.Template{
|
||||
"default": template.Must(template.New("").Parse(defaultSSHDConfig)),
|
||||
"MultiAuth": template.Must(template.New("").Parse(defaultSSHDConfig + multiAuthSshdConfigTail)),
|
||||
"MaxAuthTries": template.Must(template.New("").Parse(defaultSSHDConfig + maxAuthTriesSshdConfigTail))}
|
||||
|
||||
type server struct {
|
||||
t *testing.T
|
||||
configfile string
|
||||
|
||||
testUser string // test username for sshd
|
||||
testPasswd string // test password for sshd
|
||||
sshdTestPwSo string // dynamic library to inject a custom password into sshd
|
||||
|
||||
lastDialConn net.Conn
|
||||
}
|
||||
|
||||
type storedHostKey struct {
|
||||
// keys map from an algorithm string to binary key data.
|
||||
keys map[string][]byte
|
||||
|
||||
// checkCount counts the Check calls. Used for testing
|
||||
// rekeying.
|
||||
checkCount int
|
||||
}
|
||||
|
||||
func (k *storedHostKey) Add(key ssh.PublicKey) {
|
||||
if k.keys == nil {
|
||||
k.keys = map[string][]byte{}
|
||||
}
|
||||
k.keys[key.Type()] = key.Marshal()
|
||||
}
|
||||
|
||||
func (k *storedHostKey) Check(addr string, remote net.Addr, key ssh.PublicKey) error {
|
||||
k.checkCount++
|
||||
algo := key.Type()
|
||||
|
||||
if k.keys == nil || !bytes.Equal(key.Marshal(), k.keys[algo]) {
|
||||
return fmt.Errorf("host key mismatch. Got %q, want %q", key, k.keys[algo])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostKeyDB() *storedHostKey {
|
||||
keyChecker := &storedHostKey{}
|
||||
keyChecker.Add(testPublicKeys["ecdsa"])
|
||||
keyChecker.Add(testPublicKeys["rsa"])
|
||||
keyChecker.Add(testPublicKeys["dsa"])
|
||||
return keyChecker
|
||||
}
|
||||
|
||||
func clientConfig() *ssh.ClientConfig {
|
||||
config := &ssh.ClientConfig{
|
||||
User: username(),
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(testSigners["user"]),
|
||||
},
|
||||
HostKeyCallback: hostKeyDB().Check,
|
||||
HostKeyAlgorithms: []string{ // by default, don't allow certs as this affects the hostKeyDB checker
|
||||
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
|
||||
ssh.KeyAlgoRSA, ssh.KeyAlgoDSA,
|
||||
ssh.KeyAlgoED25519,
|
||||
},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// SSH reference tests run a connection against a reference implementation
|
||||
// (OpenSSH) of SSH and record the bytes of the resulting connection. The Go
|
||||
// code, during a test, is configured with deterministic randomness and so the
|
||||
// reference test can be reproduced exactly in the future.
|
||||
//
|
||||
// In order to save everyone who wishes to run the tests from needing the
|
||||
// reference implementation installed, the reference connections are saved in
|
||||
// files in the testdata directory. Thus running the tests involves nothing
|
||||
// external, but creating and updating them requires the reference
|
||||
// implementation.
|
||||
//
|
||||
// Tests can be updated by running them with the -update flag. This will cause
|
||||
// the test files for failing tests to be regenerated. Since the reference
|
||||
// implementation will always generate fresh random numbers, large parts of the
|
||||
// reference connection will always change.
|
||||
|
||||
var (
|
||||
update = flag.Bool("update", false, "update golden files on failure")
|
||||
)
|
||||
|
||||
func runTestAndUpdateIfNeeded(t *testing.T, name string, run func(t *testing.T, update bool)) {
|
||||
success := t.Run(name, func(t *testing.T) {
|
||||
if !*update {
|
||||
t.Parallel()
|
||||
}
|
||||
run(t, false)
|
||||
})
|
||||
|
||||
if !success && *update {
|
||||
t.Run(name+"#update", func(t *testing.T) {
|
||||
run(t, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// recordingConn is a net.Conn that records the traffic that passes through it.
|
||||
// WriteTo can be used to produce output that can be later be loaded with
|
||||
// ParseTestData.
|
||||
type recordingConn struct {
|
||||
net.Conn
|
||||
clientToServer bool
|
||||
sync.Mutex
|
||||
flows [][]byte
|
||||
reading bool
|
||||
}
|
||||
|
||||
func (r *recordingConn) Read(b []byte) (n int, err error) {
|
||||
if n, err = r.Conn.Read(b); n == 0 {
|
||||
return
|
||||
}
|
||||
b = b[:n]
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if l := len(r.flows); l == 0 || !r.reading {
|
||||
buf := make([]byte, len(b))
|
||||
copy(buf, b)
|
||||
r.flows = append(r.flows, buf)
|
||||
} else {
|
||||
r.flows[l-1] = append(r.flows[l-1], b[:n]...)
|
||||
}
|
||||
r.reading = true
|
||||
return
|
||||
}
|
||||
|
||||
func (r *recordingConn) Write(b []byte) (n int, err error) {
|
||||
if n, err = r.Conn.Write(b); n == 0 {
|
||||
return
|
||||
}
|
||||
b = b[:n]
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if l := len(r.flows); l == 0 || r.reading {
|
||||
buf := make([]byte, len(b))
|
||||
copy(buf, b)
|
||||
r.flows = append(r.flows, buf)
|
||||
} else {
|
||||
r.flows[l-1] = append(r.flows[l-1], b[:n]...)
|
||||
}
|
||||
r.reading = false
|
||||
return
|
||||
}
|
||||
|
||||
// WriteTo writes Go source code to w that contains the recorded traffic.
|
||||
func (r *recordingConn) WriteTo(w io.Writer) (int64, error) {
|
||||
var written int64
|
||||
for i, flow := range r.flows {
|
||||
source, dest := "client", "server"
|
||||
if !r.clientToServer {
|
||||
source, dest = dest, source
|
||||
}
|
||||
n, err := fmt.Fprintf(w, ">>> Flow %d (%s to %s)\n", i+1, source, dest)
|
||||
written += int64(n)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
dumper := hex.Dumper(w)
|
||||
n, err = dumper.Write(flow)
|
||||
written += int64(n)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
err = dumper.Close()
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
r.clientToServer = !r.clientToServer
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func parseTestData(r io.Reader) (flows [][]byte, err error) {
|
||||
var currentFlow []byte
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// If the line starts with ">>> " then it marks the beginning
|
||||
// of a new flow.
|
||||
if strings.HasPrefix(line, ">>> ") {
|
||||
if len(currentFlow) > 0 || len(flows) > 0 {
|
||||
flows = append(flows, currentFlow)
|
||||
currentFlow = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise the line is a line of hex dump that looks like:
|
||||
// 00000170 fc f5 06 bf (...) |.....X{&?......!|
|
||||
// (Some bytes have been omitted from the middle section.)
|
||||
_, after, ok := strings.Cut(line, " ")
|
||||
if !ok {
|
||||
return nil, errors.New("invalid test data")
|
||||
}
|
||||
line = after
|
||||
|
||||
before, _, ok := strings.Cut(line, "|")
|
||||
if !ok {
|
||||
return nil, errors.New("invalid test data")
|
||||
}
|
||||
line = before
|
||||
|
||||
hexBytes := strings.Fields(line)
|
||||
for _, hexByte := range hexBytes {
|
||||
val, err := strconv.ParseUint(hexByte, 16, 8)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid hex byte in test data: " + err.Error())
|
||||
}
|
||||
currentFlow = append(currentFlow, byte(val))
|
||||
}
|
||||
}
|
||||
|
||||
if len(currentFlow) > 0 {
|
||||
flows = append(flows, currentFlow)
|
||||
}
|
||||
|
||||
return flows, nil
|
||||
}
|
||||
|
||||
func newReplayingConn(t testing.TB, flows [][]byte) net.Conn {
|
||||
r := &replayingConn{
|
||||
t: t,
|
||||
flows: flows,
|
||||
reading: false,
|
||||
}
|
||||
r.readCond = sync.NewCond(&r.Mutex)
|
||||
return r
|
||||
}
|
||||
|
||||
// replayingConn is a net.Conn that replays flows recorded by recordingConn.
|
||||
type replayingConn struct {
|
||||
t testing.TB
|
||||
sync.Mutex
|
||||
flows [][]byte
|
||||
reading bool
|
||||
// SSH channels use a read loop goroutine, we use this condition to wait
|
||||
// until we are ready to read/write.
|
||||
readCond *sync.Cond
|
||||
}
|
||||
|
||||
var _ net.Conn = (*replayingConn)(nil)
|
||||
|
||||
func (r *replayingConn) Read(b []byte) (n int, err error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
for !r.reading {
|
||||
r.readCond.Wait()
|
||||
}
|
||||
|
||||
// Some tests run commands that return no output.
|
||||
if len(r.flows) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n = copy(b, r.flows[0])
|
||||
r.flows[0] = r.flows[0][n:]
|
||||
if len(r.flows[0]) == 0 {
|
||||
r.flows = r.flows[1:]
|
||||
r.reading = false
|
||||
r.readCond.Broadcast()
|
||||
if len(r.flows) == 0 {
|
||||
return n, io.EOF
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *replayingConn) Write(b []byte) (n int, err error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
for r.reading {
|
||||
r.readCond.Wait()
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(r.flows[0], b) {
|
||||
r.t.Errorf("write mismatch: expected %x, got %x", r.flows[0], b)
|
||||
r.reading = true
|
||||
r.readCond.Broadcast()
|
||||
return 0, fmt.Errorf("write mismatch")
|
||||
}
|
||||
r.flows[0] = r.flows[0][len(b):]
|
||||
if len(r.flows[0]) == 0 {
|
||||
r.flows = r.flows[1:]
|
||||
r.reading = true
|
||||
r.readCond.Broadcast()
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (r *replayingConn) Close() error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if len(r.flows) > 0 {
|
||||
r.t.Errorf("closed with unfinished flows: %d", len(r.flows))
|
||||
return fmt.Errorf("unexpected close")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *replayingConn) LocalAddr() net.Addr { return nil }
|
||||
func (r *replayingConn) RemoteAddr() net.Addr { return nil }
|
||||
func (r *replayingConn) SetDeadline(_ time.Time) error { return nil }
|
||||
func (r *replayingConn) SetReadDeadline(_ time.Time) error { return nil }
|
||||
func (r *replayingConn) SetWriteDeadline(_ time.Time) error { return nil }
|
||||
|
||||
func username() string {
|
||||
var username string
|
||||
if user, err := user.Current(); err == nil {
|
||||
username = user.Username
|
||||
} else {
|
||||
// user.Current() currently requires cgo. If an error is
|
||||
// returned attempt to get the username from the environment.
|
||||
log.Printf("user.Current: %v; falling back on $USER", err)
|
||||
username = os.Getenv("USER")
|
||||
}
|
||||
if username == "" {
|
||||
panic("Unable to get username")
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
func writeFile(path string, contents []byte) {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -10,7 +10,6 @@ package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@@ -29,108 +28,6 @@ func skipIfIssue64959(t *testing.T, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandSuccess(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
defer conn.Close()
|
||||
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
skipIfIssue64959(t, err)
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
err = session.Run("true")
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostKeyCheck(t *testing.T) {
|
||||
server := newServer(t)
|
||||
|
||||
conf := clientConfig()
|
||||
hostDB := hostKeyDB()
|
||||
conf.HostKeyCallback = hostDB.Check
|
||||
|
||||
// change the keys.
|
||||
hostDB.keys[ssh.KeyAlgoRSA][25]++
|
||||
hostDB.keys[ssh.KeyAlgoDSA][25]++
|
||||
hostDB.keys[ssh.KeyAlgoECDSA256][25]++
|
||||
|
||||
conn, err := server.TryDial(conf)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
t.Fatalf("dial should have failed.")
|
||||
} else if !strings.Contains(err.Error(), "host key mismatch") {
|
||||
t.Fatalf("'host key mismatch' not found in %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandStdin(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
defer conn.Close()
|
||||
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
skipIfIssue64959(t, err)
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
defer w.Close()
|
||||
session.Stdin = r
|
||||
|
||||
err = session.Run("true")
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandStdinError(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
defer conn.Close()
|
||||
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
skipIfIssue64959(t, err)
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
r, w := io.Pipe()
|
||||
defer r.Close()
|
||||
session.Stdin = r
|
||||
pipeErr := errors.New("closing write end of pipe")
|
||||
w.CloseWithError(pipeErr)
|
||||
|
||||
err = session.Run("true")
|
||||
if err != pipeErr {
|
||||
t.Fatalf("expected %v, found %v", pipeErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandFailed(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
defer conn.Close()
|
||||
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
skipIfIssue64959(t, err)
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
err = session.Run(`bash -c "kill -9 $$"`)
|
||||
if err == nil {
|
||||
t.Fatalf("session succeeded: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandWeClosed(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
@@ -290,59 +187,6 @@ func TestValidTerminalMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowChange(t *testing.T) {
|
||||
if runtime.GOOS == "aix" {
|
||||
// On AIX, sshd cannot acquire /dev/pts/* if launched as
|
||||
// a non-root user.
|
||||
t.Skipf("skipping on %s", runtime.GOOS)
|
||||
}
|
||||
server := newServer(t)
|
||||
conn := server.Dial(clientConfig())
|
||||
defer conn.Close()
|
||||
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
skipIfIssue64959(t, err)
|
||||
t.Fatalf("session failed: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to acquire stdout pipe: %s", err)
|
||||
}
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to acquire stdin pipe: %s", err)
|
||||
}
|
||||
|
||||
tm := ssh.TerminalModes{ssh.ECHO: 0}
|
||||
if err = session.RequestPty("xterm", 80, 40, tm); err != nil {
|
||||
t.Fatalf("req-pty failed: %s", err)
|
||||
}
|
||||
|
||||
if err := session.WindowChange(100, 100); err != nil {
|
||||
t.Fatalf("window-change failed: %s", err)
|
||||
}
|
||||
|
||||
err = session.Shell()
|
||||
if err != nil {
|
||||
t.Fatalf("session failed: %s", err)
|
||||
}
|
||||
|
||||
stdin.Write([]byte("stty size && exit\n"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, stdout); err != nil {
|
||||
t.Fatalf("reading failed: %s", err)
|
||||
}
|
||||
|
||||
if sttyOutput := buf.String(); !strings.Contains(sttyOutput, "100 100") {
|
||||
t.Fatalf("terminal WindowChange failure: expected \"100 100\" stty output, got %s", sttyOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func testOneCipher(t *testing.T, cipher string, cipherOrder []string) {
|
||||
server := newServer(t)
|
||||
conf := clientConfig()
|
||||
@@ -396,55 +240,6 @@ func TestCiphers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMACs(t *testing.T) {
|
||||
var config ssh.Config
|
||||
config.SetDefaults()
|
||||
macOrder := config.MACs
|
||||
|
||||
for _, mac := range macOrder {
|
||||
t.Run(mac, func(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conf := clientConfig()
|
||||
conf.MACs = []string{mac}
|
||||
// Don't fail if sshd doesn't have the MAC.
|
||||
conf.MACs = append(conf.MACs, macOrder...)
|
||||
if conn, err := server.TryDial(conf); err == nil {
|
||||
conn.Close()
|
||||
} else {
|
||||
t.Fatalf("failed for MAC %q", mac)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyExchanges(t *testing.T) {
|
||||
var config ssh.Config
|
||||
config.SetDefaults()
|
||||
kexOrder := config.KeyExchanges
|
||||
// Based on the discussion in #17230, the key exchange algorithms
|
||||
// diffie-hellman-group-exchange-sha1 and diffie-hellman-group-exchange-sha256
|
||||
// are not included in the default list of supported kex so we have to add them
|
||||
// here manually.
|
||||
kexOrder = append(kexOrder, "diffie-hellman-group-exchange-sha1", "diffie-hellman-group-exchange-sha256")
|
||||
// The key exchange algorithms diffie-hellman-group16-sha512 is disabled by
|
||||
// default so we add it here manually.
|
||||
kexOrder = append(kexOrder, "diffie-hellman-group16-sha512")
|
||||
for _, kex := range kexOrder {
|
||||
t.Run(kex, func(t *testing.T) {
|
||||
server := newServer(t)
|
||||
conf := clientConfig()
|
||||
// Don't fail if sshd doesn't have the kex.
|
||||
conf.KeyExchanges = append([]string{kex}, kexOrder...)
|
||||
conn, err := server.TryDial(conf)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
} else {
|
||||
t.Errorf("failed for kex %q", kex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAuthAlgorithms(t *testing.T) {
|
||||
for _, key := range []string{
|
||||
"rsa",
|
||||
|
||||
@@ -13,140 +13,18 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/crypto/internal/testenv"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/testdata"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSshdConfig = `
|
||||
Protocol 2
|
||||
Banner {{.Dir}}/banner
|
||||
HostKey {{.Dir}}/id_rsa
|
||||
HostKey {{.Dir}}/id_dsa
|
||||
HostKey {{.Dir}}/id_ecdsa
|
||||
HostCertificate {{.Dir}}/id_rsa-sha2-512-cert.pub
|
||||
Pidfile {{.Dir}}/sshd.pid
|
||||
#UsePrivilegeSeparation no
|
||||
KeyRegenerationInterval 3600
|
||||
ServerKeyBits 768
|
||||
SyslogFacility AUTH
|
||||
LogLevel DEBUG2
|
||||
LoginGraceTime 120
|
||||
PermitRootLogin no
|
||||
StrictModes no
|
||||
RSAAuthentication yes
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile {{.Dir}}/authorized_keys
|
||||
TrustedUserCAKeys {{.Dir}}/id_ecdsa.pub
|
||||
IgnoreRhosts yes
|
||||
RhostsRSAAuthentication no
|
||||
HostbasedAuthentication no
|
||||
PubkeyAcceptedKeyTypes=*
|
||||
`
|
||||
multiAuthSshdConfigTail = `
|
||||
UsePAM yes
|
||||
PasswordAuthentication yes
|
||||
ChallengeResponseAuthentication yes
|
||||
AuthenticationMethods {{.AuthMethods}}
|
||||
`
|
||||
maxAuthTriesSshdConfigTail = `
|
||||
PasswordAuthentication yes
|
||||
MaxAuthTries 1
|
||||
`
|
||||
)
|
||||
|
||||
var configTmpl = map[string]*template.Template{
|
||||
"default": template.Must(template.New("").Parse(defaultSshdConfig)),
|
||||
"MultiAuth": template.Must(template.New("").Parse(defaultSshdConfig + multiAuthSshdConfigTail)),
|
||||
"MaxAuthTries": template.Must(template.New("").Parse(defaultSshdConfig + maxAuthTriesSshdConfigTail))}
|
||||
|
||||
type server struct {
|
||||
t *testing.T
|
||||
configfile string
|
||||
|
||||
testUser string // test username for sshd
|
||||
testPasswd string // test password for sshd
|
||||
sshdTestPwSo string // dynamic library to inject a custom password into sshd
|
||||
|
||||
lastDialConn net.Conn
|
||||
}
|
||||
|
||||
func username() string {
|
||||
var username string
|
||||
if user, err := user.Current(); err == nil {
|
||||
username = user.Username
|
||||
} else {
|
||||
// user.Current() currently requires cgo. If an error is
|
||||
// returned attempt to get the username from the environment.
|
||||
log.Printf("user.Current: %v; falling back on $USER", err)
|
||||
username = os.Getenv("USER")
|
||||
}
|
||||
if username == "" {
|
||||
panic("Unable to get username")
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
type storedHostKey struct {
|
||||
// keys map from an algorithm string to binary key data.
|
||||
keys map[string][]byte
|
||||
|
||||
// checkCount counts the Check calls. Used for testing
|
||||
// rekeying.
|
||||
checkCount int
|
||||
}
|
||||
|
||||
func (k *storedHostKey) Add(key ssh.PublicKey) {
|
||||
if k.keys == nil {
|
||||
k.keys = map[string][]byte{}
|
||||
}
|
||||
k.keys[key.Type()] = key.Marshal()
|
||||
}
|
||||
|
||||
func (k *storedHostKey) Check(addr string, remote net.Addr, key ssh.PublicKey) error {
|
||||
k.checkCount++
|
||||
algo := key.Type()
|
||||
|
||||
if k.keys == nil || bytes.Compare(key.Marshal(), k.keys[algo]) != 0 {
|
||||
return fmt.Errorf("host key mismatch. Got %q, want %q", key, k.keys[algo])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostKeyDB() *storedHostKey {
|
||||
keyChecker := &storedHostKey{}
|
||||
keyChecker.Add(testPublicKeys["ecdsa"])
|
||||
keyChecker.Add(testPublicKeys["rsa"])
|
||||
keyChecker.Add(testPublicKeys["dsa"])
|
||||
return keyChecker
|
||||
}
|
||||
|
||||
func clientConfig() *ssh.ClientConfig {
|
||||
config := &ssh.ClientConfig{
|
||||
User: username(),
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(testSigners["user"]),
|
||||
},
|
||||
HostKeyCallback: hostKeyDB().Check,
|
||||
HostKeyAlgorithms: []string{ // by default, don't allow certs as this affects the hostKeyDB checker
|
||||
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
|
||||
ssh.KeyAlgoRSA, ssh.KeyAlgoDSA,
|
||||
ssh.KeyAlgoED25519,
|
||||
},
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// unixConnection creates two halves of a connected net.UnixConn. It
|
||||
// is used for connecting the Go SSH client with sshd without opening
|
||||
// ports.
|
||||
@@ -262,17 +140,6 @@ func (s *server) Dial(config *ssh.ClientConfig) *ssh.Client {
|
||||
return conn
|
||||
}
|
||||
|
||||
func writeFile(path string, contents []byte) {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// generate random password
|
||||
func randomPassword() (string, error) {
|
||||
b := make([]byte, 12)
|
||||
|
||||
Reference in New Issue
Block a user