crypto/ssh: allow client to specify host key algorithms.

Fixes golang/go#11722.

Change-Id: I4fa2a1db14050151f9269427ca35cf7ebd21440a
Reviewed-on: https://go-review.googlesource.com/12907
Reviewed-by: Adam Langley <agl@golang.org>
diff --git a/ssh/client.go b/ssh/client.go
index 72bd27f..0b9fbe5 100644
--- a/ssh/client.go
+++ b/ssh/client.go
@@ -203,4 +203,11 @@
 	// ClientVersion contains the version identification string that will
 	// be used for the connection. If empty, a reasonable default is used.
 	ClientVersion string
+
+	// HostKeyAlgorithms lists the key types that the client will
+	// accept from the server as host key, in order of
+	// preference. If empty, a reasonable default is used. Any
+	// string returned from PublicKey.Type method may be used, or
+	// any of the CertAlgoXxxx and KeyAlgoXxxx constants.
+	HostKeyAlgorithms []string
 }
diff --git a/ssh/handshake.go b/ssh/handshake.go
index 4acc1a0..50461be 100644
--- a/ssh/handshake.go
+++ b/ssh/handshake.go
@@ -59,7 +59,14 @@
 	serverVersion []byte
 	clientVersion []byte
 
-	hostKeys []Signer // If hostKeys are given, we are the server.
+	// hostKeys is non-empty if we are the server. In that case,
+	// it contains all host keys that can be used to sign the
+	// connection.
+	hostKeys []Signer
+
+	// hostKeyAlgorithms is non-empty if we are the client. In that case,
+	// we accept these key types from the server as host key.
+	hostKeyAlgorithms []string
 
 	// On read error, incoming is closed, and readError is set.
 	incoming  chan []byte
@@ -98,6 +105,11 @@
 	t.dialAddress = dialAddr
 	t.remoteAddr = addr
 	t.hostKeyCallback = config.HostKeyCallback
+	if config.HostKeyAlgorithms != nil {
+		t.hostKeyAlgorithms = config.HostKeyAlgorithms
+	} else {
+		t.hostKeyAlgorithms = supportedHostKeyAlgos
+	}
 	go t.readLoop()
 	return t
 }
@@ -234,7 +246,7 @@
 				msg.ServerHostKeyAlgos, k.PublicKey().Type())
 		}
 	} else {
-		msg.ServerHostKeyAlgos = supportedHostKeyAlgos
+		msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
 	}
 	packet := Marshal(msg)
 
diff --git a/ssh/handshake_test.go b/ssh/handshake_test.go
index f690627..3202371 100644
--- a/ssh/handshake_test.go
+++ b/ssh/handshake_test.go
@@ -69,6 +69,7 @@
 
 	serverConf := &ServerConfig{}
 	serverConf.AddHostKey(testSigners["ecdsa"])
+	serverConf.AddHostKey(testSigners["rsa"])
 	serverConf.SetDefaults()
 	server = newServerTransport(trS, v, v, serverConf)
 
diff --git a/ssh/session_test.go b/ssh/session_test.go
index 7ce44f5..f7f0f76 100644
--- a/ssh/session_test.go
+++ b/ssh/session_test.go
@@ -718,3 +718,57 @@
 		t.Fatalf("NewServerConn attempted to Read() from Conn while configuration is missing authentication method")
 	}
 }
+
+func TestHostKeyAlgorithms(t *testing.T) {
+	serverConf := &ServerConfig{
+		NoClientAuth: true,
+	}
+	serverConf.AddHostKey(testSigners["rsa"])
+	serverConf.AddHostKey(testSigners["ecdsa"])
+
+	connect := func(clientConf *ClientConfig, want string) {
+		var alg string
+		clientConf.HostKeyCallback = func(h string, a net.Addr, key PublicKey) error {
+			alg = key.Type()
+			return nil
+		}
+		c1, c2, err := netPipe()
+		if err != nil {
+			t.Fatalf("netPipe: %v", err)
+		}
+		defer c1.Close()
+		defer c2.Close()
+
+		go NewServerConn(c1, serverConf)
+		_, _, _, err = NewClientConn(c2, "", clientConf)
+		if err != nil {
+			t.Fatalf("NewClientConn: %v", err)
+		}
+		if alg != want {
+			t.Errorf("selected key algorithm %s, want %s", alg, want)
+		}
+	}
+
+	// By default, we get the preferred algorithm, which is ECDSA 256.
+
+	clientConf := &ClientConfig{}
+	connect(clientConf, KeyAlgoECDSA256)
+
+	// Client asks for RSA explicitly.
+	clientConf.HostKeyAlgorithms = []string{KeyAlgoRSA}
+	connect(clientConf, KeyAlgoRSA)
+
+	c1, c2, err := netPipe()
+	if err != nil {
+		t.Fatalf("netPipe: %v", err)
+	}
+	defer c1.Close()
+	defer c2.Close()
+
+	go NewServerConn(c1, serverConf)
+	clientConf.HostKeyAlgorithms = []string{"nonexistent-hostkey-algo"}
+	_, _, _, err = NewClientConn(c2, "", clientConf)
+	if err == nil {
+		t.Fatal("succeeded connecting with unknown hostkey algorithm")
+	}
+}