diff --git a/ssh/agent/client_test.go b/ssh/agent/client_test.go index a13a650017..93d3a9cd23 100644 --- a/ssh/agent/client_test.go +++ b/ssh/agent/client_test.go @@ -233,7 +233,9 @@ func TestAuth(t *testing.T) { conn.Close() }() - conf := ssh.ClientConfig{} + conf := ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers)) conn, _, _, err := ssh.NewClientConn(b, "", &conf) if err != nil { diff --git a/ssh/agent/example_test.go b/ssh/agent/example_test.go index c1130f77ab..85562253ea 100644 --- a/ssh/agent/example_test.go +++ b/ssh/agent/example_test.go @@ -6,20 +6,20 @@ package agent_test import ( "log" - "os" "net" + "os" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) func ExampleClientAgent() { // ssh-agent has a UNIX socket under $SSH_AUTH_SOCK socket := os.Getenv("SSH_AUTH_SOCK") - conn, err := net.Dial("unix", socket) - if err != nil { - log.Fatalf("net.Dial: %v", err) - } + conn, err := net.Dial("unix", socket) + if err != nil { + log.Fatalf("net.Dial: %v", err) + } agentClient := agent.NewClient(conn) config := &ssh.ClientConfig{ User: "username", @@ -29,6 +29,7 @@ func ExampleClientAgent() { // wants it. ssh.PublicKeysCallback(agentClient.Signers), }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), } sshc, err := ssh.Dial("tcp", "localhost:22", config) diff --git a/ssh/agent/server_test.go b/ssh/agent/server_test.go index ec9cdeeb54..6b0837d94c 100644 --- a/ssh/agent/server_test.go +++ b/ssh/agent/server_test.go @@ -56,7 +56,9 @@ func TestSetupForwardAgent(t *testing.T) { incoming <- conn }() - conf := ssh.ClientConfig{} + conf := ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf) if err != nil { t.Fatalf("NewClientConn: %v", err) diff --git a/ssh/certs.go b/ssh/certs.go index 6331c94d53..67600e2402 100644 --- a/ssh/certs.go +++ b/ssh/certs.go @@ -268,7 +268,7 @@ type CertChecker struct { // HostKeyFallback is called when CertChecker.CheckHostKey encounters a // public key that is not a certificate. It must implement host key // validation or else, if nil, all such keys are rejected. - HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error + HostKeyFallback HostKeyCallback // IsRevoked is called for each certificate so that revocation checking // can be implemented. It should return true if the given certificate diff --git a/ssh/client.go b/ssh/client.go index c97f2978e8..667e37118e 100644 --- a/ssh/client.go +++ b/ssh/client.go @@ -5,6 +5,7 @@ package ssh import ( + "bytes" "errors" "fmt" "net" @@ -68,6 +69,11 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client { func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) { fullConf := *config fullConf.SetDefaults() + if fullConf.HostKeyCallback == nil { + c.Close() + return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback") + } + conn := &connection{ sshConn: sshConn{conn: c}, } @@ -173,6 +179,13 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) { return NewClient(c, chans, reqs), nil } +// HostKeyCallback is the function type used for verifying server +// keys. A HostKeyCallback must return nil if the host key is OK, or +// an error to reject it. It receives the hostname as passed to Dial +// or NewClientConn. The remote address is the RemoteAddr of the +// net.Conn underlying the the SSH connection. +type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + // A ClientConfig structure is used to configure a Client. It must not be // modified after having been passed to an SSH function. type ClientConfig struct { @@ -188,10 +201,12 @@ type ClientConfig struct { // be used during authentication. Auth []AuthMethod - // HostKeyCallback, if not nil, is called during the cryptographic - // handshake to validate the server's host key. A nil HostKeyCallback - // implies that all host keys are accepted. - HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + // HostKeyCallback is called during the cryptographic + // handshake to validate the server's host key. The client + // configuration must supply this callback for the connection + // to succeed. The functions InsecureIgnoreHostKey or + // FixedHostKey can be used for simplistic host key checks. + HostKeyCallback HostKeyCallback // ClientVersion contains the version identification string that will // be used for the connection. If empty, a reasonable default is used. @@ -209,3 +224,33 @@ type ClientConfig struct { // A Timeout of zero means no timeout. Timeout time.Duration } + +// InsecureIgnoreHostKey returns a function that can be used for +// ClientConfig.HostKeyCallback to accept any host key. It should +// not be used for production code. +func InsecureIgnoreHostKey() HostKeyCallback { + return func(hostname string, remote net.Addr, key PublicKey) error { + return nil + } +} + +type fixedHostKey struct { + key PublicKey +} + +func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error { + if f.key == nil { + return fmt.Errorf("ssh: required host key was nil") + } + if !bytes.Equal(key.Marshal(), f.key.Marshal()) { + return fmt.Errorf("ssh: host key mismatch") + } + return nil +} + +// FixedHostKey returns a function for use in +// ClientConfig.HostKeyCallback to accept only a specific host key. +func FixedHostKey(key PublicKey) HostKeyCallback { + hk := &fixedHostKey{key} + return hk.check +} diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go index e384c796b7..b526f66252 100644 --- a/ssh/client_auth_test.go +++ b/ssh/client_auth_test.go @@ -92,6 +92,7 @@ func TestClientAuthPublicKey(t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("unable to dial remote side: %s", err) @@ -104,6 +105,7 @@ func TestAuthMethodPassword(t *testing.T) { Auth: []AuthMethod{ Password(clientPassword), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -123,6 +125,7 @@ func TestAuthMethodFallback(t *testing.T) { return "WRONG", nil }), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -141,6 +144,7 @@ func TestAuthMethodWrongPassword(t *testing.T) { Password("wrong"), PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -158,6 +162,7 @@ func TestAuthMethodKeyboardInteractive(t *testing.T) { Auth: []AuthMethod{ KeyboardInteractive(answers.Challenge), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -203,6 +208,7 @@ func TestAuthMethodRSAandDSA(t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["dsa"], testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("client could not authenticate with rsa key: %v", err) @@ -219,6 +225,7 @@ func TestClientHMAC(t *testing.T) { Config: Config{ MACs: []string{mac}, }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err) @@ -254,6 +261,7 @@ func TestClientUnsupportedKex(t *testing.T) { Config: Config{ KeyExchanges: []string{"diffie-hellman-group-exchange-sha256"}, // not currently supported }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") { t.Errorf("got %v, expected 'common algorithm'", err) @@ -273,7 +281,8 @@ func TestClientLoginCert(t *testing.T) { } clientConfig := &ClientConfig{ - User: "user", + User: "user", + HostKeyCallback: InsecureIgnoreHostKey(), } clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner)) @@ -363,6 +372,7 @@ func testPermissionsPassing(withPermissions bool, t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if withPermissions { clientConfig.User = "permissions" @@ -409,6 +419,7 @@ func TestRetryableAuth(t *testing.T) { }), 2), PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -430,7 +441,8 @@ func ExampleRetryableAuthMethod(t *testing.T) { } config := &ClientConfig{ - User: user, + HostKeyCallback: InsecureIgnoreHostKey(), + User: user, Auth: []AuthMethod{ RetryableAuthMethod(KeyboardInteractiveChallenge(Cb), NumberOfPrompts), }, @@ -450,7 +462,8 @@ func TestClientAuthNone(t *testing.T) { serverConfig.AddHostKey(testSigners["rsa"]) clientConfig := &ClientConfig{ - User: user, + User: user, + HostKeyCallback: InsecureIgnoreHostKey(), } c1, c2, err := netPipe() diff --git a/ssh/client_test.go b/ssh/client_test.go index 1fe790cb49..bc5e985f8c 100644 --- a/ssh/client_test.go +++ b/ssh/client_test.go @@ -6,6 +6,7 @@ package ssh import ( "net" + "strings" "testing" ) @@ -13,6 +14,7 @@ func testClientVersion(t *testing.T, config *ClientConfig, expected string) { clientConn, serverConn := net.Pipe() defer clientConn.Close() receivedVersion := make(chan string, 1) + config.HostKeyCallback = InsecureIgnoreHostKey() go func() { version, err := readVersion(serverConn) if err != nil { @@ -37,3 +39,43 @@ func TestCustomClientVersion(t *testing.T) { func TestDefaultClientVersion(t *testing.T) { testClientVersion(t, &ClientConfig{}, packageVersion) } + +func TestHostKeyCheck(t *testing.T) { + for _, tt := range []struct { + name string + wantError string + key PublicKey + }{ + {"no callback", "must specify HostKeyCallback", nil}, + {"correct key", "", testSigners["rsa"].PublicKey()}, + {"mismatch", "mismatch", testSigners["ecdsa"].PublicKey()}, + } { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + serverConf := &ServerConfig{ + NoClientAuth: true, + } + serverConf.AddHostKey(testSigners["rsa"]) + + go NewServerConn(c1, serverConf) + clientConf := ClientConfig{ + User: "user", + } + if tt.key != nil { + clientConf.HostKeyCallback = FixedHostKey(tt.key) + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("%s: got error %q, missing %q", err.Error(), tt.wantError) + } + } else if tt.wantError != "" { + t.Errorf("%s: succeeded, but want error string %q", tt.name, tt.wantError) + } + } +} diff --git a/ssh/doc.go b/ssh/doc.go index d6be894662..67b7322c05 100644 --- a/ssh/doc.go +++ b/ssh/doc.go @@ -14,5 +14,8 @@ others. References: [PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD [SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1 + +This package does not fall under the stability promise of the Go language itself, +so its API may be changed when pressing needs arise. */ package ssh // import "golang.org/x/crypto/ssh" diff --git a/ssh/example_test.go b/ssh/example_test.go index 4d2eabd0f4..618398ceae 100644 --- a/ssh/example_test.go +++ b/ssh/example_test.go @@ -5,12 +5,16 @@ package ssh_test import ( + "bufio" "bytes" "fmt" "io/ioutil" "log" "net" "net/http" + "os" + "path/filepath" + "strings" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" @@ -90,8 +94,6 @@ func ExampleNewServerConn() { // The incoming Request channel must be serviced. go ssh.DiscardRequests(reqs) - // Service the incoming Channel channel. - // Service the incoming Channel channel. for newChannel := range chans { // Channels have a type, depending on the application level @@ -131,16 +133,59 @@ func ExampleNewServerConn() { } } +func ExampleHostKeyCheck() { + // Every client must provide a host key check. Here is a + // simple-minded parse of OpenSSH's known_hosts file + host := "hostname" + file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var hostKey ssh.PublicKey + for scanner.Scan() { + fields := strings.Split(scanner.Text(), " ") + if len(fields) != 3 { + continue + } + if strings.Contains(fields[0], host) { + var err error + hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes()) + if err != nil { + log.Fatalf("error parsing %q: %v", fields[2], err) + } + break + } + } + + if hostKey == nil { + log.Fatalf("no hostkey for %s", host) + } + + config := ssh.ClientConfig{ + User: os.Getenv("USER"), + HostKeyCallback: ssh.FixedHostKey(hostKey), + } + + _, err = ssh.Dial("tcp", host+":22", &config) + log.Println(err) +} + func ExampleDial() { + var hostKey ssh.PublicKey // An SSH client is represented with a ClientConn. // // To authenticate with the remote server you must pass at least one - // implementation of AuthMethod via the Auth field in ClientConfig. + // implementation of AuthMethod via the Auth field in ClientConfig, + // and provide a HostKeyCallback. config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("yourpassword"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } client, err := ssh.Dial("tcp", "yourserver.com:22", config) if err != nil { @@ -166,6 +211,7 @@ func ExampleDial() { } func ExamplePublicKeys() { + var hostKey ssh.PublicKey // A public key may be used to authenticate against the remote // server by using an unencrypted PEM-encoded private key file. // @@ -188,6 +234,7 @@ func ExamplePublicKeys() { // Use the PublicKeys method for remote authentication. ssh.PublicKeys(signer), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Connect to the remote server and perform the SSH handshake. @@ -199,11 +246,13 @@ func ExamplePublicKeys() { } func ExampleClient_Listen() { + var hostKey ssh.PublicKey config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Dial your ssh server. conn, err := ssh.Dial("tcp", "localhost:22", config) @@ -226,12 +275,14 @@ func ExampleClient_Listen() { } func ExampleSession_RequestPty() { + var hostKey ssh.PublicKey // Create client config config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Connect to ssh server conn, err := ssh.Dial("tcp", "localhost:22", config) diff --git a/ssh/handshake.go b/ssh/handshake.go index 8de650644a..1b63ba8a6d 100644 --- a/ssh/handshake.go +++ b/ssh/handshake.go @@ -74,7 +74,7 @@ type handshakeTransport struct { startKex chan *pendingKex // data for host key checking - hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + hostKeyCallback HostKeyCallback dialAddress string remoteAddr net.Addr @@ -614,11 +614,9 @@ func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics * return nil, err } - if t.hostKeyCallback != nil { - err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey) - if err != nil { - return nil, err - } + err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey) + if err != nil { + return nil, err } return result, nil diff --git a/ssh/handshake_test.go b/ssh/handshake_test.go index 1b831127e8..77d1aac765 100644 --- a/ssh/handshake_test.go +++ b/ssh/handshake_test.go @@ -436,6 +436,7 @@ func testHandshakeErrorHandlingN(t *testing.T, readLimit, writeLimit int, couple 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() diff --git a/ssh/session_test.go b/ssh/session_test.go index f35a378f2e..7dce6dd699 100644 --- a/ssh/session_test.go +++ b/ssh/session_test.go @@ -59,7 +59,8 @@ func dial(handler serverType, t *testing.T) *Client { }() config := &ClientConfig{ - User: "testuser", + User: "testuser", + HostKeyCallback: InsecureIgnoreHostKey(), } conn, chans, reqs, err := NewClientConn(c2, "", config) @@ -641,7 +642,8 @@ func TestSessionID(t *testing.T) { } serverConf.AddHostKey(testSigners["ecdsa"]) clientConf := &ClientConfig{ - User: "user", + HostKeyCallback: InsecureIgnoreHostKey(), + User: "user", } go func() { @@ -747,7 +749,9 @@ func TestHostKeyAlgorithms(t *testing.T) { // By default, we get the preferred algorithm, which is ECDSA 256. - clientConf := &ClientConfig{} + clientConf := &ClientConfig{ + HostKeyCallback: InsecureIgnoreHostKey(), + } connect(clientConf, KeyAlgoECDSA256) // Client asks for RSA explicitly. diff --git a/ssh/test/cert_test.go b/ssh/test/cert_test.go index 364790f17d..bc83e4f5df 100644 --- a/ssh/test/cert_test.go +++ b/ssh/test/cert_test.go @@ -36,7 +36,8 @@ func TestCertLogin(t *testing.T) { } conf := &ssh.ClientConfig{ - User: username(), + User: username(), + HostKeyCallback: ssh.InsecureIgnoreHostKey(), } conf.Auth = append(conf.Auth, ssh.PublicKeys(certSigner)) client, err := s.TryDial(conf)