Skip to content

Room version 8 and 9 support #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Nov 2, 2021
72 changes: 71 additions & 1 deletion eventauth.go
Original file line number Diff line number Diff line change
@@ -36,6 +36,8 @@ const (
Invite = "invite"
// Knock is the string constant "knock"
Knock = "knock"
// Restricted is the string constant "restricted"
Restricted = "restricted"
// NOTSPEC: Peek is the string constant "peek" (MSC2753, used as the label in the sync block)
Peek = "peek"
// Public is the string constant "public"
@@ -80,6 +82,8 @@ const (
MReceipt = "m.receipt"
// MPresence https://matrix.org/docs/spec/server_server/latest#m-presence-schema
MPresence = "m.presence"
// MRoomMembership https://github.com/matrix-org/matrix-doc/blob/clokep/restricted-rooms/proposals/3083-restricted-rooms.md
MRoomMembership = "m.room_membership"
)

// StateNeeded lists the event types and state_keys needed to authenticate an event.
@@ -165,6 +169,9 @@ type membershipContent struct {
Membership string `json:"membership"`
// We use the third_party_invite key to special case thirdparty invites.
ThirdPartyInvite *MemberThirdPartyInvite `json:"third_party_invite,omitempty"`
// The user that authorised the join, in the case that the restricted join
// rule is in effect.
AuthorizedVia string `json:"join_authorised_via_users_server,omitempty"`
}

// StateNeededForEventBuilder returns the event types and state_keys needed to authenticate the
@@ -230,6 +237,9 @@ func accumulateStateNeeded(result *StateNeeded, eventType, sender string, stateK
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L370
// * And optionally may require a m.third_party_invite event
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L393
// * If using a restricted join rule, we should also include the membership event
// of the user nominated in the `join_authorised_via_users_server` key
// https://github.com/matrix-org/matrix-doc/blob/clokep/restricted-rooms/proposals/3083-restricted-rooms.md
if content == nil {
err = errorf("missing memberContent for m.room.member event")
return
@@ -250,7 +260,9 @@ func accumulateStateNeeded(result *StateNeeded, eventType, sender string, stateK
}
result.ThirdPartyInvite = append(result.ThirdPartyInvite, token)
}

if content.AuthorizedVia != "" {
result.Member = append(result.Member, content.AuthorizedVia)
}
default:
// All other events need:
// * The membership of the sender.
@@ -967,6 +979,12 @@ func (m *membershipAllower) membershipAllowed(event *Event) error { // nolint: g
return m.membershipAllowedFromThirdPartyInvite()
}

if m.joinRule.JoinRule == Restricted {
if err := m.membershipAllowedForRestrictedJoin(); err != nil {
return errorf("Failed to process restricted join: %s", err)
}
}

if m.targetID == m.senderID {
// If the state_key and the sender are the same then this is an attempt
// by a user to update their own membership.
@@ -976,6 +994,58 @@ func (m *membershipAllower) membershipAllowed(event *Event) error { // nolint: g
return m.membershipAllowedOther()
}

func (m *membershipAllower) membershipAllowedForRestrictedJoin() error {
// Special case for restricted room joins, where we will check if the membership
// event is signed by one of the allowed servers in the join rule content.
allowsRestricted, err := m.roomVersion.AllowRestrictedJoinsInEventAuth()
if err != nil {
return err
}
if !allowsRestricted {
return fmt.Errorf("restricted joins are not supported in this room version")
}

// In the case that the user is already joined, invited or there is no
// authorised via server, we should treat the join rule as if it's invite.
if m.oldMember.Membership == Join || m.oldMember.Membership == Invite || m.newMember.AuthorisedVia == "" {
m.joinRule.JoinRule = Invite
return nil
}

// Otherwise, we have to work out if the server that produced the join was
// authorised to do so. This requires the membership event to contain a
// 'join_authorised_via_users_server' key, containing the user ID of a user
// in the room that should have a suitable power level to issue invites.
// If no such key is specified then we should reject the join.
if _, _, err := SplitID('@', m.newMember.AuthorisedVia); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's stopping servers from lying about who authorised this? Can I not just be malicious and guess that "hey Alice is probably in $secret_room, let's slap an authorised key as alice and I can get in"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the authorising server has to have signed the event as well. I assume the malicious person could enter a different user ID from the same signing server, but unless that user has the power levels to invite, I don't suppose it'd help them at all.

return fmt.Errorf("the 'join_authorised_via_users_server' contains an invalid value %q", m.newMember.AuthorisedVia)
}

// If the nominated user ID is valid then there are two things that we
// need to check. First of all, is the user joined to the room?
otherMember, err := m.provider.Member(m.newMember.AuthorisedVia)
if err != nil {
return fmt.Errorf("failed to find the membership event for 'join_authorised_via_users_server' user %q", m.newMember.AuthorisedVia)
}
otherMembership, err := otherMember.Membership()
if err != nil {
return fmt.Errorf("failed to find the membership status for 'join_authorised_via_users_server' user %q", m.newMember.AuthorisedVia)
}
if otherMembership != Join {
return fmt.Errorf("the nominated 'join_authorised_via_users_server' user %q is not joined to the room", m.newMember.AuthorisedVia)
}

// And secondly, does the user have the power to issue invites in the room?
if pl := m.powerLevels.UserLevel(m.newMember.AuthorisedVia); pl < m.powerLevels.Invite {
return fmt.Errorf("the nominated 'join_authorised_via_users_server' user %q does not have permission to invite (%d < %d)", m.newMember.AuthorisedVia, pl, m.powerLevels.Invite)
}

// At this point all of the checks have proceeded, so continue as if
// the room is a public room.
m.joinRule.JoinRule = Public
return nil
}

// membershipAllowedFronThirdPartyInvite determines if the member events is following
// up the third_party_invite event it claims.
func (m *membershipAllower) membershipAllowedFromThirdPartyInvite() error {
11 changes: 10 additions & 1 deletion eventcontent.go
Original file line number Diff line number Diff line change
@@ -123,6 +123,9 @@ type MemberContent struct {
IsDirect bool `json:"is_direct,omitempty"`
// We use the third_party_invite key to special case thirdparty invites.
ThirdPartyInvite *MemberThirdPartyInvite `json:"third_party_invite,omitempty"`
// Restricted join rules require a user with invite permission to be nominated,
// so that their membership can be included in the auth events.
AuthorisedVia string `json:"join_authorised_via_users_server,omitempty"`
}

// MemberThirdPartyInvite is the "Invite" structure defined at http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member
@@ -214,7 +217,13 @@ type HistoryVisibilityContent struct {
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-join-rules for descriptions of the fields.
type JoinRuleContent struct {
// We use the join_rule key to check whether join m.room.member events are allowed.
JoinRule string `json:"join_rule"`
JoinRule string `json:"join_rule"`
Allow []JoinRuleContentAllowRule `json:"allow,omitempty"`
}

type JoinRuleContentAllowRule struct {
Type string `json:"type"`
RoomID string `json:"room_id"`
}

// NewJoinRuleContentFromAuthEvents loads the join rule content from the join rules event in the auth event.
22 changes: 10 additions & 12 deletions eventcrypto.go
Original file line number Diff line number Diff line change
@@ -83,19 +83,17 @@ func (e *Event) VerifyEventSignatures(ctx context.Context, verifier JSONVerifier
}

// For restricted join rules, the authorising server should have signed.
/*
if restricted, err := e.roomVersion.AllowRestrictedJoinsInEventAuth(); err != nil {
return fmt.Errorf("failed to check if restricted joins allowed: %w", err)
} else if restricted && membership == Join {
if v := gjson.GetBytes(e.Content(), "join_authorised_via_users_server"); v.Exists() {
_, serverName, err = SplitID('@', v.String())
if err != nil {
return fmt.Errorf("failed to split authorised server: %w", err)
}
needed[serverName] = false
if restricted, err := e.roomVersion.AllowRestrictedJoinsInEventAuth(); err != nil {
return fmt.Errorf("failed to check if restricted joins allowed: %w", err)
} else if restricted && membership == Join {
if v := gjson.GetBytes(e.Content(), "join_authorised_via_users_server"); v.Exists() {
_, serverName, err = SplitID('@', v.String())
if err != nil {
return fmt.Errorf("failed to split authorised server: %w", err)
}
needed[serverName] = struct{}{}
}
*/
}
}

strictValidityChecking, err := e.roomVersion.StrictValidityChecking()
@@ -126,7 +124,7 @@ func (e *Event) VerifyEventSignatures(ctx context.Context, verifier JSONVerifier

for _, result := range results {
if result.Error != nil {
return fmt.Errorf("validation error: %w", err)
return result.Error
}
}

47 changes: 47 additions & 0 deletions eventversion.go
Original file line number Diff line number Diff line change
@@ -28,6 +28,8 @@ const (
RoomVersionV5 RoomVersion = "5"
RoomVersionV6 RoomVersion = "6"
RoomVersionV7 RoomVersion = "7"
RoomVersionV8 RoomVersion = "8"
RoomVersionV9 RoomVersion = "9"
)

// Event format constants.
@@ -53,6 +55,8 @@ const (
const (
RedactionAlgorithmV1 RedactionAlgorithm = iota + 1 // default algorithm
RedactionAlgorithmV2 // no special meaning for m.room.aliases
RedactionAlgorithmV3 // protects join rules 'allow' key
RedactionAlgorithmV4 // protects membership 'join_authorised_via_users_server' key
)

var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
@@ -67,6 +71,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: false,
powerLevelsIncludeNotifications: false,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV2: {
Supported: true,
@@ -79,6 +84,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: false,
powerLevelsIncludeNotifications: false,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV3: {
Supported: true,
@@ -91,6 +97,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: false,
powerLevelsIncludeNotifications: false,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV4: {
Supported: true,
@@ -103,6 +110,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: false,
powerLevelsIncludeNotifications: false,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV5: {
Supported: true,
@@ -115,6 +123,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: false,
powerLevelsIncludeNotifications: false,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV6: {
Supported: true,
@@ -127,6 +136,7 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: true,
powerLevelsIncludeNotifications: true,
allowKnockingInEventAuth: false,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV7: {
Supported: true,
@@ -139,6 +149,33 @@ var roomVersionMeta = map[RoomVersion]RoomVersionDescription{
enforceCanonicalJSON: true,
powerLevelsIncludeNotifications: true,
allowKnockingInEventAuth: true,
allowRestrictedJoinsInEventAuth: false,
},
RoomVersionV8: {
Supported: true,
Stable: false,
stateResAlgorithm: StateResV2,
eventFormat: EventFormatV2,
eventIDFormat: EventIDFormatV3,
redactionAlgorithm: RedactionAlgorithmV3,
enforceSignatureChecks: true,
enforceCanonicalJSON: true,
powerLevelsIncludeNotifications: true,
allowKnockingInEventAuth: true,
allowRestrictedJoinsInEventAuth: true,
},
RoomVersionV9: {
Supported: true,
Stable: false,
stateResAlgorithm: StateResV2,
eventFormat: EventFormatV2,
eventIDFormat: EventIDFormatV3,
redactionAlgorithm: RedactionAlgorithmV4,
enforceSignatureChecks: true,
enforceCanonicalJSON: true,
powerLevelsIncludeNotifications: true,
allowKnockingInEventAuth: true,
allowRestrictedJoinsInEventAuth: true,
},
}

@@ -193,6 +230,7 @@ type RoomVersionDescription struct {
enforceCanonicalJSON bool
powerLevelsIncludeNotifications bool
allowKnockingInEventAuth bool
allowRestrictedJoinsInEventAuth bool
Supported bool
Stable bool
}
@@ -256,6 +294,15 @@ func (v RoomVersion) AllowKnockingInEventAuth() (bool, error) {
return false, UnsupportedRoomVersionError{v}
}

// AllowRestrictedJoinsInEventAuth returns true if the given room version allows
// for memberships signed by servers in the restricted join rules.
func (v RoomVersion) AllowRestrictedJoinsInEventAuth() (bool, error) {
if r, ok := roomVersionMeta[v]; ok {
return r.allowRestrictedJoinsInEventAuth, nil
}
return false, UnsupportedRoomVersionError{v}
}

// PowerLevelsIncludeNotifications returns true if the given room version calls
// for the power level checks to cover the `notifications` key or false otherwise.
func (v RoomVersion) EnforceCanonicalJSON() (bool, error) {
20 changes: 19 additions & 1 deletion redactevent.go
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ func redactEvent(eventJSON []byte, roomVersion RoomVersion) ([]byte, error) {
// Join rules events need to keep the join_rule key.
type joinRulesContent struct {
JoinRule RawJSON `json:"join_rule,omitempty"`
Allow RawJSON `json:"allow,omitempty"`
}

// powerLevelContent keeps the fields needed in a m.room.power_levels event.
@@ -84,7 +85,8 @@ func redactEvent(eventJSON []byte, roomVersion RoomVersion) ([]byte, error) {
// Member events keep the membership.
// (In an ideal world they would keep the third_party_invite see matrix-org/synapse#1831)
type memberContent struct {
Membership RawJSON `json:"membership,omitempty"`
Membership RawJSON `json:"membership,omitempty"`
AuthorisedVia string `json:"join_authorised_via_users_server,omitempty"`
}

// aliasesContent keeps the fields needed in a m.room.aliases event.
@@ -144,8 +146,24 @@ func redactEvent(eventJSON []byte, roomVersion RoomVersion) ([]byte, error) {
newContent.createContent = event.Content.createContent
case MRoomMember:
newContent.memberContent = event.Content.memberContent
if algo, err := roomVersion.RedactionAlgorithm(); err != nil {
return nil, err
} else if algo < RedactionAlgorithmV4 {
// We only stopped redacting the 'join_authorised_via_users_server'
// key in room version 9, so if the algorithm used is from an older
// room version, we should ensure this field is redacted.
newContent.memberContent.AuthorisedVia = ""
}
case MRoomJoinRules:
newContent.joinRulesContent = event.Content.joinRulesContent
if algo, err := roomVersion.RedactionAlgorithm(); err != nil {
return nil, err
} else if algo < RedactionAlgorithmV3 {
// We only stopped redacting the 'allow' key in room version 8,
// so if the algorithm used is from an older room version, we
// should ensure this field is redacted.
newContent.joinRulesContent.Allow = nil
}
case MRoomPowerLevels:
newContent.powerLevelContent = event.Content.powerLevelContent
case MRoomHistoryVisibility:
34 changes: 34 additions & 0 deletions redactevent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gomatrixserverlib

import (
"bytes"
"testing"
)

func TestRedactionAlgorithmV4(t *testing.T) {
// Specifically, the version 4 redaction algorithm used in room
// version 9 is ensuring that the `join_authorised_via_users_server`
// key doesn't get redacted.

input := []byte(`{"content":{"avatar_url":"mxc://something/somewhere","displayname":"Someone","join_authorised_via_users_server":"@someoneelse:somewhere.org","membership":"join"},"origin_server_ts":1633108629915,"sender":"@someone:somewhere.org","state_key":"@someone:somewhere.org","type":"m.room.member","unsigned":{"age":539338},"room_id":"!someroom:matrix.org"}`)
expectedv8 := []byte(`{"sender":"@someone:somewhere.org","room_id":"!someroom:matrix.org","content":{"membership":"join"},"type":"m.room.member","state_key":"@someone:somewhere.org","origin_server_ts":1633108629915}`)
expectedv9 := []byte(`{"sender":"@someone:somewhere.org","room_id":"!someroom:matrix.org","content":{"membership":"join","join_authorised_via_users_server":"@someoneelse:somewhere.org"},"type":"m.room.member","state_key":"@someone:somewhere.org","origin_server_ts":1633108629915}`)

redactedv8, err := redactEvent(input, RoomVersionV8)
if err != nil {
t.Fatal(err)
}

redactedv9, err := redactEvent(input, RoomVersionV9)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(redactedv8, expectedv8) {
t.Fatalf("room version 8 redaction produced unexpected result\nexpected: %s\ngot: %s", string(expectedv8), string(redactedv8))
}

if !bytes.Equal(redactedv9, expectedv9) {
t.Fatalf("room version 9 redaction produced unexpected result\nexpected: %s\ngot: %s", string(expectedv9), string(redactedv9))
}
}