From b5e7d8968fe91da03054b5ffcb7a9510f553f316 Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Wed, 7 Sep 2016 21:32:58 +1000 Subject: [PATCH] accounts: Add SASL EXTERNAL handler --- irc/accounts.go | 120 +++++++++++++++++++++++++++++++++----------- irc/client.go | 8 ++- irc/registration.go | 26 ++++++++-- irc/socket.go | 7 ++- 4 files changed, 121 insertions(+), 40 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index 7b8ad976..f17ef89c 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -44,6 +44,37 @@ type ClientAccount struct { 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). func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { // sasl abort @@ -101,13 +132,17 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) } client.saslValue += rawData - data, err := base64.StdEncoding.DecodeString(client.saslValue) - if err != nil { - client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Invalid b64 encoding") - client.saslInProgress = false - client.saslMechanism = "" - client.saslValue = "" - return false + var data []byte + var err error + if client.saslValue != "+" { + data, err = base64.StdEncoding.DecodeString(client.saslValue) + if err != nil { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Invalid b64 encoding") + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } } // call actual handler @@ -139,28 +174,22 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] return false } - accountString := string(splitValue[0]) + accountKey := string(splitValue[0]) 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") return false } // casefolding, rough for now bit will improve later. // 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. // as noted elsewhere, change to proper locking for Account type later probably err := server.store.Update(func(tx *buntdb.Tx) error { - credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountString)) - if err != nil { - return err - } - - var creds AccountCredentials - err = json.Unmarshal([]byte(credText), &creds) + creds, err := loadAccountCredentials(tx, accountKey) if err != nil { return err } @@ -173,18 +202,9 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password) // succeeded, load account info if necessary - account, exists := server.accounts[accountString] + account, exists := server.accounts[accountKey] if !exists { - name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountString)) - 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 = loadAccount(server, tx, accountKey) } 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. 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 } diff --git a/irc/client.go b/irc/client.go index f501b0f8..0ffe23dc 100644 --- a/irc/client.go +++ b/irc/client.go @@ -75,6 +75,9 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { } if isTLS { 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 { _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) @@ -205,11 +208,6 @@ func (client *Client) Register() { if client.registered { 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.Touch() } diff --git a/irc/registration.go b/irc/registration.go index 46c68e91..4a33f4ad 100644 --- a/irc/registration.go +++ b/irc/registration.go @@ -22,10 +22,12 @@ const ( keyAccountName = "account %s name" // stores the 'preferred name' of the account, not casemapped keyAccountRegTime = "account %s registered.time" keyAccountCredentials = "account %s credentials" + keyCertToAccount = "account.creds.certfp %s" ) 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. @@ -91,8 +93,6 @@ func removeFailedRegCreateData(store buntdb.DB, account string) { // regCreateHandler parses the REG CREATE command. func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { - client.Notice("Parsing CREATE") - // get and sanitise account name account := NewName(msg.Params[1]) //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 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 // 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 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()) removeFailedRegCreateData(server.store, accountString) return false diff --git a/irc/socket.go b/irc/socket.go index c7ea3054..d58f949f 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -16,7 +16,7 @@ import ( ) var ( - errNotTls = errors.New("Not a TLS connection") + errNotTLS = errors.New("Not a TLS connection") errNoPeerCerts = errors.New("Client did not provide a certificate") ) @@ -48,9 +48,12 @@ func (socket *Socket) Close() { func (socket *Socket) CertFP() (string, error) { var tlsConn, isTLS = socket.conn.(*tls.Conn) if !isTLS { - return "", errNotTls + return "", errNotTLS } + // ensure handehake is performed + tlsConn.Handshake() + peerCerts := tlsConn.ConnectionState().PeerCertificates if len(peerCerts) < 1 { return "", errNoPeerCerts