mlkem768x25519-sha256 requires the crypto/mlkem package introduced in Go 1.24. Thanks to Damien Miller for posting an early version to the OpenSSH mailing list. Co-authored-by: Damien Miller <djm@mindrot.org> Change-Id: I4235cf906903524a9a97283834cc8f43b5f76f91 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/646075 Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Nicola Murino <nicola.murino@gmail.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
287 lines
6.8 KiB
Go
287 lines
6.8 KiB
Go
// 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.SetDefaults()
|
|
// Remove ML-KEM since it only works with Go 1.24.
|
|
config.SetDefaults()
|
|
if config.KeyExchanges[0] == "mlkem768x25519-sha256" {
|
|
config.KeyExchanges = config.KeyExchanges[1:]
|
|
}
|
|
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.
|
|
// Exclude ML-KEM because server side is not deterministic.
|
|
if !strings.HasPrefix(kex, "ecdh-") && !strings.HasPrefix(kex, "mlkem") {
|
|
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)
|
|
}
|
|
}
|