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
}
}
}