From 77143ad1e6770266b21248dd1a58296e2af3ace2 Mon Sep 17 00:00:00 2001 From: Andrey Petrov <andrey.petrov@shazow.net> Date: Wed, 15 Apr 2020 14:19:28 -0400 Subject: [PATCH 1/4] main: Add --unsafe-passphrase --- cmd/ssh-chat/cmd.go | 46 +++++++++++++++++++++++++++++++++++---------- sshd/auth.go | 1 + 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index ac7639a..23db63e 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "fmt" "io/ioutil" "net/http" @@ -9,6 +10,7 @@ import ( "os/signal" "os/user" "strings" + "time" "github.com/alexcesaro/log" "github.com/alexcesaro/log/golog" @@ -19,23 +21,25 @@ import ( "github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/sshd" + + _ "net/http/pprof" ) -import _ "net/http/pprof" // Version of the binary, assigned during build. var Version string = "dev" // Options contains the flag options type Options struct { - Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` - Version bool `long:"version" description:"Print version and exit."` - Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` - Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` - Admin string `long:"admin" description:"File of public keys who are admins."` - Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` - Motd string `long:"motd" description:"Optional Message of the Day file."` - Log string `long:"log" description:"Write chat log to this file."` - Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` + Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` + Version bool `long:"version" description:"Print version and exit."` + Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` + Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` + Admin string `long:"admin" description:"File of public keys who are admins."` + Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` + Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."` + Motd string `long:"motd" description:"Optional Message of the Day file."` + Log string `long:"log" description:"Write chat log to this file."` + Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` } var logLevels = []log.Level{ @@ -110,6 +114,28 @@ func main() { config.AddHostKey(signer) config.ServerVersion = "SSH-2.0-Go ssh-chat" + if options.Passphrase != "" { + cb := config.KeyboardInteractiveCallback + config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { + perm, err := cb(conn, challenge) + if err != nil { + return perm, err + } + answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) + if err != nil { + return nil, err + } + if len(answers) == 1 && answers[0] == options.Passphrase { + // Success + return perm, nil + } + // It's not gonna do much but may as well throttle brute force attempts a little + time.Sleep(2 * time.Second) + + return nil, errors.New("incorrect passphrase") + } + } + s, err := sshd.ListenSSH(options.Bind, config) if err != nil { fail(4, "Failed to listen on socket: %v\n", err) diff --git a/sshd/auth.go b/sshd/auth.go index afa7271..a140413 100644 --- a/sshd/auth.go +++ b/sshd/auth.go @@ -19,6 +19,7 @@ type Auth interface { } // MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation. +// TODO: Switch to using ssh.AuthMethod instead? func MakeAuth(auth Auth) *ssh.ServerConfig { config := ssh.ServerConfig{ NoClientAuth: false, From b1bce027ad9948e217f85e200d9b760b0a79f294 Mon Sep 17 00:00:00 2001 From: Andrey Petrov <andrey.petrov@shazow.net> Date: Thu, 16 Apr 2020 11:30:13 -0400 Subject: [PATCH 2/4] main: Force passphrase auth even with pubkey auth --- cmd/ssh-chat/cmd.go | 57 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 23db63e..91813de 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -82,7 +82,8 @@ func main() { } logLevel := logLevels[numVerbose] - sshchat.SetLogger(golog.New(os.Stderr, logLevel)) + logger := golog.New(os.Stderr, logLevel) + sshchat.SetLogger(logger) if logLevel == log.Debug { // Enable logging from submodules @@ -113,27 +114,45 @@ func main() { config := sshd.MakeAuth(auth) config.AddHostKey(signer) config.ServerVersion = "SSH-2.0-Go ssh-chat" + // FIXME: Should we be using config.NoClientAuth = true by default? if options.Passphrase != "" { - cb := config.KeyboardInteractiveCallback - config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { - perm, err := cb(conn, challenge) - if err != nil { - return perm, err - } - answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) - if err != nil { - return nil, err - } - if len(answers) == 1 && answers[0] == options.Passphrase { - // Success - return perm, nil - } - // It's not gonna do much but may as well throttle brute force attempts a little - time.Sleep(2 * time.Second) - - return nil, errors.New("incorrect passphrase") + if options.Whitelist != "" { + logger.Warning("Passphrase is disabled while whitelist is enabled.") } + { + cb := config.KeyboardInteractiveCallback + config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { + perm, err := cb(conn, challenge) + if err != nil { + return perm, err + } + answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) + if err != nil { + return nil, err + } + if len(answers) == 1 && answers[0] == options.Passphrase { + // Success + return perm, nil + } + // It's not gonna do much but may as well throttle brute force attempts a little + time.Sleep(2 * time.Second) + + return nil, errors.New("incorrect passphrase") + } + } + { + // We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing + cb := config.PublicKeyCallback + config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + perms, err := cb(conn, key) + if err == nil { + err = errors.New("passphrase authentication required") + } + return perms, err + } + } + } s, err := sshd.ListenSSH(options.Bind, config) From 6e9705faf516f655226a90c6392e8f770feff9e5 Mon Sep 17 00:00:00 2001 From: Andrey Petrov <andrey.petrov@shazow.net> Date: Thu, 16 Apr 2020 12:32:12 -0400 Subject: [PATCH 3/4] main: Clarify passphrase shenanigans --- cmd/ssh-chat/cmd.go | 57 ++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 91813de..1a2134b 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -120,37 +120,36 @@ func main() { if options.Whitelist != "" { logger.Warning("Passphrase is disabled while whitelist is enabled.") } - { - cb := config.KeyboardInteractiveCallback - config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { - perm, err := cb(conn, challenge) - if err != nil { - return perm, err - } - answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) - if err != nil { - return nil, err - } - if len(answers) == 1 && answers[0] == options.Passphrase { - // Success - return perm, nil - } - // It's not gonna do much but may as well throttle brute force attempts a little - time.Sleep(2 * time.Second) - - return nil, errors.New("incorrect passphrase") - } + if config.KeyboardInteractiveCallback != nil { + fail(1, "Passphrase authentication conflicts with existing KeyboardInteractive setup.") // This should not happen } - { - // We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing - cb := config.PublicKeyCallback - config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - perms, err := cb(conn, key) - if err == nil { - err = errors.New("passphrase authentication required") - } - return perms, err + + // We use KeyboardInteractiveCallback instead of PasswordCallback to + // avoid preventing the client from including a pubkey in the user + // identification. + config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { + answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) + if err != nil { + return nil, err } + if len(answers) == 1 && answers[0] == options.Passphrase { + // Success + return nil, nil + } + // It's not gonna do much but may as well throttle brute force attempts a little + time.Sleep(2 * time.Second) + + return nil, errors.New("incorrect passphrase") + } + + // We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing + cb := config.PublicKeyCallback + config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + perms, err := cb(conn, key) + if err == nil { + err = errors.New("passphrase authentication required") + } + return perms, err } } From 99d303e19672dcbc11d61d431b15f104ea552999 Mon Sep 17 00:00:00 2001 From: Andrey Petrov <andrey.petrov@shazow.net> Date: Thu, 16 Apr 2020 12:44:20 -0400 Subject: [PATCH 4/4] main: Add extraHelp --- cmd/ssh-chat/cmd.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 1a2134b..abce387 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -30,18 +30,31 @@ var Version string = "dev" // Options contains the flag options type Options struct { - Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` - Version bool `long:"version" description:"Print version and exit."` - Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` - Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` - Admin string `long:"admin" description:"File of public keys who are admins."` - Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` - Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."` - Motd string `long:"motd" description:"Optional Message of the Day file."` - Log string `long:"log" description:"Write chat log to this file."` - Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` + Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` + Version bool `long:"version" description:"Print version and exit."` + Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` + Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` + Admin string `long:"admin" description:"File of public keys who are admins."` + Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` + Motd string `long:"motd" description:"Optional Message of the Day file."` + Log string `long:"log" description:"Write chat log to this file."` + Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` + + // Hidden flags, because they're discouraged from being used casually. + Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure." hidden:"true"` } +const extraHelp = `There are hidden options and easter eggs in ssh-chat. The source code is a good +place to start looking. Some useful links: + +* Project Repository: + https://github.com/shazow/ssh-chat +* Project Wiki FAQ: + https://github.com/shazow/ssh-chat/wiki/FAQ +* Command Flags Declaration: + https://github.com/shazow/ssh-chat/blob/master/cmd/ssh-chat/cmd.go#L29 +` + var logLevels = []log.Level{ log.Warning, log.Info, @@ -61,6 +74,9 @@ func main() { if p == nil { fmt.Print(err) } + if flagErr, ok := err.(*flags.Error); ok && flagErr.Type == flags.ErrHelp { + fmt.Print(extraHelp) + } return }