package wgsync // Group C — the SSH push client against an IN-PROCESS x/crypto/ssh server (Scenarios A/B/C-c4). // The server captures the exact stdin payload, so tests assert the pushed BYTES; the host-key // tests prove the pin both ways (correct key accepted, wrong key refused). import ( "bytes" "context" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "encoding/pem" "io" "log" "net" "strings" "sync" "testing" "time" "golang.org/x/crypto/ssh" ) // testKeys generates an ed25519 keypair and returns (PEM private key, ssh.Signer). func testKeys(t *testing.T) ([]byte, ssh.Signer) { t.Helper() _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("ed25519: %v", err) } block, err := ssh.MarshalPrivateKey(priv, "") if err != nil { t.Fatalf("MarshalPrivateKey: %v", err) } signer, err := ssh.NewSignerFromKey(priv) if err != nil { t.Fatalf("NewSignerFromKey: %v", err) } return pem.EncodeToMemory(block), signer } // testServer is a single-shot in-process SSH server handling one exec session. type testServer struct { addr string mu sync.Mutex captured []byte // stdin the "script" received cmd string // the exec command string requested } // startTestServer runs an SSH server that accepts clientSigner's key, serves with hostSigner, // reads all stdin, replies stdoutResp/stderrResp and exitStatus. func startTestServer(t *testing.T, hostSigner ssh.Signer, clientSigner ssh.Signer, stdoutResp, stderrResp string, exitStatus uint32) *testServer { t.Helper() cfg := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { if bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) { return &ssh.Permissions{}, nil } return nil, io.EOF }, } cfg.AddHostKey(hostSigner) ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } t.Cleanup(func() { ln.Close() }) srv := &testServer{addr: ln.Addr().String()} go func() { conn, err := ln.Accept() if err != nil { return } sconn, chans, reqs, err := ssh.NewServerConn(conn, cfg) if err != nil { return // e.g. the wrong-host-key client aborts the handshake } defer sconn.Close() go ssh.DiscardRequests(reqs) for newCh := range chans { if newCh.ChannelType() != "session" { newCh.Reject(ssh.UnknownChannelType, "unsupported") continue } ch, chReqs, err := newCh.Accept() if err != nil { continue } go func() { for req := range chReqs { if req.Type == "exec" { var p struct{ Command string } ssh.Unmarshal(req.Payload, &p) srv.mu.Lock() srv.cmd = p.Command srv.mu.Unlock() req.Reply(true, nil) data, _ := io.ReadAll(ch) // the pushed payload (client EOFs stdin) srv.mu.Lock() srv.captured = data srv.mu.Unlock() if stderrResp != "" { ch.Stderr().Write([]byte(stderrResp)) } if stdoutResp != "" { ch.Write([]byte(stdoutResp)) } ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{exitStatus})) ch.Close() return } req.Reply(false, nil) } }() } }() return srv } func hostKeyLine(t *testing.T, s ssh.Signer) string { t.Helper() return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(s.PublicKey()))) } func newTestClient(t *testing.T, addr, hostKey string, clientPEM []byte) *Client { t.Helper() c, err := New(Config{ Addr: addr, User: "felhom-peersync", PrivateKey: clientPEM, HostKeyLine: hostKey, Timeout: 5 * time.Second, }, log.New(io.Discard, "", 0)) if err != nil { t.Fatalf("wgsync.New: %v", err) } return c } func TestPush_PayloadDeliveredExactly(t *testing.T) { clientPEM, clientSigner := testKeys(t) _, hostSigner := testKeys(t) srv := startTestServer(t, hostSigner, clientSigner, `{"status":"ok","applied":1}`, "", 0) c := newTestClient(t, srv.addr, hostKeyLine(t, hostSigner), clientPEM) payload := []byte(`{"version":1,"interface":"wg0","peers":[{"pubkey":"PK1","allowed_ip":"10.77.0.2/32"}]}`) if err := c.Push(context.Background(), payload); err != nil { t.Fatalf("Push: %v", err) } srv.mu.Lock() defer srv.mu.Unlock() if !bytes.Equal(srv.captured, payload) { t.Errorf("server captured %q, want the exact payload %q", srv.captured, payload) } if srv.cmd != "felhom-peersync" { t.Errorf("exec command = %q, want felhom-peersync (documents intent even under forced command)", srv.cmd) } } func TestPush_RemoteFailureSurfacesStderr(t *testing.T) { clientPEM, clientSigner := testKeys(t) _, hostSigner := testKeys(t) srv := startTestServer(t, hostSigner, clientSigner, "", "boom", 1) c := newTestClient(t, srv.addr, hostKeyLine(t, hostSigner), clientPEM) err := c.Push(context.Background(), []byte(`{}`)) if err == nil { t.Fatal("Push succeeded against exit-1 server") } if !strings.Contains(err.Error(), "boom") { t.Errorf("error %q does not carry the remote stderr", err) } } func TestPush_MalformedResponseIsError(t *testing.T) { clientPEM, clientSigner := testKeys(t) _, hostSigner := testKeys(t) srv := startTestServer(t, hostSigner, clientSigner, "garbage-not-json", "", 0) c := newTestClient(t, srv.addr, hostKeyLine(t, hostSigner), clientPEM) if err := c.Push(context.Background(), []byte(`{}`)); err == nil { t.Fatal("Push accepted a malformed script response — it must not guess") } } func TestPush_WrongHostKeyRefused(t *testing.T) { clientPEM, clientSigner := testKeys(t) _, hostSigner := testKeys(t) _, otherSigner := testKeys(t) // NOT the server's key srv := startTestServer(t, hostSigner, clientSigner, `{"status":"ok","applied":0}`, "", 0) c := newTestClient(t, srv.addr, hostKeyLine(t, otherSigner), clientPEM) err := c.Push(context.Background(), []byte(`{}`)) if err == nil { t.Fatal("Push succeeded against a server with the WRONG host key — the pin is dead") } if !strings.Contains(err.Error(), "host key") && !strings.Contains(err.Error(), "handshake") { t.Errorf("error %q is not a host-key refusal", err) } srv.mu.Lock() defer srv.mu.Unlock() if len(srv.captured) != 0 { t.Errorf("payload leaked to a mis-keyed server: %q", srv.captured) } } // TestPush_MultiHostkeyServerStillMatchesPin reproduces the live S1 failure: a server holding // MULTIPLE host keys (like a stock sshd: ECDSA + ed25519) must still present the PINNED type. // Without constraining ClientConfig.HostKeyAlgorithms to the pinned key's algorithm, default // negotiation can select the other key and FixedHostKey refuses a legitimate server. func TestPush_MultiHostkeyServerStillMatchesPin(t *testing.T) { clientPEM, clientSigner := testKeys(t) _, edSigner := testKeys(t) ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("ecdsa: %v", err) } ecSigner, err := ssh.NewSignerFromKey(ecKey) if err != nil { t.Fatalf("ecdsa signer: %v", err) } // Server holds BOTH keys, ECDSA added first (the tempting default pick). cfg := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { if bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) { return &ssh.Permissions{}, nil } return nil, io.EOF }, } cfg.AddHostKey(ecSigner) cfg.AddHostKey(edSigner) ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } t.Cleanup(func() { ln.Close() }) go func() { conn, err := ln.Accept() if err != nil { return } sconn, chans, reqs, err := ssh.NewServerConn(conn, cfg) if err != nil { return } defer sconn.Close() go ssh.DiscardRequests(reqs) for newCh := range chans { ch, chReqs, err := newCh.Accept() if err != nil { continue } go func() { for req := range chReqs { if req.Type == "exec" { req.Reply(true, nil) io.ReadAll(ch) ch.Write([]byte(`{"status":"ok","applied":0}`)) ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{0})) ch.Close() return } req.Reply(false, nil) } }() } }() // Pin the ed25519 key — Push must negotiate exactly that type and succeed. c := newTestClient(t, ln.Addr().String(), hostKeyLine(t, edSigner), clientPEM) if err := c.Push(context.Background(), []byte(`{"peers":[]}`)); err != nil { t.Fatalf("Push against multi-hostkey server with correct pin: %v", err) } } func TestNew_BadInputsFailEarly(t *testing.T) { clientPEM, _ := testKeys(t) _, hostSigner := testKeys(t) hk := hostKeyLine(t, hostSigner) logger := log.New(io.Discard, "", 0) if _, err := New(Config{Addr: "x:22", User: "u", PrivateKey: []byte("not-a-key"), HostKeyLine: hk}, logger); err == nil { t.Error("bad private key accepted") } if _, err := New(Config{Addr: "x:22", User: "u", PrivateKey: clientPEM, HostKeyLine: "not a key line"}, logger); err == nil { t.Error("bad host key line accepted") } if _, err := New(Config{User: "u", PrivateKey: clientPEM, HostKeyLine: hk}, logger); err == nil { t.Error("missing addr accepted") } }