mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-22 11:40:32 +03:00
refactor checks, IP-ban incorrect passphrases, renames
- s/assword/assphrase/, typo fixes - bans are checked separately from public keys - an incorrect passphrase results in a one-minute IP ban - whitelists no longer override bans (i.e. you can get banned if you're whitelisted)
This commit is contained in:
parent
bd345ce012
commit
2e203b3238
81
auth.go
81
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
|
||||
}
|
||||
|
38
auth_test.go
38
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.")
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
45
sshd/auth.go
45
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
|
||||
},
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user