accounts: Add SASL EXTERNAL handler

This commit is contained in:
Daniel Oaks 2016-09-07 21:32:58 +10:00
parent 70665850aa
commit b5e7d8968f
4 changed files with 121 additions and 40 deletions

View File

@ -44,6 +44,37 @@ type ClientAccount struct {
Clients []*Client Clients []*Client
} }
// loadAccountCredentials loads an account's credentials from the store.
func loadAccountCredentials(tx *buntdb.Tx, accountKey string) (*AccountCredentials, error) {
credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountKey))
if err != nil {
return nil, err
}
var creds AccountCredentials
err = json.Unmarshal([]byte(credText), &creds)
if err != nil {
return nil, err
}
return &creds, nil
}
// loadAccount loads an account from the store, note that the account must actually exist.
func loadAccount(server *Server, tx *buntdb.Tx, accountKey string) *ClientAccount {
name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountKey))
regTime, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, accountKey))
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
accountInfo := ClientAccount{
Name: name,
RegisteredAt: time.Unix(regTimeInt, 0),
Clients: []*Client{},
}
server.accounts[accountKey] = &accountInfo
return &accountInfo
}
// authenticateHandler parses the AUTHENTICATE command (for SASL authentication). // authenticateHandler parses the AUTHENTICATE command (for SASL authentication).
func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
// sasl abort // sasl abort
@ -101,13 +132,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage)
} }
client.saslValue += rawData client.saslValue += rawData
data, err := base64.StdEncoding.DecodeString(client.saslValue) var data []byte
if err != nil { var err error
client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Invalid b64 encoding") if client.saslValue != "+" {
client.saslInProgress = false data, err = base64.StdEncoding.DecodeString(client.saslValue)
client.saslMechanism = "" if err != nil {
client.saslValue = "" client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Invalid b64 encoding")
return false client.saslInProgress = false
client.saslMechanism = ""
client.saslValue = ""
return false
}
} }
// call actual handler // call actual handler
@ -139,28 +174,22 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
return false return false
} }
accountString := string(splitValue[0]) accountKey := string(splitValue[0])
authzid := string(splitValue[1]) authzid := string(splitValue[1])
if accountString != authzid { if accountKey != authzid {
client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: authcid and authzid should be the same") client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: authcid and authzid should be the same")
return false return false
} }
// casefolding, rough for now bit will improve later. // casefolding, rough for now bit will improve later.
// keep it the same as in the REG CREATE stage // keep it the same as in the REG CREATE stage
accountString = NewName(accountString).String() accountKey = NewName(accountKey).String()
// load and check acct data all in one update to prevent races. // load and check acct data all in one update to prevent races.
// as noted elsewhere, change to proper locking for Account type later probably // as noted elsewhere, change to proper locking for Account type later probably
err := server.store.Update(func(tx *buntdb.Tx) error { err := server.store.Update(func(tx *buntdb.Tx) error {
credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountString)) creds, err := loadAccountCredentials(tx, accountKey)
if err != nil {
return err
}
var creds AccountCredentials
err = json.Unmarshal([]byte(credText), &creds)
if err != nil { if err != nil {
return err return err
} }
@ -173,18 +202,9 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password) err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password)
// succeeded, load account info if necessary // succeeded, load account info if necessary
account, exists := server.accounts[accountString] account, exists := server.accounts[accountKey]
if !exists { if !exists {
name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountString)) account = loadAccount(server, tx, accountKey)
regTime, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, accountString))
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
accountInfo := ClientAccount{
Name: name,
RegisteredAt: time.Unix(regTimeInt, 0),
Clients: []*Client{},
}
server.accounts[accountString] = &accountInfo
account = &accountInfo
} }
account.Clients = append(account.Clients, client) account.Clients = append(account.Clients, client)
@ -205,6 +225,48 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
// authExternalHandler parses the SASL EXTERNAL mechanism. // authExternalHandler parses the SASL EXTERNAL mechanism.
func authExternalHandler(server *Server, client *Client, mechanism string, value []byte) bool { func authExternalHandler(server *Server, client *Client, mechanism string, value []byte) bool {
client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Mechanism not yet implemented") if client.certfp == "" {
client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed, you are not connecting with a certificate")
return false
}
err := server.store.Update(func(tx *buntdb.Tx) error {
// certfp lookup key
accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, client.certfp))
if err != nil {
return errSaslFail
}
// confirm account exists
_, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
if err != nil {
return errSaslFail
}
// confirm the certfp in that account's credentials
creds, err := loadAccountCredentials(tx, accountKey)
if err != nil || creds.Certificate != client.certfp {
return errSaslFail
}
// succeeded, load account info if necessary
account, exists := server.accounts[accountKey]
if !exists {
account = loadAccount(server, tx, accountKey)
}
account.Clients = append(account.Clients, client)
client.account = account
return nil
})
if err != nil {
client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed")
return false
}
client.Send(nil, server.nameString, RPL_LOGGEDIN, client.nickString, client.nickMaskString, client.account.Name, fmt.Sprintf("You are now logged in as %s", client.account.Name))
client.Send(nil, server.nameString, RPL_SASLSUCCESS, client.nickString, "SASL authentication successful")
return false return false
} }

View File

@ -75,6 +75,9 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
} }
if isTLS { if isTLS {
client.flags[TLS] = true client.flags[TLS] = true
// error is not useful to us here anyways so we can ignore it
client.certfp, _ = client.socket.CertFP()
} }
if server.checkIdent { if server.checkIdent {
_, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String())
@ -205,11 +208,6 @@ func (client *Client) Register() {
if client.registered { if client.registered {
return return
} }
if client.flags[TLS] {
// error is not useful to us here anyways, so we can ignore it
client.certfp, _ = client.socket.CertFP()
//TODO(dan): login based on certfp
}
client.registered = true client.registered = true
client.Touch() client.Touch()
} }

View File

@ -22,10 +22,12 @@ const (
keyAccountName = "account %s name" // stores the 'preferred name' of the account, not casemapped keyAccountName = "account %s name" // stores the 'preferred name' of the account, not casemapped
keyAccountRegTime = "account %s registered.time" keyAccountRegTime = "account %s registered.time"
keyAccountCredentials = "account %s credentials" keyAccountCredentials = "account %s credentials"
keyCertToAccount = "account.creds.certfp %s"
) )
var ( var (
errAccountCreation = errors.New("Account could not be created") errAccountCreation = errors.New("Account could not be created")
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
) )
// AccountRegistration manages the registration of accounts. // AccountRegistration manages the registration of accounts.
@ -91,8 +93,6 @@ func removeFailedRegCreateData(store buntdb.DB, account string) {
// regCreateHandler parses the REG CREATE command. // regCreateHandler parses the REG CREATE command.
func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
client.Notice("Parsing CREATE")
// get and sanitise account name // get and sanitise account name
account := NewName(msg.Params[1]) account := NewName(msg.Params[1])
//TODO(dan): probably don't need explicit check for "*" here... until we actually casemap properly as per rfc7700 //TODO(dan): probably don't need explicit check for "*" here... until we actually casemap properly as per rfc7700
@ -196,6 +196,20 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
// store details // store details
err = server.store.Update(func(tx *buntdb.Tx) error { err = server.store.Update(func(tx *buntdb.Tx) error {
// certfp special lookup key
if credentialType == "certfp" {
assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
// make sure certfp doesn't already exist because that'd be silly
_, err := tx.Get(assembledKeyCertToAccount)
if err != buntdb.ErrNotFound {
return errCertfpAlreadyExists
}
tx.Set(assembledKeyCertToAccount, account.String(), nil)
}
// make creds
var creds AccountCredentials var creds AccountCredentials
// always set passphrase salt // always set passphrase salt
@ -223,7 +237,11 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
// details could not be stored and relevant numerics have been dispatched, abort // details could not be stored and relevant numerics have been dispatched, abort
if err != nil { if err != nil {
client.Send(nil, server.nameString, ERR_UNKNOWNERROR, client.nickString, "REG", "CREATE", "Could not register") errMsg := "Could not register"
if err == errCertfpAlreadyExists {
errMsg = "An account already exists for your certificate fingerprint"
}
client.Send(nil, server.nameString, ERR_UNKNOWNERROR, client.nickString, "REG", "CREATE", errMsg)
log.Println("Could not save registration creds:", err.Error()) log.Println("Could not save registration creds:", err.Error())
removeFailedRegCreateData(server.store, accountString) removeFailedRegCreateData(server.store, accountString)
return false return false

View File

@ -16,7 +16,7 @@ import (
) )
var ( var (
errNotTls = errors.New("Not a TLS connection") errNotTLS = errors.New("Not a TLS connection")
errNoPeerCerts = errors.New("Client did not provide a certificate") errNoPeerCerts = errors.New("Client did not provide a certificate")
) )
@ -48,9 +48,12 @@ func (socket *Socket) Close() {
func (socket *Socket) CertFP() (string, error) { func (socket *Socket) CertFP() (string, error) {
var tlsConn, isTLS = socket.conn.(*tls.Conn) var tlsConn, isTLS = socket.conn.(*tls.Conn)
if !isTLS { if !isTLS {
return "", errNotTls return "", errNotTLS
} }
// ensure handehake is performed
tlsConn.Handshake()
peerCerts := tlsConn.ConnectionState().PeerCertificates peerCerts := tlsConn.ConnectionState().PeerCertificates
if len(peerCerts) < 1 { if len(peerCerts) < 1 {
return "", errNoPeerCerts return "", errNoPeerCerts