// Package auth implements the Matrix authorisation rules: given an event // and the room state it refers to, may that event be in the room? // // This is the part of a homeserver that must not be improvised. Every // server in a room runs these rules over the same events and has to reach // the same verdict, or the room's history forks. The rules are therefore // written out here in the order and the shape the spec gives them, with // the room version differences driven by the roomversion registry rather // than by guesswork, and the comments say which rule is which so that the // code can be read against the spec side by side. // // # Rule numbers // // "Rule N" in these comments means the numbering on the room version 12 // page, the most recent one. Version 12 inserted a new rule 2 (the room ID // is the create event's ID), so on the pages for versions 6 to 11 each rule // after the first is one lower: rule 5 here is rule 4 there, rule 10 here // is rule 9 there. Versions 1 to 5 happen to line up with version 12 again // from rule 5 on, because their rule 4 was the m.room.aliases rule that // version 6 removed. Where a sub-rule exists in only some versions, or is // numbered differently beyond that shift, the comment says so. // // Nothing here touches storage or the network. The one exception is the // restricted join rule, which needs somebody else's signature verified; // that comes in through the Verifier interface. // // What this package does *not* decide: // // - Whether a redaction should be applied. The rules allow nearly every // m.room.redaction event; whether the redaction takes effect is a // separate test (ShouldApplyRedaction) the roomserver runs once it has // both events. // - Soft failure. An event can be allowed by the rules against its own // auth events and still be refused a place in the room's forward // extremities because it fails against the *current* state. That is // the roomserver's business (PLAN.md phase 3). package auth import ( "context" "encoding/json" "errors" "fmt" "slices" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // The join rules a room may have. const ( JoinRulePublic = "public" JoinRuleInvite = "invite" JoinRuleKnock = "knock" JoinRuleRestricted = "restricted" JoinRuleKnockRestricted = "knock_restricted" JoinRulePrivate = "private" ) // ErrRejected means the authorisation rules refused the event. Every // rejection from Check wraps it, and the message says which rule said no. var ErrRejected = errors.New("rejected by the authorisation rules") // ErrNoVerifier means a decision needed somebody else's signature checked // and no Verifier was supplied. It is kept separate from ErrRejected // because the event is not known to be bad — we are simply not equipped to // judge it, and retrying with a verifier may well allow it. var ErrNoVerifier = errors.New("cannot verify the signature this rule needs") // Verifier checks that an event carries a valid signature from a server. // // Only one rule needs this: a join under the restricted join rule is // authorised by a third party, named in // content.join_authorised_via_users_server, and the event must be signed // by *that user's* server rather than only by the joining user's. Verifying // it may mean fetching another server's keys, which is why it is an // interface here instead of a function. type Verifier interface { VerifyEventSignature(ctx context.Context, serverName string, e *event.Event) error } // reject builds a rejection error. func reject(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrRejected, fmt.Sprintf(format, args...)) } // Check applies the authorisation rules to an event. // // st is the room state the event is judged against: for an event arriving // from a client or over federation that is the state its auth_events name, // and during state resolution it is the state resolved so far. A nil error // means the event is allowed. // // verifier may be nil. It is needed only for a join authorised by another // user under the restricted join rule; without one, such a join fails with // ErrNoVerifier rather than being waved through. func Check(ctx context.Context, e *event.Event, st State, verifier Verifier) error { rules := e.Rules() // Rule 1: the create event answers to nothing but itself. if e.Type() == event.TypeCreate { return checkCreate(e) } create := st.Create() if create == nil { // Not one of the numbered rules but a precondition for all of them: // without the create event we do not know the room's creators, and // from room version 12 we do not even know its ID. (The numbered // rule about a missing create event, 2.4 on the version 11 page, // judges the declared auth_events and lives in CheckAuthEvents.) return reject("the room state has no m.room.create event") } // Rule 2 (room version 12+): the room ID is the create event's ID, so // an event claiming a different room is not in this room. if rules.RoomIDIsCreateEventID { roomID, err := event.RoomIDOfCreateEvent(create) if err != nil { return err } if e.RoomID() != roomID { return reject("room_id %q is not the ID of this room's m.room.create event (%q)", e.RoomID(), roomID) } } // Rule 4: a room created with m.federate false never leaves the // creator's server. if err := checkFederate(e, create); err != nil { return err } pl, err := ResolvePowerLevels(st.PowerLevels(), create, rules) if err != nil { return err } // The m.room.aliases rule, rule 4 on the pages for room versions 1 to // 5 and absent from version 6 on: every server managed its own alias // list, so an aliases event needed only to match its sender's domain. // It was dropped because it let any server in the room edit state. if rules.AliasesEventAuth && e.Type() == event.TypeAliases { return checkAliases(e) } // Rule 5: membership changes have their own rules, and are the only // thing a user who is not in the room may send. if e.Type() == event.TypeMember { return checkMember(ctx, e, st, create, pl, verifier) } // Rule 6: everything else requires the sender to be in the room. if m := st.Membership(e.Sender()); m != event.MembershipJoin { return reject("the sender is not joined to the room (membership is %q)", m) } senderLevel := pl.UserLevel(e.Sender()) // Rule 7: an invite to a third party needs the power to invite. if e.Type() == event.TypeThirdPartyInvite { if senderLevel < pl.Invite { return reject("the sender's power level %s is below the invite level %d", levelString(senderLevel), pl.Invite) } return nil } // Rule 8: the sender must be allowed to send this type of event. if required := pl.SendLevel(e.Type(), e.IsState()); senderLevel < required { return reject("the sender's power level %s is below the %d needed to send %s", levelString(senderLevel), required, e.Type()) } // Rule 9: state keyed by a user ID belongs to that user. It is what // stops one member rewriting another's per-user state. if sk, ok := e.StateKey(); ok && len(sk) > 0 && sk[0] == '@' && sk != e.Sender() { return reject("the state_key %q is a user ID that is not the sender", sk) } // Rule 10: a power levels event may not grant or revoke more than the // sender holds. if e.Type() == event.TypePowerLevels { return checkPowerLevels(e, st.PowerLevels(), create, senderLevel) } // Rule 11: otherwise, allow. return nil } // checkCreate applies rule 1, the rules for the event that starts a room. func checkCreate(e *event.Event) error { rules := e.Rules() // Rule 1.1: a create event is the root of the DAG. if len(e.PrevEventIDs()) > 0 { return reject("an m.room.create event must have no prev_events") } // Rule 1.2. if rules.RoomIDIsCreateEventID { if e.RoomID() != "" { return reject("an m.room.create event in a version %s room must not have a room_id", rules.Version) } } else { roomServer, err := id.ServerNameOfRoomID(e.RoomID()) if err != nil { return reject("room_id: %v", err) } senderServer, err := e.SenderServer() if err != nil { return reject("sender: %v", err) } if roomServer != senderServer { return reject("the room_id domain %q does not match the sender's domain %q", roomServer, senderServer) } } var content struct { RoomVersion *string `json:"room_version"` Creator *string `json:"creator"` Additional *json.RawMessage `json:"additional_creators"` } if err := json.Unmarshal(e.Content(), &content); err != nil { return reject("m.room.create content: %v", err) } // Rule 1.3: a room version we do not implement is one we must not // pretend to be in. if content.RoomVersion != nil { if !roomversion.IsSupported(roomversion.Version(*content.RoomVersion)) { return reject("room version %q is not one this server recognises", *content.RoomVersion) } } // Rule 1.4 (room version 12+): additional creators must be user IDs. // Creators cannot be changed later, so a malformed entry here would be // permanent. if rules.AdditionalCreators && content.Additional != nil { var additional []string if err := json.Unmarshal(*content.Additional, &additional); err != nil { return reject("m.room.create additional_creators must be an array of strings: %v", err) } for _, userID := range additional { if err := id.ValidateUserID(userID); err != nil { return reject("m.room.create additional_creators: %v", err) } } } // Room versions up to 10 required the creator property, which only // ever repeated the sender. if rules.CreateEventRequiresCreator && content.Creator == nil { return reject("an m.room.create event in a version %s room must have a creator", rules.Version) } return nil } // checkFederate applies rule 4: a room whose create event says // m.federate false accepts events only from the creator's own server. func checkFederate(e *event.Event, create *event.Event) error { var content struct { Federate *bool `json:"m.federate"` } if err := json.Unmarshal(create.Content(), &content); err != nil { return reject("m.room.create content: %v", err) } if content.Federate == nil || *content.Federate { return nil } senderServer, err := e.SenderServer() if err != nil { return reject("sender: %v", err) } createServer, err := create.SenderServer() if err != nil { return reject("the create event's sender: %v", err) } if senderServer != createServer { return reject("the room does not federate and %q is not %q", senderServer, createServer) } return nil } // checkAliases applies the room version 1 to 5 rule for m.room.aliases. func checkAliases(e *event.Event) error { sk, ok := e.StateKey() if !ok { return reject("an m.room.aliases event must have a state_key") } senderServer, err := e.SenderServer() if err != nil { return reject("sender: %v", err) } if sk != senderServer { return reject("the state_key %q is not the sender's server %q", sk, senderServer) } return nil } // checkPowerLevels applies rule 10: the content is well-formed, and no // level is moved past the sender's own. // // The asymmetry in the rules is deliberate. You may lower a level you // could have reached yourself, and you may raise one up to your own level, // but you may not touch anybody whose level equals yours — otherwise two // administrators could demote each other, and whoever's event won state // resolution would end up alone in charge. func checkPowerLevels(e, old, create *event.Event, senderLevel int64) error { rules := e.Rules() // Rules 10.1 to 10.3: parsing the content is the check. ResolvePowerLevels // rejects a level that is not an integer, an events or notifications // map whose values are not integers, and a users key that is not a // user ID. if _, err := ResolvePowerLevels(e, create, rules); err != nil { return err } newContent, err := levelContent(e) if err != nil { return err } // Rule 10.4 (room version 12+): creators have infinite power, so // naming one here could only ever be an attempt to take it away. // // Being new, this sub-rule shifts the ones after it: on the version 11 // page both halves of their numbers are one lower, so 10.5 here is 9.4 // there and 10.10 here is 9.9. if rules.CreatorsHaveInfinitePower { creators, err := Creators(create) if err != nil { return err } for _, creator := range creators { if _, ok := newContent.sub("users")[creator]; ok { return reject("m.room.power_levels must not give a level to the room creator %q", creator) } } } // Rule 10.5: the first power levels event in a room is always allowed. // Until it exists the creator is the only person with any power, so // there is nobody to protect anything from. if old == nil { return nil } oldContent, err := levelContent(old) if err != nil { return err } // Rule 10.6: the named levels. for _, name := range []string{ "users_default", "events_default", "state_default", "ban", "redact", "kick", "invite", } { if err := checkLevelChange(name, "", oldContent.top, newContent.top, senderLevel, false); err != nil { return err } } // Rules 10.7 and 10.8: the events and notifications maps. for _, section := range []string{"events", "notifications"} { oldSection, newSection := oldContent.sub(section), newContent.sub(section) for _, key := range unionKeys(oldSection, newSection) { if err := checkLevelChange(key, section, oldSection, newSection, senderLevel, false); err != nil { return err } } } // Rules 10.9 and 10.10: the users map, where an entry other than the // sender's own is protected at the sender's own level rather than // above it. oldUsers, newUsers := oldContent.sub("users"), newContent.sub("users") for _, userID := range unionKeys(oldUsers, newUsers) { if err := checkLevelChange(userID, "users", oldUsers, newUsers, senderLevel, userID != e.Sender()); err != nil { return err } } return nil } // checkLevelChange compares one level before and after. // // A level that is absent on one side is not compared on that side: the // rules speak of "the current value" and "the new value", and where there // is no value there is nothing to check. func checkLevelChange(key, section string, old, new map[string]int64, senderLevel int64, protectPeers bool) error { oldLevel, hadOld := old[key] newLevel, hasNew := new[key] if hadOld && hasNew && oldLevel == newLevel { return nil } where := key if section != "" { where = section + "." + key } // The peer rule: you may not change or remove the entry of somebody at // your own level. if protectPeers && hadOld && oldLevel == senderLevel { return reject("%s is at the sender's own power level %s and may not be changed by them", where, levelString(senderLevel)) } if hadOld && oldLevel > senderLevel { return reject("%s is %d, above the sender's power level %s", where, oldLevel, levelString(senderLevel)) } if hasNew && newLevel > senderLevel { return reject("%s would become %d, above the sender's power level %s", where, newLevel, levelString(senderLevel)) } return nil } // levels holds a power levels event's content as plain integers, keeping // track of which keys were actually present. type levels struct { top map[string]int64 subs map[string]map[string]int64 } func (l levels) sub(name string) map[string]int64 { if m, ok := l.subs[name]; ok { return m } return map[string]int64{} } // levelContent reads a power levels event's content for comparison. // // It parses leniently on purpose: by the time this runs, the new event has // already been through ResolvePowerLevels, and the old one was accepted // into the room's state under whatever rules applied then. A value we // cannot read is left out rather than treated as zero, which would invent // a level nobody set. func levelContent(e *event.Event) (levels, error) { out := levels{top: map[string]int64{}, subs: map[string]map[string]int64{}} var obj map[string]json.RawMessage if err := json.Unmarshal(e.Content(), &obj); err != nil { return out, reject("m.room.power_levels content: %v", err) } rules := e.Rules() for _, name := range []string{ "users_default", "events_default", "state_default", "ban", "redact", "kick", "invite", } { if raw, ok := obj[name]; ok { if level, err := parseLevel(raw, rules); err == nil { out.top[name] = level } } } for _, name := range []string{"events", "notifications", "users"} { raw, ok := obj[name] if !ok { continue } if levels, err := parseLevelMap(raw, rules); err == nil { out.subs[name] = levels } } return out, nil } // unionKeys returns every key in either map, in a stable order so that the // first rejection reported for a given pair of events is always the same // one — a rule that rejects differently on different servers is as bad as // one that rejects differently from the spec. func unionKeys(a, b map[string]int64) []string { seen := make(map[string]bool, len(a)+len(b)) keys := make([]string, 0, len(a)+len(b)) for _, m := range []map[string]int64{a, b} { for k := range m { if !seen[k] { seen[k] = true keys = append(keys, k) } } } slices.Sort(keys) return keys } // levelString renders a power level for an error message, spelling out the // one value that is not a number anybody wrote down. func levelString(level int64) string { if level == InfinitePowerLevel { return "infinite (room creator)" } return fmt.Sprintf("%d", level) } // joinRuleOf reads the join rule from an m.room.join_rules event. func joinRuleOf(e *event.Event) (string, error) { var content struct { JoinRule string `json:"join_rule"` } if err := json.Unmarshal(e.Content(), &content); err != nil { return "", reject("m.room.join_rules content: %v", err) } return content.JoinRule, nil } // ShouldApplyRedaction reports whether a redaction event takes effect on // the event it targets. // // The authorisation rules let almost any redaction event into the room; // this is the separate test the spec puts under "Handling redactions", // which a server can only run once it holds both events. Either the sender // has the redact level, or the redaction comes from the same server as the // event it redacts — a server may always take back its own users' events. // // Note that the power level test passes even when the sender could not have // sent the event they are redacting. Moderation is not the same permission // as authorship. func ShouldApplyRedaction(redaction, target *event.Event, pl *PowerLevels) bool { if pl.UserLevel(redaction.Sender()) >= pl.Redact { return true } return id.SameServer(redaction.Sender(), target.Sender()) } // VerifyThirdPartyInviteSignature reports whether any signature on a // third-party invite's signed block was made by one of the public keys the // m.room.third_party_invite event published. // // The keys travel in the event itself, so unlike the restricted join rule // this needs nothing from outside: the identity server's key was recorded // when the invite was issued. func VerifyThirdPartyInviteSignature(signed json.RawMessage, inviteEvent *event.Event) bool { keys, err := thirdPartyInviteKeys(inviteEvent) if err != nil || len(keys) == 0 { return false } sigs, err := signing.ExtractSignatures(signed) if err != nil { return false } for serverName, byKeyID := range sigs { for keyID := range byKeyID { if algorithm, _, ok := signing.SplitKeyID(keyID); !ok || algorithm != signing.KeyIDPrefix { continue } for _, pub := range keys { if err := signing.VerifyJSON(serverName, keyID, pub, signed); err == nil { return true } } } } return false }