diff --git a/proposals/2730-verifiable-forwarded-events.md b/proposals/2730-verifiable-forwarded-events.md new file mode 100644 index 00000000..01cfb684 --- /dev/null +++ b/proposals/2730-verifiable-forwarded-events.md @@ -0,0 +1,289 @@ +# Verifiable forwarded events +This is an alternative to [MSC2723](https://github.com/matrix-org/matrix-doc/pull/2723) +that handles the issue of faking forwards. + +## Proposal +The proposed solution is copying the entire federation event data, which allows +recipients to validate the signatures even if they are not in the origin room. + +As clients generally don't have access to signatures nor any way to validate +them, both sending and validating require server support. Sending is +implemented as a new endpoint, while validating happens automatically and the +server adds the validation result to the top-level `unsigned` object. + +### `PUT /_matrix/client/r0/rooms/{roomId}/event/{eventId}/forward/{targetRoomId}/{txnId}` +This endpoint requests the server to find `eventId` from `roomId` and forward +it to `targetRoomId`. The `txnId` behaves the same way as in the `/send` +endpoint. The request body has an optional `decryption_keys` field that will be +copied to `content`->`m.forwarded`->`unsigned` in the resulting event when +present. The content of the `decryption_keys` object varies based on whether or +not the target room is encrypted, see the "Encrypted events" section below. + +Only unredacted message events can be forwarded. If the given event ID is a +state event, a redaction or a redacted message event, the request will be +rejected with a standard error response using the code `M_NOT_FORWARDABLE`. + +If the generated event is too large, the request is rejected with a standard +error response using the code `M_TOO_LARGE`. Before rejecting the request, +servers MAY check if the event would be small enough without the profile data +in `unsigned`, and send the event without that data if it is. + +Similar to the `/send` endpoint, this endpoint returns an object containing the +`event_id` of the forwarded event. + +#### Generating forwarded event +To forward an event, the server creates a new event with the same event type +and normal top-level fields. To determine the content, the server has to +inspect the content of the source event: + +* If the source event was already forwarded from some other room, the `content` + should simply be copied with no modifications. This means that an event + forwarded many times will only remember the original source, not any hops it + made on the way. +* If the source event was not a forward, but contains (invalid) data in the + `m.forwarded` key, the request will be rejected with `M_NOT_FORWARDABLE` like + other unforwardable events. This limitation also can be used to intentionally + mark messages as unforwardable (e.g. `"m.forwarded": {"allow": false}`). +* If the source event does not contain `m.forwarded` at all, the server must + generate a new one. After generating the object, it is placed in `content` of + the new event along with everything from the `content` of the source event. + +##### Generating `m.forwarded` object +`m.forwarded` is an object that contains all the top-level keys of the source +event, except for `type`, `content` and `unsigned`. The following keys are +therefore at least required: + +* `auth_events` +* `prev_events` +* `room_id` +* `sender` +* `depth` +* `origin` +* `origin_server_ts` +* `hashes` +* `signatures` + +The following keys may also be present: + +* `prev_state`, may be present as an empty array even in non-state events +* `event_id`, only in v1 and v2 rooms + +Additionally, the server MUST include an `unsigned` object, containing a +`room_version` field that specifies the version of the source room. The server +SHOULD also include the sender's profile metadata in the unsigned object under +the fields `displayname` and `avatar_url`. + +#### Example + +
+Source event (federation format) + +```json +{ + "auth_events": [ + "$wChClfXonLE8RZikJ446AXvRpbh_JjDK8sNpMpZbqPs", + "$RaXN_RayMvoEmMnUHlZScIdSpShT8zggd4p6qcQk9L8", + "$kFop6R7AohiYSTh_ijUctTujdVTg3rwBPdaMLeZMNrg" + ], + "prev_events": [ + "$pIFO6_sI1Ul_3jPixtbnJn_h0Pe0yB__TJD_VCW9Q-Q" + ], + "type": "m.room.message", + "room_id": "!FIIWlyqwNLyMAtmRBF:maunium.net", + "sender": "@tulir:maunium.net", + "content": { + "msgtype": "m.text", + "body": "test" + }, + "depth": 115, + "prev_state": [], + "origin": "maunium.net", + "origin_server_ts": 1597257769634, + "hashes": { + "sha256": "xBR7NmH2WQBx0auQWEDEYNbcPf9ATlDSwkv9EBxueMI" + }, + "signatures": { + "maunium.net": { + "ed25519:a_xxeS": "cc9XnH9ByO7yadC6CdMhh3c/TN1tQ9FiZdKYyRDi4Og1dZMylmBM9uSI7c4GUEqswLBLxW5DTFU3n7vMHAGhAw" + } + }, + "unsigned": { + "age_ts": 1597257769634 + } +} +``` + +
+ +Request: + +``` +PUT /_matrix/client/r0/rooms/!FIIWlyqwNLyMAtmRBF:maunium.net/event/$BfxMy-oNFOeE0eFt6r-l3h7MtwNVIX0GrructyJq1wA/forward/!eVRGrjZQgJZGNllOkw:grin.hu/myTxnId1 +{} +``` + +Response: + +```json +{ + "event_id": "$r8h8W9A5KS8D65_Df8fwLkTe7aqOm48KmyaJ6tRNAmE" +} +``` + +
+Forwarded event (client format) + +```json +{ + "type": "m.room.message", + "room_id": "!eVRGrjZQgJZGNllOkw:grin.hu", + "event_id": "$r8h8W9A5KS8D65_Df8fwLkTe7aqOm48KmyaJ6tRNAmE", + "sender": "@tulir:maunium.net", + "origin_server_ts": 1597263764138, + "content": { + "msgtype": "m.text", + "body": "test", + "m.forwarded": { + "auth_events": [ + "$wChClfXonLE8RZikJ446AXvRpbh_JjDK8sNpMpZbqPs", + "$RaXN_RayMvoEmMnUHlZScIdSpShT8zggd4p6qcQk9L8", + "$kFop6R7AohiYSTh_ijUctTujdVTg3rwBPdaMLeZMNrg" + ], + "prev_events": [ + "$pIFO6_sI1Ul_3jPixtbnJn_h0Pe0yB__TJD_VCW9Q-Q" + ], + "room_id": "!FIIWlyqwNLyMAtmRBF:maunium.net", + "sender": "@tulir:maunium.net", + "depth": 115, + "prev_state": [], + "origin": "maunium.net", + "origin_server_ts": 1597257769634, + "hashes": { + "sha256": "xBR7NmH2WQBx0auQWEDEYNbcPf9ATlDSwkv9EBxueMI" + }, + "signatures": { + "maunium.net": { + "ed25519:a_xxeS": "cc9XnH9ByO7yadC6CdMhh3c/TN1tQ9FiZdKYyRDi4Og1dZMylmBM9uSI7c4GUEqswLBLxW5DTFU3n7vMHAGhAw" + } + }, + "unsigned": { + "displayname": "tulir", + "avatar_url": "mxc://maunium.net/jdlSfvudiMSmcRrleeiYjjFO" + } + } + }, + "unsigned": { + "m.forwarded": { + "valid": true, + "event_id": "$BfxMy-oNFOeE0eFt6r-l3h7MtwNVIX0GrructyJq1wA" + } + } +} +``` + +
+ +### Receiving events with `m.forwarded` +When a server receives a message event that has the `m.forwarded` key in its +`content`, the server MUST use the data to validate the signatures, then add a +`m.forwarded` key to the top-level `unsigned` of the event with the validation +information. + +#### Validating signatures +To validate a signature, the server should start with the `m.forwarded` object +and modify it as follows: + +* If the object is missing any of the required keys, mark it as invalid without + trying to validate it. +* Remove the `unsigned` key (if present). +* Copy `type` from the top level into `m.forwarded`. +* Make a copy of the top-level `content`, remove `m.forwarded` and put it in + the `m.forwarded`. +* Using the result object, validate the signature, calculate the reference hash + and check the content hash of the event as specified in sections 26.2 through + 26.4 of the server-server specification: https://matrix.org/docs/spec/server_server/r0.1.4#validating-hashes-and-signatures-on-received-events + +#### Unsigned `m.forwarded` object +For any message event with `m.forwarded` in the content, the server MUST add or +override the `m.forwarded` key in the `unsigned` object of the event. The key +MUST be an object that contains the keys `valid` and `event_id`. + +If the `m.forwarded` object was valid and the signatures were validated, the +`valid` value should be `true`. In any other case (invalid signature, bogus +data, etc), the value should be `false`. + +In v1 and v2 rooms, the `event_id` is copied from the `m.forwarded` object in +`content`. In v3 and up, the `event_id` is based on the reference hash that was +calculated in the previous section. Copying the event ID in v1/v2 rooms is for +convenience of clients: they only need to look in one place regardless of the +room version. + +### Encrypted events +In some cases, users may want to forward encrypted messages to rooms with users +who are not in the origin room. In order to allow everyone in the recipient +room to decrypt the forwarded message, the keys must be sent with the message. +However, only keys for the message being forwarded should be sent, any other +messages in the origin room must not be decryptable with those keys. + +To achieve this, the user forwarding the message includes the message-specific +symmetric AES and HMAC keys (see [Message encryption] in the Megolm spec). Each +of the keys are encoded as unpadded base64 and placed in the `aes_key`, +`hmac_key` and `aes_iv` fields in the `decryption_keys` object in the forward +request. + +[Message encryption]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md#message-encryption + +When forwarding encrypted messages to encrypted rooms, the `decryption_keys` +object is encrypted the same way `content` would be in normal messages. +Recipient clients should check which fields are present in the `decryption_keys` +object to determine whether or not it is encrypted. + +The decryption keys should be included even if forwarding a message to the same +room, as there may be new users in the room who didn't receive keys to old +messages. + +## Client behavior +Clients SHOULD NOT trust forward metadata in the event content without an +explicit `"valid": true` in the unsigned `m.forwarded` object. Additionally, +clients SHOULD make sure the server supports this proposal before trusting +forwards even if the `valid` flag is present. + +Not trusting forward metadata does not necessarily mean it must be completely +ignored. For example, clients could render the event as a forward, but include +a notice saying it's unverified. + +When receiving forwarded encrypted events, clients should treat the message +like they treat forwarded keys, i.e. not confirmed to originate from the user. + +Clients may discourage users from forwarding encrypted messages to unencrypted +rooms, as that would leak the message content to the servers. + +If a forwarded event contains relation metadata such as a reply, clients should +not display it to users. This behavior is consistent with other platforms (e.g. +Telegram and WhatsApp) and removes any problems if some users can't get the +relation target event. The existence of reply metadata may still be used to +remove reply fallbacks. + +## Potential issues +* This is not as simple as MSC2723 and requires server support. +* Events with bogus data in `m.forwarded` can't be forwarded. +* Events that are close to the 64 KiB size limit can't be forwarded. MSC2723 + has the same problem, but this proposal has even more extra data. The amount + of extra data in both proposals is rather low (<1kb), so this should not be + a problem in practice. + +## Alternatives +### Endpoint behavior +Instead of an endpoint for sending a forward, the new endpoint could be used to +generate the forward content and leave sending it up to the client with the +normal /send endpoint. However, this is an extra roundtrip for the client and +it is not clear if there are any significant benefits in doing so. + +## Unstable prefix +While this MSC is not in a released version of the spec, implementations should +use `net.maunium.msc2730` as a prefix and as a `unstable_features` flag in the +`/versions` endpoint. + +* `PUT /_matrix/client/unstable/net.maunium.msc2730/rooms/{roomId}/event/{eventId}/forward/{targetRoomId}/{txnId}` as the endpoint +* `net.maunium.msc2730.forwarded` instead of `m.forwarded` in `content` and `unsigned` +* `NET.MAUNIUM.MSC2730_NOT_FORWARDABLE` instead of `M_NOT_FORWARDABLE` as the error code