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
This commit is contained in:
Han-Wen Nienhuys
2013-06-06 10:44:12 -04:00
committed by Adam Langley
parent 36bf31eb71
commit c7df565dd4
5 changed files with 284 additions and 0 deletions
+117
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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