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
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>
202 lines
7.0 KiB
Go
202 lines
7.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
|
|
)
|
|
|
|
// AuthEventKeys returns the state an event must cite in its auth_events,
|
|
// per the auth events selection algorithm in the server specification.
|
|
//
|
|
// The list is what makes an event self-contained: a server receiving it
|
|
// years later, with no idea what the room looks like now, can fetch
|
|
// exactly these events and re-run the rules. That is also why the set is
|
|
// as small as it is — it is the state the rules read, and nothing else.
|
|
func AuthEventKeys(e *event.Event) ([]StateKey, error) {
|
|
stateKey := (*string)(nil)
|
|
if sk, ok := e.StateKey(); ok {
|
|
stateKey = &sk
|
|
}
|
|
return AuthEventKeysFor(e.Rules(), e.Type(), e.Sender(), stateKey, e.Content())
|
|
}
|
|
|
|
// AuthEventKeysFor is AuthEventKeys for an event that does not exist yet,
|
|
// so that the roomserver can look up the state it needs before building it.
|
|
func AuthEventKeysFor(rules roomversion.Rules, eventType, sender string, stateKey *string, content json.RawMessage) ([]StateKey, error) {
|
|
// The create event starts the room; there is nothing for it to cite.
|
|
if eventType == event.TypeCreate {
|
|
return nil, nil
|
|
}
|
|
|
|
var keys []StateKey
|
|
// From room version 12 the create event is left out: the room ID *is*
|
|
// its event ID, so every event already refers to it.
|
|
if !rules.CreateEventExcludedFromAuthEvents {
|
|
keys = append(keys, StateKey{Type: event.TypeCreate})
|
|
}
|
|
keys = append(keys,
|
|
StateKey{Type: event.TypePowerLevels},
|
|
StateKey{Type: event.TypeMember, StateKey: sender},
|
|
)
|
|
|
|
if eventType != event.TypeMember {
|
|
return keys, nil
|
|
}
|
|
if stateKey == nil {
|
|
return nil, reject("an m.room.member event must have a state_key")
|
|
}
|
|
keys = append(keys, StateKey{Type: event.TypeMember, StateKey: *stateKey})
|
|
|
|
var parsed memberContent
|
|
if err := json.Unmarshal(content, &parsed); err != nil {
|
|
return nil, reject("m.room.member content: %v", err)
|
|
}
|
|
|
|
switch parsed.Membership {
|
|
case event.MembershipJoin, event.MembershipInvite, event.MembershipKnock:
|
|
keys = append(keys, StateKey{Type: event.TypeJoinRules})
|
|
}
|
|
|
|
if parsed.Membership == event.MembershipInvite && parsed.ThirdPartyInvite != nil {
|
|
var signed struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.Unmarshal(parsed.ThirdPartyInvite.Signed, &signed); err == nil && signed.Token != "" {
|
|
keys = append(keys, StateKey{Type: event.TypeThirdPartyInvite, StateKey: signed.Token})
|
|
}
|
|
}
|
|
if parsed.Membership == event.MembershipJoin &&
|
|
parsed.JoinAuthorisedViaUsersServer != "" &&
|
|
rules.RestrictedJoinRule {
|
|
keys = append(keys, StateKey{Type: event.TypeMember, StateKey: parsed.JoinAuthorisedViaUsersServer})
|
|
}
|
|
return dedupe(keys), nil
|
|
}
|
|
|
|
// CheckAuthEvents applies rule 3 (rule 2 on the version 11 page): the
|
|
// auth_events an event declares are well-formed and are the ones the
|
|
// selection algorithm asks for.
|
|
//
|
|
// authEvents are the events the IDs in e.AuthEventIDs() resolve to, with
|
|
// any that this server rejected already left out — rule 3.3 asks whether
|
|
// an auth event was itself rejected, which only storage can answer, so
|
|
// leaving it out of the slice is how the caller reports it. A missing
|
|
// entry is then caught here as a citation that does not resolve.
|
|
func CheckAuthEvents(e *event.Event, authEvents []*event.Event) error {
|
|
if e.Type() == event.TypeCreate {
|
|
if len(e.AuthEventIDs()) > 0 {
|
|
return reject("an m.room.create event must have no auth_events")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
allowed, err := AuthEventKeys(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allowedSet := make(map[StateKey]bool, len(allowed))
|
|
for _, k := range allowed {
|
|
allowedSet[k] = true
|
|
}
|
|
|
|
seen := make(map[StateKey]bool, len(authEvents))
|
|
hasCreate := false
|
|
resolved := 0
|
|
for _, auth := range authEvents {
|
|
if auth == nil {
|
|
// A caller builds this slice from storage lookups, so a nil is
|
|
// a citation that resolved to nothing. Counting it as missing
|
|
// is both the right answer and cheaper than trusting every
|
|
// caller to filter their own misses.
|
|
continue
|
|
}
|
|
resolved++
|
|
|
|
sk, ok := auth.StateKey()
|
|
if !ok {
|
|
return reject("auth_events contains %s, which is not a state event", auth.EventID())
|
|
}
|
|
key := StateKey{Type: auth.Type(), StateKey: sk}
|
|
|
|
// Rule 3.1: two events for the same piece of state leave it
|
|
// ambiguous which one authorised this event.
|
|
if seen[key] {
|
|
return reject("auth_events has two entries for (%s, %q)", key.Type, key.StateKey)
|
|
}
|
|
seen[key] = true
|
|
|
|
// Rule 3.2.
|
|
if !allowedSet[key] {
|
|
return reject("auth_events contains (%s, %q), which the auth events selection algorithm does not ask for",
|
|
key.Type, key.StateKey)
|
|
}
|
|
|
|
// Rule 3.4 (2.5 on the version 11 page): an auth event from
|
|
// another room proves nothing about this one.
|
|
if auth.RoomID() != e.RoomID() {
|
|
return reject("the auth event %s belongs to room %q, not %q", auth.EventID(), auth.RoomID(), e.RoomID())
|
|
}
|
|
if key.Type == event.TypeCreate {
|
|
hasCreate = true
|
|
}
|
|
}
|
|
|
|
// Rule 2.4 on the version 11 page; version 12 has no such sub-rule,
|
|
// because the create event is not cited at all and its new rule 2 —
|
|
// the room ID is the create event's ID — carries the same guarantee.
|
|
if !e.Rules().CreateEventExcludedFromAuthEvents && !hasCreate {
|
|
return reject("auth_events has no m.room.create event")
|
|
}
|
|
|
|
// Rule 3.3 is the caller's to answer, and this is where their answer
|
|
// lands: an ID that resolved to nothing was either never seen or was
|
|
// itself rejected, and either way it authorises nothing. The check is
|
|
// last so that a specific fault above is reported as itself.
|
|
if resolved < len(e.AuthEventIDs()) {
|
|
return reject("%d of the %d auth_events are missing or were themselves rejected",
|
|
len(e.AuthEventIDs())-resolved, len(e.AuthEventIDs()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SelectAuthEvents picks the events from a room's state that belong in an
|
|
// event's auth_events, ready to be turned into a list of event IDs.
|
|
func SelectAuthEvents(rules roomversion.Rules, eventType, sender string, stateKey *string, content json.RawMessage, st State) ([]*event.Event, error) {
|
|
keys, err := AuthEventKeysFor(rules, eventType, sender, stateKey, content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]*event.Event, 0, len(keys))
|
|
for _, k := range keys {
|
|
if e := st[k]; e != nil {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
if len(out) > event.MaxAuthEvents {
|
|
// The selection algorithm cannot produce this many, so reaching it
|
|
// means the algorithm and the limit have drifted apart.
|
|
return nil, fmt.Errorf("auth events selection produced %d events, more than the %d an event may carry",
|
|
len(out), event.MaxAuthEvents)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// dedupe removes repeated keys, which happens when a user invites or
|
|
// authorises themselves: the sender's member event and the target's are
|
|
// then the same piece of state.
|
|
func dedupe(keys []StateKey) []StateKey {
|
|
seen := make(map[StateKey]bool, len(keys))
|
|
out := make([]StateKey, 0, len(keys))
|
|
for _, k := range keys {
|
|
if seen[k] {
|
|
continue
|
|
}
|
|
seen[k] = true
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|