diff --git a/eventauth.go b/eventauth.go index 52c98980..bbbf8dee 100644 --- a/eventauth.go +++ b/eventauth.go @@ -186,8 +186,9 @@ func accumulateStateNeeded(result *StateNeeded, eventType string, sender spec.Se } result.Create = true result.PowerLevels = true + result.Member = append(result.Member, string(sender)) if stateKey != nil { - result.Member = append(result.Member, string(sender), *stateKey) + result.Member = append(result.Member, *stateKey) } if content.Membership == spec.Join || content.Membership == spec.Knock || content.Membership == spec.Invite { result.JoinRules = true @@ -887,6 +888,9 @@ func (e *eventAllower) commonChecks(event PDU) error { if err != nil { return err } + if userID == nil { + return errorf("userID not found for sender %q in room %q", event.SenderID(), event.RoomID()) + } if err := e.create.UserIDAllowed(*userID); err != nil { return err } @@ -990,6 +994,9 @@ func (m *membershipAllower) membershipAllowed(event PDU) error { // nolint: gocy if err != nil { return err } + if sender == nil { + return errorf("userID not found for sender %q in room %q", m.senderID, event.RoomID()) + } if err := m.create.UserIDAllowed(*sender); err != nil { return err } diff --git a/eventcrypto.go b/eventcrypto.go index b551325c..ec83aed3 100644 --- a/eventcrypto.go +++ b/eventcrypto.go @@ -42,35 +42,42 @@ func VerifyEventSignatures(ctx context.Context, e PDU, verifier JSONVerifier, us panic("UserIDForSender func is nil") } + var serverName spec.ServerName needed := map[spec.ServerName]struct{}{} - - // The sender should have signed the event in all cases. - validRoomID, err := spec.NewRoomID(e.RoomID()) - if err != nil { - return err - } - sender, err := userIDForSender(*validRoomID, e.SenderID()) - if err != nil { - return fmt.Errorf("invalid sender userID: %w", err) - } - serverName := sender.Domain() - needed[serverName] = struct{}{} - verImpl, err := GetRoomVersion(e.Version()) if err != nil { return err } - // In room versions 1 and 2, we should also check that the server - // that created the event is included too. This is probably the - // same as the sender. - format := verImpl.EventIDFormat() - if format == EventIDFormatV1 { - _, serverName, err = SplitID('$', e.EventID()) + // The sender should have signed the event in all cases. + switch e.Version() { + case RoomVersionPseudoIDs: + needed[spec.ServerName(e.SenderID())] = struct{}{} + default: + validRoomID, err := spec.NewRoomID(e.RoomID()) if err != nil { - return fmt.Errorf("failed to split event ID: %w", err) + return err + } + sender, err := userIDForSender(*validRoomID, e.SenderID()) + if err != nil { + return fmt.Errorf("invalid sender userID: %w", err) + } + if sender != nil { + serverName = sender.Domain() + needed[serverName] = struct{}{} + } + + // In room versions 1 and 2, we should also check that the server + // that created the event is included too. This is probably the + // same as the sender. + format := verImpl.EventIDFormat() + if format == EventIDFormatV1 { + _, serverName, err = SplitID('$', e.EventID()) + if err != nil { + return fmt.Errorf("failed to split event ID: %w", err) + } + needed[serverName] = struct{}{} } - needed[serverName] = struct{}{} } // Special checks for membership events. @@ -92,8 +99,7 @@ func VerifyEventSignatures(ctx context.Context, e PDU, verifier JSONVerifier, us if membership == spec.Invite { switch e.Version() { case RoomVersionPseudoIDs: - // TODO: (pseudoIDs) revisit this logic for event signing - needed[spec.ServerName(e.SenderID())] = struct{}{} + needed[spec.ServerName(*e.StateKey())] = struct{}{} default: _, serverName, err = SplitID('@', *e.StateKey()) if err != nil { diff --git a/fclient/federationclient.go b/fclient/federationclient.go index 1333d661..a0cef587 100644 --- a/fclient/federationclient.go +++ b/fclient/federationclient.go @@ -31,6 +31,7 @@ type FederationClient interface { MakeLeave(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res RespMakeLeave, err error) SendLeave(ctx context.Context, origin, s spec.ServerName, event gomatrixserverlib.PDU) (err error) SendInviteV2(ctx context.Context, origin, s spec.ServerName, request InviteV2Request) (res RespInviteV2, err error) + SendInviteV3(ctx context.Context, origin, s spec.ServerName, request InviteV3Request, userID spec.UserID) (res RespInviteV2, err error) GetEvent(ctx context.Context, origin, s spec.ServerName, eventID string) (res gomatrixserverlib.Transaction, err error) @@ -123,6 +124,7 @@ func (ac *federationClient) doRequest(ctx context.Context, r FederationRequest, var federationPathPrefixV1 = "/_matrix/federation/v1" var federationPathPrefixV2 = "/_matrix/federation/v2" +var federationPathPrefixV3 = "/_matrix/federation/v3" // SendTransaction sends a transaction func (ac *federationClient) SendTransaction( @@ -409,6 +411,23 @@ func (ac *federationClient) SendInviteV2( return } +// SendInviteV3 sends an invite m.room.member event to an invited server to be +// signed by it. This is used to invite a user that is not on the local server. +// V3 sends a partial event to allow the invitee to populate the mxid_mapping. +func (ac *federationClient) SendInviteV3( + ctx context.Context, origin, s spec.ServerName, request InviteV3Request, userID spec.UserID, +) (res RespInviteV2, err error) { + path := federationPathPrefixV3 + "/invite/" + + url.PathEscape(request.Event().RoomID) + "/" + + url.PathEscape(userID.String()) + req := NewFederationRequest("PUT", origin, s, path) + if err = req.SetContent(request); err != nil { + return + } + err = ac.doRequest(ctx, req, &res) + return +} + // ExchangeThirdPartyInvite sends the builder of a m.room.member event of // "invite" membership derived from a response from invites sent by an identity // server. diff --git a/fclient/invitev3.go b/fclient/invitev3.go new file mode 100644 index 00000000..56e42110 --- /dev/null +++ b/fclient/invitev3.go @@ -0,0 +1,62 @@ +package fclient + +import ( + "encoding/json" + + "github.com/matrix-org/gomatrixserverlib" +) + +func NewInviteV3Request(event gomatrixserverlib.ProtoEvent, version gomatrixserverlib.RoomVersion, state []gomatrixserverlib.InviteStrippedState) ( + request InviteV3Request, err error, +) { + if !gomatrixserverlib.KnownRoomVersion(version) { + err = gomatrixserverlib.UnsupportedRoomVersionError{ + Version: version, + } + return + } + request.fields.inviteV2RequestHeaders = inviteV2RequestHeaders{ + RoomVersion: version, + InviteRoomState: state, + } + request.fields.Event = event + return +} + +// InviteV3Request is used in the body of a /_matrix/federation/v3/invite request. +type InviteV3Request struct { + fields struct { + inviteV2RequestHeaders + Event gomatrixserverlib.ProtoEvent `json:"event"` + } +} + +// MarshalJSON implements json.Marshaller +func (i InviteV3Request) MarshalJSON() ([]byte, error) { + return json.Marshal(i.fields) +} + +// UnmarshalJSON implements json.Unmarshaller +func (i *InviteV3Request) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &i.fields) + if err != nil { + return err + } + return err +} + +// Event returns the invite event. +func (i *InviteV3Request) Event() gomatrixserverlib.ProtoEvent { + return i.fields.Event +} + +// RoomVersion returns the room version of the invited room. +func (i *InviteV3Request) RoomVersion() gomatrixserverlib.RoomVersion { + return i.fields.RoomVersion +} + +// InviteRoomState returns stripped state events for the room, containing +// enough information for the client to identify the room. +func (i *InviteV3Request) InviteRoomState() []gomatrixserverlib.InviteStrippedState { + return i.fields.InviteRoomState +} diff --git a/fclient/invitev3_test.go b/fclient/invitev3_test.go new file mode 100644 index 00000000..0330f927 --- /dev/null +++ b/fclient/invitev3_test.go @@ -0,0 +1,79 @@ +package fclient + +import ( + "encoding/json" + "testing" + + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/spec" +) + +func TestMarshalInviteV3Request(t *testing.T) { + expected := `{"room_version":"org.matrix.msc4014","invite_room_state":[],"event":{"sender":"@test:localhost","room_id":"!19Mp0U9hjajeIiw1:localhost","type":"m.room.name","state_key":"","prev_events":["upCsBqUhNUgT2/+zkzg8TbqdQpWWKQnZpGJc6KcbUC4"],"auth_events":["abjkiDSg1RkuZrbj2jZoGMlQaaj1Ue3Jhi7I7NlKfXY","X7RUj46hM/8sUHNBIFkStbOauPvbDzjSdH4NibYWnko","k9eM6utkCH8vhLW9/oRsH74jOBS/6RVK42iGDFbylno"],"depth":7,"signatures":{"localhost":{"ed25519:u9kP":"5IzSuRXkxvbTp0vZhhXYZeOe+619iG3AybJXr7zfNn/4vHz4TH7qSJVQXSaHHvcTcDodAKHnTG1WDulgO5okAQ"}},"content":{"name":"test3"}}}` + + senderID := "@test:localhost" + roomID := "!19Mp0U9hjajeIiw1:localhost" + eventType := "m.room.name" + stateKey := "" + prevEvents := []string{"upCsBqUhNUgT2/+zkzg8TbqdQpWWKQnZpGJc6KcbUC4"} + authEvents := []string{"abjkiDSg1RkuZrbj2jZoGMlQaaj1Ue3Jhi7I7NlKfXY", "X7RUj46hM/8sUHNBIFkStbOauPvbDzjSdH4NibYWnko", "k9eM6utkCH8vhLW9/oRsH74jOBS/6RVK42iGDFbylno"} + depth := int64(7) + signatures := spec.RawJSON(`{"localhost": {"ed25519:u9kP": "5IzSuRXkxvbTp0vZhhXYZeOe+619iG3AybJXr7zfNn/4vHz4TH7qSJVQXSaHHvcTcDodAKHnTG1WDulgO5okAQ"}}`) + content := spec.RawJSON(`{"name":"test3"}`) + + output := gomatrixserverlib.ProtoEvent{ + SenderID: senderID, + RoomID: roomID, + Type: eventType, + StateKey: &stateKey, + PrevEvents: prevEvents, + AuthEvents: authEvents, + Depth: depth, + Signature: signatures, + Content: content, + } + + inviteReq, err := NewInviteV3Request(output, gomatrixserverlib.RoomVersionPseudoIDs, []gomatrixserverlib.InviteStrippedState{}) + if err != nil { + t.Fatal(err) + } + + j, err := json.Marshal(inviteReq) + if err != nil { + t.Fatal(err) + } + + if string(j) != expected { + t.Fatalf("\nresult: %q\nwanted: %q", string(j), expected) + } + + var newRequest InviteV3Request + err = json.Unmarshal(j, &newRequest) + if err != nil { + t.Fatal(err) + } + + if newRequest.RoomVersion() != gomatrixserverlib.RoomVersionPseudoIDs { + t.Fatalf("unmatched room version. expected: %v, got: %v", gomatrixserverlib.RoomVersionPseudoIDs, newRequest.RoomVersion()) + } + if len(newRequest.InviteRoomState()) != 0 { + t.Fatalf("invite room state should not have any events") + } + if newRequest.Event().SenderID != senderID { + t.Fatalf("unmatched senderID. expected: %v, got: %v", newRequest.Event().SenderID, senderID) + + } + if newRequest.Event().RoomID != roomID { + t.Fatalf("unmatched roomID. expected: %v, got: %v", newRequest.Event().RoomID, roomID) + } + if newRequest.Event().Type != eventType { + t.Fatalf("unmatched type. expected: %v, got: %v", newRequest.Event().Type, eventType) + + } + if *newRequest.Event().StateKey != stateKey { + t.Fatalf("unmatched state key. expected: %v, got: %v", *newRequest.Event().StateKey, stateKey) + } + if newRequest.Event().Depth != depth { + t.Fatalf("unmatched depth. expected: %v, got: %v", newRequest.Event().Depth, depth) + } +} diff --git a/handleinvite.go b/handleinvite.go index 7aa46624..1e027c85 100644 --- a/handleinvite.go +++ b/handleinvite.go @@ -17,6 +17,7 @@ package gomatrixserverlib import ( "context" "fmt" + "time" "github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/util" @@ -42,6 +43,13 @@ type HandleInviteInput struct { UserIDQuerier spec.UserIDForSender // Provides userIDs given a senderID } +type HandleInviteV3Input struct { + HandleInviteInput + + InviteProtoEvent ProtoEvent // The original invite event + GetOrCreateSenderID spec.CreateSenderID // Creates, if needed, a new senderID & private key +} + // HandleInvite - Ensures the incoming invite request is valid and signs the event // to return back to the remote server. // On success returns a fully formed & signed Invite Event @@ -95,26 +103,79 @@ func HandleInvite(ctx context.Context, input HandleInviteInput) (PDU, error) { return nil, spec.Forbidden("The invite must be signed by the server it originated on") } - // Sign the event so that other servers will know that we have received the invite. signedEvent := input.InviteEvent.Sign( string(input.InvitedUser.Domain()), input.KeyID, input.PrivateKey, ) + return handleInviteCommonChecks(ctx, input, signedEvent, *sender) +} + +func HandleInviteV3(ctx context.Context, input HandleInviteV3Input) (PDU, error) { + if input.RoomQuerier == nil || input.MembershipQuerier == nil || input.StateQuerier == nil || input.UserIDQuerier == nil { + panic("Missing valid Querier") + } + if input.Verifier == nil { + panic("Missing valid JSONVerifier") + } + + if ctx == nil { + panic("Missing valid Context") + } + + // Check that we can accept invites for this room version. + verImpl, err := GetRoomVersion(input.RoomVersion) + if err != nil { + return nil, spec.UnsupportedRoomVersion( + fmt.Sprintf("Room version %q is not supported by this server.", input.RoomVersion), + ) + } + + // Check that the room ID is correct. + if input.InviteProtoEvent.RoomID != input.RoomID.String() { + return nil, spec.BadJSON("The room ID in the request path must match the room ID in the invite event JSON") + } + + // NOTE: If we already have a senderID for this user in this room, + // this could be because they are already invited/joined or were previously. + // In that case, use the existing senderID to complete this invite event. + // Otherwise we need to create a new senderID + invitedSenderID, signingKey, err := input.GetOrCreateSenderID(ctx, input.InvitedUser, input.RoomID, string(input.RoomVersion)) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("GetOrCreateSenderID failed") + return nil, spec.InternalServerError{} + } + + input.InviteProtoEvent.StateKey = (*string)(&invitedSenderID) + + // Sign the event so that other servers will know that we have received the invite. + keyID := KeyID("ed25519:1") + origin := spec.ServerName(invitedSenderID) + fullEventBuilder := verImpl.NewEventBuilderFromProtoEvent(&input.InviteProtoEvent) + fullEvent, err := fullEventBuilder.Build(time.Now(), origin, keyID, signingKey) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("failed building invite event") + return nil, spec.InternalServerError{} + } + + return handleInviteCommonChecks(ctx, input.HandleInviteInput, fullEvent, spec.UserID{}) +} + +func handleInviteCommonChecks(ctx context.Context, input HandleInviteInput, event PDU, sender spec.UserID) (PDU, error) { isKnownRoom, err := input.RoomQuerier.IsKnownRoom(ctx, input.RoomID) if err != nil { util.GetLogger(ctx).WithError(err).Error("failed querying known room") return nil, spec.InternalServerError{} } - logger := createInviteLogger(ctx, signedEvent, input.RoomID) + logger := createInviteLogger(ctx, input.RoomID, sender, input.InvitedUser, event.EventID()) logger.WithFields(logrus.Fields{ - "room_version": signedEvent.Version(), + "room_version": event.Version(), "room_info_exists": isKnownRoom, }).Debug("processing incoming federation invite event") inviteState := input.StrippedState if len(inviteState) == 0 { - inviteState, err = GenerateStrippedState(ctx, input.RoomID, signedEvent, input.StateQuerier) + inviteState, err = GenerateStrippedState(ctx, input.RoomID, input.StateQuerier) if err != nil { util.GetLogger(ctx).WithError(err).Error("failed generating stripped state") return nil, spec.InternalServerError{} @@ -132,10 +193,10 @@ func HandleInvite(ctx context.Context, input HandleInviteInput) (PDU, error) { } } - err = setUnsignedFieldForInvite(signedEvent, inviteState) + err = setUnsignedFieldForInvite(event, inviteState) if err != nil { return nil, err } - return signedEvent, nil + return event, nil } diff --git a/handleinvite_test.go b/handleinvite_test.go index 394d9f4c..aab1935b 100644 --- a/handleinvite_test.go +++ b/handleinvite_test.go @@ -439,3 +439,429 @@ func TestHandleInviteNilContext(t *testing.T) { }) }) } + +func TestHandleInviteV3(t *testing.T) { + userID, err := spec.NewUserID("@user:server", true) + assert.Nil(t, err) + validRoom, err := spec.NewRoomID("!room:server") + assert.Nil(t, err) + badRoom, err := spec.NewRoomID("!bad:room") + assert.Nil(t, err) + + pk, sk, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(t, err) + keyID := KeyID("ed25519:1234") + verifier := &KeyRing{[]KeyFetcher{&TestRequestKeyDummy{}}, &joinKeyDatabase{key: pk}} + + stateKey := userID.String() + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + assert.Nil(t, err) + + stateKey = "" + createEB := MustGetRoomVersion(RoomVersionV10).NewEventBuilderFromProtoEvent(&ProtoEvent{ + SenderID: userID.String(), + RoomID: validRoom.String(), + Type: "m.room.create", + StateKey: &stateKey, + PrevEvents: []interface{}{}, + AuthEvents: []interface{}{}, + Depth: 0, + Content: spec.RawJSON(`{"creator":"@user:server","m.federate":true,"room_version":"10"}`), + Unsigned: spec.RawJSON(""), + }) + createEvent, err := createEB.Build(time.Now(), userID.Domain(), keyID, sk) + if err != nil { + t.Fatalf("Failed building create event: %v", err) + } + + type ErrorType int + const ( + InternalErr ErrorType = iota + MatrixErr + ) + + tests := map[string]struct { + input HandleInviteV3Input + expectedErr bool + errType ErrorType + errCode spec.MatrixErrorCode + }{ + "unsupported_room_version": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorUnsupportedRoomVersion, + }, + "mismatched_room_ids": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *badRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorBadJSON, + }, + "room_querier_error": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{shouldFail: true}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: InternalErr, + }, + "known_room_no_state": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{knownRoom: true}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: InternalErr, + }, + "known_room_already_joined": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{knownRoom: true}, + MembershipQuerier: &TestMembershipQuerier{membership: spec.Join}, + StateQuerier: &TestStateQuerier{state: []PDU{createEvent}}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorForbidden, + }, + "known_room_state_query_error": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{knownRoom: true}, + MembershipQuerier: &TestMembershipQuerier{membership: ""}, + StateQuerier: &TestStateQuerier{shouldFailState: true}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: InternalErr, + }, + "known_room_not_already_joined_membership_error": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{knownRoom: true}, + MembershipQuerier: &TestMembershipQuerier{memberEventErr: true}, + StateQuerier: &TestStateQuerier{state: []PDU{createEvent}}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: true, + errType: InternalErr, + }, + "known_room_not_already_joined": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{knownRoom: true}, + MembershipQuerier: &TestMembershipQuerier{membership: ""}, + StateQuerier: &TestStateQuerier{state: []PDU{createEvent}}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: false, + }, + "success_no_room_state": { + input: HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + InvitedUser: *userID, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + UserIDQuerier: UserIDForSenderTest, + }, + InviteProtoEvent: inviteEvent, + GetOrCreateSenderID: CreateSenderID, + }, + expectedErr: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + _, joinErr := HandleInviteV3(context.Background(), tc.input) + if tc.expectedErr { + switch e := joinErr.(type) { + case nil: + t.Fatalf("Error should not be nil") + case spec.InternalServerError: + assert.Equal(t, tc.errType, InternalErr) + case spec.MatrixError: + assert.Equal(t, tc.errType, MatrixErr) + assert.Equal(t, tc.errCode, e.ErrCode) + default: + t.Fatalf("Unexpected Error Type") + } + } else { + jsonBytes, err := json.Marshal(&joinErr) + assert.Nil(t, err) + assert.Nil(t, joinErr, string(jsonBytes)) + } + }) + } +} + +func TestHandleInviteV3NilVerifier(t *testing.T) { + validRoom, err := spec.NewRoomID("!room:remote") + assert.Nil(t, err) + + _, sk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("Failed generating key: %v", err) + } + keyID := KeyID("ed25519:1234") + + assert.Panics(t, func() { + _, _ = HandleInviteV3(context.Background(), HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: nil, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} + +func TestHandleInviteV3NilRoomQuerier(t *testing.T) { + 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() { + _, _ = HandleInviteV3(context.Background(), HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + RoomQuerier: nil, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} + +func TestHandleInviteV3NilMembershipQuerier(t *testing.T) { + 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() { + _, _ = HandleInviteV3(context.Background(), HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: nil, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} + +func TestHandleInviteV3NilStateQuerier(t *testing.T) { + 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() { + _, _ = HandleInviteV3(context.Background(), HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: nil, + UserIDQuerier: UserIDForSenderTest, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} + +func TestHandleInviteV3NilUserIDQuerier(t *testing.T) { + 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() { + _, _ = HandleInviteV3(context.Background(), HandleInviteV3Input{ + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: nil, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} + +func TestHandleInviteV3NilContext(t *testing.T) { + 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() { + _, _ = HandleInviteV3(nil, HandleInviteV3Input{ //nolint + HandleInviteInput: HandleInviteInput{ + RoomID: *validRoom, + RoomVersion: "", + KeyID: keyID, + PrivateKey: sk, + Verifier: verifier, + RoomQuerier: &TestRoomQuerier{}, + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + }, + GetOrCreateSenderID: CreateSenderID, + }) + }) +} diff --git a/handlejoin.go b/handlejoin.go index 9fe8ce76..dcc2d3d4 100644 --- a/handlejoin.go +++ b/handlejoin.go @@ -378,8 +378,10 @@ func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error) // 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. + toVerify := sender.Domain() if input.RoomVersion == RoomVersionPseudoIDs { input.Verifier = JSONVerifierSelf{} + toVerify = spec.ServerName(event.SenderID()) } // Check that the room ID is correct. @@ -417,8 +419,9 @@ func HandleSendJoin(input HandleSendJoinInput) (*HandleSendJoinResponse, error) util.GetLogger(input.Context).WithError(err).Error("RedactEventJSON failed") return nil, spec.BadJSON("The event JSON could not be redacted") } + verifyRequests := []VerifyJSONRequest{{ - ServerName: sender.Domain(), + ServerName: toVerify, Message: redacted, AtTS: event.OriginServerTS(), ValidityCheckingFunc: StrictValiditySignatureCheck, diff --git a/handlejoin_test.go b/handlejoin_test.go index 578b877e..5c61a7e2 100644 --- a/handlejoin_test.go +++ b/handlejoin_test.go @@ -682,7 +682,7 @@ func TestHandleSendJoin(t *testing.T) { 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) + joinEventPseudoID, err := eb.Build(time.Now(), spec.ServerName(pseudoID), "ed25519:1", userPriv) assert.Nil(t, err) ebNotJoin := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"ban"}`)) diff --git a/invite.go b/invite.go index ea723849..03614e3f 100644 --- a/invite.go +++ b/invite.go @@ -33,8 +33,16 @@ type StateQuerier interface { GetState(ctx context.Context, roomID spec.RoomID, stateWanted []StateKeyTuple) ([]PDU, error) } +type LatestEvents struct { + RoomExists bool + StateEvents []PDU + PrevEventIDs []string + Depth int64 +} + type FederatedInviteClient interface { SendInvite(ctx context.Context, event PDU, strippedState []InviteStrippedState) (PDU, error) + SendInviteV3(ctx context.Context, event ProtoEvent, userID spec.UserID, roomVersion RoomVersion, strippedState []InviteStrippedState) (PDU, error) } // InviteStrippedState is a cut-down set of fields from room state @@ -89,7 +97,7 @@ func (i *InviteStrippedState) Sender() string { } func GenerateStrippedState( - ctx context.Context, roomID spec.RoomID, inviteEvent PDU, stateQuerier StateQuerier, + ctx context.Context, roomID spec.RoomID, stateQuerier StateQuerier, ) ([]InviteStrippedState, error) { // "If they are set on the room, at least the state for m.room.avatar, m.room.canonical_alias, m.room.join_rules, and m.room.name SHOULD be included." // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-member @@ -110,10 +118,7 @@ func GenerateStrippedState( return []InviteStrippedState{}, err } if stateEvents != nil { - inviteState := []InviteStrippedState{ - NewInviteStrippedState(inviteEvent), - } - stateEvents = append(stateEvents, inviteEvent) + inviteState := []InviteStrippedState{} for _, event := range stateEvents { inviteState = append(inviteState, NewInviteStrippedState(event)) } @@ -165,12 +170,12 @@ func abortIfAlreadyJoined(ctx context.Context, roomID spec.RoomID, invitedUser s return nil } -func createInviteLogger(ctx context.Context, event PDU, roomID spec.RoomID) *logrus.Entry { +func createInviteLogger(ctx context.Context, roomID spec.RoomID, inviter spec.UserID, invitee spec.UserID, eventID string) *logrus.Entry { return util.GetLogger(ctx).WithFields(map[string]interface{}{ - "inviter": event.SenderID(), - "invitee": *event.StateKey(), + "inviter": inviter.String(), + "invitee": invitee.String(), "room_id": roomID.String(), - "event_id": event.EventID(), + "event_id": eventID, }) } @@ -187,3 +192,17 @@ func setUnsignedFieldForInvite(event PDU, inviteState []InviteStrippedState) err return nil } + +func setUnsignedFieldForProtoInvite(event *ProtoEvent, inviteState []InviteStrippedState) error { + if len(inviteState) == 0 { + if err := event.SetUnsigned(map[string]interface{}{"invite_room_state": struct{}{}}); err != nil { + return fmt.Errorf("event.SetUnsignedField: %w", err) + } + } else { + if err := event.SetUnsigned(map[string]interface{}{"invite_room_state": inviteState}); err != nil { + return fmt.Errorf("event.SetUnsignedField: %w", err) + } + } + + return nil +} diff --git a/invite_test.go b/invite_test.go new file mode 100644 index 00000000..af0076d3 --- /dev/null +++ b/invite_test.go @@ -0,0 +1,88 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomatrixserverlib + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/matrix-org/gomatrixserverlib/spec" +) + +const TestInviteV2ExampleEvent = `{"_room_version":"1","auth_events":[["$oXL79cT7fFxR7dPH:localhost",{"sha256":"abjkiDSg1RkuZrbj2jZoGMlQaaj1Ue3Jhi7I7NlKfXY"}],["$IVUsaSkm1LBAZYYh:localhost",{"sha256":"X7RUj46hM/8sUHNBIFkStbOauPvbDzjSdH4NibYWnko"}],["$VS2QT0EeArZYi8wf:localhost",{"sha256":"k9eM6utkCH8vhLW9/oRsH74jOBS/6RVK42iGDFbylno"}]],"content":{"name":"test3"},"depth":7,"event_id":"$yvN1b43rlmcOs5fY:localhost","hashes":{"sha256":"Oh1mwI1jEqZ3tgJ+V1Dmu5nOEGpCE4RFUqyJv2gQXKs"},"origin":"localhost","origin_server_ts":1510854416361,"prev_events":[["$FqI6TVvWpcbcnJ97:localhost",{"sha256":"upCsBqUhNUgT2/+zkzg8TbqdQpWWKQnZpGJc6KcbUC4"}]],"prev_state":[],"room_id":"!19Mp0U9hjajeIiw1:localhost","sender":"@test:localhost","signatures":{"localhost":{"ed25519:u9kP":"5IzSuRXkxvbTp0vZhhXYZeOe+619iG3AybJXr7zfNn/4vHz4TH7qSJVQXSaHHvcTcDodAKHnTG1WDulgO5okAQ"}},"state_key":"","type":"m.room.name"}` + +func TestEmptyUnsignedFieldIsSetForPDU(t *testing.T) { + output, err := NewEventFromHeaderedJSON([]byte(TestInviteV2ExampleEvent), false) + if err != nil { + t.Fatal(err) + } + + inviteState := []InviteStrippedState{} + + err = setUnsignedFieldForInvite(output, inviteState) + if err != nil { + t.Fatal(err) + } + + inviteStateJSON, err := json.Marshal(map[string]interface{}{"invite_room_state": struct{}{}}) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(output.Unsigned(), inviteStateJSON) { + t.Fatalf("Expected: %v, Got: %v", string(inviteStateJSON[:]), string(output.Unsigned()[:])) + } +} + +func TestEmptyUnsignedFieldIsSetForProtoEvent(t *testing.T) { + senderID := "@test:localhost" + roomID := "!19Mp0U9hjajeIiw1:localhost" + eventType := "m.room.name" + stateKey := "" + prevEvents := []string{"upCsBqUhNUgT2/+zkzg8TbqdQpWWKQnZpGJc6KcbUC4"} + authEvents := []string{"abjkiDSg1RkuZrbj2jZoGMlQaaj1Ue3Jhi7I7NlKfXY", "X7RUj46hM/8sUHNBIFkStbOauPvbDzjSdH4NibYWnko", "k9eM6utkCH8vhLW9/oRsH74jOBS/6RVK42iGDFbylno"} + depth := int64(7) + signatures := spec.RawJSON(`{"localhost": {"ed25519:u9kP": "5IzSuRXkxvbTp0vZhhXYZeOe+619iG3AybJXr7zfNn/4vHz4TH7qSJVQXSaHHvcTcDodAKHnTG1WDulgO5okAQ"}}`) + content := spec.RawJSON(`{"name":"test3"}`) + + output := ProtoEvent{ + SenderID: senderID, + RoomID: roomID, + Type: eventType, + StateKey: &stateKey, + PrevEvents: prevEvents, + AuthEvents: authEvents, + Depth: depth, + Signature: signatures, + Content: content, + } + + inviteState := []InviteStrippedState{} + + err := setUnsignedFieldForProtoInvite(&output, inviteState) + if err != nil { + t.Fatal(err) + } + + inviteStateJSON, err := json.Marshal(map[string]interface{}{"invite_room_state": struct{}{}}) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(output.Unsigned, inviteStateJSON) { + t.Fatalf("Expected: %v, Got: %v", string(inviteStateJSON[:]), string(output.Unsigned[:])) + } +} diff --git a/keyring.go b/keyring.go index 3e109d41..3d37c75b 100644 --- a/keyring.go +++ b/keyring.go @@ -2,7 +2,6 @@ package gomatrixserverlib import ( "context" - "encoding/json" "errors" "fmt" "strings" @@ -375,37 +374,20 @@ 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() + key, err := spec.SenderID(requests[i].ServerName).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 { + if err = VerifyJSON(string(requests[i].ServerName), "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 @@ -491,7 +473,9 @@ func (p *PerspectiveKeyFetcher) FetchKeys( // This may be suitable for local deployments that are firewalled from the public internet where DNS can be trusted. type DirectKeyFetcher struct { // The federation client to use to fetch keys with. - Client KeyClient + Client KeyClient + IsLocalServerName func(server spec.ServerName) bool + LocalPublicKey spec.Base64Bytes } // FetcherName implements KeyFetcher @@ -505,8 +489,13 @@ func (d *DirectKeyFetcher) FetchKeys( ) (map[PublicKeyLookupRequest]PublicKeyLookupResult, error) { fetcherLogger := util.GetLogger(ctx).WithField("fetcher", d.FetcherName()) + localServerRequests := []PublicKeyLookupRequest{} byServer := map[spec.ServerName]map[PublicKeyLookupRequest]spec.Timestamp{} for req, ts := range requests { + if d.IsLocalServerName(req.ServerName) { + localServerRequests = append(localServerRequests, req) + continue + } server := byServer[req.ServerName] if server == nil { server = map[PublicKeyLookupRequest]spec.Timestamp{} @@ -526,6 +515,17 @@ func (d *DirectKeyFetcher) FetchKeys( // Prepare somewhere to put the results. This map is protected // by the below mutex. results := map[PublicKeyLookupRequest]PublicKeyLookupResult{} + + // Populate the results map with any requests directed at the local server + localKey := &PublicKeyLookupResult{ + VerifyKey: VerifyKey{Key: d.LocalPublicKey}, + ExpiredTS: PublicKeyNotExpired, + // This must evaluate to a year which is 4 digits (ie. 2020), or the code breaks currently + ValidUntilTS: spec.AsTimestamp(time.Unix(1<<37, 0)), // NOTE: 6325-04-08 15:04:32 +0000 UTC (a date very far in the future) + } + for _, req := range localServerRequests { + results[req] = *localKey + } var resultsMutex sync.Mutex // Populate the wait group with the number of workers. diff --git a/keyring_test.go b/keyring_test.go index 61d92377..b4d2b3ef 100644 --- a/keyring_test.go +++ b/keyring_test.go @@ -365,45 +365,38 @@ func TestJSONVerifierSelf_VerifyJSONs(t *testing.T) { { name: "successfully verified", requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"self":{"ed25519:1":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, + {ServerName: "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk":{"ed25519:1":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, }, want: []VerifyJSONResult{{}}, }, { name: "tempered event", // auth events are removed requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":[],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"self":{"ed25519:1":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, + {ServerName: "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", Message: []byte(`{"auth_events":[],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk":{"ed25519:1":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, }, - want: []VerifyJSONResult{{Error: fmt.Errorf("Bad signature from %q with ID %q", "self", "ed25519:1")}}, + want: []VerifyJSONResult{{Error: fmt.Errorf("Bad signature from %q with ID %q", "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", "ed25519:1")}}, }, { name: "invalid signature", // changed one character for the signature requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"self":{"ed25519:1":"caXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, + {ServerName: "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk":{"ed25519:1":"caXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, }, - want: []VerifyJSONResult{{Error: fmt.Errorf("Bad signature from %q with ID %q", "self", "ed25519:1")}}, + want: []VerifyJSONResult{{Error: fmt.Errorf("Bad signature from %q with ID %q", "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", "ed25519:1")}}, }, { name: "missing signature", // search ed25519:1, only ed25519:2 exists requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"self":{"ed25519:2":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, + {ServerName: "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","signatures":{"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk":{"ed25519:2":"cmXVJmTqnxme1LYl5P5PP5EVtPLJxgGwXpXU2FOEw9FHtHz9WzrfRMRcrO45/55FrBl+g7kEMWEvr9hmOY/VBA"}},"state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, }, - want: []VerifyJSONResult{{Error: fmt.Errorf("No signature from %q with ID %q", "self", "ed25519:1")}}, + want: []VerifyJSONResult{{Error: fmt.Errorf("No signature from %q with ID %q", "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", "ed25519:1")}}, }, { name: "no signatures at all", // signatures field removed requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, + {ServerName: "nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk", Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, }, want: []VerifyJSONResult{{Error: fmt.Errorf("No signatures")}}, }, - { - name: "empty sender", - requests: []VerifyJSONRequest{ - {Message: []byte(`{"auth_events":["$JO2AluDC5p0BXHN_2fUPa2Bup4zO0os74kkw9B5Zg8Q","$tjiFWWvrQ7p9lePYf7NgsH4167NnDV77dbaK5pFAUPM","$uhHChO86B5V9JoStcemXOXzyJQ4vCvzxDdcuF2sswBw"],"content":{"avatar_url":"","displayname":"anon-20230619_110507-7","membership":"join","mxid_mapping":{"signatures":{"localhost:8802":{"ed25519:CrqzCX":"aodhnvDkPTkU69e/AbHhltztMoLpeGfIYMNe+hvQkG54KNyN8MHlL0u5qVkYqTYOcoXrJzZ4dYydmWOi5/TRCw"}},"user_id":"@anon-20230619_110507-7:localhost:8802","user_room_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk"}},"depth":8,"hashes":{"sha256":"sXx8rtwS0n405SuPHUSpICiCYHr+/CCw9jJ3nuwnVqs"},"origin":"self","origin_server_ts":1687172711324,"prev_events":["$8zL0XKqRUOWB9kVQyCjKY7ANLuDtCjLglWd6CWW6XrM"],"prev_state":[],"room_id":"!X0Rr8baajYOjRVQ3:localhost:8800","sender":"","state_key":"nlJIYQHzMN0qNbkrX57e5i0CuUl1CfAdlkw5h1s+TDk","type":"m.room.member","unsigned":{}}`)}, - }, - want: []VerifyJSONResult{{Error: fmt.Errorf("unable to get senderID from event: empty sender")}}, - }, } ctx := context.Background() diff --git a/performinvite.go b/performinvite.go index 97a2da6c..75b2e5bd 100644 --- a/performinvite.go +++ b/performinvite.go @@ -16,40 +16,54 @@ package gomatrixserverlib import ( "context" + "crypto/ed25519" + "fmt" + "time" "github.com/matrix-org/gomatrixserverlib/spec" - "github.com/matrix-org/util" "github.com/sirupsen/logrus" ) +type GetLatestEvents func(ctx context.Context, roomID spec.RoomID, eventsNeeded []StateKeyTuple) (LatestEvents, error) + type PerformInviteInput struct { - RoomID spec.RoomID // The room the user is being invited to join - InvitedUser spec.UserID // The user being invited join the room - InvitedSenderID spec.SenderID // The senderID of the user being invited to join the room - IsTargetLocal bool // Whether the user being invited is local to this server - InviteEvent PDU // The original invite event - StrippedState []InviteStrippedState // A small set of state events that can be used to identify the room - - MembershipQuerier MembershipQuerier // Provides information about the room's membership - StateQuerier StateQuerier // Provides access to state events - UserIDQuerier spec.UserIDForSender // Provides userID for a given senderID + RoomID spec.RoomID // The room the user is being invited to join + RoomVersion RoomVersion + Inviter spec.UserID // The user doing the inviting + Invitee spec.UserID // The user being invited join the room + IsTargetLocal bool // Whether the user being invited is local to this server + EventTemplate ProtoEvent // The original invite event + StrippedState []InviteStrippedState // A small set of state events that can be used to identify the room + KeyID KeyID + SigningKey ed25519.PrivateKey + EventTime time.Time + + MembershipQuerier MembershipQuerier // Provides information about the room's membership + StateQuerier StateQuerier // Provides access to state events + UserIDQuerier spec.UserIDForSender // Provides userID for a given senderID + SenderIDQuerier spec.SenderIDForUser // Provides senderID for a given userID + SenderIDCreator spec.CreateSenderID + EventQuerier GetLatestEvents + StoreSenderIDFromPublicID spec.StoreSenderIDFromPublicID // Creates the senderID -> userID for the room creator } // PerformInvite - Performs all the checks required to validate the invite is allowed // to happen. // On success will return either nothing (in the case of inviting a local user) or // a fully formed & signed Invite Event (in the case of inviting a remote user) +// nolint:gocyclo func PerformInvite(ctx context.Context, input PerformInviteInput, fedClient FederatedInviteClient) (PDU, error) { - if input.MembershipQuerier == nil || input.StateQuerier == nil || input.UserIDQuerier == nil { + if input.MembershipQuerier == nil || input.StateQuerier == nil || input.UserIDQuerier == nil || + input.SenderIDQuerier == nil || input.SenderIDCreator == nil || input.EventQuerier == nil { panic("Missing valid Querier") } if ctx == nil { panic("Missing valid Context") } - logger := createInviteLogger(ctx, input.InviteEvent, input.RoomID) + logger := createInviteLogger(ctx, input.RoomID, input.Inviter, input.Invitee, "") logger.WithFields(logrus.Fields{ - "room_version": input.InviteEvent.Version(), + "room_version": input.RoomVersion, "target_local": input.IsTargetLocal, "origin_local": true, }).Debug("processing invite event") @@ -57,55 +71,213 @@ func PerformInvite(ctx context.Context, input PerformInviteInput, fedClient Fede inviteState := input.StrippedState if len(inviteState) == 0 { var err error - inviteState, err = GenerateStrippedState(ctx, input.RoomID, input.InviteEvent, input.StateQuerier) + inviteState, err = GenerateStrippedState(ctx, input.RoomID, input.StateQuerier) if err != nil { - util.GetLogger(ctx).WithError(err).Error("failed generating stripped state") + logger.WithError(err).Error("failed generating stripped state") return nil, spec.InternalServerError{} } } - err := abortIfAlreadyJoined(ctx, input.RoomID, input.InvitedSenderID, input.MembershipQuerier) + err := setUnsignedFieldForProtoInvite(&input.EventTemplate, inviteState) if err != nil { return nil, err } - err = setUnsignedFieldForInvite(input.InviteEvent, inviteState) + // Check that we can accept invites for this room version. + verImpl, err := GetRoomVersion(input.RoomVersion) + if err != nil { + return nil, spec.UnsupportedRoomVersion( + fmt.Sprintf("Room version %q is not supported by this server.", input.RoomVersion), + ) + } + + invitedSenderID, err := input.SenderIDQuerier(input.RoomID, input.Invitee) if err != nil { return nil, err } - // The invite originated locally. Therefore we have a responsibility to - // try and see if the user is allowed to make this invite. We can't do - // this for invites coming in over federation - we have to take those on - // trust. - authEventProvider, err := input.StateQuerier.GetAuthEvents(ctx, input.InviteEvent) + if invitedSenderID != "" { + err = abortIfAlreadyJoined(ctx, input.RoomID, invitedSenderID, input.MembershipQuerier) + if err != nil { + return nil, err + } + } + + stateNeeded, err := StateNeededForProtoEvent(&input.EventTemplate) if err != nil { - logger.WithError(err).WithField("event_id", input.InviteEvent.EventID()).WithField("auth_event_ids", input.InviteEvent.AuthEventIDs()).Error( - "ProcessInvite.getAuthEvents failed for event", - ) - return nil, spec.Forbidden(err.Error()) + return nil, err } - // Check if the event is allowed. - if err = Allowed(input.InviteEvent, authEventProvider, input.UserIDQuerier); err != nil { - logger.WithError(err).WithField("event_id", input.InviteEvent.EventID()).WithField("auth_event_ids", input.InviteEvent.AuthEventIDs()).Error( - "ProcessInvite: event not allowed", - ) - return nil, spec.Forbidden(err.Error()) + if len(stateNeeded.Tuples()) == 0 { + return nil, spec.InternalServerError{} + } + + latestEvents, err := input.EventQuerier(ctx, input.RoomID, stateNeeded.Tuples()) + if err != nil { + return nil, err + } + + if !latestEvents.RoomExists { + return nil, spec.InternalServerError{} + } + + input.EventTemplate.Depth = latestEvents.Depth + + authEvents := NewAuthEvents(nil) + + for _, event := range latestEvents.StateEvents { + err := authEvents.AddEvent(event) + if err != nil { + return nil, fmt.Errorf("authEvents.AddEvent: %w", err) + } + } + + refs, err := stateNeeded.AuthEventReferences(&authEvents) + if err != nil { + return nil, fmt.Errorf("eventsNeeded.AuthEventReferences: %w", err) + } + + input.EventTemplate.AuthEvents, input.EventTemplate.PrevEvents = truncateAuthAndPrevEvents(refs, latestEvents.PrevEventIDs) + + checkEventAllowed := func(inviteEvent PDU) error { + // The invite originated locally. Therefore we have a responsibility to + // try and see if the user is allowed to make this invite. We can't do + // this for invites coming in over federation - we have to take those on + // trust. + authEventProvider, err := input.StateQuerier.GetAuthEvents(ctx, inviteEvent) + if err != nil { + logger.WithError(err).WithField("event_id", inviteEvent.EventID()).WithField("auth_event_ids", inviteEvent.AuthEventIDs()).Error( + "ProcessInvite.getAuthEvents failed for event", + ) + return spec.Forbidden(err.Error()) + } + + // Check if the event is allowed. + if err = Allowed(inviteEvent, authEventProvider, input.UserIDQuerier); err != nil { + logger.WithError(err).WithField("event_id", inviteEvent.EventID()).WithField("auth_event_ids", inviteEvent.AuthEventIDs()).Error( + "ProcessInvite: event not allowed", + ) + return spec.Forbidden(err.Error()) + } + + return nil } // If the target isn't local then we should send the invite // over federation. It might be that the remote user doesn't exist, // in which case we can give up processing here. - var signedEvent PDU - if !input.IsTargetLocal { - signedEvent, err = fedClient.SendInvite(ctx, input.InviteEvent, inviteState) + var inviteEvent PDU + switch input.RoomVersion { + case RoomVersionPseudoIDs: + keyID := KeyID("ed25519:1") + origin := spec.ServerName(spec.SenderIDFromPseudoIDKey(input.SigningKey)) + + if input.IsTargetLocal { + // if we invited a local user, we can also create a user room key, if it doesn't exist yet. + inviteeSenderID, inviteeSigningKey, err := input.SenderIDCreator(ctx, input.Invitee, input.RoomID, string(input.RoomVersion)) + if err != nil { + return nil, err + } + + inviteeSenderIDString := string(inviteeSenderID) + input.EventTemplate.StateKey = &inviteeSenderIDString + + // Sign the event so that other servers will know that we have received the invite. + fullEventBuilder := verImpl.NewEventBuilderFromProtoEvent(&input.EventTemplate) + inviteEvent, err = fullEventBuilder.Build(input.EventTime, origin, keyID, input.SigningKey) + if err != nil { + logger.WithError(err).Error("failed building invite event") + return nil, spec.InternalServerError{} + } + + // Have the invitee also sign the event + inviteEvent = inviteEvent.Sign(string(origin), keyID, inviteeSigningKey) + + err = checkEventAllowed(inviteEvent) + if err != nil { + return nil, err + } + } else { + inviteEvent, err = fedClient.SendInviteV3(ctx, input.EventTemplate, input.Invitee, input.RoomVersion, inviteState) + if err != nil { + logger.WithError(err).Error("fedClient.SendInviteV3 failed") + return nil, spec.Forbidden(err.Error()) + } + logger.Debugf("Federated SendInviteV3 success to user %s", input.Invitee.String()) + + inviteEvent = inviteEvent.Sign( + string(origin), keyID, input.SigningKey, + ) + + verifier := JSONVerifierSelf{} + err := VerifyEventSignatures(ctx, inviteEvent, verifier, input.UserIDQuerier) + if err != nil { + logger.WithError(err).Error("fedClient.SendInviteV3 returned event with invalid signatures") + return nil, spec.Forbidden(err.Error()) + } + + err = input.StoreSenderIDFromPublicID(ctx, spec.SenderID(*inviteEvent.StateKey()), input.Invitee.String(), input.RoomID) + if err != nil { + logger.WithError(err).Errorf("failed storing senderID for %s", input.Invitee.String()) + return nil, spec.InternalServerError{} + } + + // TODO: This should happen before the federation call ideally, + // but we don't have a full PDU yet in this case by that point. + err = checkEventAllowed(inviteEvent) + if err != nil { + return nil, err + } + } + default: + inviteeSenderID := input.Invitee.String() + input.EventTemplate.StateKey = &inviteeSenderID + + // Sign the event so that other servers will know that we have received the invite. + fullEventBuilder := verImpl.NewEventBuilderFromProtoEvent(&input.EventTemplate) + fullEvent, err := fullEventBuilder.Build(input.EventTime, input.Inviter.Domain(), input.KeyID, input.SigningKey) + if err != nil { + logger.WithError(err).Error("failed building invite event") + return nil, spec.InternalServerError{} + } + + inviteEvent = fullEvent.Sign( + string(input.Invitee.Domain()), input.KeyID, input.SigningKey, + ) + + err = checkEventAllowed(inviteEvent) if err != nil { - logger.WithError(err).WithField("event_id", input.InviteEvent.EventID()).Error("fedClient.SendInvite failed") - return nil, spec.Forbidden(err.Error()) + return nil, err + } + + if !input.IsTargetLocal { + eventID := inviteEvent.EventID() + inviteEvent, err = fedClient.SendInvite(ctx, inviteEvent, inviteState) + if err != nil { + logger.WithError(err).WithField("event_id", eventID).Error("fedClient.SendInvite failed") + return nil, spec.Forbidden(err.Error()) + } + logger.Debugf("Federated SendInvite success with event ID %s", eventID) } - logger.Debugf("Federated SendInvite success with event ID %s", input.InviteEvent.EventID()) } - return signedEvent, nil + return inviteEvent, nil +} + +// truncateAuthAndPrevEvents limits the number of events we add into +// an event as prev_events or auth_events. +// NOTSPEC: The limits here feel a bit arbitrary but they are currently +// here because of https://github.com/matrix-org/matrix-doc/issues/2307 +// and because Synapse will just drop events that don't comply. +func truncateAuthAndPrevEvents(auth, prev []string) ( + truncAuth, truncPrev []string, +) { + truncAuth, truncPrev = auth, prev + if len(truncAuth) > 10 { + truncAuth = truncAuth[:10] + } + if len(truncPrev) > 20 { + truncPrev = truncPrev[:20] + } + return } diff --git a/performinvite_test.go b/performinvite_test.go index 34c3ee58..00a02685 100644 --- a/performinvite_test.go +++ b/performinvite_test.go @@ -13,6 +13,22 @@ import ( "golang.org/x/crypto/ed25519" ) +func SenderIDForUserTest(roomID spec.RoomID, userID spec.UserID) (spec.SenderID, error) { + return spec.SenderID(userID.String()), nil +} + +func CreateSenderID(ctx context.Context, userID spec.UserID, roomID spec.RoomID, roomVersion string) (spec.SenderID, ed25519.PrivateKey, error) { + _, key, err := ed25519.GenerateKey(nil) + if err != nil { + panic("failed generating ed25519 key") + } + return spec.SenderID(userID.String()), key, nil +} + +func StoreSenderIDTest(ctx context.Context, senderID spec.SenderID, userID string, id spec.RoomID) error { + return nil +} + type TestFederatedInviteClient struct { shouldFail bool } @@ -24,6 +40,62 @@ func (f *TestFederatedInviteClient) SendInvite(ctx context.Context, event PDU, s return nil, nil } +func (f *TestFederatedInviteClient) SendInviteV3(ctx context.Context, event ProtoEvent, userID spec.UserID, roomVersion RoomVersion, strippedState []InviteStrippedState) (PDU, error) { + if f.shouldFail { + return nil, fmt.Errorf("failed sending invite") + } + + _, sk, _ := ed25519.GenerateKey(rand.Reader) + keyID := KeyID("ed25519:1") + + verImpl, err := GetRoomVersion(roomVersion) + if err != nil { + return nil, err + } + + stateKey := string(spec.SenderIDFromPseudoIDKey(sk)) + event.StateKey = &stateKey + eb := verImpl.NewEventBuilderFromProtoEvent(&event) + inviteEvent, err := eb.Build(time.Now(), spec.ServerName(stateKey), keyID, sk) + + return inviteEvent, err +} + +type TestEventQuerier struct { + createEvent PDU +} + +func (q *TestEventQuerier) GetLatestEventsTest(ctx context.Context, roomID spec.RoomID, eventsNeeded []StateKeyTuple) (LatestEvents, error) { + stateEvents := []PDU{} + prevEvents := []string{} + for _, event := range eventsNeeded { + switch event.EventType { + case spec.MRoomCreate: + stateEvents = append(stateEvents, q.createEvent) + } + prevEvents = append(prevEvents, "random_event_id") + } + return LatestEvents{ + RoomExists: true, + StateEvents: stateEvents, + PrevEventIDs: prevEvents, + }, nil +} + +func createMemberProtoEvent(sender string, roomID string, stateKey *string, content spec.RawJSON) ProtoEvent { + return ProtoEvent{ + SenderID: sender, + RoomID: roomID, + Type: "m.room.member", + StateKey: stateKey, + PrevEvents: []interface{}{}, + AuthEvents: []interface{}{}, + Depth: 0, + Content: content, + Unsigned: spec.RawJSON(""), + } +} + func TestPerformInvite(t *testing.T) { inviteeID, err := spec.NewUserID("@invitee:server", true) assert.Nil(t, err) @@ -39,14 +111,10 @@ func TestPerformInvite(t *testing.T) { keyID := KeyID("ed25519:1234") stateKey := inviteeID.String() - eb := createMemberEventBuilder(RoomVersionV10, inviterID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) - inviteEvent, err := eb.Build(time.Now(), inviteeID.Domain(), keyID, sk) - assert.Nil(t, err) + inviteEvent := createMemberProtoEvent(inviterID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) stateKey = inviteeIDRemote.String() - inviteRemoteEB := createMemberEventBuilder(RoomVersionV10, inviterID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) - inviteEventRemote, err := inviteRemoteEB.Build(time.Now(), inviteeIDRemote.Domain(), keyID, sk) - assert.Nil(t, err) + inviteEventRemote := createMemberProtoEvent(inviterID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) stateKey = inviterID.String() inviterMemberEventEB := createMemberEventBuilder(RoomVersionV10, inviterID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"join"}`)) @@ -70,6 +138,8 @@ func TestPerformInvite(t *testing.T) { t.Fatalf("Failed building create event: %v", err) } + eventQuerier := TestEventQuerier{createEvent: createEvent} + type ErrorType int const ( InternalErr ErrorType = iota @@ -86,13 +156,20 @@ func TestPerformInvite(t *testing.T) { "not_allowed_by_auth_events": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeID, + RoomVersion: RoomVersionV10, + Invitee: *inviteeID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: true, @@ -102,13 +179,20 @@ func TestPerformInvite(t *testing.T) { "auth_provider_error": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeID, + RoomVersion: RoomVersionV10, + Invitee: *inviteeID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{shouldFailAuth: true}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: true, @@ -118,13 +202,20 @@ func TestPerformInvite(t *testing.T) { "state_provider_error": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeID, + RoomVersion: RoomVersionV10, + Invitee: *inviteeID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{shouldFailState: true}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: true, @@ -133,13 +224,20 @@ func TestPerformInvite(t *testing.T) { "already_joined_failure": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeID, + RoomVersion: RoomVersionV10, + Invitee: *inviteeID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{membership: spec.Join}, StateQuerier: &TestStateQuerier{}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: true, @@ -149,13 +247,20 @@ func TestPerformInvite(t *testing.T) { "remote_invite_federation_error": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeIDRemote, + RoomVersion: RoomVersionV10, + Invitee: *inviteeIDRemote, IsTargetLocal: false, - InviteEvent: inviteEventRemote, + EventTemplate: inviteEventRemote, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterMemberEvent}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{shouldFail: true}, expectedErr: true, @@ -165,13 +270,20 @@ func TestPerformInvite(t *testing.T) { "success_local": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeID, + RoomVersion: RoomVersionV10, + Invitee: *inviteeID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterMemberEvent}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: false, @@ -179,13 +291,20 @@ func TestPerformInvite(t *testing.T) { "success_remote": { input: PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *inviteeIDRemote, + RoomVersion: RoomVersionV10, + Invitee: *inviteeIDRemote, IsTargetLocal: false, - InviteEvent: inviteEventRemote, + EventTemplate: inviteEventRemote, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterMemberEvent}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, fedClient: &TestFederatedInviteClient{}, expectedErr: false, @@ -227,20 +346,26 @@ func TestPerformInviteNilMembershipQuerier(t *testing.T) { keyID := KeyID("ed25519:1234") stateKey := userID.String() - 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) + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} assert.Panics(t, func() { _, _ = PerformInvite(context.Background(), PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *userID, + RoomVersion: RoomVersionV10, + Invitee: *userID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: nil, StateQuerier: &TestStateQuerier{}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, &TestFederatedInviteClient{}) }) } @@ -256,20 +381,26 @@ func TestPerformInviteNilStateQuerier(t *testing.T) { keyID := KeyID("ed25519:1234") stateKey := userID.String() - 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) + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} assert.Panics(t, func() { _, _ = PerformInvite(context.Background(), PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *userID, + RoomVersion: RoomVersionV10, + Invitee: *userID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: nil, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, &TestFederatedInviteClient{}) }) } @@ -285,25 +416,66 @@ func TestPerformInviteNilUserIDQuerier(t *testing.T) { keyID := KeyID("ed25519:1234") stateKey := userID.String() - 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) + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} assert.Panics(t, func() { _, _ = PerformInvite(context.Background(), PerformInviteInput{ RoomID: *validRoom, - InvitedUser: *userID, + RoomVersion: RoomVersionV10, + Invitee: *userID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{}, UserIDQuerier: nil, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, &TestFederatedInviteClient{}) }) } -func TestPerformInviteNilContext(t *testing.T) { +func TestPerformInviteNilSenderIDQuerier(t *testing.T) { + userID, err := spec.NewUserID("@user:server", true) + assert.Nil(t, err) + validRoom, err := spec.NewRoomID("!room:remote") + assert.Nil(t, err) + + _, sk, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(t, err) + keyID := KeyID("ed25519:1234") + + stateKey := userID.String() + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} + + assert.Panics(t, func() { + _, _ = PerformInvite(context.Background(), PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + Invitee: *userID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: nil, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + }, &TestFederatedInviteClient{}) + }) +} + +func TestPerformInviteNilSenderIDCreator(t *testing.T) { userID, err := spec.NewUserID("@user:server", true) assert.Nil(t, err) validRoom, err := spec.NewRoomID("!room:remote") @@ -314,20 +486,374 @@ func TestPerformInviteNilContext(t *testing.T) { keyID := KeyID("ed25519:1234") stateKey := userID.String() - eb := createMemberEventBuilder(RoomVersionV10, userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) - inviteEvent, err := eb.Build(time.Now(), userID.Domain(), keyID, sk) + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} + + assert.Panics(t, func() { + _, _ = PerformInvite(context.Background(), PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + Invitee: *userID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: nil, + EventQuerier: eventQuerier.GetLatestEventsTest, + }, &TestFederatedInviteClient{}) + }) +} + +func TestPerformInviteNilEventQuerier(t *testing.T) { + userID, err := spec.NewUserID("@user:server", true) + assert.Nil(t, err) + validRoom, err := spec.NewRoomID("!room:remote") assert.Nil(t, err) + _, sk, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(t, err) + keyID := KeyID("ed25519:1234") + + stateKey := userID.String() + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + + assert.Panics(t, func() { + _, _ = PerformInvite(context.Background(), PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionV10, + Invitee: *userID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: nil, + }, &TestFederatedInviteClient{}) + }) +} + +func TestPerformInviteNilContext(t *testing.T) { + userID, err := spec.NewUserID("@user:server", true) + assert.Nil(t, err) + validRoom, err := spec.NewRoomID("!room:remote") + assert.Nil(t, err) + + _, sk, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(t, err) + keyID := KeyID("ed25519:1234") + + stateKey := userID.String() + inviteEvent := createMemberProtoEvent(userID.String(), validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + eventQuerier := TestEventQuerier{} + assert.Panics(t, func() { _, _ = PerformInvite(nil, PerformInviteInput{ // nolint RoomID: *validRoom, - InvitedUser: *userID, + RoomVersion: RoomVersionV10, + Invitee: *userID, IsTargetLocal: true, - InviteEvent: inviteEvent, + EventTemplate: inviteEvent, StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: sk, + EventTime: time.Now(), MembershipQuerier: &TestMembershipQuerier{}, StateQuerier: &TestStateQuerier{}, UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, }, &TestFederatedInviteClient{}) }) } + +func TestPerformInvitePseudoIDs(t *testing.T) { + inviteeID, err := spec.NewUserID("@invitee:server", true) + assert.Nil(t, err) + inviteeIDRemote, err := spec.NewUserID("@invitee:remote", true) + assert.Nil(t, err) + + inviterID, err := spec.NewUserID("@inviter:server", true) + assert.Nil(t, err) + _, inviterKey, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(t, err) + + inviterPseudoID := string(spec.SenderIDFromPseudoIDKey(inviterKey)) + + validRoom, err := spec.NewRoomID("!room:remote") + assert.Nil(t, err) + + keyID := KeyID("ed25519:1234") + + stateKey := inviteeID.String() + inviteEvent := createMemberProtoEvent(inviterPseudoID, validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + + stateKey = inviteeIDRemote.String() + inviteEventRemote := createMemberProtoEvent(inviterPseudoID, validRoom.String(), &stateKey, spec.RawJSON(`{"membership":"invite"}`)) + + rv := RoomVersionPseudoIDs + federate := true + cr := CreateContent{Creator: inviterPseudoID, RoomVersion: &rv, Federate: &federate} + crBytes, err := json.Marshal(cr) + assert.Nil(t, err) + + stateKey = "" + createEventEB := MustGetRoomVersion(rv).NewEventBuilderFromProtoEvent(&ProtoEvent{ + SenderID: inviterPseudoID, + RoomID: validRoom.String(), + Type: "m.room.create", + StateKey: &stateKey, + PrevEvents: []interface{}{}, + AuthEvents: []interface{}{}, + Depth: 1, + Content: crBytes, + Unsigned: spec.RawJSON(""), + }) + createEvent, err := createEventEB.Build(time.Now(), spec.ServerName(inviterPseudoID), "ed25519:1", inviterKey) + if err != nil { + t.Fatalf("Failed building create event: %v", err) + } + + mapping := MXIDMapping{UserID: inviterID.String(), UserRoomKey: spec.SenderID(inviterPseudoID)} + err = mapping.Sign("server", keyID, inviterKey) + assert.Nil(t, err) + content := MemberContent{Membership: spec.Join, MXIDMapping: &mapping} + contentBytes, err := json.Marshal(content) + assert.Nil(t, err) + + stateKey = inviterPseudoID + inviterJoinEB := MustGetRoomVersion(rv).NewEventBuilderFromProtoEvent(&ProtoEvent{ + SenderID: inviterPseudoID, + RoomID: validRoom.String(), + Type: "m.room.member", + StateKey: &stateKey, + PrevEvents: []interface{}{createEvent.EventID()}, + AuthEvents: []interface{}{createEvent.EventID()}, + Depth: 2, + Content: contentBytes, + Unsigned: spec.RawJSON(""), + }) + inviterJoinEvent, err := inviterJoinEB.Build(time.Now(), spec.ServerName(inviterPseudoID), "ed25519:1", inviterKey) + if err != nil { + t.Fatalf("Failed building create event: %v", err) + } + + eventQuerier := TestEventQuerier{createEvent: createEvent} + + type ErrorType int + const ( + InternalErr ErrorType = iota + MatrixErr + ) + + userIDForSender := func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) { + return spec.NewUserID(inviterID.String(), true) + } + + tests := map[string]struct { + input PerformInviteInput + fedClient FederatedInviteClient + expectedErr bool + errType ErrorType + errCode spec.MatrixErrorCode + }{ + "not_allowed_by_auth_events": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorForbidden, + }, + "auth_provider_error": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{shouldFailAuth: true}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorForbidden, + }, + "state_provider_error": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{shouldFailState: true}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: true, + errType: InternalErr, + }, + "already_joined_failure": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{membership: spec.Join}, + StateQuerier: &TestStateQuerier{}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorForbidden, + }, + "remote_invite_federation_error": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeIDRemote, + IsTargetLocal: false, + EventTemplate: inviteEventRemote, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterJoinEvent}, + UserIDQuerier: UserIDForSenderTest, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{shouldFail: true}, + expectedErr: true, + errType: MatrixErr, + errCode: spec.ErrorForbidden, + }, + "success_local": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeID, + IsTargetLocal: true, + EventTemplate: inviteEvent, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterJoinEvent}, + UserIDQuerier: userIDForSender, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: false, + }, + "success_remote": { + input: PerformInviteInput{ + RoomID: *validRoom, + RoomVersion: RoomVersionPseudoIDs, + Invitee: *inviteeIDRemote, + IsTargetLocal: false, + EventTemplate: inviteEventRemote, + StrippedState: []InviteStrippedState{}, + KeyID: keyID, + SigningKey: inviterKey, + EventTime: time.Now(), + MembershipQuerier: &TestMembershipQuerier{}, + StateQuerier: &TestStateQuerier{createEvent: createEvent, inviterMemberEvent: inviterJoinEvent}, + UserIDQuerier: userIDForSender, + SenderIDQuerier: SenderIDForUserTest, + SenderIDCreator: CreateSenderID, + EventQuerier: eventQuerier.GetLatestEventsTest, + StoreSenderIDFromPublicID: StoreSenderIDTest, + }, + fedClient: &TestFederatedInviteClient{}, + expectedErr: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + _, joinErr := PerformInvite(context.Background(), tc.input, tc.fedClient) + if tc.expectedErr { + switch e := joinErr.(type) { + case nil: + t.Fatalf("Error should not be nil") + case spec.InternalServerError: + assert.Equal(t, tc.errType, InternalErr) + case spec.MatrixError: + assert.Equal(t, tc.errType, MatrixErr) + assert.Equal(t, tc.errCode, e.ErrCode) + default: + t.Fatalf("Unexpected Error Type") + } + } else { + jsonBytes, err := json.Marshal(&joinErr) + assert.Nil(t, err) + assert.Nil(t, joinErr, string(jsonBytes)) + } + }) + } +} diff --git a/performjoin.go b/performjoin.go index 60340b5d..f0f319fd 100644 --- a/performjoin.go +++ b/performjoin.go @@ -133,7 +133,7 @@ func PerformJoin( } } keyID = "ed25519:1" - origin = "self" + origin = spec.ServerName(senderID) mapping := MXIDMapping{ UserRoomKey: senderID, diff --git a/performjoin_test.go b/performjoin_test.go index b7759a80..7d0e2e3c 100644 --- a/performjoin_test.go +++ b/performjoin_test.go @@ -353,7 +353,7 @@ func TestPerformJoinPseudoID(t *testing.T) { Content: crBytes, Unsigned: spec.RawJSON(""), }) - createEvent, err := eb.Build(time.Now(), "self", "ed25519:1", userPriv) + createEvent, err := eb.Build(time.Now(), spec.ServerName(pseudoID), "ed25519:1", userPriv) if err != nil { t.Fatalf("Failed building create event: %v", err) } @@ -371,7 +371,7 @@ func TestPerformJoinPseudoID(t *testing.T) { Unsigned: spec.RawJSON(""), } joinEB := MustGetRoomVersion(rv).NewEventBuilderFromProtoEvent(&joinProto) - joinEvent, err := joinEB.Build(time.Now(), "self", "ed25519:1", sk) + joinEvent, err := joinEB.Build(time.Now(), spec.ServerName(spec.SenderIDFromPseudoIDKey(sk)), "ed25519:1", sk) if err != nil { t.Fatalf("Failed building create event: %v", err) } diff --git a/spec/senderid.go b/spec/senderid.go index 473f9e36..44954c15 100644 --- a/spec/senderid.go +++ b/spec/senderid.go @@ -23,6 +23,7 @@ import ( type SenderID string type UserIDForSender func(roomID RoomID, senderID SenderID) (*UserID, error) +type SenderIDForUser func(roomID RoomID, userID UserID) (SenderID, error) // CreateSenderID is a function used to create the pseudoID private key. type CreateSenderID func(ctx context.Context, userID UserID, roomID RoomID, roomVersion string) (SenderID, ed25519.PrivateKey, error)