additional challenge api support and pam challenger implement
This commit is contained in:
parent
b3d6855032
commit
d2c05aed8c
4 changed files with 247 additions and 87 deletions
|
|
@ -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
|
||||
|
|
|
|||
40
sshpiperd/challenger/challenger.go
Normal file
40
sshpiperd/challenger/challenger.go
Normal file
|
|
@ -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
|
||||
}
|
||||
70
sshpiperd/challenger/pam_challenger.go
Normal file
70
sshpiperd/challenger/pam_challenger.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue