From d2c05aed8c2ab61b9f9ffb471e91de84c492ce74 Mon Sep 17 00:00:00 2001 From: tgic Date: Fri, 12 Dec 2014 15:09:09 +0800 Subject: [PATCH] additional challenge api support and pam challenger implement --- ssh/sshpiper.go | 40 +++++- sshpiperd/challenger/challenger.go | 40 ++++++ sshpiperd/challenger/pam_challenger.go | 70 ++++++++++ sshpiperd/sshpiperd.go | 184 +++++++++++++------------ 4 files changed, 247 insertions(+), 87 deletions(-) create mode 100644 sshpiperd/challenger/challenger.go create mode 100644 sshpiperd/challenger/pam_challenger.go diff --git a/ssh/sshpiper.go b/ssh/sshpiper.go index 5c171370..931623d9 100644 --- a/ssh/sshpiper.go +++ b/ssh/sshpiper.go @@ -9,8 +9,9 @@ import ( type SSHPiper struct { DownstreamConfig ServerConfig - FindUpstream func(conn ConnMetadata) (net.Conn, *ClientConfig, error) - MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error) + AdditionalChallenge func(conn ConnMetadata, client KeyboardInteractiveChallenge) (bool, error) + FindUpstream func(conn ConnMetadata) (net.Conn, *ClientConfig, error) + MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error) } type upstream struct{ *connection } @@ -39,6 +40,41 @@ func (piper *SSHPiper) Serve(conn net.Conn) error { d.user = userAuthReq.User + // need additional challenge + if piper.AdditionalChallenge != nil { + + for { + err := d.transport.writePacket(Marshal(&userAuthFailureMsg{ + Methods: []string{"keyboard-interactive"}, + })) + + if err != nil { + return err + } + + userAuthReq, err := d.nextAuthMsg() + + if err != nil { + return err + } + + if userAuthReq.Method == "keyboard-interactive" { + break + } + } + + prompter := &sshClientKeyboardInteractive{d.connection} + ok, err := piper.AdditionalChallenge(d, prompter.Challenge) + + if err != nil { + return err + } + + if !ok { + return fmt.Errorf("additional challenge failed") + } + } + upconn, upconfig, err := piper.FindUpstream(d) if err != nil { return err diff --git a/sshpiperd/challenger/challenger.go b/sshpiperd/challenger/challenger.go new file mode 100644 index 00000000..7a4e8720 --- /dev/null +++ b/sshpiperd/challenger/challenger.go @@ -0,0 +1,40 @@ +package challenger + +import ( + "fmt" + "github.com/tg123/sshpiper/ssh" + "sort" +) + +type Challenger func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (bool, error) + +var challengers = make(map[string]Challenger) + +// copied from database/sql + +func Register(name string, challenger Challenger) { + if challenger == nil { + panic("challenger is nil") + } + if _, dup := challengers[name]; dup { + panic("Register twice for challenger" + name) + } + challengers[name] = challenger +} + +func Challengers() []string { + var list []string + for name := range challengers { + list = append(list, name) + } + sort.Strings(list) + return list +} + +func GetChallenger(name string) (Challenger, error) { + challenger, ok := challengers[name] + if !ok { + return nil, fmt.Errorf("no such challenger:" + name) + } + return challenger, nil +} diff --git a/sshpiperd/challenger/pam_challenger.go b/sshpiperd/challenger/pam_challenger.go new file mode 100644 index 00000000..d68b01b1 --- /dev/null +++ b/sshpiperd/challenger/pam_challenger.go @@ -0,0 +1,70 @@ +// +build pam + +package challenger + +import ( + "fmt" + "github.com/tg123/sshpiper/ssh" + pam "github.com/vvanpo/golang-pam" + "os" +) + +const ( + SSHPIPER_PAM_SERVICE_FILE = "/etc/pam.d/sshpiperd" +) + +func pamChallenger(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (bool, error) { + + user := conn.User() + + sendQuesttion := func(question string, echo bool) (string, bool) { + ans, err := client(user, "", []string{question}, []bool{echo}) + + // TODO lost err + if err != nil { + return "", false + } + + return ans[0], true + } + + sendInstruction := func(instruction string) (string, bool) { + _, err := client(user, instruction, nil, nil) + return "", err == nil + } + + t, status := pam.Start("sshpiperd", user, pam.ResponseFunc(func(style int, msg string) (string, bool) { + switch style { + case pam.PROMPT_ECHO_OFF: + return sendQuesttion(msg, false) + case pam.PROMPT_ECHO_ON: + return sendQuesttion(msg, true) + case pam.ERROR_MSG: + return sendInstruction(fmt.Sprintf("Error: %s", msg)) + case pam.TEXT_INFO: + return sendInstruction(msg) + } + return "", false + })) + + if status != pam.SUCCESS { + return false, fmt.Errorf("pam.Start() failed: %s\n", t.Error(status)) + } + defer func() { t.End(status) }() + + status = t.Authenticate(0) + if status != pam.SUCCESS { + return false, fmt.Errorf("Auth failed: %s\n", t.Error(status)) + } + + return true, nil +} + +func init() { + + if _, err := os.Stat(SSHPIPER_PAM_SERVICE_FILE); os.IsNotExist(err) { + return + } + + Register("pam", pamChallenger) +} diff --git a/sshpiperd/sshpiperd.go b/sshpiperd/sshpiperd.go index 6b40f727..60f7e9ce 100644 --- a/sshpiperd/sshpiperd.go +++ b/sshpiperd/sshpiperd.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "github.com/tg123/sshpiper/ssh" + "github.com/tg123/sshpiper/sshpiperd/challenger" "io/ioutil" "log" "net" @@ -26,6 +27,7 @@ var ( WorkingDir string PiperKeyFile string ShowHelp bool + Challenger string logger = log.New(os.Stdout, "", log.Ldate|log.Ltime) ) @@ -35,6 +37,7 @@ func init() { flag.UintVar(&Port, "p", 2222, "Listening Port") flag.StringVar(&WorkingDir, "w", "/var/sshpiper", "Working Dir") flag.StringVar(&PiperKeyFile, "i", "/etc/ssh/ssh_host_rsa_key", "Key file for SSH Piper") + flag.StringVar(&Challenger, "c", "", "Additional challenger name, e.g. pam, emtpy for no additional challenge") flag.BoolVar(&ShowHelp, "h", false, "Print help and exit") flag.Parse() } @@ -72,6 +75,92 @@ func (file userFile) check400(user string) error { return nil } +func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, *ssh.ClientConfig, error) { + user := conn.User() + + err := UserUpstreamFile.check400(user) + if err != nil { + return nil, nil, err + } + + addr, err := UserUpstreamFile.read(user) + if err != nil { + return nil, nil, err + } + + saddr := strings.TrimSpace(string(addr)) + + logger.Printf("mapping user [%s] to [%s]", user, saddr) + + c, err := net.Dial("tcp", saddr) + if err != nil { + return nil, nil, err + } + + return c, &ssh.ClientConfig{}, nil +} + +func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.Signer, error) { + user := conn.User() + + var err error + defer func() { // print error when func exit + if err != nil { + logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr()) + } + }() + + err = UserAuthorizedKeysFile.check400(user) + if err != nil { + return nil, err + } + + keydata := key.Marshal() + + var rest []byte + rest, err = UserAuthorizedKeysFile.read(user) + if err != nil { + return nil, err + } + + var authedPubkey ssh.PublicKey + + for len(rest) > 0 { + authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) + + if err != nil { + return nil, err + } + + if bytes.Equal(authedPubkey.Marshal(), keydata) { + err = UserKeyFile.check400(user) + if err != nil { + return nil, err + } + + var privateBytes []byte + privateBytes, err = UserKeyFile.read(user) + if err != nil { + return nil, err + } + + var private ssh.Signer + private, err = ssh.ParsePrivateKey(privateBytes) + if err != nil { + return nil, err + } + + // in log may see this twice, one is for query the other is real sign again + logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", UserKeyFile.realPath(user), user, conn.RemoteAddr()) + return private, nil + } + } + + logger.Printf("public key auth failed user [%v] from [%v]", conn.User(), conn.RemoteAddr()) + + return nil, nil +} + func main() { if ShowHelp { @@ -80,93 +169,18 @@ func main() { } piper := &ssh.SSHPiper{ - FindUpstream: func(conn ssh.ConnMetadata) (net.Conn, *ssh.ClientConfig, error) { + FindUpstream: findUpstreamFromUserfile, + MapPublicKey: mapPublicKeyFromUserfile, + } - user := conn.User() + if Challenger != "" { + ac, err := challenger.GetChallenger(Challenger) + if err != nil { + logger.Fatalln(err) + } - err := UserUpstreamFile.check400(user) - if err != nil { - return nil, nil, err - } - - addr, err := UserUpstreamFile.read(user) - if err != nil { - return nil, nil, err - } - - saddr := strings.TrimSpace(string(addr)) - - logger.Printf("mapping user [%s] to [%s]", user, saddr) - - c, err := net.Dial("tcp", saddr) - if err != nil { - return nil, nil, err - } - - return c, &ssh.ClientConfig{}, nil - }, - - MapPublicKey: func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.Signer, error) { - - user := conn.User() - - var err error - defer func() { // print error when func exit - if err != nil { - logger.Printf("mapping private key error: %v, public key auth denied for [%v] from [%v]", err, user, conn.RemoteAddr()) - } - }() - - err = UserAuthorizedKeysFile.check400(user) - if err != nil { - return nil, err - } - - keydata := key.Marshal() - - var rest []byte - rest, err = UserAuthorizedKeysFile.read(user) - if err != nil { - return nil, err - } - - var authedPubkey ssh.PublicKey - - for len(rest) > 0 { - authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest) - - if err != nil { - return nil, err - } - - if bytes.Equal(authedPubkey.Marshal(), keydata) { - err = UserKeyFile.check400(user) - if err != nil { - return nil, err - } - - var privateBytes []byte - privateBytes, err = UserKeyFile.read(user) - if err != nil { - return nil, err - } - - var private ssh.Signer - private, err = ssh.ParsePrivateKey(privateBytes) - if err != nil { - return nil, err - } - - // in log may see this twice, one is for query the other is real sign again - logger.Printf("auth succ, using mapped private key [%v] for user [%v] from [%v]", UserKeyFile.realPath(user), user, conn.RemoteAddr()) - return private, nil - } - } - - logger.Printf("public key auth failed user [%v] from [%v]", conn.User(), conn.RemoteAddr()) - - return nil, nil - }, + logger.Printf("using additional challenger %s", Challenger) + piper.AdditionalChallenge = ac } privateBytes, err := ioutil.ReadFile(PiperKeyFile)