Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions x/crypto/ssh/client_research.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions x/crypto/ssh/client_research_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}