Files
Warp/server/internal/matrix/auth/auth.go
T
tajniak81andClaude Opus 5.5 2fbd5f53ce
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 2: the event core
Implements PLAN.md Phase 2: the library every later phase is built on,
as pure code under server/internal/matrix with no HTTP and no storage.

canonicaljson: the canonical JSON encoding, hand-written because
encoding/json escapes <, > and & and accepts floats. Integers only, in
[-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned
into U+FFFD.

signing: the Signing JSON algorithm alongside the existing key handling,
and one lenient base64 decoder shared by the packages above it.

id: identifier grammars as they appear inside events, deliberately laxer
than userapi's account rules because rooms carry historical user IDs.

roomversion: the registry. Versions 3 to 12 are supported, each written
out in full as a Rules value with the flags the other packages read.
Versions 1 and 2 are known but refused: their event format and state
resolution v1 wait for federation. createRoom will default to 11, as the
spec asks for a while after 1.16, with 12 available.

event: PDU parsing and validation, content and reference hashes, event
IDs, the redaction algorithm for every supported version, signing and
verification, and a Builder.

auth: the authorisation rules in spec order, power levels with their
defaults, v12 creators with infinite power, the auth events selection
algorithm, rule 3's auth_events checks, and the separate test for
whether a redaction applies. Comments use the room version 12 rule
numbering, with the version 11 numbers given where they differ.

stateres: state resolution v2, and version 12's amendment (an empty
starting state and the conflicted state subgraph).

Tested against the spec's own vectors byte for byte: the canonical JSON
examples, both JSON signing vectors and both event signing vectors.
State resolution ports scenarios from Synapse's suite. canonicaljson
and event.Parse have fuzz targets.

The capabilities endpoint now advertises the registry instead of a
hard-coded "11". docs/event-core.md describes the packages and the
known gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:23:35 +02:00

560 lines
19 KiB
Go

// 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
}