From 306f91edb318eb6ecc791e8e81c5b6b40ba829c8 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 10 Sep 2015 16:46:22 +0100 Subject: [PATCH 01/10] Specify third party room invitations SYN-458 --- api/client-server/v1/membership.yaml | 116 ++++++++++++++++++++++++++- templating/matrix_templates/units.py | 2 +- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/api/client-server/v1/membership.yaml b/api/client-server/v1/membership.yaml index 04922656d..17b5795e4 100644 --- a/api/client-server/v1/membership.yaml +++ b/api/client-server/v1/membership.yaml @@ -66,11 +66,16 @@ paths: aliases: - /join/{roomId} - "/rooms/{roomId}/invite": + # With an extra " " to disambiguate from the 3pid invite endpoint + # The extra space makes it sort first for what I'm sure is a good reason. + "/rooms/{roomId}/invite ": post: summary: Invite a user to participate in a particular room. - # It's a crying shame that I don't know how to force line breaks. description: |- + *Note that there are two forms of this API, which are documented separately. + This version of the API requires that the inviter knows the Matrix + identifier of the invitee.* + This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room. @@ -82,6 +87,9 @@ paths: Only users currently in a particular room can invite other users to join that room. + + If the user was invited to the room, the home server will append a + ``m.room.member`` event to the room. security: - accessToken: [] parameters: @@ -92,7 +100,7 @@ paths: required: true x-example: "!d41d8cd:matrix.org" - in: body - name: user_id + name: body required: true schema: type: object @@ -112,7 +120,107 @@ paths: application/json: |- {} schema: - type: object # empty json object + type: object + 403: + description: |- + You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are: + - The invitee has been banned from the room. + - The invitee is already a member of the room. + - The inviter is not currently in the room. + - The inviter's power level is insufficient to invite users to the room. + examples: + application/json: |- + {"errcode": "M_FORBIDDEN", "error": "@cheeky_monkey:matrix.org is banned from the room"} + 429: + description: This request was rate-limited. + schema: + "$ref": "definitions/error.yaml" + + "/rooms/{roomId}/invite": + post: + summary: Invite a user to participate in a particular room. + description: |- + *Note that there are two forms of this API, which are documented separately. + This version of the API does not require that the inviter know the Matrix + identifier of the invitee, and instead relies on third party identifiers.* + + This API invites a user to participate in a particular room. + They do not start participating in the room until they actually join the + room. + + This serves two purposes; firstly, to notify the user that the room + exists (and that their presence is requested). Secondly, some rooms can + only be joined if a user is invited to join it; sending the invite gives + that user permission to join the room. + + Only users currently in a particular room can invite other users to + join that room. + + If the identity server does not know a Matrix user identifier for the + passed third party identifier, the homeserver will issue an invitation + which can be accepted upon providing proof of ownership of the third + party identifier. This is achieved by requesting a nonce and digest from + the identity server. When a user binds the invited third party + identifier to a Matrix user ID, the identity server will give the user a + list of pending invitations, each containing: + + - The room ID to which they were invited + + - The digest given to the homeserver + + - A secret which, when appended to the nonce, digests to the above digest + + The digest algorithm to be used is SHA256. + + If the identity server did know the Matrix user identifier for the + third party identifier, the home server will append a ``m.room.member`` + event to the room. + + If a digest and nonce are requested from the identity server, the home + server will append a ``m.room.token_based_invite`` event to the room. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: roomId + description: The room identifier (not alias) to which to invite the user. + required: true + x-example: "!d41d8cd:matrix.org" + - in: body + name: body + required: true + schema: + type: object + example: |- + { + "identity_server": "matrix.org", + "medium": "email", + "address": "cheeky@monkey.com", + "display_name": "A very cheeky monkey" + } + properties: + identity_server: + type: string + description: The hostname+port of the identity server which should be used for third party identifier lookups. + medium: + type: string + description: The kind of address being passed in the address field. + address: + type: string + description: The invitee's third party identifier. + display_name: + type: string + description: A user-friendly string describing who has been invited. It should not contain the address of the invitee, to avoid leaking mappings between third party identities and matrix user IDs. + required: ["identity_server", "medium", "address", "display_name"] + responses: + 200: + description: The user has been invited to join the room. + examples: + application/json: |- + {} + schema: + type: object 403: description: |- You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are: diff --git a/templating/matrix_templates/units.py b/templating/matrix_templates/units.py index d8cc213de..150b0942b 100644 --- a/templating/matrix_templates/units.py +++ b/templating/matrix_templates/units.py @@ -110,7 +110,7 @@ class MatrixUnits(Units): "title": single_api.get("summary", ""), "desc": single_api.get("description", single_api.get("summary", "")), "method": method.upper(), - "path": full_path, + "path": full_path.strip(), "requires_auth": "security" in single_api, "rate_limited": 429 in single_api.get("responses", {}), "req_params": [], From f483340033f86a878daf82a2501e3049b74a392f Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Mon, 14 Sep 2015 15:16:30 +0100 Subject: [PATCH 02/10] Clarify digest computation --- api/client-server/v1/membership.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/client-server/v1/membership.yaml b/api/client-server/v1/membership.yaml index 17b5795e4..c4d658fba 100644 --- a/api/client-server/v1/membership.yaml +++ b/api/client-server/v1/membership.yaml @@ -168,7 +168,8 @@ paths: - The digest given to the homeserver - - A secret which, when appended to the nonce, digests to the above digest + - A secret which, when appended to the nonce, digests to the above digest, + i.e. digest = sha256(nonce + secret) The digest algorithm to be used is SHA256. From af7d2ca9fcc7ebb49aa4d35fa06b7dcdb1494447 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 1 Oct 2015 22:19:11 -0500 Subject: [PATCH 03/10] Update 3pid invite spec This takes into account: 1) That finding the existing servers of a room is hard 2) Federation --- api/client-server/v1/membership.yaml | 29 +++++++------- event-schemas/examples/v1/m.room.member | 7 +++- .../examples/v1/m.room.third_party_invite | 14 +++++++ event-schemas/schema/v1/m.room.member | 24 +++++++++++- .../schema/v1/m.room.third_party_invite | 37 ++++++++++++++++++ specification/modules/third_party_invites.rst | 39 +++++++++++++++++++ specification/targets.yaml | 1 + 7 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 event-schemas/examples/v1/m.room.third_party_invite create mode 100644 event-schemas/schema/v1/m.room.third_party_invite create mode 100644 specification/modules/third_party_invites.rst diff --git a/api/client-server/v1/membership.yaml b/api/client-server/v1/membership.yaml index c4d658fba..ff35fa350 100644 --- a/api/client-server/v1/membership.yaml +++ b/api/client-server/v1/membership.yaml @@ -159,26 +159,29 @@ paths: If the identity server does not know a Matrix user identifier for the passed third party identifier, the homeserver will issue an invitation which can be accepted upon providing proof of ownership of the third - party identifier. This is achieved by requesting a nonce and digest from - the identity server. When a user binds the invited third party - identifier to a Matrix user ID, the identity server will give the user a - list of pending invitations, each containing: + party identifier. This is achieved by the identity server generating a + token, which it gives to the inviting homeserver. The homeserver will + add an ``m.room.third_party_invite`` event into the graph for the room, + containing that token. + + When a user binds the invited third party identifier to a Matrix user ID, + the identity server will give the user a list of pending invitations, + each containing: - The room ID to which they were invited - - The digest given to the homeserver + - The token given to the homeserver - - A secret which, when appended to the nonce, digests to the above digest, - i.e. digest = sha256(nonce + secret) + - A signature of the token, signed with the identity server's private key - The digest algorithm to be used is SHA256. + - The matrix user ID who invited them to the room If the identity server did know the Matrix user identifier for the third party identifier, the home server will append a ``m.room.member`` event to the room. - If a digest and nonce are requested from the identity server, the home - server will append a ``m.room.token_based_invite`` event to the room. + If a token is requested from the identity server, the home server will + append a ``m.room.third_party_invite`` event to the room. security: - accessToken: [] parameters: @@ -195,13 +198,13 @@ paths: type: object example: |- { - "identity_server": "matrix.org", + "id_server": "matrix.org", "medium": "email", "address": "cheeky@monkey.com", "display_name": "A very cheeky monkey" } properties: - identity_server: + id_server: type: string description: The hostname+port of the identity server which should be used for third party identifier lookups. medium: @@ -213,7 +216,7 @@ paths: display_name: type: string description: A user-friendly string describing who has been invited. It should not contain the address of the invitee, to avoid leaking mappings between third party identities and matrix user IDs. - required: ["identity_server", "medium", "address", "display_name"] + required: ["id_server", "medium", "address", "display_name"] responses: 200: description: The user has been invited to join the room. diff --git a/event-schemas/examples/v1/m.room.member b/event-schemas/examples/v1/m.room.member index b9cd26715..279eb484e 100644 --- a/event-schemas/examples/v1/m.room.member +++ b/event-schemas/examples/v1/m.room.member @@ -3,7 +3,12 @@ "content": { "membership": "join", "avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto", - "displayname": "Alice Margatroid" + "displayname": "Alice Margatroid", + "token": "pc98", + "public_key": "abc123", + "key_validity_url": "https://magic.forest/verifykey", + "signature": "q1w2e3", + "sender": "@zun:zun.soft" }, "state_key": "@alice:localhost", "origin_server_ts": 1431961217939, diff --git a/event-schemas/examples/v1/m.room.third_party_invite b/event-schemas/examples/v1/m.room.third_party_invite new file mode 100644 index 000000000..82ac52487 --- /dev/null +++ b/event-schemas/examples/v1/m.room.third_party_invite @@ -0,0 +1,14 @@ +{ + "age": 242352, + "content": { + "display_name": "Alice Margatroid", + "key_validity_url": "https://magic.forest/verifykey", + "public_key": "abc123" + }, + "state_key": "pc98", + "origin_server_ts": 1431961217939, + "event_id": "$WLGTSEFSEF:localhost", + "type": "m.room.third_party_invite", + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost" +} diff --git a/event-schemas/schema/v1/m.room.member b/event-schemas/schema/v1/m.room.member index c0fb103c4..a3be9a1d5 100644 --- a/event-schemas/schema/v1/m.room.member +++ b/event-schemas/schema/v1/m.room.member @@ -1,7 +1,7 @@ { "type": "object", "title": "The current membership state of a user in the room.", - "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.", + "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. The token, public_key, key_validity_url, signature, and sender properties either must all or none be set - they are set if the invite was an ``m.room.third_party_invite`` event, and absent if the invite was an ``m.room.member`` event.", "allOf": [{ "$ref": "core-event-schema/state_event.json" }], @@ -21,7 +21,27 @@ "displayname": { "type": ["string", "null"], "description": "The display name for this user, if any. This is added by the homeserver." - } + }, + "token": { + "type": "string", + "description": "A token which must be correctly signed, in order to join the room." + }, + "key_validity_url": { + "type": "string", + "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + }, + "public_key": { + "type": "string", + "description": "A base64-encoded ed25519 key with which token must be signed." + }, + "signature": { + "type": "string", + "description": "A base64-encoded signature of token with public_key." + }, + "sender": { + "type": "string", + "description": "The matrix user ID of the user who send the invite which is being used." + } }, "required": ["membership"] }, diff --git a/event-schemas/schema/v1/m.room.third_party_invite b/event-schemas/schema/v1/m.room.third_party_invite new file mode 100644 index 000000000..eae8c0158 --- /dev/null +++ b/event-schemas/schema/v1/m.room.third_party_invite @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "An invitation to a room issued to a third party identifier, rather than a matrix user ID.", + "description": "Acts as an ``m.room.member`` invite event, where there isn't a target user_id to invite. This event contains a token and a public key whose private key must be used to sign the token. Any user who can present that signature may use this invitation to join the target room.", + "allOf": [{ + "$ref": "core#/definitions/state_event" + }], + "properties": { + "content": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "description": "A user-readable string which represents the user who has been invited. This should not contain the user's third party ID, as otherwise when the invite is accepted it would leak the association between the matrix ID and the third party ID." + }, + "key_validity_url": { + "type": "string", + "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + }, + "public_key": { + "type": "string", + "description": "A base64-encoded ed25519 key with which token must be signed." + } + }, + "required": ["display_name", "key_validity_url", "public_key"] + }, + "state_key": { + "type": "string", + "description": "The token, of which a signature must be produced in order to join the room." + }, + "type": { + "type": "string", + "enum": ["m.room.third_party_invite"] + } + } +} diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst new file mode 100644 index 000000000..ae0d74e8c --- /dev/null +++ b/specification/modules/third_party_invites.rst @@ -0,0 +1,39 @@ +Third party invites +=================== + +.. _module:third_party_invites: + +This module adds in support for inviting new members to a room where their +Matrix user ID is not known, instead addressing them by a third party identifier +such as an email address. + +Events +------ + +{{m_room_third_party_invite_event}} + +Client behaviour +---------------- + +A client asks a server to invite a user by their third party identifier. + +See the documentation for /invite for more information. + +Server behaviour +---------------- + +All homeservers MUST verify that sign(``token``, ``public_key``) = ``signature``. + +If a client of the current homeserver is joining by an +``m.room.third_party_invite``, that homesever MUST validate that the public +key used for signing is still valid, by checking ``key_validity_url``. + +If a homeserver is joining a room for the first time because of an +``m.room.third_party_invite``, the server which is already participating in the +room MUST validate that the public key used for signing is still valid, by +checking ``key_validity_url``. + +No other homeservers may reject the joining of the room on the basis of +``key_validity_url``, this is so that all homeservers have a consistent view of +the room. + diff --git a/specification/targets.yaml b/specification/targets.yaml index d77bf8b59..4b404ff1f 100644 --- a/specification/targets.yaml +++ b/specification/targets.yaml @@ -22,6 +22,7 @@ groups: # reusable blobs of files when prefixed with 'group:' - modules/content_repo.rst - modules/end_to_end_encryption.rst - modules/history_visibility.rst + - modules/third_party_invites.rst - modules/push_overview.rst # relative depth - { 1: [modules/push_cs_api.rst , modules/push_push_gw_api.rst] } From e996933a29badce5676ebc2ecd52ff8374cd44b1 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 1 Oct 2015 22:21:34 -0500 Subject: [PATCH 04/10] Banish the tabs --- .../schema/v1/m.room.third_party_invite | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/event-schemas/schema/v1/m.room.third_party_invite b/event-schemas/schema/v1/m.room.third_party_invite index eae8c0158..dc010fe78 100644 --- a/event-schemas/schema/v1/m.room.third_party_invite +++ b/event-schemas/schema/v1/m.room.third_party_invite @@ -14,14 +14,14 @@ "type": "string", "description": "A user-readable string which represents the user who has been invited. This should not contain the user's third party ID, as otherwise when the invite is accepted it would leak the association between the matrix ID and the third party ID." }, - "key_validity_url": { - "type": "string", - "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." - }, - "public_key": { - "type": "string", - "description": "A base64-encoded ed25519 key with which token must be signed." - } + "key_validity_url": { + "type": "string", + "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + }, + "public_key": { + "type": "string", + "description": "A base64-encoded ed25519 key with which token must be signed." + } }, "required": ["display_name", "key_validity_url", "public_key"] }, From fdccf35c4745147db601c06673e3641fa5a7ad70 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 1 Oct 2015 22:23:24 -0500 Subject: [PATCH 05/10] Banish more tabs --- event-schemas/schema/v1/m.room.member | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/event-schemas/schema/v1/m.room.member b/event-schemas/schema/v1/m.room.member index a3be9a1d5..47ce4d6c2 100644 --- a/event-schemas/schema/v1/m.room.member +++ b/event-schemas/schema/v1/m.room.member @@ -22,26 +22,26 @@ "type": ["string", "null"], "description": "The display name for this user, if any. This is added by the homeserver." }, - "token": { - "type": "string", - "description": "A token which must be correctly signed, in order to join the room." - }, - "key_validity_url": { - "type": "string", - "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." - }, - "public_key": { - "type": "string", - "description": "A base64-encoded ed25519 key with which token must be signed." - }, - "signature": { - "type": "string", - "description": "A base64-encoded signature of token with public_key." - }, - "sender": { - "type": "string", - "description": "The matrix user ID of the user who send the invite which is being used." - } + "token": { + "type": "string", + "description": "A token which must be correctly signed, in order to join the room." + }, + "key_validity_url": { + "type": "string", + "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + }, + "public_key": { + "type": "string", + "description": "A base64-encoded ed25519 key with which token must be signed." + }, + "signature": { + "type": "string", + "description": "A base64-encoded signature of token with public_key." + }, + "sender": { + "type": "string", + "description": "The matrix user ID of the user who send the invite which is being used." + } }, "required": ["membership"] }, From 9cda004848b230b943dd3fcf7310e9c549b96b3b Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 1 Oct 2015 22:27:19 -0500 Subject: [PATCH 06/10] Fix ref --- event-schemas/schema/v1/m.room.third_party_invite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event-schemas/schema/v1/m.room.third_party_invite b/event-schemas/schema/v1/m.room.third_party_invite index dc010fe78..ba66100f1 100644 --- a/event-schemas/schema/v1/m.room.third_party_invite +++ b/event-schemas/schema/v1/m.room.third_party_invite @@ -4,7 +4,7 @@ "title": "An invitation to a room issued to a third party identifier, rather than a matrix user ID.", "description": "Acts as an ``m.room.member`` invite event, where there isn't a target user_id to invite. This event contains a token and a public key whose private key must be used to sign the token. Any user who can present that signature may use this invitation to join the target room.", "allOf": [{ - "$ref": "core#/definitions/state_event" + "$ref": "core-event-schema/state_event.json" }], "properties": { "content": { From 9d8dc7971dd697c50747432784ae4ccc80b6e313 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Fri, 2 Oct 2015 08:33:12 -0500 Subject: [PATCH 07/10] Review comments --- event-schemas/examples/v1/m.room.member | 12 +++-- event-schemas/schema/v1/m.room.member | 47 ++++++++++------- specification/modules/third_party_invites.rst | 52 ++++++++++++++++--- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/event-schemas/examples/v1/m.room.member b/event-schemas/examples/v1/m.room.member index 279eb484e..a5ab79b5c 100644 --- a/event-schemas/examples/v1/m.room.member +++ b/event-schemas/examples/v1/m.room.member @@ -4,11 +4,13 @@ "membership": "join", "avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto", "displayname": "Alice Margatroid", - "token": "pc98", - "public_key": "abc123", - "key_validity_url": "https://magic.forest/verifykey", - "signature": "q1w2e3", - "sender": "@zun:zun.soft" + "third_party_invite": { + "token": "pc98", + "public_key": "abc123", + "key_validity_url": "https://magic.forest/verifykey", + "signature": "q1w2e3", + "sender": "@zun:zun.soft" + } }, "state_key": "@alice:localhost", "origin_server_ts": 1431961217939, diff --git a/event-schemas/schema/v1/m.room.member b/event-schemas/schema/v1/m.room.member index 47ce4d6c2..561043289 100644 --- a/event-schemas/schema/v1/m.room.member +++ b/event-schemas/schema/v1/m.room.member @@ -22,26 +22,33 @@ "type": ["string", "null"], "description": "The display name for this user, if any. This is added by the homeserver." }, - "token": { - "type": "string", - "description": "A token which must be correctly signed, in order to join the room." - }, - "key_validity_url": { - "type": "string", - "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." - }, - "public_key": { - "type": "string", - "description": "A base64-encoded ed25519 key with which token must be signed." - }, - "signature": { - "type": "string", - "description": "A base64-encoded signature of token with public_key." - }, - "sender": { - "type": "string", - "description": "The matrix user ID of the user who send the invite which is being used." - } + "third_party_invite": { + "type": "object", + "title": "invite", + "properties": { + "token": { + "type": "string", + "description": "A token which must be correctly signed, in order to join the room." + }, + "key_validity_url": { + "type": "string", + "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + }, + "public_key": { + "type": "string", + "description": "A base64-encoded ed25519 key with which token must be signed." + }, + "signature": { + "type": "string", + "description": "A base64-encoded signature of token with public_key." + }, + "sender": { + "type": "string", + "description": "The matrix user ID of the user who send the invite which is being used." + } + }, + "required": ["token", "key_validity_url", "public_key", "signature", "sender"] + } }, "required": ["membership"] }, diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index ae0d74e8c..3f5a38a43 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -7,6 +7,22 @@ This module adds in support for inviting new members to a room where their Matrix user ID is not known, instead addressing them by a third party identifier such as an email address. +There are two flows here; one if a Matrix user ID is known for the third party +identifier, and one if not. Either way, the client calls /invite with the +details of the third party identifier. + +The homeserver asks the identity server whether a Matrix user ID is known for +that identifier. If it is, an invite is simply issued for that user. + +If it is not, the homeserver asks the identity server to record the details of +the invitation, and to notify the client of this pending invitation if it gets +a binding for this identifier in the future. The identity server returns to the +homeserver a token, as well as its public key. + +If a client then tries to join the room in the future, it will be allowed to if +it presents both the token, and a signature of that token from the identity +server which can be verified with the public key. + Events ------ @@ -17,23 +33,47 @@ Client behaviour A client asks a server to invite a user by their third party identifier. -See the documentation for /invite for more information. - Server behaviour ---------------- -All homeservers MUST verify that sign(``token``, ``public_key``) = ``signature``. +All homeservers MUST verify that sig(``token``, ``public_key``) = ``signature``. If a client of the current homeserver is joining by an ``m.room.third_party_invite``, that homesever MUST validate that the public -key used for signing is still valid, by checking ``key_validity_url``. +key used for signing is still valid, by checking ``key_validity_url``. It does +this by making an HTTP GET request to ``key_validity_url``, with the querystring +?public_key=``public_key``. A JSON object will be returned, and the key is +considered valid if the object contains a key named ``valid`` whose value is +``true``. If this cannot be verified, the invitation must be rejected. If a homeserver is joining a room for the first time because of an ``m.room.third_party_invite``, the server which is already participating in the room MUST validate that the public key used for signing is still valid, by -checking ``key_validity_url``. +checking ``key_validity_url`` in the above described way. No other homeservers may reject the joining of the room on the basis of ``key_validity_url``, this is so that all homeservers have a consistent view of -the room. +the room. They may, however, indicate to their clients that a member's' +membership is questionable. + +For example: + +If room R has two participating homeservers, H1, H2 + +And user A on H1 invites a third party identifier to room R + +H1 asks the identity server for a binding to a Matrix user ID, and has none, +so issues an ``m.room.third_party_invite`` event to the room. + +When the third party user validates their identity, they are told about the +invite, and ask their homeserver, H3, to join the room. + +H3 validates that sign(``token``, ``public_key``) = ``signature``, and may check +``key_validity_url``. + +H3 then asks H1 to join it to the room. H1 *must* validate that +sign(``token``, ``public_key``) = ``signature`` *and* check ``key_validity_url``. + +Having validated these things, H1 writes the join event to the room, and H3 +begins participating in the room. H2 *must* accept this event. From b29fdebec22ab12b7cc8c613a6018d9a96883744 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Fri, 2 Oct 2015 09:14:31 -0500 Subject: [PATCH 08/10] Give example of verification dance --- specification/modules/third_party_invites.rst | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index 3f5a38a43..3365501e5 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -41,7 +41,28 @@ All homeservers MUST verify that sig(``token``, ``public_key``) = ``signature``. If a client of the current homeserver is joining by an ``m.room.third_party_invite``, that homesever MUST validate that the public key used for signing is still valid, by checking ``key_validity_url``. It does -this by making an HTTP GET request to ``key_validity_url``, with the querystring +this by making an HTTP GET request to ``key_validity_url``: + +Schema:: + + => GET $key_validity_url?public_key=$public_key + <= HTTP/1.1 200 OK + { + "valid": true|false + } + + +Example:: + + key_validity_url = https://identity.server/is_valid + public_key = ALJWLAFQfqffQHFqFfeqFUOEHf4AIHfefh4 + => GET https://identity.server/is_valid?public_key=ALJWLAFQfqffQHFqFfeqFUOEHf4AIHfefh4 + <= HTTP/1.1 200 OK + { + "valid": true + } + +with the querystring ?public_key=``public_key``. A JSON object will be returned, and the key is considered valid if the object contains a key named ``valid`` whose value is ``true``. If this cannot be verified, the invitation must be rejected. From dc3c02aff5ed34686dd62b39fcce72cae8065f5f Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 8 Oct 2015 10:08:38 -0500 Subject: [PATCH 09/10] Review comments --- api/client-server/v1/membership.yaml | 31 +++++++------------ event-schemas/schema/v1/m.room.member | 4 +-- specification/modules/third_party_invites.rst | 29 ++++++++++++----- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/api/client-server/v1/membership.yaml b/api/client-server/v1/membership.yaml index ff35fa350..efc825786 100644 --- a/api/client-server/v1/membership.yaml +++ b/api/client-server/v1/membership.yaml @@ -80,11 +80,6 @@ paths: They do not start participating in the room until they actually join the room. - This serves two purposes; firstly, to notify the user that the room - exists (and that their presence is requested). Secondly, some rooms can - only be joined if a user is invited to join it; sending the invite gives - that user permission to join the room. - Only users currently in a particular room can invite other users to join that room. @@ -142,20 +137,21 @@ paths: description: |- *Note that there are two forms of this API, which are documented separately. This version of the API does not require that the inviter know the Matrix - identifier of the invitee, and instead relies on third party identifiers.* + identifier of the invitee, and instead relies on third party identifiers. + The homeserver uses an identity server to perform the mapping from + third party identifier to a Matrix identifier.* This API invites a user to participate in a particular room. They do not start participating in the room until they actually join the room. - This serves two purposes; firstly, to notify the user that the room - exists (and that their presence is requested). Secondly, some rooms can - only be joined if a user is invited to join it; sending the invite gives - that user permission to join the room. - Only users currently in a particular room can invite other users to join that room. + If the identity server did know the Matrix user identifier for the + third party identifier, the home server will append a ``m.room.member`` + event to the room. + If the identity server does not know a Matrix user identifier for the passed third party identifier, the homeserver will issue an invitation which can be accepted upon providing proof of ownership of the third @@ -164,9 +160,9 @@ paths: add an ``m.room.third_party_invite`` event into the graph for the room, containing that token. - When a user binds the invited third party identifier to a Matrix user ID, - the identity server will give the user a list of pending invitations, - each containing: + When the invitee binds the invited third party identifier to a Matrix + user ID, the identity server will give the user a list of pending + invitations, each containing: - The room ID to which they were invited @@ -176,10 +172,6 @@ paths: - The matrix user ID who invited them to the room - If the identity server did know the Matrix user identifier for the - third party identifier, the home server will append a ``m.room.member`` - event to the room. - If a token is requested from the identity server, the home server will append a ``m.room.third_party_invite`` event to the room. security: @@ -209,7 +201,8 @@ paths: description: The hostname+port of the identity server which should be used for third party identifier lookups. medium: type: string - description: The kind of address being passed in the address field. + # TODO: Link to identity service spec when it eixsts + description: The kind of address being passed in the address field, for example ``email``. address: type: string description: The invitee's third party identifier. diff --git a/event-schemas/schema/v1/m.room.member b/event-schemas/schema/v1/m.room.member index 561043289..912f6cf30 100644 --- a/event-schemas/schema/v1/m.room.member +++ b/event-schemas/schema/v1/m.room.member @@ -1,7 +1,7 @@ { "type": "object", "title": "The current membership state of a user in the room.", - "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. The token, public_key, key_validity_url, signature, and sender properties either must all or none be set - they are set if the invite was an ``m.room.third_party_invite`` event, and absent if the invite was an ``m.room.member`` event.", + "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. The ``third_party_invite`` property will be set if the invite was an ``m.room.third_party_invite`` event, and absent if the invite was an ``m.room.member`` event.", "allOf": [{ "$ref": "core-event-schema/state_event.json" }], @@ -32,7 +32,7 @@ }, "key_validity_url": { "type": "string", - "description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." + "description": "A URL which can be fetched, with querystring ``public_key=public_key``, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'." }, "public_key": { "type": "string", diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index 3365501e5..9b3b52e1d 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -8,7 +8,7 @@ Matrix user ID is not known, instead addressing them by a third party identifier such as an email address. There are two flows here; one if a Matrix user ID is known for the third party -identifier, and one if not. Either way, the client calls /invite with the +identifier, and one if not. Either way, the client calls ``/invite`` with the details of the third party identifier. The homeserver asks the identity server whether a Matrix user ID is known for @@ -16,8 +16,8 @@ that identifier. If it is, an invite is simply issued for that user. If it is not, the homeserver asks the identity server to record the details of the invitation, and to notify the client of this pending invitation if it gets -a binding for this identifier in the future. The identity server returns to the -homeserver a token, as well as its public key. +a binding for this identifier in the future. The identity server returns a token +and public key to the homeserver. If a client then tries to join the room in the future, it will be allowed to if it presents both the token, and a signature of that token from the identity @@ -43,6 +43,8 @@ If a client of the current homeserver is joining by an key used for signing is still valid, by checking ``key_validity_url``. It does this by making an HTTP GET request to ``key_validity_url``: +.. TODO: Link to identity server spec when it exists + Schema:: => GET $key_validity_url?public_key=$public_key @@ -63,14 +65,16 @@ Example:: } with the querystring -?public_key=``public_key``. A JSON object will be returned, and the key is -considered valid if the object contains a key named ``valid`` whose value is -``true``. If this cannot be verified, the invitation must be rejected. +?public_key=``public_key``. A JSON object will be returned. +The invitation is valid if the object contains a key named ``valid`` which is +``true``. Otherwise, the invitation MUST be rejected. This request is +idempotent, and may be retried by the homeserver. If a homeserver is joining a room for the first time because of an ``m.room.third_party_invite``, the server which is already participating in the -room MUST validate that the public key used for signing is still valid, by -checking ``key_validity_url`` in the above described way. +room (which is chosen as per the standard server-server specification) MUST +validate that the public key used for signing is still valid, by checking +``key_validity_url`` in the above described way. No other homeservers may reject the joining of the room on the basis of ``key_validity_url``, this is so that all homeservers have a consistent view of @@ -98,3 +102,12 @@ sign(``token``, ``public_key``) = ``signature`` *and* check ``key_validity_url`` Having validated these things, H1 writes the join event to the room, and H3 begins participating in the room. H2 *must* accept this event. +The reason that no other homeserver may reject the event based on checking +``key_validity_url`` is that we must ensure event acceptance is deterministic. +If some other participating server doesn't have a network path to the keyserver, +or if the keyserver were to go offline, or revoke its keys, that other server +would reject the event and cause the participating servers' graphs to diverge. +This relies on participating servers trusting each other, but that trust is +already implied by the server-server protocol. Also, the public key signature +verification must still be performed, so the attack surface here is minimized. + From 4a1fb74897d949cee53431dae03fe4d17c3fcaaa Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Mon, 12 Oct 2015 18:08:51 +0100 Subject: [PATCH 10/10] Review comments --- specification/modules/third_party_invites.rst | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index 9b3b52e1d..a9883db53 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -68,7 +68,7 @@ with the querystring ?public_key=``public_key``. A JSON object will be returned. The invitation is valid if the object contains a key named ``valid`` which is ``true``. Otherwise, the invitation MUST be rejected. This request is -idempotent, and may be retried by the homeserver. +idempotent and may be retried by the homeserver. If a homeserver is joining a room for the first time because of an ``m.room.third_party_invite``, the server which is already participating in the @@ -83,24 +83,24 @@ membership is questionable. For example: -If room R has two participating homeservers, H1, H2 + If room R has two participating homeservers, H1, H2 -And user A on H1 invites a third party identifier to room R + And user A on H1 invites a third party identifier to room R -H1 asks the identity server for a binding to a Matrix user ID, and has none, -so issues an ``m.room.third_party_invite`` event to the room. + H1 asks the identity server for a binding to a Matrix user ID, and has none, + so issues an ``m.room.third_party_invite`` event to the room. -When the third party user validates their identity, they are told about the -invite, and ask their homeserver, H3, to join the room. + When the third party user validates their identity, they are told about the + invite, and ask their homeserver, H3, to join the room. -H3 validates that sign(``token``, ``public_key``) = ``signature``, and may check -``key_validity_url``. + H3 validates that sign(``token``, ``public_key``) = ``signature``, and may check + ``key_validity_url``. -H3 then asks H1 to join it to the room. H1 *must* validate that -sign(``token``, ``public_key``) = ``signature`` *and* check ``key_validity_url``. + H3 then asks H1 to join it to the room. H1 *must* validate that + sign(``token``, ``public_key``) = ``signature`` *and* check ``key_validity_url``. -Having validated these things, H1 writes the join event to the room, and H3 -begins participating in the room. H2 *must* accept this event. + Having validated these things, H1 writes the join event to the room, and H3 + begins participating in the room. H2 *must* accept this event. The reason that no other homeserver may reject the event based on checking ``key_validity_url`` is that we must ensure event acceptance is deterministic.