From f1fce55697df421af3840edce4e60071adf0e818 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 11 Sep 2017 19:18:19 +0100 Subject: [PATCH] Add config and checks for trusted ID servers (#206) * Add config for trusted ID servers * Add new error * Implement check for trusted ID server * Complete unfinished comment * Make comment more explicit in the config file * Use go standard errors in membership.go * Use standard errors instead of JSON responses in threepid * Doc errors * Remove unused parameter --- dendrite-config.yaml | 6 + .../dendrite/clientapi/jsonerror/jsonerror.go | 9 ++ .../dendrite/clientapi/readers/threepid.go | 30 ++++- .../dendrite/clientapi/routing/routing.go | 4 +- .../dendrite/clientapi/threepid/invites.go | 59 +++++----- .../dendrite/clientapi/threepid/threepid.go | 34 +++++- .../dendrite/clientapi/writers/membership.go | 105 ++++++++++++------ .../dendrite/common/config/config.go | 8 ++ 8 files changed, 175 insertions(+), 80 deletions(-) diff --git a/dendrite-config.yaml b/dendrite-config.yaml index a91429c2..c9cab73e 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -12,6 +12,12 @@ matrix: private_key: "/etc/dendrite/matrix_key.pem" # The x509 certificates used by the federation listeners for this server federation_certificates: ["/etc/dendrite/server.pem"] + # The list of identity servers trusted to verify third party identifiers by this server. + # Defaults to no trusted servers. + trusted_third_party_id_servers: + - vector.im + - matrix.org + - riot.im # The media repository config media: diff --git a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go index 3a424dde..d267355e 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go +++ b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go @@ -104,3 +104,12 @@ func LimitExceeded(msg string, retryAfterMS int64) *LimitExceededError { RetryAfterMS: retryAfterMS, } } + +// NotTrusted is an error which is returned when the client asks the server to +// proxy a request (e.g. 3PID association) to a server that isn't trusted +func NotTrusted(serverName string) *MatrixError { + return &MatrixError{ + ErrCode: "M_SERVER_NOT_TRUSTED", + Err: fmt.Sprintf("Untrusted server '%s'", serverName), + } +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/readers/threepid.go b/src/github.com/matrix-org/dendrite/clientapi/readers/threepid.go index 4b86108e..b0d79287 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/readers/threepid.go +++ b/src/github.com/matrix-org/dendrite/clientapi/readers/threepid.go @@ -22,6 +22,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/threepid" + "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -38,7 +39,7 @@ type threePIDsResponse struct { // RequestEmailToken implements: // POST /account/3pid/email/requestToken // POST /register/email/requestToken -func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSONResponse { +func RequestEmailToken(req *http.Request, accountDB *accounts.Database, cfg config.Dendrite) util.JSONResponse { var body threepid.EmailAssociationRequest if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { return *reqErr @@ -63,8 +64,13 @@ func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSO } } - resp.SID, err = threepid.CreateSession(body) - if err != nil { + resp.SID, err = threepid.CreateSession(body, cfg) + if err == threepid.ErrNotTrusted { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotTrusted(body.IDServer), + } + } else if err != nil { return httputil.LogThenError(req, err) } @@ -77,6 +83,7 @@ func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSO // CheckAndSave3PIDAssociation implements POST /account/3pid func CheckAndSave3PIDAssociation( req *http.Request, accountDB *accounts.Database, device *authtypes.Device, + cfg config.Dendrite, ) util.JSONResponse { var body threepid.EmailAssociationCheckRequest if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { @@ -84,8 +91,13 @@ func CheckAndSave3PIDAssociation( } // Check if the association has been validated - verified, address, medium, err := threepid.CheckAssociation(body.Creds) - if err != nil { + verified, address, medium, err := threepid.CheckAssociation(body.Creds, cfg) + if err == threepid.ErrNotTrusted { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotTrusted(body.Creds.IDServer), + } + } else if err != nil { return httputil.LogThenError(req, err) } @@ -101,7 +113,13 @@ func CheckAndSave3PIDAssociation( if body.Bind { // Publish the association on the identity server if requested - if err = threepid.PublishAssociation(body.Creds, device.UserID); err != nil { + err = threepid.PublishAssociation(body.Creds, device.UserID, cfg) + if err == threepid.ErrNotTrusted { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotTrusted(body.Creds.IDServer), + } + } else if err != nil { return httputil.LogThenError(req, err) } } diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 58e852fe..1620d6df 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -241,7 +241,7 @@ func Setup( r0mux.Handle("/account/3pid", common.MakeAuthAPI("account_3pid", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { - return readers.CheckAndSave3PIDAssociation(req, accountDB, device) + return readers.CheckAndSave3PIDAssociation(req, accountDB, device, cfg) }), ).Methods("POST", "OPTIONS") @@ -253,7 +253,7 @@ func Setup( r0mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken", common.MakeAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse { - return readers.RequestEmailToken(req, accountDB) + return readers.RequestEmailToken(req, accountDB, cfg) }), ).Methods("POST", "OPTIONS") diff --git a/src/github.com/matrix-org/dendrite/clientapi/threepid/invites.go b/src/github.com/matrix-org/dendrite/clientapi/threepid/invites.go index 85f0b5dc..27cdf343 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/threepid/invites.go +++ b/src/github.com/matrix-org/dendrite/clientapi/threepid/invites.go @@ -26,15 +26,11 @@ import ( "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/events" - "github.com/matrix-org/dendrite/clientapi/httputil" - "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/gomatrixserverlib" - - "github.com/matrix-org/util" ) // MembershipRequest represents the body of an incoming POST request @@ -66,6 +62,15 @@ type idServerStoreInviteResponse struct { PublicKeys []common.PublicKey `json:"public_keys"` } +var ( + // ErrMissingParameter is the error raised if a request for 3PID invite has + // an incomplete body + ErrMissingParameter = errors.New("'address', 'id_server' and 'medium' must all be supplied") + // ErrNotTrusted is the error raised if an identity server isn't in the list + // of trusted servers in the configuration file. + ErrNotTrusted = errors.New("untrusted server") +) + // CheckAndProcessInvite analyses the body of an incoming membership request. // If the fields relative to a third-party-invite are all supplied, lookups the // matching Matrix ID from the given identity server. If no Matrix ID is @@ -80,27 +85,24 @@ type idServerStoreInviteResponse struct { // fills the Matrix ID in the request body so a normal invite membership event // can be emitted. func CheckAndProcessInvite( - req *http.Request, device *authtypes.Device, body *MembershipRequest, - cfg config.Dendrite, queryAPI api.RoomserverQueryAPI, db *accounts.Database, + device *authtypes.Device, body *MembershipRequest, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, db *accounts.Database, producer *producers.RoomserverProducer, membership string, roomID string, -) *util.JSONResponse { +) (inviteStoredOnIDServer bool, err error) { if membership != "invite" || (body.Address == "" && body.IDServer == "" && body.Medium == "") { // If none of the 3PID-specific fields are supplied, it's a standard invite // so return nil for it to be processed as such - return nil + return } else if body.Address == "" || body.IDServer == "" || body.Medium == "" { // If at least one of the 3PID-specific fields is supplied but not all // of them, return an error - return &util.JSONResponse{ - Code: 400, - JSON: jsonerror.BadJSON("'address', 'id_server' and 'medium' must all be supplied"), - } + err = ErrMissingParameter + return } lookupRes, storeInviteRes, err := queryIDServer(db, cfg, device, body, roomID) if err != nil { - resErr := httputil.LogThenError(req, err) - return &resErr + return } if lookupRes.MXID == "" { @@ -108,28 +110,16 @@ func CheckAndProcessInvite( // "m.room.third_party_invite" have to be emitted from the data in // storeInviteRes. err = emit3PIDInviteEvent(body, storeInviteRes, device, roomID, cfg, queryAPI, producer) - if err == events.ErrRoomNoExists { - return &util.JSONResponse{ - Code: 404, - JSON: jsonerror.NotFound(err.Error()), - } - } else if err != nil { - resErr := httputil.LogThenError(req, err) - return &resErr - } + inviteStoredOnIDServer = err == nil - // If everything went well, returns with an empty response. - return &util.JSONResponse{ - Code: 200, - JSON: struct{}{}, - } + return } // A Matrix ID have been found: set it in the body request and let the process // continue to create a "m.room.member" event with an "invite" membership body.UserID = lookupRes.MXID - return nil + return } // queryIDServer handles all the requests to the identity server, starting by @@ -142,9 +132,13 @@ func CheckAndProcessInvite( // Returns a representation of the response for both cases. // Returns an error if a check or a request failed. func queryIDServer( - db *accounts.Database, cfg config.Dendrite, - device *authtypes.Device, body *MembershipRequest, roomID string, + db *accounts.Database, cfg config.Dendrite, device *authtypes.Device, + body *MembershipRequest, roomID string, ) (lookupRes *idServerLookupResponse, storeInviteRes *idServerStoreInviteResponse, err error) { + if err = isTrusted(body.IDServer, cfg); err != nil { + return + } + // Lookup the 3PID lookupRes, err = queryIDServerLookup(body) if err != nil { @@ -249,7 +243,6 @@ func queryIDServerStoreInvite( } if resp.StatusCode != http.StatusOK { - // TODO: Log the error supplied with the identity server? errMsg := fmt.Sprintf("Identity server %s responded with a %d error code", body.IDServer, resp.StatusCode) return nil, errors.New(errMsg) } @@ -275,7 +268,6 @@ func queryIDServerPubKey(idServerName string, keyID string) ([]byte, error) { } if resp.StatusCode != http.StatusOK { - // TODO: Log the error supplied with the identity server? errMsg := fmt.Sprintf("Couldn't retrieve key %s from server %s", keyID, idServerName) return nil, errors.New(errMsg) } @@ -297,7 +289,6 @@ func checkIDServerSignatures(body *MembershipRequest, res *idServerLookupRespons return err } - // TODO: Check if the domain is part of a list of trusted ID servers signatures, ok := res.Signatures[body.IDServer] if !ok { return errors.New("No signature for domain " + body.IDServer) diff --git a/src/github.com/matrix-org/dendrite/clientapi/threepid/threepid.go b/src/github.com/matrix-org/dendrite/clientapi/threepid/threepid.go index 2ec4599a..f07a3a14 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/threepid/threepid.go +++ b/src/github.com/matrix-org/dendrite/clientapi/threepid/threepid.go @@ -22,6 +22,8 @@ import ( "net/url" "strconv" "strings" + + "github.com/matrix-org/dendrite/common/config" ) // EmailAssociationRequest represents the request defined at https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register-email-requesttoken @@ -49,8 +51,10 @@ type Credentials struct { // Returns the session's ID. // Returns an error if there was a problem sending the request or decoding the // response, or if the identity server responded with a non-OK status. -func CreateSession(req EmailAssociationRequest) (string, error) { - // TODO: Check if the ID server is trusted +func CreateSession(req EmailAssociationRequest, cfg config.Dendrite) (string, error) { + if err := isTrusted(req.IDServer, cfg); err != nil { + return "", err + } // Create a session on the ID server postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/validate/email/requestToken", req.IDServer) @@ -93,8 +97,11 @@ func CreateSession(req EmailAssociationRequest) (string, error) { // identifier and its medium. // Returns an error if there was a problem sending the request or decoding the // response, or if the identity server responded with a non-OK status. -func CheckAssociation(creds Credentials) (bool, string, string, error) { - // TODO: Check if the ID server is trusted +func CheckAssociation(creds Credentials, cfg config.Dendrite) (bool, string, string, error) { + if err := isTrusted(creds.IDServer, cfg); err != nil { + return false, "", "", err + } + url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/getValidated3pid?sid=%s&client_secret=%s", creds.IDServer, creds.SID, creds.Secret) resp, err := http.Get(url) if err != nil { @@ -126,8 +133,11 @@ func CheckAssociation(creds Credentials) (bool, string, string, error) { // identifier and a Matrix ID. // Returns an error if there was a problem sending the request or decoding the // response, or if the identity server responded with a non-OK status. -func PublishAssociation(creds Credentials, userID string) error { - // TODO: Check if the ID server is trusted +func PublishAssociation(creds Credentials, userID string, cfg config.Dendrite) error { + if err := isTrusted(creds.IDServer, cfg); err != nil { + return err + } + postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/bind", creds.IDServer) data := url.Values{} @@ -154,3 +164,15 @@ func PublishAssociation(creds Credentials, userID string) error { return nil } + +// isTrusted checks if a given identity server is part of the list of trusted +// identity servers in the configuration file. +// Returns an error if the server isn't trusted. +func isTrusted(idServer string, cfg config.Dendrite) error { + for _, server := range cfg.Matrix.TrustedIDServers { + if idServer == server { + return nil + } + } + return ErrNotTrusted +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go index 6dd52a00..4173e51a 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/membership.go @@ -15,6 +15,7 @@ package writers import ( + "errors" "net/http" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" @@ -32,6 +33,8 @@ import ( "github.com/matrix-org/util" ) +var errMissingUserID = errors.New("'user_id' must be supplied") + // SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite) // by building a m.room.member event then sending it to the room server func SendMembership( @@ -44,20 +47,78 @@ func SendMembership( return *reqErr } - if res := threepid.CheckAndProcessInvite( - req, device, &body, cfg, queryAPI, accountDB, producer, membership, roomID, - ); res != nil { - return *res + inviteStored, err := threepid.CheckAndProcessInvite( + device, &body, cfg, queryAPI, accountDB, producer, membership, roomID, + ) + if err == threepid.ErrMissingParameter { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON(err.Error()), + } + } else if err == threepid.ErrNotTrusted { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotTrusted(body.IDServer), + } + } else if err == events.ErrRoomNoExists { + return util.JSONResponse{ + Code: 404, + JSON: jsonerror.NotFound(err.Error()), + } + } else if err != nil { + return httputil.LogThenError(req, err) } - stateKey, reason, reqErr := getMembershipStateKey(body, device, membership) - if reqErr != nil { - return *reqErr + // If an invite has been stored on an identity server, it means that a + // m.room.third_party_invite event has been emitted and that we shouldn't + // emit a m.room.member one. + if inviteStored { + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } + } + + event, err := buildMembershipEvent( + body, accountDB, device, membership, roomID, cfg, queryAPI, + ) + if err == errMissingUserID { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON(err.Error()), + } + } else if err == events.ErrRoomNoExists { + return util.JSONResponse{ + Code: 404, + JSON: jsonerror.NotFound(err.Error()), + } + } else if err != nil { + return httputil.LogThenError(req, err) + } + + if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { + return httputil.LogThenError(req, err) + } + + return util.JSONResponse{ + Code: 200, + JSON: struct{}{}, + } +} + +func buildMembershipEvent( + body threepid.MembershipRequest, accountDB *accounts.Database, + device *authtypes.Device, membership string, roomID string, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, +) (*gomatrixserverlib.Event, error) { + stateKey, reason, err := getMembershipStateKey(body, device, membership) + if err != nil { + return nil, err } profile, err := loadProfile(stateKey, cfg, accountDB) if err != nil { - return httputil.LogThenError(req, err) + return nil, err } builder := gomatrixserverlib.EventBuilder{ @@ -80,27 +141,10 @@ func SendMembership( } if err = builder.SetContent(content); err != nil { - return httputil.LogThenError(req, err) + return nil, err } - event, err := events.BuildEvent(&builder, cfg, queryAPI, nil) - if err == events.ErrRoomNoExists { - return util.JSONResponse{ - Code: 404, - JSON: jsonerror.NotFound(err.Error()), - } - } else if err != nil { - return httputil.LogThenError(req, err) - } - - if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil { - return httputil.LogThenError(req, err) - } - - return util.JSONResponse{ - Code: 200, - JSON: struct{}{}, - } + return events.BuildEvent(&builder, cfg, queryAPI, nil) } // loadProfile lookups the profile of a given user from the database and returns @@ -130,16 +174,13 @@ func loadProfile(userID string, cfg config.Dendrite, accountDB *accounts.Databas // returns a JSONResponse with a corresponding error code and message. func getMembershipStateKey( body threepid.MembershipRequest, device *authtypes.Device, membership string, -) (stateKey string, reason string, response *util.JSONResponse) { +) (stateKey string, reason string, err error) { if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" { // If we're in this case, the state key is contained in the request body, // possibly along with a reason (for "kick" and "ban") so we need to parse // it if body.UserID == "" { - response = &util.JSONResponse{ - Code: 400, - JSON: jsonerror.BadJSON("'user_id' must be supplied."), - } + err = errMissingUserID return } diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index 8d76a03d..86234b8d 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -70,6 +70,10 @@ type Dendrite struct { // by remote servers. // Defaults to 24 hours. KeyValidityPeriod time.Duration `yaml:"key_validity_period"` + // List of domains that the server will trust as identity servers to + // verify third-party identifiers. + // Defaults to an empty array. + TrustedIDServers []string `yaml:"trusted_third_party_id_servers"` } `yaml:"matrix"` // The configuration specific to the media repostitory. @@ -273,6 +277,10 @@ func (config *Dendrite) setDefaults() { config.Matrix.KeyValidityPeriod = 24 * time.Hour } + if config.Matrix.TrustedIDServers == nil { + config.Matrix.TrustedIDServers = []string{} + } + if config.Media.MaxThumbnailGenerators == 0 { config.Media.MaxThumbnailGenerators = 10 }