From b2b33e41760331549ba971c169d2364cfe55144c Mon Sep 17 00:00:00 2001 From: msuiche Date: Mon, 7 Sep 2026 21:03:04 +0200 Subject: [PATCH] ssh: add UnauthClientConn.RequestKeyExchange for client-requested pre-auth rekey Expose the ability to trigger a key re-exchange from an unauthenticated client connection. The transport already queues packets written during a rekey and flushes them on completion, so the connection remains usable immediately after the request. This enables pre-authentication state-transition research, e.g. probing how a server handles connection-protocol messages after a rekey that was requested before userauth completed (needed to detect RouterOS CVE-2026-67279, where a pre-auth rekey makes the server accept session channels without authentication). --- x/crypto/ssh/client_research.go | 22 +++++++++ x/crypto/ssh/client_research_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 x/crypto/ssh/client_research_test.go diff --git a/x/crypto/ssh/client_research.go b/x/crypto/ssh/client_research.go index 9bb99a2e35..c32b6ccd01 100644 --- a/x/crypto/ssh/client_research.go +++ b/x/crypto/ssh/client_research.go @@ -129,6 +129,28 @@ func (uac *UnauthClientConn) clientHandshakeUnauth(dialAddress string) error { return nil } +// RequestKeyExchange initiates a client-requested key re-exchange (rekey) +// on the underlying transport, without waiting for it to complete. Packets +// written while the re-exchange is in progress are queued by the transport +// and flushed once it finishes, so callers can continue using the connection +// immediately; if the re-exchange fails, the error surfaces on the next +// read or write. +// +// This enables pre-authentication state-transition research, e.g. probing +// how a server handles connection-protocol messages after a rekey that was +// requested before userauth completed. +func (uac *UnauthClientConn) RequestKeyExchange() error { + if uac.c == nil || uac.c.transport == nil { + return errors.New("ssh: no transport") + } + ht, ok := uac.c.transport.(*handshakeTransport) + if !ok { + return errors.New("ssh: transport does not support client-requested rekey") + } + ht.requestKeyExchange() + return nil +} + func (uac *UnauthClientConn) RequestUserAuth() (map[string][]byte, error) { c := uac.c extensions := make(map[string][]byte) diff --git a/x/crypto/ssh/client_research_test.go b/x/crypto/ssh/client_research_test.go new file mode 100644 index 0000000000..c0da66df60 --- /dev/null +++ b/x/crypto/ssh/client_research_test.go @@ -0,0 +1,67 @@ +package ssh + +import ( + "errors" + "testing" +) + +// TestUnauthClientConnRequestKeyExchange verifies that a client-requested +// rekey before userauth completes cleanly and leaves the connection usable +// for subsequent authentication. +func TestUnauthClientConnRequestKeyExchange(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + + serverConfig := &ServerConfig{ + PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) { + if conn.User() == "testuser" && string(password) == "tiger" { + return nil, nil + } + return nil, errors.New("password rejected") + }, + } + serverConfig.AddHostKey(testSigners["rsa"]) + + serverDone := make(chan error, 1) + go func() { + _, err := newServer(c1, serverConfig) + serverDone <- err + }() + + clientConfig := &ClientConfig{ + User: "testuser", + HostKeyCallback: InsecureIgnoreHostKey(), + } + + uac, err := NewUnauthClientConn(c2, "pipe", clientConfig) + if err != nil { + t.Fatalf("NewUnauthClientConn: %v", err) + } + defer uac.c.Close() + + // Request a key re-exchange before any authentication. + if err := uac.RequestKeyExchange(); err != nil { + t.Fatalf("RequestKeyExchange: %v", err) + } + + // The rekey is asynchronous; packets written while it runs are queued + // and flushed once it completes. Authenticate over the rekeyed + // transport to prove the connection is still usable. + exts, err := uac.RequestUserAuth() + if err != nil { + t.Fatalf("RequestUserAuth after rekey: %v", err) + } + ares, _, err := uac.Authenticate(Password("tiger"), exts) + if err != nil { + t.Fatalf("Authenticate after rekey: %v", err) + } + if ares != AuthResultSuccess { + t.Fatalf("Authenticate after rekey: got %v, want %v", ares, AuthResultSuccess) + } + + if err := <-serverDone; err != nil { + t.Fatalf("server handshake: %v", err) + } +}