Skip to content

Commit

Permalink
Handle pseudo identity room federated joins (#397)
Browse files Browse the repository at this point in the history
Feels a little bit hacky, but allows federated joins for
`org.matrix.msc4014` rooms.
  • Loading branch information
S7evinK authored Jun 28, 2023
1 parent 90ad5fa commit f6e3c7f
Show file tree
Hide file tree
Showing 11 changed files with 578 additions and 60 deletions.
2 changes: 1 addition & 1 deletion eventcontent.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type CreateContent struct {
// The version of the room. Should be treated as "1" when the key doesn't exist.
RoomVersion *RoomVersion `json:"room_version,omitempty"`
// The predecessor of the room.
Predecessor PreviousRoom `json:"predecessor,omitempty"`
Predecessor *PreviousRoom `json:"predecessor,omitempty"`
}

// PreviousRoom is the "Previous Room" structure defined at https://matrix.org/docs/spec/client_server/r0.5.0#m-room-create
Expand Down
10 changes: 9 additions & 1 deletion eventcrypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func VerifyEventSignatures(ctx context.Context, e PDU, verifier JSONVerifier, us
}

// Validate the MXIDMapping is signed correctly
if verImpl.Version() == RoomVersionPseudoIDs {
if verImpl.Version() == RoomVersionPseudoIDs && membership == spec.Join {
err = validateMXIDMappingSignature(ctx, e, verifier, verImpl)
if err != nil {
return err
Expand Down Expand Up @@ -132,6 +132,12 @@ func VerifyEventSignatures(ctx context.Context, e PDU, verifier JSONVerifier, us
toVerify = append(toVerify, v)
}

if verImpl.Version() == RoomVersionPseudoIDs {
// we already verified the mxid_mapping at this stage, so replace the KeyRing verifier
// with the self verifier to validate pseudoID events
verifier = JSONVerifierSelf{}
}

results, err := verifier.VerifyJSONs(ctx, toVerify)
if err != nil {
return fmt.Errorf("failed to verify JSONs: %w", err)
Expand All @@ -154,6 +160,7 @@ func validateMXIDMappingSignature(ctx context.Context, e PDU, verifier JSONVerif
return err
}

// if there is no mapping, we can't check the signature
if content.MXIDMapping == nil {
return fmt.Errorf("missing mxid_mapping, unable to validate event")
}
Expand All @@ -174,6 +181,7 @@ func validateMXIDMappingSignature(ctx context.Context, e PDU, verifier JSONVerif
toVerify = append(toVerify, v)
}

// check that the mapping is correctly signed by the server
results, err := verifier.VerifyJSONs(ctx, toVerify)
if err != nil {
return fmt.Errorf("failed to verify MXIDMapping: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion handleinvite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestHandleInvite(t *testing.T) {
verifier := &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}}

stateKey := userID.String()
eb := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`))
eb := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`))
inviteEvent, err := eb.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

Expand Down
54 changes: 42 additions & 12 deletions handlejoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
)

type HandleMakeJoinInput struct {
Expand Down Expand Up @@ -291,25 +292,27 @@ func checkRestrictedJoin(
}

type HandleSendJoinInput struct {
Context context.Context
RoomID spec.RoomID
EventID string
JoinEvent spec.RawJSON
RoomVersion RoomVersion // The room version for the room being joined
RequestOrigin spec.ServerName // The server that sent the /make_join federation request
LocalServerName spec.ServerName // The name of this local server
KeyID KeyID
PrivateKey ed25519.PrivateKey
Verifier JSONVerifier
MembershipQuerier MembershipQuerier
UserIDQuerier spec.UserIDForSender // Provides userIDs given a senderID
Context context.Context
RoomID spec.RoomID
EventID string
JoinEvent spec.RawJSON
RoomVersion RoomVersion // The room version for the room being joined
RequestOrigin spec.ServerName // The server that sent the /make_join federation request
LocalServerName spec.ServerName // The name of this local server
KeyID KeyID
PrivateKey ed25519.PrivateKey
Verifier JSONVerifier
MembershipQuerier MembershipQuerier
UserIDQuerier spec.UserIDForSender // Provides userIDs given a senderID
StoreSenderIDFromPublicID spec.StoreSenderIDFromPublicID
}

type HandleSendJoinResponse struct {
AlreadyJoined bool
JoinEvent PDU
}

// nolint: gocyclo
func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error) {
if input.Verifier == nil {
panic("Missing valid JSONVerifier")
Expand All @@ -323,6 +326,9 @@ func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error)
if input.Context == nil {
panic("Missing valid Context")
}
if input.StoreSenderIDFromPublicID == nil {
panic("Missing valid StoreSenderID")
}

verImpl, err := GetRoomVersion(input.RoomVersion)
if err != nil {
Expand All @@ -342,6 +348,24 @@ func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error)
return nil, spec.BadJSON("Event state key must match the event sender.")
}

// validate the mxid_mapping of the event
if input.RoomVersion == RoomVersionPseudoIDs {
// validate the signature first
if err = validateMXIDMappingSignature(input.Context, event, input.Verifier, verImpl); err != nil {
return nil, spec.Forbidden(err.Error())
}

mapping := MXIDMapping{}
err = json.Unmarshal([]byte(gjson.GetBytes(input.JoinEvent, "content.mxid_mapping").Raw), &mapping)
if err != nil {
return nil, err
}
// store the user room public key -> userID mapping
if err = input.StoreSenderIDFromPublicID(input.Context, mapping.UserRoomKey, mapping.UserID, input.RoomID); err != nil {
return nil, err
}
}

// Check that the sender belongs to the server that is sending us
// the request. By this point we've already asserted that the sender
// and the state key are equal so we don't need to check both.
Expand All @@ -352,6 +376,12 @@ func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error)
return nil, spec.Forbidden("The sender does not match the server that originated the request")
}

// In pseudoID rooms we don't need to hit federation endpoints to get e.g. signing keys,
// so we can replace the verifier with a more simple one which uses the senderID to verify the event.
if input.RoomVersion == RoomVersionPseudoIDs {
input.Verifier = JSONVerifierSelf{}
}

// Check that the room ID is correct.
if event.RoomID() != input.RoomID.String() {
return nil, spec.BadJSON(
Expand Down
87 changes: 78 additions & 9 deletions handlejoin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,8 @@ func TestHandleMakeJoinNilContext(t *testing.T) {
})
}

func createMemberEventBuilder(sender string, roomID string, stateKey *string, content spec.RawJSON) *EventBuilder {
return MustGetRoomVersion(RoomVersionV10).NewEventBuilderFromProtoEvent(&ProtoEvent{
func createMemberEventBuilder(roomVersion RoomVersion, sender string, roomID string, stateKey *string, content spec.RawJSON) *EventBuilder {
return MustGetRoomVersion(roomVersion).NewEventBuilderFromProtoEvent(&ProtoEvent{
SenderID: sender,
RoomID: roomID,
Type: "m.room.member",
Expand Down Expand Up @@ -666,33 +666,48 @@ func TestHandleSendJoin(t *testing.T) {
badVerifier := &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: badPK}}

stateKey := userID.String()
eb := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
eb := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
joinEvent, err := eb.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

ebNotJoin := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"ban"}`))
// create a pseudoID join event
_, userPriv, err := ed25519.GenerateKey(rand.Reader)
assert.Nil(t, err)
pseudoID := spec.SenderIDFromPseudoIDKey(userPriv)
stateKey = string(pseudoID)
mapping := MXIDMapping{UserID: userID.String(), UserRoomKey: pseudoID}
err = mapping.Sign(remoteServer, keyID, sk)
assert.Nil(t, err)
content := MemberContent{Membership: spec.Join, MXIDMapping: &mapping}
contentBytes, err := json.Marshal(content)
assert.Nil(t, err)
eb = createMemberEventBuilder(RoomVersionPseudoIDs, stateKey, validRoom.String(), &stateKey, contentBytes)
joinEventPseudoID, err := eb.Build(time.Now(), "self", "ed25519:1", userPriv)
assert.Nil(t, err)

ebNotJoin := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"ban"}`))
notJoinEvent, err := ebNotJoin.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

eb2 := createMemberEventBuilder("@asdf:asdf", validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
eb2 := createMemberEventBuilder(RoomVersionV10, "@asdf:asdf", validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
joinEventInvalidSender, err := eb2.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

stateKey = ""
eb3 := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
eb3 := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`))
joinEventNoState, err := eb3.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

stateKey = userID.String()
badAuthViaEB := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"baduser"}`))
badAuthViaEB := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"baduser"}`))
badAuthViaEvent, err := badAuthViaEB.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

authViaNotLocalEB := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"@user:notlocalserver"}`))
authViaNotLocalEB := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"@user:notlocalserver"}`))
authViaNotLocalEvent, err := authViaNotLocalEB.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

authViaEB := createMemberEventBuilder(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"@user:local"}`))
authViaEB := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join","join_authorised_via_users_server":"@user:local"}`))
authViaEvent, err := authViaEB.Build(time.Now(), userID.Domain(), keyID, sk)
assert.Nil(t, err)

Expand Down Expand Up @@ -1005,10 +1020,34 @@ func TestHandleSendJoin(t *testing.T) {
},
expectedErr: false,
},
"pseudo_id_success": {
input: HandleSendJoinInput{
Context: context.Background(),
RoomID: *validRoom,
EventID: joinEventPseudoID.EventID(),
JoinEvent: joinEventPseudoID.JSON(),
RoomVersion: RoomVersionPseudoIDs,
RequestOrigin: remoteServer,
LocalServerName: localServer,
MembershipQuerier: &TestMembershipQuerier{membership: "join"},
UserIDQuerier: func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
return userID, nil
},
KeyID: keyID,
PrivateKey: sk,
Verifier: verifier,
},
expectedErr: false,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if tc.input.StoreSenderIDFromPublicID == nil {
tc.input.StoreSenderIDFromPublicID = func(ctx context.Context, senderID spec.SenderID, userID string, id spec.RoomID) error {
return nil
}
}
_, joinErr := HandleSendJoin(tc.input)
if tc.expectedErr {
switch e := joinErr.(type) {
Expand Down Expand Up @@ -1149,3 +1188,33 @@ func TestHandleSendJoinNilContext(t *testing.T) {
})
})
}

func TestHandleSendJoinNilStoreSenderIDFromPublicID(t *testing.T) {
remoteServer := spec.ServerName("remote")
localServer := spec.ServerName("local")
validRoom, err := spec.NewRoomID("!room:remote")
assert.Nil(t, err)

pk, sk, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Failed generating key: %v", err)
}
keyID := KeyID("ed25519:1234")
verifier := &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}}

assert.Panics(t, func() {
_, _ = HandleSendJoin(HandleSendJoinInput{
Context: context.Background(),
RoomID: *validRoom,
EventID: "#event",
RoomVersion: RoomVersionV10,
RequestOrigin: remoteServer,
LocalServerName: localServer,
MembershipQuerier: &TestMembershipQuerier{},
UserIDQuerier: UserIDForSenderTest,
KeyID: keyID,
PrivateKey: sk,
Verifier: verifier,
})
})
}
43 changes: 43 additions & 0 deletions keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gomatrixserverlib

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -371,6 +372,48 @@ func (k *KeyRing) checkUsingKeys(
}
}

// JSONVerifierSelf provides methods to validate signatures signed by pseudo identities.
type JSONVerifierSelf struct{}

type senderIDObj struct {
SenderID spec.SenderID `json:"sender"`
}

// VerifyJSONs implements JSONVerifier.
func (v JSONVerifierSelf) VerifyJSONs(ctx context.Context, requests []VerifyJSONRequest) ([]VerifyJSONResult, error) {
results := make([]VerifyJSONResult, len(requests))

for i := range requests {
// first of all, extract the sender key
var obj senderIDObj

err := json.Unmarshal(requests[i].Message, &obj)
if err != nil {
results[i].Error = fmt.Errorf("unable to get senderID from event: %w", err)
continue
}

if len(obj.SenderID) == 0 {
results[i].Error = fmt.Errorf("unable to get senderID from event: empty sender")
continue
}
// convert to public key
key, err := obj.SenderID.RawBytes()
if err != nil {
results[i].Error = fmt.Errorf("unable to get key from senderID: %w", err)
continue
}

// verify the JSON is valid
if err = VerifyJSON("self", "ed25519:1", ed25519.PublicKey(key), requests[i].Message); err != nil {
// The signature wasn't valid, record the error and try the next key ID.
results[i].Error = err
continue
}
}
return results, nil
}

type KeyClient interface {
GetServerKeys(ctx context.Context, matrixServer spec.ServerName) (ServerKeys, error)
LookupServerKeys(ctx context.Context, matrixServer spec.ServerName, keyRequests map[PublicKeyLookupRequest]spec.Timestamp) ([]ServerKeys, error)
Expand Down
Loading

0 comments on commit f6e3c7f

Please sign in to comment.