Explorar o código

go.crypto/ssh: implement keyboard-interactive auth (RFC 4256), both
on client and server-side.

R=dave, agl
CC=gobot, golang-dev
https://golang.org/cl/9853050

Han-Wen Nienhuys %!s(int64=12) %!d(string=hai) anos
pai
achega
c7df565dd4
Modificáronse 5 ficheiros con 284 adicións e 0 borrados
  1. 117 0
      ssh/client_auth.go
  2. 60 0
      ssh/client_auth_test.go
  3. 15 0
      ssh/common.go
  4. 16 0
      ssh/messages.go
  5. 76 0
      ssh/server.go

+ 117 - 0
ssh/client_auth.go

@@ -375,3 +375,120 @@ func (kr *agentKeyring) Sign(i int, rand io.Reader, data []byte) (sig []byte, er
 	}
 	return sig, nil
 }
+
+// ClientKeyboardInteractive should prompt the user for the given
+// questions.
+type ClientKeyboardInteractive interface {
+	// Challenge should print the questions, optionally disabling
+	// echoing (eg. for passwords), and return all the answers.
+	// Challenge may be called multiple times in a single
+	// session. After successful authentication, the server may
+	// send a challenge with no questions, for which the user and
+	// instruction messages should be printed.  RFC 4256 section
+	// 3.3 details how the UI should behave for both CLI and
+	// GUI environments.
+	Challenge(user, instruction string, questions []string, echos []bool) ([]string, error)
+}
+
+// ClientAuthKeyboardInteractive returns a ClientAuth using a
+// prompt/response sequence controlled by the server.
+func ClientAuthKeyboardInteractive(impl ClientKeyboardInteractive) ClientAuth {
+	return &keyboardInteractiveAuth{impl}
+}
+
+type keyboardInteractiveAuth struct {
+	ClientKeyboardInteractive
+}
+
+func (c *keyboardInteractiveAuth) method() string {
+	return "keyboard-interactive"
+}
+
+func (c *keyboardInteractiveAuth) auth(session []byte, user string, t *transport, rand io.Reader) (bool, []string, error) {
+	type initiateMsg struct {
+		User       string
+		Service    string
+		Method     string
+		Language   string
+		Submethods string
+	}
+
+	if err := t.writePacket(marshal(msgUserAuthRequest, initiateMsg{
+		User:    user,
+		Service: serviceSSH,
+		Method:  "keyboard-interactive",
+	})); err != nil {
+		return false, nil, err
+	}
+
+	for {
+		packet, err := t.readPacket()
+		if err != nil {
+			return false, nil, err
+		}
+
+		// like handleAuthResponse, but with less options.
+		switch packet[0] {
+		case msgUserAuthInfoRequest:
+			// OK
+		case msgUserAuthFailure:
+			var msg userAuthFailureMsg
+			if err := unmarshal(&msg, packet, msgUserAuthFailure); err != nil {
+				return false, nil, err
+			}
+			return false, msg.Methods, nil
+		case msgUserAuthSuccess:
+			return true, nil, nil
+		default:
+			return false, nil, UnexpectedMessageError{msgUserAuthInfoRequest, packet[0]}
+		}
+
+		var msg userAuthInfoRequestMsg
+		if err := unmarshal(&msg, packet, packet[0]); err != nil {
+			return false, nil, err
+		}
+
+		// Manually unpack the prompt/echo pairs.
+		rest := msg.Prompts
+		var prompts []string
+		var echos []bool
+		for i := 0; i < int(msg.NumPrompts); i++ {
+			prompt, r, ok := parseString(rest)
+			if !ok || len(r) == 0 {
+				return false, nil, errors.New("ssh: prompt format error")
+			}
+			prompts = append(prompts, string(prompt))
+			echos = append(echos, r[0] != 0)
+			rest = r[1:]
+		}
+
+		if len(rest) != 0 {
+			return false, nil, fmt.Errorf("ssh: junk following message %q", rest)
+		}
+
+		answers, err := c.Challenge(msg.User, msg.Instruction, prompts, echos)
+		if err != nil {
+			return false, nil, err
+		}
+
+		if len(answers) != len(prompts) {
+			return false, nil, errors.New("ssh: not enough answers from keyboard-interactive callback")
+		}
+		responseLength := 1 + 4
+		for _, a := range answers {
+			responseLength += stringLength(len(a))
+		}
+		serialized := make([]byte, responseLength)
+		p := serialized
+		p[0] = msgUserAuthInfoResponse
+		p = p[1:]
+		p = marshalUint32(p, uint32(len(answers)))
+		for _, a := range answers {
+			p = marshalString(p, []byte(a))
+		}
+
+		if err := t.writePacket(serialized); err != nil {
+			return false, nil, err
+		}
+	}
+}

+ 60 - 0
ssh/client_auth_test.go

@@ -112,6 +112,16 @@ func (p password) Password(user string) (string, error) {
 	return string(p), nil
 }
 
+type keyboardInteractive map[string]string
+
+func (cr *keyboardInteractive) Challenge(user string, instruction string, questions []string, echos []bool) ([]string, error) {
+	var answers []string
+	for _, q := range questions {
+		answers = append(answers, (*cr)[q])
+	}
+	return answers, nil
+}
+
 // reused internally by tests
 var (
 	rsakey         *rsa.PrivateKey
@@ -128,6 +138,18 @@ var (
 			algoname := algoName(key)
 			return user == "testuser" && algo == algoname && bytes.Equal(pubkey, expected)
 		},
+		KeyboardInteractiveCallback: func(conn *ServerConn, user string, client ClientKeyboardInteractive) bool {
+			ans, err := client.Challenge("user",
+				"instruction",
+				[]string{"question1", "question2"},
+				[]bool{true, true})
+			if err != nil {
+				return false
+			}
+			ok := user == "testuser" && ans[0] == "answer1" && ans[1] == "answer2"
+			client.Challenge("user", "motd", nil, nil)
+			return ok
+		},
 	}
 )
 
@@ -221,6 +243,44 @@ func TestClientAuthWrongPassword(t *testing.T) {
 	c.Close()
 }
 
+func TestClientAuthKeyboardInteractive(t *testing.T) {
+	answers := keyboardInteractive(map[string]string{
+		"question1": "answer1",
+		"question2": "answer2",
+	})
+	config := &ClientConfig{
+		User: "testuser",
+		Auth: []ClientAuth{
+			ClientAuthKeyboardInteractive(&answers),
+		},
+	}
+
+	c, err := Dial("tcp", newMockAuthServer(t), config)
+	if err != nil {
+		t.Fatalf("unable to dial remote side: %s", err)
+	}
+	c.Close()
+}
+
+func TestClientAuthWrongKeyboardInteractive(t *testing.T) {
+	answers := keyboardInteractive(map[string]string{
+		"question1": "answer1",
+		"question2": "WRONG",
+	})
+	config := &ClientConfig{
+		User: "testuser",
+		Auth: []ClientAuth{
+			ClientAuthKeyboardInteractive(&answers),
+		},
+	}
+
+	c, err := Dial("tcp", newMockAuthServer(t), config)
+	if err == nil {
+		c.Close()
+		t.Fatalf("wrong answers should not have authenticated with KeyboardInteractive")
+	}
+}
+
 // the mock server will only authenticate ssh-rsa keys
 func TestClientAuthInvalidPublickey(t *testing.T) {
 	kc := new(keychain)

+ 15 - 0
ssh/common.go

@@ -329,6 +329,21 @@ func appendInt(buf []byte, n int) []byte {
 	return appendU32(buf, uint32(n))
 }
 
+func appendString(buf []byte, s string) []byte {
+	buf = appendU32(buf, uint32(len(s)))
+	buf = append(buf, s...)
+	return buf
+}
+
+func appendBool(buf []byte, b bool) []byte {
+	if b {
+		buf = append(buf, 1)
+	} else {
+		buf = append(buf, 0)
+	}
+	return buf
+}
+
 // newCond is a helper to hide the fact that there is no usable zero
 // value for sync.Cond.
 func newCond() *sync.Cond { return sync.NewCond(new(sync.Mutex)) }

+ 16 - 0
ssh/messages.go

@@ -25,19 +25,26 @@ const (
 	msgKexInit = 20
 	msgNewKeys = 21
 
+	// Diffie-Helman
 	msgKexDHInit  = 30
 	msgKexDHReply = 31
 
+	// Standard authentication messages
 	msgUserAuthRequest  = 50
 	msgUserAuthFailure  = 51
 	msgUserAuthSuccess  = 52
 	msgUserAuthBanner   = 53
 	msgUserAuthPubKeyOk = 60
 
+	// Method specific messages
+	msgUserAuthInfoRequest  = 60
+	msgUserAuthInfoResponse = 61
+
 	msgGlobalRequest  = 80
 	msgRequestSuccess = 81
 	msgRequestFailure = 82
 
+	// Channel manipulation
 	msgChannelOpen         = 90
 	msgChannelOpenConfirm  = 91
 	msgChannelOpenFailure  = 92
@@ -117,6 +124,15 @@ type userAuthFailureMsg struct {
 	PartialSuccess bool
 }
 
+// See RFC 4256, section 3.2
+type userAuthInfoRequestMsg struct {
+	User               string
+	Instruction        string
+	DeprecatedLanguage string
+	NumPrompts         uint32
+	Prompts            []byte `ssh:"rest"`
+}
+
 // See RFC 4254, section 5.1.
 type channelOpenMsg struct {
 	ChanType         string

+ 76 - 0
ssh/server.go

@@ -42,6 +42,15 @@ type ServerConfig struct {
 	// valid for the given user.
 	PublicKeyCallback func(conn *ServerConn, user, algo string, pubkey []byte) bool
 
+	// KeyboardInteractiveCallback, if non-nil, is called when
+	// keyboard-interactive authentication is selected (RFC
+	// 4256). The client object's Challenge function should be
+	// used to query the user. The callback may offer multiple
+	// Challenge rounds. To avoid information leaks, the client
+	// should be presented a challenge even if the user is
+	// unknown.
+	KeyboardInteractiveCallback func(conn *ServerConn, user string, client ClientKeyboardInteractive) bool
+
 	// Cryptographic-related configuration.
 	Crypto CryptoConfig
 }
@@ -415,6 +424,15 @@ userAuthLoop:
 			if s.config.PasswordCallback(s, userAuthReq.User, string(password)) {
 				break userAuthLoop
 			}
+		case "keyboard-interactive":
+			if s.config.KeyboardInteractiveCallback == nil {
+				break
+			}
+
+			s.User = userAuthReq.User
+			if s.config.KeyboardInteractiveCallback(s, s.User, &sshClientKeyboardInteractive{s}) {
+				break userAuthLoop
+			}
 		case "publickey":
 			if s.config.PublicKeyCallback == nil {
 				break
@@ -498,6 +516,9 @@ userAuthLoop:
 		if s.config.PublicKeyCallback != nil {
 			failureMsg.Methods = append(failureMsg.Methods, "publickey")
 		}
+		if s.config.KeyboardInteractiveCallback != nil {
+			failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
+		}
 
 		if len(failureMsg.Methods) == 0 {
 			return errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
@@ -516,6 +537,61 @@ userAuthLoop:
 	return nil
 }
 
+// sshClientKeyboardInteractive implements a ClientKeyboardInteractive by
+// asking the client on the other side of a ServerConn.
+type sshClientKeyboardInteractive struct {
+	*ServerConn
+}
+
+func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
+	if len(questions) != len(echos) {
+		return nil, errors.New("ssh: echos and questions must have equal length")
+	}
+
+	var prompts []byte
+	for i := range questions {
+		prompts = appendString(prompts, questions[i])
+		prompts = appendBool(prompts, echos[i])
+	}
+
+	if err := c.writePacket(marshal(msgUserAuthInfoRequest, userAuthInfoRequestMsg{
+		Instruction: instruction,
+		NumPrompts:  uint32(len(questions)),
+		Prompts:     prompts,
+	})); err != nil {
+		return nil, err
+	}
+
+	packet, err := c.readPacket()
+	if err != nil {
+		return nil, err
+	}
+	if packet[0] != msgUserAuthInfoResponse {
+		return nil, UnexpectedMessageError{msgUserAuthInfoResponse, packet[0]}
+	}
+	packet = packet[1:]
+
+	n, packet, ok := parseUint32(packet)
+	if !ok || int(n) != len(questions) {
+		return nil, &ParseError{msgUserAuthInfoResponse}
+	}
+
+	for i := uint32(0); i < n; i++ {
+		ans, rest, ok := parseString(packet)
+		if !ok {
+			return nil, &ParseError{msgUserAuthInfoResponse}
+		}
+
+		answers = append(answers, string(ans))
+		packet = rest
+	}
+	if len(packet) != 0 {
+		return nil, errors.New("ssh: junk at end of message")
+	}
+
+	return answers, nil
+}
+
 const defaultWindowSize = 32768
 
 // Accept reads and processes messages on a ServerConn. It must be called