Skip to content

Commit e4e2799

Browse files
committed
ssh: require host key checking in the ClientConfig
This change breaks existing behavior. Before, a missing ClientConfig.HostKeyCallback would cause host key checking to be disabled. In this configuration, establishing a connection to any host just works, so today, most SSH client code in the wild does not perform any host key checks. This makes it easy to perform a MITM attack: * SSH installations that use keyboard-interactive or password authentication can be attacked with MITM, thereby stealing passwords. * Clients that use public-key authentication with agent forwarding are also vulnerable: the MITM server could allow the login to succeed, and then immediately ask the agent to authenticate the login to the real server. * Clients that use public-key authentication without agent forwarding are harder to attack unnoticedly: an attacker cannot authenticate the login to the real server, so it cannot in general present a convincing server to the victim. Now, a missing HostKeyCallback will cause the handshake to fail. This change also provides InsecureIgnoreHostKey() and FixedHostKey(key) as ready made host checkers. A simplistic parser for OpenSSH's known_hosts file is given as an example. This change does not provide a full-fledged parser, as it has complexity (wildcards, revocation, hashed addresses) that will need further consideration. When introduced, the host checking feature maintained backward compatibility at the expense of security. We have decided this is not the right tradeoff for the SSH library. Fixes golang/go#19767 Change-Id: I45fc7ba9bd1ea29c31ec23f115cdbab99913e814 Reviewed-on: https://go-review.googlesource.com/38701 Run-TryBot: Han-Wen Nienhuys <hanwen@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1 parent 459e265 commit e4e2799

13 files changed

+193
-30
lines changed

ssh/agent/client_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ func TestAuth(t *testing.T) {
233233
conn.Close()
234234
}()
235235

236-
conf := ssh.ClientConfig{}
236+
conf := ssh.ClientConfig{
237+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
238+
}
237239
conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers))
238240
conn, _, _, err := ssh.NewClientConn(b, "", &conf)
239241
if err != nil {

ssh/agent/example_test.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ package agent_test
66

77
import (
88
"log"
9-
"os"
109
"net"
10+
"os"
1111

12-
"golang.org/x/crypto/ssh"
13-
"golang.org/x/crypto/ssh/agent"
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/crypto/ssh/agent"
1414
)
1515

1616
func ExampleClientAgent() {
1717
// ssh-agent has a UNIX socket under $SSH_AUTH_SOCK
1818
socket := os.Getenv("SSH_AUTH_SOCK")
19-
conn, err := net.Dial("unix", socket)
20-
if err != nil {
21-
log.Fatalf("net.Dial: %v", err)
22-
}
19+
conn, err := net.Dial("unix", socket)
20+
if err != nil {
21+
log.Fatalf("net.Dial: %v", err)
22+
}
2323
agentClient := agent.NewClient(conn)
2424
config := &ssh.ClientConfig{
2525
User: "username",
@@ -29,6 +29,7 @@ func ExampleClientAgent() {
2929
// wants it.
3030
ssh.PublicKeysCallback(agentClient.Signers),
3131
},
32+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
3233
}
3334

3435
sshc, err := ssh.Dial("tcp", "localhost:22", config)

ssh/agent/server_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ func TestSetupForwardAgent(t *testing.T) {
5656
incoming <- conn
5757
}()
5858

59-
conf := ssh.ClientConfig{}
59+
conf := ssh.ClientConfig{
60+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
61+
}
6062
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
6163
if err != nil {
6264
t.Fatalf("NewClientConn: %v", err)

ssh/certs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ type CertChecker struct {
268268
// HostKeyFallback is called when CertChecker.CheckHostKey encounters a
269269
// public key that is not a certificate. It must implement host key
270270
// validation or else, if nil, all such keys are rejected.
271-
HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error
271+
HostKeyFallback HostKeyCallback
272272

273273
// IsRevoked is called for each certificate so that revocation checking
274274
// can be implemented. It should return true if the given certificate

ssh/client.go

+49-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package ssh
66

77
import (
8+
"bytes"
89
"errors"
910
"fmt"
1011
"net"
@@ -68,6 +69,11 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
6869
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
6970
fullConf := *config
7071
fullConf.SetDefaults()
72+
if fullConf.HostKeyCallback == nil {
73+
c.Close()
74+
return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback")
75+
}
76+
7177
conn := &connection{
7278
sshConn: sshConn{conn: c},
7379
}
@@ -173,6 +179,13 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) {
173179
return NewClient(c, chans, reqs), nil
174180
}
175181

182+
// HostKeyCallback is the function type used for verifying server
183+
// keys. A HostKeyCallback must return nil if the host key is OK, or
184+
// an error to reject it. It receives the hostname as passed to Dial
185+
// or NewClientConn. The remote address is the RemoteAddr of the
186+
// net.Conn underlying the the SSH connection.
187+
type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
188+
176189
// A ClientConfig structure is used to configure a Client. It must not be
177190
// modified after having been passed to an SSH function.
178191
type ClientConfig struct {
@@ -188,10 +201,12 @@ type ClientConfig struct {
188201
// be used during authentication.
189202
Auth []AuthMethod
190203

191-
// HostKeyCallback, if not nil, is called during the cryptographic
192-
// handshake to validate the server's host key. A nil HostKeyCallback
193-
// implies that all host keys are accepted.
194-
HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
204+
// HostKeyCallback is called during the cryptographic
205+
// handshake to validate the server's host key. The client
206+
// configuration must supply this callback for the connection
207+
// to succeed. The functions InsecureIgnoreHostKey or
208+
// FixedHostKey can be used for simplistic host key checks.
209+
HostKeyCallback HostKeyCallback
195210

196211
// ClientVersion contains the version identification string that will
197212
// be used for the connection. If empty, a reasonable default is used.
@@ -209,3 +224,33 @@ type ClientConfig struct {
209224
// A Timeout of zero means no timeout.
210225
Timeout time.Duration
211226
}
227+
228+
// InsecureIgnoreHostKey returns a function that can be used for
229+
// ClientConfig.HostKeyCallback to accept any host key. It should
230+
// not be used for production code.
231+
func InsecureIgnoreHostKey() HostKeyCallback {
232+
return func(hostname string, remote net.Addr, key PublicKey) error {
233+
return nil
234+
}
235+
}
236+
237+
type fixedHostKey struct {
238+
key PublicKey
239+
}
240+
241+
func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error {
242+
if f.key == nil {
243+
return fmt.Errorf("ssh: required host key was nil")
244+
}
245+
if !bytes.Equal(key.Marshal(), f.key.Marshal()) {
246+
return fmt.Errorf("ssh: host key mismatch")
247+
}
248+
return nil
249+
}
250+
251+
// FixedHostKey returns a function for use in
252+
// ClientConfig.HostKeyCallback to accept only a specific host key.
253+
func FixedHostKey(key PublicKey) HostKeyCallback {
254+
hk := &fixedHostKey{key}
255+
return hk.check
256+
}

ssh/client_auth_test.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func TestClientAuthPublicKey(t *testing.T) {
9292
Auth: []AuthMethod{
9393
PublicKeys(testSigners["rsa"]),
9494
},
95+
HostKeyCallback: InsecureIgnoreHostKey(),
9596
}
9697
if err := tryAuth(t, config); err != nil {
9798
t.Fatalf("unable to dial remote side: %s", err)
@@ -104,6 +105,7 @@ func TestAuthMethodPassword(t *testing.T) {
104105
Auth: []AuthMethod{
105106
Password(clientPassword),
106107
},
108+
HostKeyCallback: InsecureIgnoreHostKey(),
107109
}
108110

109111
if err := tryAuth(t, config); err != nil {
@@ -123,6 +125,7 @@ func TestAuthMethodFallback(t *testing.T) {
123125
return "WRONG", nil
124126
}),
125127
},
128+
HostKeyCallback: InsecureIgnoreHostKey(),
126129
}
127130

128131
if err := tryAuth(t, config); err != nil {
@@ -141,6 +144,7 @@ func TestAuthMethodWrongPassword(t *testing.T) {
141144
Password("wrong"),
142145
PublicKeys(testSigners["rsa"]),
143146
},
147+
HostKeyCallback: InsecureIgnoreHostKey(),
144148
}
145149

146150
if err := tryAuth(t, config); err != nil {
@@ -158,6 +162,7 @@ func TestAuthMethodKeyboardInteractive(t *testing.T) {
158162
Auth: []AuthMethod{
159163
KeyboardInteractive(answers.Challenge),
160164
},
165+
HostKeyCallback: InsecureIgnoreHostKey(),
161166
}
162167

163168
if err := tryAuth(t, config); err != nil {
@@ -203,6 +208,7 @@ func TestAuthMethodRSAandDSA(t *testing.T) {
203208
Auth: []AuthMethod{
204209
PublicKeys(testSigners["dsa"], testSigners["rsa"]),
205210
},
211+
HostKeyCallback: InsecureIgnoreHostKey(),
206212
}
207213
if err := tryAuth(t, config); err != nil {
208214
t.Fatalf("client could not authenticate with rsa key: %v", err)
@@ -219,6 +225,7 @@ func TestClientHMAC(t *testing.T) {
219225
Config: Config{
220226
MACs: []string{mac},
221227
},
228+
HostKeyCallback: InsecureIgnoreHostKey(),
222229
}
223230
if err := tryAuth(t, config); err != nil {
224231
t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err)
@@ -254,6 +261,7 @@ func TestClientUnsupportedKex(t *testing.T) {
254261
Config: Config{
255262
KeyExchanges: []string{"diffie-hellman-group-exchange-sha256"}, // not currently supported
256263
},
264+
HostKeyCallback: InsecureIgnoreHostKey(),
257265
}
258266
if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") {
259267
t.Errorf("got %v, expected 'common algorithm'", err)
@@ -273,7 +281,8 @@ func TestClientLoginCert(t *testing.T) {
273281
}
274282

275283
clientConfig := &ClientConfig{
276-
User: "user",
284+
User: "user",
285+
HostKeyCallback: InsecureIgnoreHostKey(),
277286
}
278287
clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner))
279288

@@ -363,6 +372,7 @@ func testPermissionsPassing(withPermissions bool, t *testing.T) {
363372
Auth: []AuthMethod{
364373
PublicKeys(testSigners["rsa"]),
365374
},
375+
HostKeyCallback: InsecureIgnoreHostKey(),
366376
}
367377
if withPermissions {
368378
clientConfig.User = "permissions"
@@ -409,6 +419,7 @@ func TestRetryableAuth(t *testing.T) {
409419
}), 2),
410420
PublicKeys(testSigners["rsa"]),
411421
},
422+
HostKeyCallback: InsecureIgnoreHostKey(),
412423
}
413424

414425
if err := tryAuth(t, config); err != nil {
@@ -430,7 +441,8 @@ func ExampleRetryableAuthMethod(t *testing.T) {
430441
}
431442

432443
config := &ClientConfig{
433-
User: user,
444+
HostKeyCallback: InsecureIgnoreHostKey(),
445+
User: user,
434446
Auth: []AuthMethod{
435447
RetryableAuthMethod(KeyboardInteractiveChallenge(Cb), NumberOfPrompts),
436448
},
@@ -450,7 +462,8 @@ func TestClientAuthNone(t *testing.T) {
450462
serverConfig.AddHostKey(testSigners["rsa"])
451463

452464
clientConfig := &ClientConfig{
453-
User: user,
465+
User: user,
466+
HostKeyCallback: InsecureIgnoreHostKey(),
454467
}
455468

456469
c1, c2, err := netPipe()

ssh/client_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ package ssh
66

77
import (
88
"net"
9+
"strings"
910
"testing"
1011
)
1112

1213
func testClientVersion(t *testing.T, config *ClientConfig, expected string) {
1314
clientConn, serverConn := net.Pipe()
1415
defer clientConn.Close()
1516
receivedVersion := make(chan string, 1)
17+
config.HostKeyCallback = InsecureIgnoreHostKey()
1618
go func() {
1719
version, err := readVersion(serverConn)
1820
if err != nil {
@@ -37,3 +39,43 @@ func TestCustomClientVersion(t *testing.T) {
3739
func TestDefaultClientVersion(t *testing.T) {
3840
testClientVersion(t, &ClientConfig{}, packageVersion)
3941
}
42+
43+
func TestHostKeyCheck(t *testing.T) {
44+
for _, tt := range []struct {
45+
name string
46+
wantError string
47+
key PublicKey
48+
}{
49+
{"no callback", "must specify HostKeyCallback", nil},
50+
{"correct key", "", testSigners["rsa"].PublicKey()},
51+
{"mismatch", "mismatch", testSigners["ecdsa"].PublicKey()},
52+
} {
53+
c1, c2, err := netPipe()
54+
if err != nil {
55+
t.Fatalf("netPipe: %v", err)
56+
}
57+
defer c1.Close()
58+
defer c2.Close()
59+
serverConf := &ServerConfig{
60+
NoClientAuth: true,
61+
}
62+
serverConf.AddHostKey(testSigners["rsa"])
63+
64+
go NewServerConn(c1, serverConf)
65+
clientConf := ClientConfig{
66+
User: "user",
67+
}
68+
if tt.key != nil {
69+
clientConf.HostKeyCallback = FixedHostKey(tt.key)
70+
}
71+
72+
_, _, _, err = NewClientConn(c2, "", &clientConf)
73+
if err != nil {
74+
if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) {
75+
t.Errorf("%s: got error %q, missing %q", err.Error(), tt.wantError)
76+
}
77+
} else if tt.wantError != "" {
78+
t.Errorf("%s: succeeded, but want error string %q", tt.name, tt.wantError)
79+
}
80+
}
81+
}

ssh/doc.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ others.
1414
References:
1515
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
1616
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
17+
18+
This package does not fall under the stability promise of the Go language itself,
19+
so its API may be changed when pressing needs arise.
1720
*/
1821
package ssh // import "golang.org/x/crypto/ssh"

0 commit comments

Comments
 (0)