You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
matrix-spec-proposals/proposals/2730-verifiable-forwarded-e...

11 KiB

Verifiable forwarded events

This is an alternative to MSC2723 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)
{
  "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:

{
  "event_id": "$r8h8W9A5KS8D65_Df8fwLkTe7aqOm48KmyaJ6tRNAmE"
}
Forwarded event (client format)
{
  "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.

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