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>
140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
// Package id validates the Matrix identifiers that appear inside events.
|
|
//
|
|
// The checks here are deliberately laxer than the ones in userapi, which
|
|
// guard account creation on this server and enforce the current, strict
|
|
// localpart grammar. Events are different: a room can contain user IDs
|
|
// minted years ago by servers that allowed upper case and other characters
|
|
// the grammar has since dropped, and the spec requires receivers to accept
|
|
// them. Rejecting them would mean refusing to authorise perfectly valid
|
|
// history, so the rules below are the "historical" ones — a sigil, a
|
|
// non-empty localpart, a colon, a server name, and a length limit.
|
|
package id
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// MaxLength is the spec's cap on a user ID, room ID, alias or event ID,
|
|
// sigil and server name included.
|
|
const MaxLength = 255
|
|
|
|
// ErrInvalidID is the root of every error this package returns.
|
|
var ErrInvalidID = errors.New("invalid Matrix identifier")
|
|
|
|
// ValidateUserID checks a user ID against the historical grammar:
|
|
// "@localpart:server_name", at most 255 bytes, with no NUL anywhere.
|
|
//
|
|
// It is the check behind the authorisation rules that call for a "valid
|
|
// user ID", such as the keys of m.room.power_levels users.
|
|
func ValidateUserID(s string) error {
|
|
_, _, err := SplitUserID(s)
|
|
return err
|
|
}
|
|
|
|
// SplitUserID splits a user ID into its localpart and server name, without
|
|
// the sigil or the colon.
|
|
func SplitUserID(s string) (localpart, serverName string, err error) {
|
|
if err := checkCommon("user ID", s, '@'); err != nil {
|
|
return "", "", err
|
|
}
|
|
localpart, serverName, ok := strings.Cut(s[1:], ":")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("%w: user ID %q has no server name; expected @localpart:server", ErrInvalidID, s)
|
|
}
|
|
if localpart == "" {
|
|
return "", "", fmt.Errorf("%w: user ID %q has an empty localpart", ErrInvalidID, s)
|
|
}
|
|
if serverName == "" {
|
|
return "", "", fmt.Errorf("%w: user ID %q has an empty server name", ErrInvalidID, s)
|
|
}
|
|
return localpart, serverName, nil
|
|
}
|
|
|
|
// ServerNameOf returns the server name from a user ID.
|
|
func ServerNameOf(userID string) (string, error) {
|
|
_, serverName, err := SplitUserID(userID)
|
|
return serverName, err
|
|
}
|
|
|
|
// SameServer reports whether two user IDs belong to the same homeserver.
|
|
//
|
|
// Server names are compared byte for byte: the spec says they are
|
|
// case-sensitive, and treating "Example.org" and "example.org" as one
|
|
// server here would quietly widen rules that turn on a domain match, such
|
|
// as the m.federate check and the right to redact another user's event.
|
|
func SameServer(userIDA, userIDB string) bool {
|
|
a, errA := ServerNameOf(userIDA)
|
|
b, errB := ServerNameOf(userIDB)
|
|
if errA != nil || errB != nil {
|
|
return false
|
|
}
|
|
return a == b
|
|
}
|
|
|
|
// ValidateRoomID checks a room ID: a '!' sigil and at most 255 bytes.
|
|
//
|
|
// Whether the rest of it is "opaque:server" or the hash of the create
|
|
// event depends on the room version, and in the hashed form there is
|
|
// nothing left to check beyond the sigil and the length — so the
|
|
// version-specific part of the check lives with the authorisation rules,
|
|
// which have the create event to compare against.
|
|
func ValidateRoomID(s string) error {
|
|
if err := checkCommon("room ID", s, '!'); err != nil {
|
|
return err
|
|
}
|
|
if len(s) == 1 {
|
|
return fmt.Errorf("%w: room ID %q is only a sigil", ErrInvalidID, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServerNameOfRoomID returns the server name from a room ID of the
|
|
// "!opaque:server" form used up to room version 11.
|
|
//
|
|
// It fails for a room version 12 room ID, which is a hash of the create
|
|
// event and names no server at all — the caller should only ask in the
|
|
// versions where the answer exists.
|
|
func ServerNameOfRoomID(roomID string) (string, error) {
|
|
if err := ValidateRoomID(roomID); err != nil {
|
|
return "", err
|
|
}
|
|
_, serverName, ok := strings.Cut(roomID[1:], ":")
|
|
if !ok || serverName == "" {
|
|
return "", fmt.Errorf("%w: room ID %q names no server", ErrInvalidID, roomID)
|
|
}
|
|
return serverName, nil
|
|
}
|
|
|
|
// ValidateEventID checks an event ID: a '$' sigil and at most 255 bytes.
|
|
func ValidateEventID(s string) error {
|
|
if err := checkCommon("event ID", s, '$'); err != nil {
|
|
return err
|
|
}
|
|
if len(s) == 1 {
|
|
return fmt.Errorf("%w: event ID %q is only a sigil", ErrInvalidID, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkCommon applies the three rules every identifier shares.
|
|
func checkCommon(kind, s string, sigil byte) error {
|
|
if s == "" {
|
|
return fmt.Errorf("%w: %s must not be empty", ErrInvalidID, kind)
|
|
}
|
|
if s[0] != sigil {
|
|
return fmt.Errorf("%w: %s %q does not start with %q", ErrInvalidID, kind, s, string(sigil))
|
|
}
|
|
if len(s) > MaxLength {
|
|
return fmt.Errorf("%w: %s is %d bytes, the limit is %d", ErrInvalidID, kind, len(s), MaxLength)
|
|
}
|
|
// A NUL would terminate the string in most of the software that ever
|
|
// sees it, which makes two identifiers that differ after it look the
|
|
// same to some servers and not to others.
|
|
if strings.IndexByte(s, 0) >= 0 {
|
|
return fmt.Errorf("%w: %s %q contains a NUL byte", ErrInvalidID, kind, s)
|
|
}
|
|
return nil
|
|
}
|