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>
529 lines
12 KiB
Go
529 lines
12 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
type testChecker struct {
|
|
calls []string
|
|
}
|
|
|
|
func (t *testChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
|
|
if dialAddr == "bad" {
|
|
return fmt.Errorf("dialAddr is bad")
|
|
}
|
|
|
|
if tcpAddr, ok := addr.(*net.TCPAddr); !ok || tcpAddr == nil {
|
|
return fmt.Errorf("testChecker: got %T want *net.TCPAddr", addr)
|
|
}
|
|
|
|
t.calls = append(t.calls, fmt.Sprintf("%s %v %s %x", dialAddr, addr, key.Type(), key.Marshal()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
|
|
// therefore is buffered (net.Pipe deadlocks if both sides start with
|
|
// a write.)
|
|
func netPipe() (net.Conn, net.Conn, error) {
|
|
listener, err := net.Listen("tcp", ":0")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer listener.Close()
|
|
c1, err := net.Dial("tcp", listener.Addr().String())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
c2, err := listener.Accept()
|
|
if err != nil {
|
|
c1.Close()
|
|
return nil, nil, err
|
|
}
|
|
|
|
return c1, c2, nil
|
|
}
|
|
|
|
// noiseTransport inserts ignore messages to check that the read loop
|
|
// and the key exchange filters out these messages.
|
|
type noiseTransport struct {
|
|
keyingTransport
|
|
}
|
|
|
|
func (t *noiseTransport) writePacket(p []byte) error {
|
|
ignore := []byte{msgIgnore}
|
|
if err := t.keyingTransport.writePacket(ignore); err != nil {
|
|
return err
|
|
}
|
|
debug := []byte{msgDebug, 1, 2, 3}
|
|
if err := t.keyingTransport.writePacket(debug); err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.keyingTransport.writePacket(p)
|
|
}
|
|
|
|
func addNoiseTransport(t keyingTransport) keyingTransport {
|
|
return &noiseTransport{t}
|
|
}
|
|
|
|
// handshakePair creates two handshakeTransports connected with each
|
|
// other. If the noise argument is true, both transports will try to
|
|
// confuse the other side by sending ignore and debug messages.
|
|
func handshakePair(clientConf *ClientConfig, addr string, noise bool) (client *handshakeTransport, server *handshakeTransport, err error) {
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var trC, trS keyingTransport
|
|
|
|
trC = newTransport(a, rand.Reader, true)
|
|
trS = newTransport(b, rand.Reader, false)
|
|
if noise {
|
|
trC = addNoiseTransport(trC)
|
|
trS = addNoiseTransport(trS)
|
|
}
|
|
clientConf.SetDefaults()
|
|
|
|
v := []byte("version")
|
|
client = newClientTransport(trC, v, v, clientConf, addr, a.RemoteAddr())
|
|
|
|
serverConf := &ServerConfig{}
|
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
serverConf.SetDefaults()
|
|
server = newServerTransport(trS, v, v, serverConf)
|
|
|
|
if err := server.waitSession(); err != nil {
|
|
return nil, nil, fmt.Errorf("server.waitSession: %v", err)
|
|
}
|
|
if err := client.waitSession(); err != nil {
|
|
return nil, nil, fmt.Errorf("client.waitSession: %v", err)
|
|
}
|
|
|
|
return client, server, nil
|
|
}
|
|
|
|
func TestHandshakeBasic(t *testing.T) {
|
|
if runtime.GOOS == "plan9" {
|
|
t.Skip("see golang.org/issue/7237")
|
|
}
|
|
|
|
checker := &syncChecker{
|
|
waitCall: make(chan int, 10),
|
|
called: make(chan int, 10),
|
|
}
|
|
|
|
checker.waitCall <- 1
|
|
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr", false)
|
|
if err != nil {
|
|
t.Fatalf("handshakePair: %v", err)
|
|
}
|
|
|
|
defer trC.Close()
|
|
defer trS.Close()
|
|
|
|
// Let first kex complete normally.
|
|
<-checker.called
|
|
|
|
clientDone := make(chan int, 0)
|
|
gotHalf := make(chan int, 0)
|
|
const N = 20
|
|
|
|
go func() {
|
|
defer close(clientDone)
|
|
// Client writes a bunch of stuff, and does a key
|
|
// change in the middle. This should not confuse the
|
|
// handshake in progress. We do this twice, so we test
|
|
// that the packet buffer is reset correctly.
|
|
for i := 0; i < N; i++ {
|
|
p := []byte{msgRequestSuccess, byte(i)}
|
|
if err := trC.writePacket(p); err != nil {
|
|
t.Fatalf("sendPacket: %v", err)
|
|
}
|
|
if (i % 10) == 5 {
|
|
<-gotHalf
|
|
// halfway through, we request a key change.
|
|
trC.requestKeyExchange()
|
|
|
|
// Wait until we can be sure the key
|
|
// change has really started before we
|
|
// write more.
|
|
<-checker.called
|
|
}
|
|
if (i % 10) == 7 {
|
|
// write some packets until the kex
|
|
// completes, to test buffering of
|
|
// packets.
|
|
checker.waitCall <- 1
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Server checks that client messages come in cleanly
|
|
i := 0
|
|
err = nil
|
|
for ; i < N; i++ {
|
|
var p []byte
|
|
p, err = trS.readPacket()
|
|
if err != nil {
|
|
break
|
|
}
|
|
if (i % 10) == 5 {
|
|
gotHalf <- 1
|
|
}
|
|
|
|
want := []byte{msgRequestSuccess, byte(i)}
|
|
if bytes.Compare(p, want) != 0 {
|
|
t.Errorf("message %d: got %v, want %v", i, p, want)
|
|
}
|
|
}
|
|
<-clientDone
|
|
if err != nil && err != io.EOF {
|
|
t.Fatalf("server error: %v", err)
|
|
}
|
|
if i != N {
|
|
t.Errorf("received %d messages, want 10.", i)
|
|
}
|
|
|
|
close(checker.called)
|
|
if _, ok := <-checker.called; ok {
|
|
// If all went well, we registered exactly 2 key changes: one
|
|
// that establishes the session, and one that we requested
|
|
// additionally.
|
|
t.Fatalf("got another host key checks after 2 handshakes")
|
|
}
|
|
}
|
|
|
|
func TestForceFirstKex(t *testing.T) {
|
|
// like handshakePair, but must access the keyingTransport.
|
|
checker := &testChecker{}
|
|
clientConf := &ClientConfig{HostKeyCallback: checker.Check}
|
|
a, b, err := netPipe()
|
|
if err != nil {
|
|
t.Fatalf("netPipe: %v", err)
|
|
}
|
|
|
|
var trC, trS keyingTransport
|
|
|
|
trC = newTransport(a, rand.Reader, true)
|
|
|
|
// This is the disallowed packet:
|
|
trC.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth}))
|
|
|
|
// Rest of the setup.
|
|
trS = newTransport(b, rand.Reader, false)
|
|
clientConf.SetDefaults()
|
|
|
|
v := []byte("version")
|
|
client := newClientTransport(trC, v, v, clientConf, "addr", a.RemoteAddr())
|
|
|
|
serverConf := &ServerConfig{}
|
|
serverConf.AddHostKey(testSigners["ecdsa"])
|
|
serverConf.AddHostKey(testSigners["rsa"])
|
|
serverConf.SetDefaults()
|
|
server := newServerTransport(trS, v, v, serverConf)
|
|
|
|
defer client.Close()
|
|
defer server.Close()
|
|
|
|
// We setup the initial key exchange, but the remote side
|
|
// tries to send serviceRequestMsg in cleartext, which is
|
|
// disallowed.
|
|
|
|
if err := server.waitSession(); err == nil {
|
|
t.Errorf("server first kex init should reject unexpected packet")
|
|
}
|
|
}
|
|
|
|
func TestHandshakeAutoRekeyWrite(t *testing.T) {
|
|
checker := &syncChecker{
|
|
called: make(chan int, 10),
|
|
waitCall: nil,
|
|
}
|
|
clientConf := &ClientConfig{HostKeyCallback: checker.Check}
|
|
clientConf.RekeyThreshold = 500
|
|
trC, trS, err := handshakePair(clientConf, "addr", false)
|
|
if err != nil {
|
|
t.Fatalf("handshakePair: %v", err)
|
|
}
|
|
defer trC.Close()
|
|
defer trS.Close()
|
|
|
|
input := make([]byte, 251)
|
|
input[0] = msgRequestSuccess
|
|
|
|
done := make(chan int, 1)
|
|
const numPacket = 5
|
|
go func() {
|
|
defer close(done)
|
|
j := 0
|
|
for ; j < numPacket; j++ {
|
|
if p, err := trS.readPacket(); err != nil {
|
|
break
|
|
} else if !bytes.Equal(input, p) {
|
|
t.Errorf("got packet type %d, want %d", p[0], input[0])
|
|
}
|
|
}
|
|
|
|
if j != numPacket {
|
|
t.Errorf("got %d, want 5 messages", j)
|
|
}
|
|
}()
|
|
|
|
<-checker.called
|
|
|
|
for i := 0; i < numPacket; i++ {
|
|
p := make([]byte, len(input))
|
|
copy(p, input)
|
|
if err := trC.writePacket(p); err != nil {
|
|
t.Errorf("writePacket: %v", err)
|
|
}
|
|
if i == 2 {
|
|
// Make sure the kex is in progress.
|
|
<-checker.called
|
|
}
|
|
|
|
}
|
|
<-done
|
|
}
|
|
|
|
type syncChecker struct {
|
|
waitCall chan int
|
|
called chan int
|
|
}
|
|
|
|
func (c *syncChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
|
|
c.called <- 1
|
|
if c.waitCall != nil {
|
|
<-c.waitCall
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestHandshakeAutoRekeyRead(t *testing.T) {
|
|
sync := &syncChecker{
|
|
called: make(chan int, 2),
|
|
waitCall: nil,
|
|
}
|
|
clientConf := &ClientConfig{
|
|
HostKeyCallback: sync.Check,
|
|
}
|
|
clientConf.RekeyThreshold = 500
|
|
|
|
trC, trS, err := handshakePair(clientConf, "addr", false)
|
|
if err != nil {
|
|
t.Fatalf("handshakePair: %v", err)
|
|
}
|
|
defer trC.Close()
|
|
defer trS.Close()
|
|
|
|
packet := make([]byte, 501)
|
|
packet[0] = msgRequestSuccess
|
|
if err := trS.writePacket(packet); err != nil {
|
|
t.Fatalf("writePacket: %v", err)
|
|
}
|
|
|
|
// While we read out the packet, a key change will be
|
|
// initiated.
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
defer close(done)
|
|
if _, err := trC.readPacket(); err != nil {
|
|
t.Fatalf("readPacket(client): %v", err)
|
|
}
|
|
|
|
}()
|
|
|
|
<-done
|
|
<-sync.called
|
|
}
|
|
|
|
// errorKeyingTransport generates errors after a given number of
|
|
// read/write operations.
|
|
type errorKeyingTransport struct {
|
|
packetConn
|
|
readLeft, writeLeft int
|
|
}
|
|
|
|
func (n *errorKeyingTransport) prepareKeyChange(*algorithms, *kexResult) error {
|
|
return nil
|
|
}
|
|
|
|
func (n *errorKeyingTransport) getSessionID() []byte {
|
|
return nil
|
|
}
|
|
|
|
func (n *errorKeyingTransport) writePacket(packet []byte) error {
|
|
if n.writeLeft == 0 {
|
|
n.Close()
|
|
return errors.New("barf")
|
|
}
|
|
|
|
n.writeLeft--
|
|
return n.packetConn.writePacket(packet)
|
|
}
|
|
|
|
func (n *errorKeyingTransport) readPacket() ([]byte, error) {
|
|
if n.readLeft == 0 {
|
|
n.Close()
|
|
return nil, errors.New("barf")
|
|
}
|
|
|
|
n.readLeft--
|
|
return n.packetConn.readPacket()
|
|
}
|
|
|
|
func TestHandshakeErrorHandlingRead(t *testing.T) {
|
|
for i := 0; i < 20; i++ {
|
|
testHandshakeErrorHandlingN(t, i, -1, false)
|
|
}
|
|
}
|
|
|
|
func TestHandshakeErrorHandlingWrite(t *testing.T) {
|
|
for i := 0; i < 20; i++ {
|
|
testHandshakeErrorHandlingN(t, -1, i, false)
|
|
}
|
|
}
|
|
|
|
func TestHandshakeErrorHandlingReadCoupled(t *testing.T) {
|
|
for i := 0; i < 20; i++ {
|
|
testHandshakeErrorHandlingN(t, i, -1, true)
|
|
}
|
|
}
|
|
|
|
func TestHandshakeErrorHandlingWriteCoupled(t *testing.T) {
|
|
for i := 0; i < 20; i++ {
|
|
testHandshakeErrorHandlingN(t, -1, i, true)
|
|
}
|
|
}
|
|
|
|
// testHandshakeErrorHandlingN runs handshakes, injecting errors. If
|
|
// handshakeTransport deadlocks, the go runtime will detect it and
|
|
// panic.
|
|
func testHandshakeErrorHandlingN(t *testing.T, readLimit, writeLimit int, coupled bool) {
|
|
msg := Marshal(&serviceRequestMsg{strings.Repeat("x", int(minRekeyThreshold)/4)})
|
|
|
|
a, b := memPipe()
|
|
defer a.Close()
|
|
defer b.Close()
|
|
|
|
key := testSigners["ecdsa"]
|
|
serverConf := Config{RekeyThreshold: minRekeyThreshold}
|
|
serverConf.SetDefaults()
|
|
serverConn := newHandshakeTransport(&errorKeyingTransport{a, readLimit, writeLimit}, &serverConf, []byte{'a'}, []byte{'b'})
|
|
serverConn.hostKeys = []Signer{key}
|
|
go serverConn.readLoop()
|
|
go serverConn.kexLoop()
|
|
|
|
clientConf := Config{RekeyThreshold: 10 * minRekeyThreshold}
|
|
clientConf.SetDefaults()
|
|
clientConn := newHandshakeTransport(&errorKeyingTransport{b, -1, -1}, &clientConf, []byte{'a'}, []byte{'b'})
|
|
clientConn.hostKeyAlgorithms = []string{key.PublicKey().Type()}
|
|
clientConn.hostKeyCallback = InsecureIgnoreHostKey()
|
|
go clientConn.readLoop()
|
|
go clientConn.kexLoop()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, hs := range []packetConn{serverConn, clientConn} {
|
|
if !coupled {
|
|
wg.Add(2)
|
|
go func(c packetConn) {
|
|
for i := 0; ; i++ {
|
|
str := fmt.Sprintf("%08x", i) + strings.Repeat("x", int(minRekeyThreshold)/4-8)
|
|
err := c.writePacket(Marshal(&serviceRequestMsg{str}))
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
wg.Done()
|
|
c.Close()
|
|
}(hs)
|
|
go func(c packetConn) {
|
|
for {
|
|
_, err := c.readPacket()
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
wg.Done()
|
|
}(hs)
|
|
} else {
|
|
wg.Add(1)
|
|
go func(c packetConn) {
|
|
for {
|
|
_, err := c.readPacket()
|
|
if err != nil {
|
|
break
|
|
}
|
|
if err := c.writePacket(msg); err != nil {
|
|
break
|
|
}
|
|
|
|
}
|
|
wg.Done()
|
|
}(hs)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestDisconnect(t *testing.T) {
|
|
if runtime.GOOS == "plan9" {
|
|
t.Skip("see golang.org/issue/7237")
|
|
}
|
|
checker := &testChecker{}
|
|
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr", false)
|
|
if err != nil {
|
|
t.Fatalf("handshakePair: %v", err)
|
|
}
|
|
|
|
defer trC.Close()
|
|
defer trS.Close()
|
|
|
|
trC.writePacket([]byte{msgRequestSuccess, 0, 0})
|
|
errMsg := &disconnectMsg{
|
|
Reason: 42,
|
|
Message: "such is life",
|
|
}
|
|
trC.writePacket(Marshal(errMsg))
|
|
trC.writePacket([]byte{msgRequestSuccess, 0, 0})
|
|
|
|
packet, err := trS.readPacket()
|
|
if err != nil {
|
|
t.Fatalf("readPacket 1: %v", err)
|
|
}
|
|
if packet[0] != msgRequestSuccess {
|
|
t.Errorf("got packet %v, want packet type %d", packet, msgRequestSuccess)
|
|
}
|
|
|
|
_, err = trS.readPacket()
|
|
if err == nil {
|
|
t.Errorf("readPacket 2 succeeded")
|
|
} else if !reflect.DeepEqual(err, errMsg) {
|
|
t.Errorf("got error %#v, want %#v", err, errMsg)
|
|
}
|
|
|
|
_, err = trS.readPacket()
|
|
if err == nil {
|
|
t.Errorf("readPacket 3 succeeded")
|
|
}
|
|
}
|