diff --git a/auth.go b/auth.go index bf3dbcd..bb74670 100644 --- a/auth.go +++ b/auth.go @@ -19,9 +19,12 @@ import ( // when whitelisting is enabled. var ErrNotWhitelisted = errors.New("not whitelisted") -// ErrBanned is the error returned when a key is checked that is banned. +// ErrBanned is the error returned when a client is banned. var ErrBanned = errors.New("banned") +// ErrIncorrectPassphrase is the error returned when a provided passphrase is incorrect. +var ErrIncorrectPassphrase = errors.New("incorrect passphrase") + // newAuthKey returns string from an ssh.PublicKey used to index the key in our lookup. func newAuthKey(key ssh.PublicKey) string { if key == nil { @@ -45,17 +48,17 @@ func newAuthAddr(addr net.Addr) string { } // Auth stores lookups for bans, whitelists, and ops. It implements the sshd.Auth interface. -// If the contained password is not empty, it complements a whitelist. +// If the contained passphrase is not empty, it complements a whitelist. type Auth struct { - passwordHash []byte - bannedAddr *set.Set - bannedClient *set.Set - banned *set.Set - whitelist *set.Set - ops *set.Set + passphraseHash []byte + bannedAddr *set.Set + bannedClient *set.Set + banned *set.Set + whitelist *set.Set + ops *set.Set } -// NewAuth creates a Auth, optionally with a password. +// NewAuth creates a new empty Auth. func NewAuth() *Auth { return &Auth{ bannedAddr: set.New(), @@ -66,40 +69,31 @@ func NewAuth() *Auth { } } -// SetPassword anables password authentication with the given password. -// If an emty password is geiven, disable password authentication. -func (a *Auth) SetPassword(password string) { - if password == "" { - a.passwordHash = nil +// SetPassphrase enables passphrase authentication with the given passphrase. +// If an empty passphrase is given, disable passphrase authentication. +func (a *Auth) SetPassphrase(passphrase string) { + if passphrase == "" { + a.passphraseHash = nil } else { - hashArray := sha256.Sum256([]byte(password)) - a.passwordHash = hashArray[:] + hashArray := sha256.Sum256([]byte(passphrase)) + a.passphraseHash = hashArray[:] } } // AllowAnonymous determines if anonymous users are permitted. func (a *Auth) AllowAnonymous() bool { - return a.whitelist.Len() == 0 && a.passwordHash == nil + return a.whitelist.Len() == 0 && a.passphraseHash == nil } -// AcceptPassword determines if password authentication is accepted. -func (a *Auth) AcceptPassword() bool { - return a.passwordHash != nil +// AcceptPassphrase determines if passphrase authentication is accepted. +func (a *Auth) AcceptPassphrase() bool { + return a.passphraseHash != nil } -// Check determines if a pubkey fingerprint is permitted. -func (a *Auth) Check(addr net.Addr, key ssh.PublicKey, clientVersion string) error { +// CheckBans checks IP, key and client bans. +func (a *Auth) CheckBans(addr net.Addr, key ssh.PublicKey, clientVersion string) error { authkey := newAuthKey(key) - if !a.AllowAnonymous() { - // Only check whitelist if we don't allow everyone to connect. - whitelisted := a.whitelist.In(authkey) - if !whitelisted { - return ErrNotWhitelisted - } - return nil - } - var banned bool if authkey != "" { banned = a.banned.In(authkey) @@ -118,14 +112,25 @@ func (a *Auth) Check(addr net.Addr, key ssh.PublicKey, clientVersion string) err return nil } -// CheckPassword determines if a password is permitted. -func (a *Auth) CheckPassword(password string) error { - if !a.AcceptPassword() { - return errors.New("passwords not accepted") // this should never happen +// CheckPubkey determines if a pubkey fingerprint is permitted. +func (a *Auth) CheckPubkey(key ssh.PublicKey) error { + authkey := newAuthKey(key) + whitelisted := a.whitelist.In(authkey) + if a.AllowAnonymous() || whitelisted { + return nil + } else { + return ErrNotWhitelisted } - passedPasswordhash := sha256.Sum256([]byte(password)) - if subtle.ConstantTimeCompare(passedPasswordhash[:], a.passwordHash) == 0 { - return errors.New("incorrect password") +} + +// CheckPassphrase determines if a passphrase is permitted. +func (a *Auth) CheckPassphrase(passphrase string) error { + if !a.AcceptPassphrase() { + return errors.New("passphrases not accepted") // this should never happen + } + passedPassphraseHash := sha256.Sum256([]byte(passphrase)) + if subtle.ConstantTimeCompare(passedPassphraseHash[:], a.passphraseHash) == 0 { + return ErrIncorrectPassphrase } return nil } diff --git a/auth_test.go b/auth_test.go index 7af3b9b..4cbfcea 100644 --- a/auth_test.go +++ b/auth_test.go @@ -28,7 +28,7 @@ func TestAuthWhitelist(t *testing.T) { } auth := NewAuth() - err = auth.Check(nil, key, "") + err = auth.CheckPubkey(key) if err != nil { t.Error("Failed to permit in default state:", err) } @@ -44,7 +44,7 @@ func TestAuthWhitelist(t *testing.T) { t.Error("Clone key does not match.") } - err = auth.Check(nil, keyClone, "") + err = auth.CheckPubkey(keyClone) if err != nil { t.Error("Failed to permit whitelisted:", err) } @@ -54,42 +54,42 @@ func TestAuthWhitelist(t *testing.T) { t.Fatal(err) } - err = auth.Check(nil, key2, "") + err = auth.CheckPubkey(key2) if err == nil { t.Error("Failed to restrict not whitelisted:", err) } } -func TestAuthPasswords(t *testing.T) { +func TestAuthPassphrases(t *testing.T) { auth := NewAuth() - if auth.AcceptPassword() { - t.Error("Doesn't known it won't accept passwords.") + if auth.AcceptPassphrase() { + t.Error("Doesn't known it won't accept passphrases.") } - auth.SetPassword("") - if auth.AcceptPassword() { - t.Error("Doesn't known it won't accept passwords.") + auth.SetPassphrase("") + if auth.AcceptPassphrase() { + t.Error("Doesn't known it won't accept passphrases.") } - err := auth.CheckPassword("Pa$$w0rd") + err := auth.CheckPassphrase("Pa$$w0rd") if err == nil { - t.Error("Failed to deny without password:", err) + t.Error("Failed to deny without passphrase:", err) } - auth.SetPassword("Pa$$w0rd") + auth.SetPassphrase("Pa$$w0rd") - err = auth.CheckPassword("Pa$$w0rd") + err = auth.CheckPassphrase("Pa$$w0rd") if err != nil { - t.Error("Failed to allow vaild password:", err) + t.Error("Failed to allow vaild passphrase:", err) } - err = auth.CheckPassword("something else") + err = auth.CheckPassphrase("something else") if err == nil { - t.Error("Failed to restrict wrong password:", err) + t.Error("Failed to restrict wrong passphrase:", err) } - auth.SetPassword("") - if auth.AcceptPassword() { - t.Error("Didn't clear password.") + auth.SetPassphrase("") + if auth.AcceptPassphrase() { + t.Error("Didn't clear passphrase.") } } diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 7675336..a13e7ff 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -135,7 +135,7 @@ func main() { host.Version = Version if options.Passphrase != "" { - auth.SetPassword(options.Passphrase) + auth.SetPassphrase(options.Passphrase) } err = fromFile(options.Admin, func(line []byte) error { diff --git a/sshd/auth.go b/sshd/auth.go index 8ea842b..feab16d 100644 --- a/sshd/auth.go +++ b/sshd/auth.go @@ -5,21 +5,26 @@ import ( "encoding/base64" "errors" "net" + "time" "github.com/shazow/ssh-chat/internal/sanitize" "golang.org/x/crypto/ssh" ) -// Auth is used to authenticate connections based on public keys. +// Auth is used to authenticate connections. type Auth interface { // Whether to allow connections without a public key. AllowAnonymous() bool - // If password authtication is accepted - AcceptPassword() bool - // Given address and public key and client agent string, returns nil if the connection should be allowed. - Check(net.Addr, ssh.PublicKey, string) error - // Given a password, returns nil if the connection should be allowed - CheckPassword(string) error + // If passphrase authentication is accepted + AcceptPassphrase() bool + // Given address and public key and client agent string, returns nil if the connection is not banned. + CheckBans(net.Addr, ssh.PublicKey, string) error + // Given a public key, returns nil if the connection should be allowed. + CheckPubkey(ssh.PublicKey) error + // Given a passphrase, returns nil if the connection should be allowed. + CheckPassphrase(string) error + // BanAddr bans an IP address for the specified amount of time. + BanAddr(net.Addr, time.Duration) } // MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation. @@ -29,7 +34,11 @@ func MakeAuth(auth Auth) *ssh.ServerConfig { NoClientAuth: false, // Auth-related things should be constant-time to avoid timing attacks. PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - err := auth.Check(conn.RemoteAddr(), key, sanitize.Data(string(conn.ClientVersion()), 64)) + err := auth.CheckBans(conn.RemoteAddr(), key, sanitize.Data(string(conn.ClientVersion()), 64)) + if err != nil { + return nil, err + } + err = auth.CheckPubkey(key) if err != nil { return nil, err } @@ -43,20 +52,26 @@ func MakeAuth(auth Auth) *ssh.ServerConfig { // avoid preventing the client from including a pubkey in the user // identification. KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) { - var err error - if auth.AcceptPassword() { + err := auth.CheckBans(conn.RemoteAddr(), nil, sanitize.Data(string(conn.ClientVersion()), 64)) + if err != nil { + return nil, err + } + if auth.AcceptPassphrase() { var answers []string answers, err = challenge("", "", []string{"Passphrase required to connect: "}, []bool{true}) if err == nil { if len(answers) != 1 { - err = errors.New("didn't get password") + err = errors.New("didn't get passphrase") } else { - err = auth.CheckPassword(answers[0]) - // TODO: some kind of brute force throttling here? + err = auth.CheckPassphrase(answers[0]) + if err != nil { + // TODO: make rate-limiting configurable + auth.BanAddr(conn.RemoteAddr(), time.Minute * 1) + } } } - } else { - err = auth.Check(conn.RemoteAddr(), nil, sanitize.Data(string(conn.ClientVersion()), 64)) + } else if !auth.AllowAnonymous(){ + err = errors.New("public key authentication required") } return nil, err }, diff --git a/sshd/client_test.go b/sshd/client_test.go index 6c7c511..cec5f0d 100644 --- a/sshd/client_test.go +++ b/sshd/client_test.go @@ -4,6 +4,7 @@ import ( "errors" "net" "testing" + "time" "golang.org/x/crypto/ssh" ) @@ -15,15 +16,19 @@ type RejectAuth struct{} func (a RejectAuth) AllowAnonymous() bool { return false } -func (a RejectAuth) AcceptPassword() bool { +func (a RejectAuth) AcceptPassphrase() bool { return false } -func (a RejectAuth) Check(net.Addr, ssh.PublicKey, string) error { +func (a RejectAuth) CheckBans(addr net.Addr, key ssh.PublicKey, clientVersion string) error { return errRejectAuth } -func (a RejectAuth) CheckPassword(string) error { +func (a RejectAuth) CheckPubkey(ssh.PublicKey) error { return errRejectAuth } +func (a RejectAuth) CheckPassphrase(string) error { + return errRejectAuth +} +func (a RejectAuth) BanAddr(net.Addr, time.Duration) {} func TestClientReject(t *testing.T) { signer, err := NewRandomSigner(512)