From bbd909d09f6f0d1b91f66655ed815a5726f3e2ab Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 20 Jul 2018 17:19:32 +0100 Subject: [PATCH 01/73] State Resolution: Reloaded MSC --- proposals/0000-state-resolution.md | 447 +++++++++++++++++++++++++++++ proposals/images/state-res.png | Bin 0 -> 61233 bytes 2 files changed, 447 insertions(+) create mode 100644 proposals/0000-state-resolution.md create mode 100644 proposals/images/state-res.png diff --git a/proposals/0000-state-resolution.md b/proposals/0000-state-resolution.md new file mode 100644 index 00000000..66d1eb38 --- /dev/null +++ b/proposals/0000-state-resolution.md @@ -0,0 +1,447 @@ +# State Resolution: Reloaded + + +Thoughts on the next iteration of the state resolution algorithm that aims to mitigate currently known attacks + + +# Background + +The state of a room at an event is a mapping from key to event, which is built +up and updated by sending state events into the room. All the information about +the room is encoded in the state, from metadata like the name and topic to +membership of the room to security policies like bans and join rules. + +It is therefore important that─wherever possible─the view of the state of the +room is consistent across all servers. If different servers have different +views of the state then it can lead to the room bifurcating, due to differing +ideas on who is in the room, who is allowed to talk, etc. + +The difficulty comes when the room DAG forks and then merges again (which can +happen naturally if two servers send events at the same time or when a network +partition is resolved). The state after the merge has to be resolved from the +state of the two branches: the algorithm to resolve this is called the _state +resolution algorithm_. + +Since the result of state resolution must be consistent across servers, the +information that the algorithm can use is strictly limited to the information +that will always be available to all servers (including future servers that may +not even be in the room at that point) at any point in time where the +resolution needs to be calculated. In particular, this has the consequence that +the algorithm cannot use information from the room DAG, since servers are not +required to store events for any length of time. + +**As such, the state resolution algorithm is effectively a pure function from +sets of state to a single resolved set of state.** + +The final important property for state resolution is that it should not allow +malicious servers to avoid moderation action by forking and merging the room +DAG. For example, if a server gets banned and then forks the room before the +ban, any merge back should always ensure that the ban is still in the state. + + +# Current Algorithm + +The current state resolution is known to have some undesirable properties, +which can be summarized into two separate cases: + +1. Moderation evasion ─ where an attacker can avoid e.g. bans by forking and joining the room DAG in particular ways. +1. State resets ─ where a server (often innocently) sends an event that points to disparate parts of the graph, causing state resolution to pick old state rather than later versions. + +These have the following causes: + +1. Conflicting state must pass auth checks to be eligible to be picked, but the algorithm does not consider previous (superseded) state changes in a fork. For example, where Alice gives Bob power and then Bob gives Charlie power on one branch of a conflict, when the latter power level event is authed against the original power level (where Bob didn't have power), it fails. +1. The algorithm relies on the deprecated and untrustable depth parameter to try and ensure that the "most recent" state is picked. Without having a copy of the complete room DAG the algorithm doesn't know that e.g. one topic event came strictly after another in the DAG. For efficiency and storage reasons servers are not required (or expected) to store the whole room DAG. +1. The algorithm always accepts events where there are no conflicting alternatives in other forks. This means that if an admin changed the join rules to `private`, then new joins on forks based on parts of the DAG which predate that change would always be accepted without being authed against the join_rules event. + + +# Desirable Properties + +As well as the important properties listed in the "Background" section, there are also some other properties that would significantly improve the experience of end users, though not strictly essential. These include: + +* Banning and changing power levels should "do the right thing", i.e. end users shouldn't have to take extra steps to make the state resolution produce the "right" results. +* Minimise occurences of "state resets". Servers will sometimes point to disparate parts of the room DAG (due to a variety of reasons), which ideally should not result in changes in the state. +* Be efficient; state resolution can happen a lot on some large rooms. Ideally it would also support efficiently working on "state deltas" - i.e. the ability to calculate state resolution incrementally from snapshots rather than having to consider the full state of each fork each time a conflict is resolved + + +# Ideas for New Algorithm + + +## Auth Chain + +The _auth events_ of a given event is the set of events which justify why a +given event is allowed to be sent into a room (e.g. an m.room.create, an +m.room.power_levels and the sender's m.room.membership). The _auth chain_ of an +event is its auth events and their auth events, recursively. The auth chains of +a set of events in a given room form a DAG. + +"Auth events" are events that can appear as auth events of an event. These +include power levels, membership etc.[^1] + +Servers in a room are required to have the full auth chain for all events that +they have seen, and so the auth chain is available to be used by state +resolution algorithms. + + +## Unconflicted State + +The current algorithm defines the notion of "unconflicted state" to be all +entries that for each set of state either has the same event or no entry. All +unconflicted state entries are included in the resolved state. This is +problematic due to the fact that any new entries introduced on forks always +appear in the resolved state, regardless of if they would pass the checks +applied to conflicted state. + +The new algorithm could redefine "unconflicted state" to be all entries which +both exist and are the same in every state set (as opposed to previously where +the entry didn't need to exist in every state set). + + +## Replacing Depth + +Since depth of an event cannot be reliably calculated without possessing the +full DAG, and cannot be trusted when provided by other servers, it can not be +used in future versions of state resolution. A potential alternative, however, +is to use "origin_server_ts". While it cannot be relied on to be accurate─an +attacker can set it to arbitrary values─it has the advantage over depth that +end users can clearly see when a server is using incorrect values. (Note that +server clocks don't need to be particularly accurate for the ordering to still +be more useful than other arbitrary orderings). + +It can also be assumed that in most cases the origin_server_ts for a given +benign server will be mostly consistent. For example, if a server sends a join +and then a leave in the vast majority of cases the leave would have a greater +origin_server_ts. + +This makes "origin_server_ts" a good candidate to be used as a last resort to +order events if necessary, where otherwise a different arbitrary ordering would +be used. However, it's important that there is some other mechanism to ensure +that malicious servers can't abuse origin_server_ts to ensure their state +always gets picked during resolution (In the proposal below we use the auth DAG +ordering to override users who set state with malicious origin_server_ts.) + + +## Ordering and Authing + +Roughly, the current algorithm tries to ensure that moderation evasion doesn't +happen by ordering conflicted events by depth and (re)authing them +sequentially. The exact implementation has several issues, but the idea of +ensuring that state events from forks still need to pass auth subject to e.g. +bans and power level changes is a powerful one, as it reduces the utility of +maliciously forking. + +For that to work we need to ensure that there is a suitable ordering that puts +e.g. bans before events sent in other forks. (However events can point to old +parts of the DAG, for a variety of reasons, and ideally in that case the +resolved state would closely match the recent state). + + +## Power Level Ordering + +Actions that malicious servers would try and evade are actions that require +greater power levels to perform, for example banning, reducing power level, +etc. We define "power events" as events that have the potential to remove the +ability of another user to do something.[^2] (Note that they are a subset of +auth events.) + +In all these cases it is desirable for those privileged actions to take +precedence over events in other forks. This can be achieved by first +considering "power events", and requiring the remaining events to pass auth +based on them. + + +## Mainline + +An issue caused by servers not storing the full room DAG is that one can't tell +how two arbitrary events are ordered. The auth chain gives a partial ordering +to certain events, though far from complete; however, all events do contain a +reference to the current power levels in their auth events. As such if two +state events reference two different power levels events, and one power levels' +auth chain references the other, then there is a strong likelihood that the +event referencing the latter power level came after the other event. + +A "mainline" is a list of power levels events created if you pick a particular +power levels event (usually the current resolved power level) and recursively +follow each power level referenced in auth_events back to the first power level +event. + +The mainline can then be used to induce an ordering on events by looking at +where the power level referenced in their auth_events is in the mainline (or +recursively following the chain of power level events back until one is found +that appears in the mainline). This effectively partitions the room into +epochs, where a new epoch is started whenever a new power level is sent. + +If this mainline ordering is combined with ordering by origin_server_ts, then +it gives an ordering that is correct for servers that don't lie about the time, +while giving a mechanism that can be used to deal if a server lied (by room +admins starting a new epoch). + +The natural course of action for a room admin to take when noticing a +user/server is misbehaving is to ban them from the room, rather than changing +the power levels. It would therefore be useful if banning a user or server +started a new epoch as well. This would require being able to create a mainline +that includes power level events and bans[^3], which would suggest that power +level and ban events would need to point to the latest ban event as well. (This +would be significantly easier if we maintained a list of bans in a single +event, however there is concern that would limit the number of possible bans in +a room.) + + +# Proposed Algorithm + +First we define: + +* **"State sets"** are the sets of state that the resolution algorithm tries to resolve, i.e. the inputs to the algorithm. +* **"Power events"** are events that have the potential to remove the ability of another user to do something. These are power levels, join rules, bans and kicks. +* The **"unconflicted state map"** is the state where the value of each key exists and is the same in every state set. The** "conflicted state map"** is everything else. (Note that this is subtly different to the definition used in the existing algorithm, which considered the merge of a present event with an absent event to be unconflicted rather than conflicted) +* The "**auth difference"** is calculated by first calculating the full auth chain for each state set and taking every event that doesn't appear in every auth chain. +* The **"full conflicted set"** is the union of the conflicted state map and auth difference. +* The **"reverse topological power ordering"**[^4] of a set of events is an ordering of the given events, plus any events in their auth chains that appear in the auth difference, ordered such that x < y if: + + 1. x is in the auth chain of y, or if + 1. x's sender has a greater power level than y (calculated by looking at their respective auth events, or if + 1. x's origin_server_ts is less than y's, or if + 1. x's event_id is lexicographically less than y's + + This is also known as a lexicographical topological sort. + +* The **"mainline ordering"** based on a power level event P of a set of events is calculated as follows: + 1. Generate the list of power levels starting at P and recursively take the power level from its auth events. This list is called the mainline, ordered such that P is last. + 1. We say the "closest mainline event" of an event is the first power level event encountered in mainline when iteratively descending through the power level events in the auth events. + 1. Order the set of events such that x < y if: + 1. The closest mainline event of x appears strictly before the closest of y in the mainline list, or if + 1. x's origin_server_ts is less than y's, or if + 1. x's event_id lexicographically sorts before y's +* The **"iterative auth checks"** algorithm is where given a sorted list of events, the auth check algorithm is applied to each event in turn. The state events used to auth are built up from previous events that passed the auth checks, starting from a base set of state. If a required auth key doesn't exist in the state, then the one in the event's auth_events is used. (See _Variations_ and _Attack Vectors_ below). + +The algorithm proceeds as follows: + + +1. Take all power events and any events in their auth chains that appear in the _full_ _conflicted set_ and order them by the _reverse topological power ordering._ +1. Apply the _iterative auth checks_ algorithm based on the unconflicted state map to get a partial set of resolved state. +1. Take all remaining events that weren't picked in step 1 and order them by the _mainline ordering_ based on the power level in the partially resolved state. +1. Apply the _iterative auth checks algorithm_ based on the partial resolved state. +1. Update the result with the _unconflicted state_ to get the final resolved state[^5]. +(_Note_: this is different from the current algorithm, which considered different event types at distinct stages) + +An example python implementation can be found on github +[here](https://github.com/matrix-org/matrix-test-state-resolution-ideas). + +Note that this works best if we also change which events to include as an +event's auth_events. See the "Auth Events" section below. + + +## Discussion + +Essentially, the algorithm works by producing a sorted list of all conflicted +events (and differences in auth chains), and applies the auth checks one by +one, building up the state as it goes. The list is produced in two parts: first +the power events and auth dependencies are ordered by power level of the +senders and resolved, then the remaining events are ordered using the +"mainline" of the resolved power levels and then resolved to produce the final +resolved state. + +(This is equivalent to linearizing the full conflicted set of events and +reapplying the usual state updates and auth rules.) + + +### Variations + +There are multiple options for what to use as the base state for _iterative +auth checks_ algorithm; while it needs to be some variation of auth events and +unconflicted events, it is unclear exactly which combination is best (and least +manipulatable by malicious servers). + +Care has to be taken if we want to ensure that old auth events that appear in +the _auth chain difference_ can't supercede unconflicted state entries. + +Due to auth chain differences being added to the resolved states during +_iterative auth checks_, we therefore need to re-apply the unconflicted state +at the end to ensure that they appear in the final resolved state. This feels +like an odd fudge that shouldn't be necessary, and may point to a flaw in the +proposed algorithm. + + +### State Resets + +The proposed algorithm still has some potentially unexpected behaviour. + +One example of this is when Alice sets a topic and then gets banned. If an +event gets created (potentially much later) that points to both before and +after the topic and ban then the proposed algorithm will resolve and apply the +ban before resolving the topic, causing the topic to be denied and dropped from +the resolved state. This will result in no topic being set in the resolved +state. + + +### Auth Events + +The algorithm relies heavily on the ordering induced by the auth chain DAG. + +There are two types of auth events (not necessarily distinct): + +* Those that give authorization to do something +* Those that revoke authorization to do something. + +For example, invites/joins are in the former category, leaves/kicks/bans are in +the latter and power levels are both. + +Assuming[^6] revocations always point to (i.e., have in their auth chain) the +authorization event that they are revoking, and authorization events point to +revocations that they are superseding, then the algorithm will ensure that the +authorization events are applied in order (so generally the "latest" +authorization state would win). + +This helps ensure that e.g. an invite cannot be reused after a leave/kick, +since the leave (revocation) would have the invite in their auth chain. + +This idea also relies on revocations replacing the state that granted +authorization to do an action (and vice versa). For example, in the current +model bans (basically) revoke the ability for a particular user from being able +to join. If the user later gets unbanned and then rejoins, the join would point +to the join rules as the authorization that lets them join, but would not +(necessarily) point to the unban. This has the effect that if a state +resolution happened between the new join and the ban, the unban would not be +included in the resolution and so the join would be rejected. + +The changes to the current model that would be required to make the above +assumptions true would be, for example: + + + +1. By default permissions are closed. +1. Bans would need to be a list in either the join rules event or a separate event type which all membership events pointed to. +1. Bans would only revoke the ability to join, not automatically remove users from the room. +1. Change the defaults of join_rules to be closed by default + + +### Efficiency and Delta State Resolution + +The current (unoptimised) implementation of the algorithm is 10x slower than +the current algorithm, based on a single, large test case. While hopefully some +optimisations can be made, the ability to [incrementally calculate state +resolution via deltas](https://github.com/matrix-org/synapse/pull/3122) will +also mitigate some of the slow down. + +Another aspect that should be considered is the amount of data that is required +to perform the resolution. The current algorithm only requires the events for +the conflicted set, plus the events from the unconflicted set needed to auth +them. The proposed algorithm also requires the events in the auth chain +difference (calculating the auth chain difference may also require more data to +calculate). + +Delta state resolution is where if you have, say, two state sets and their +resolution, then you can use that result to work out the new resolution where +there has been a small change to the state sets. For the proposed algorithm, if +the following properties hold true then the result can be found by simply +applying steps 3 and 4 to the state deltas. The properties are: + + + +1. The delta contains no power events +1. The origin_server_ts of all events in state delta are strictly greater than those in the previous state sets +1. Any event that has been removed must not have been used to auth subsequent events (e.g. if we replaced a member event and that user had also set a topic) + +These properties will likely hold true for most state updates that happen in a +room, allowing servers to use this more efficient algorithm the majority of the +time. + + +### Full DAG + +It's worth noting that if the algorithm had access to the full room DAG that it +would really only help by ensuring that the ordering in "reverse topological +ordering" and "mainline ordering" respected the ordering induced by the DAG. + +This would help, e.g., ensure the latest topic was always picked rather than +rely on origin_server_ts and mainline. As well as obviate the need to maintain +a separate auth chain, and the difficulties that entails (like having to +reapply the unconflicted state at the end). + + +### Attack Vectors + +The main potential attack vector that needs to be considered is in the +_iterative auth checks_ algorithm, and whether an attacker could make use of +the fact that it's based on the unconflicted state and/or auth events of the +event. + + +# Appendix + +The following is an example room DAG, where time flows down the page. We shall +work through resolving the state at both _Message 2_ and _Message 3_. + + +![alt_text](images/state-res.png) + + +(Note that green circles are events sent by Alice, blue circles sent by Bob and +black arrows point to previous events. The red arrows are the mainline computed +during resolution.) + +First we resolve the state at _Message 2_. The conflicted event types are the +power levels and topics, and since the auth chains are the same for both state +sets the auth difference is the empty set. + +Step 1: The _full conflicted set_ are the events _P2, P3, Topic 2 _and _Topic +3_, of which _P2_ and _P3_ are the only power events. Since Alice (the room +creator) has a greater power level than Bob (and neither _P2 _and _P3_ appear +in each other's auth chain), the reverse topological ordering is: [_P2, P3_]. + +Step 2: Now we apply the auth rules iteratively, _P2_ trivially passes based on +the unconflicted state, but _P3_ does not pass since after _P2_ Bob no longer +has sufficient power to set state. This results in the power levels resolving +to _P2_. + +Step 3: Now we work out the mainline based on P2, which is coloured in red on +the diagram. We use the mainline to order _Topic 2_ and _Topic 3_. _Topic 2_ +points to_ P1_, while the closest mainline to _Topic 3_ is also _P1_. We then +order based on the _origin_server_ts_ of the two events, let's assume that +gives us: [_Topic 2_, _Topic 3_]. + +Step 4: Iteratively applying the auth rules results in _Topic 2_ being allowed, +but _Topic 3 _being denied (since Bob doesn't have power to set state anymore), +so the topic is resolved to _Topic 2_. + +This gives the resolved state at _Message 2_ to be _P2 _and _Topic 2_. (This is +actually the same result as the existing algorithm gives) + +Now let's look at the state at _Message 3_. + +Step 1: The full conflicted set are simple: _Topic 2_ and _Topic 4_. There are +no conflicted power events. + +Step 2: N/A + +Step 3: _Topic 2_ points to _P1_ in the mainline, and _Topic 4_ points to _P2_ +in its auth events. Since _P2_ comes after _P1_ in the mainline, this gives an +ordering of [_Topic 2, Topic 4_]. + +Step 4: Iteratively applying the auth rules results in both topics passing the +auth checks, and so the last topic, _Topic 4_, is chosen. + +This gives the resolved state at _Message 3_ to be _Topic 4_. + + +## Notes + +[^1]: + In the current room protocol these are: the create event, power levels, membership, join rules and third party invites. See the [spec](https://matrix.org/docs/spec/server_server/unstable.html#pdu-fields). + +[^2]: + In the current protocol these are: power levels, kicks, bans and join rules. + +[^3]: + Future room versions may have a concept of server ban event that works like existing bans, which would also be included + +[^4]: + The topology being considered here is the auth chain DAG, rather than the room DAG, so this ordering is only applicable to events which appear in the auth chain DAG. + +[^5]: + We do this so that, if we receive events with misleading auth_events, this ensures that the unconflicted state at least is correct. + +[^6]: + This isn't true in the current protocol + + + diff --git a/proposals/images/state-res.png b/proposals/images/state-res.png new file mode 100644 index 0000000000000000000000000000000000000000..573d1927afa216cd3a59e0d8e015aa300a30c295 GIT binary patch literal 61233 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�~ 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 literal 0 HcmV?d00001 From 4f97b80dccf9413b362f53d75cf85836b44bce19 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Jul 2018 23:35:54 +0100 Subject: [PATCH 02/73] fix MD --- proposals/0000-state-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-state-resolution.md b/proposals/0000-state-resolution.md index 66d1eb38..78d4059b 100644 --- a/proposals/0000-state-resolution.md +++ b/proposals/0000-state-resolution.md @@ -192,7 +192,7 @@ First we define: * **"State sets"** are the sets of state that the resolution algorithm tries to resolve, i.e. the inputs to the algorithm. * **"Power events"** are events that have the potential to remove the ability of another user to do something. These are power levels, join rules, bans and kicks. -* The **"unconflicted state map"** is the state where the value of each key exists and is the same in every state set. The** "conflicted state map"** is everything else. (Note that this is subtly different to the definition used in the existing algorithm, which considered the merge of a present event with an absent event to be unconflicted rather than conflicted) +* The **"unconflicted state map"** is the state where the value of each key exists and is the same in every state set. The **"conflicted state map"** is everything else. (Note that this is subtly different to the definition used in the existing algorithm, which considered the merge of a present event with an absent event to be unconflicted rather than conflicted) * The "**auth difference"** is calculated by first calculating the full auth chain for each state set and taking every event that doesn't appear in every auth chain. * The **"full conflicted set"** is the union of the conflicted state map and auth difference. * The **"reverse topological power ordering"**[^4] of a set of events is an ordering of the given events, plus any events in their auth chains that appear in the auth difference, ordered such that x < y if: From 3f891681ece09119781fa7222578eb1095ba9118 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 1 Aug 2018 14:12:46 +0100 Subject: [PATCH 03/73] Move proposal to have MSC number prefix --- proposals/{0000-state-resolution.md => 1442-state-resolution.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{0000-state-resolution.md => 1442-state-resolution.md} (100%) diff --git a/proposals/0000-state-resolution.md b/proposals/1442-state-resolution.md similarity index 100% rename from proposals/0000-state-resolution.md rename to proposals/1442-state-resolution.md From 67757a30274da1aec27dadb60759de729337c77a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 2 Aug 2018 13:48:44 +0100 Subject: [PATCH 04/73] Wrap lines --- proposals/1442-state-resolution.md | 174 +++++++++++++++++++---------- 1 file changed, 118 insertions(+), 56 deletions(-) diff --git a/proposals/1442-state-resolution.md b/proposals/1442-state-resolution.md index 78d4059b..4dd0fe7c 100644 --- a/proposals/1442-state-resolution.md +++ b/proposals/1442-state-resolution.md @@ -1,7 +1,8 @@ # State Resolution: Reloaded -Thoughts on the next iteration of the state resolution algorithm that aims to mitigate currently known attacks +Thoughts on the next iteration of the state resolution algorithm that aims to +mitigate currently known attacks # Background @@ -44,23 +45,48 @@ ban, any merge back should always ensure that the ban is still in the state. The current state resolution is known to have some undesirable properties, which can be summarized into two separate cases: -1. Moderation evasion ─ where an attacker can avoid e.g. bans by forking and joining the room DAG in particular ways. -1. State resets ─ where a server (often innocently) sends an event that points to disparate parts of the graph, causing state resolution to pick old state rather than later versions. +1. Moderation evasion ─ where an attacker can avoid e.g. bans by forking and + joining the room DAG in particular ways. +1. State resets ─ where a server (often innocently) sends an event that points + to disparate parts of the graph, causing state resolution to pick old state + rather than later versions. These have the following causes: -1. Conflicting state must pass auth checks to be eligible to be picked, but the algorithm does not consider previous (superseded) state changes in a fork. For example, where Alice gives Bob power and then Bob gives Charlie power on one branch of a conflict, when the latter power level event is authed against the original power level (where Bob didn't have power), it fails. -1. The algorithm relies on the deprecated and untrustable depth parameter to try and ensure that the "most recent" state is picked. Without having a copy of the complete room DAG the algorithm doesn't know that e.g. one topic event came strictly after another in the DAG. For efficiency and storage reasons servers are not required (or expected) to store the whole room DAG. -1. The algorithm always accepts events where there are no conflicting alternatives in other forks. This means that if an admin changed the join rules to `private`, then new joins on forks based on parts of the DAG which predate that change would always be accepted without being authed against the join_rules event. +1. Conflicting state must pass auth checks to be eligible to be picked, but the + algorithm does not consider previous (superseded) state changes in a fork. + For example, where Alice gives Bob power and then Bob gives Charlie power on + one branch of a conflict, when the latter power level event is authed + against the original power level (where Bob didn't have power), it fails. +1. The algorithm relies on the deprecated and untrustable depth parameter to + try and ensure that the "most recent" state is picked. Without having a copy + of the complete room DAG the algorithm doesn't know that e.g. one topic + event came strictly after another in the DAG. For efficiency and storage + reasons servers are not required (or expected) to store the whole room DAG. +1. The algorithm always accepts events where there are no conflicting + alternatives in other forks. This means that if an admin changed the join + rules to `private`, then new joins on forks based on parts of the DAG which + predate that change would always be accepted without being authed against + the join_rules event. # Desirable Properties -As well as the important properties listed in the "Background" section, there are also some other properties that would significantly improve the experience of end users, though not strictly essential. These include: +As well as the important properties listed in the "Background" section, there +are also some other properties that would significantly improve the experience +of end users, though not strictly essential. These include: -* Banning and changing power levels should "do the right thing", i.e. end users shouldn't have to take extra steps to make the state resolution produce the "right" results. -* Minimise occurences of "state resets". Servers will sometimes point to disparate parts of the room DAG (due to a variety of reasons), which ideally should not result in changes in the state. -* Be efficient; state resolution can happen a lot on some large rooms. Ideally it would also support efficiently working on "state deltas" - i.e. the ability to calculate state resolution incrementally from snapshots rather than having to consider the full state of each fork each time a conflict is resolved +* Banning and changing power levels should "do the right thing", i.e. end + users shouldn't have to take extra steps to make the state resolution + produce the "right" results. +* Minimise occurences of "state resets". Servers will sometimes point to + disparate parts of the room DAG (due to a variety of reasons), which ideally + should not result in changes in the state. +* Be efficient; state resolution can happen a lot on some large rooms. Ideally + it would also support efficiently working on "state deltas" - i.e. the + ability to calculate state resolution incrementally from snapshots rather + than having to consider the full state of each fork each time a conflict is + resolved # Ideas for New Algorithm @@ -132,7 +158,7 @@ maliciously forking. For that to work we need to ensure that there is a suitable ordering that puts e.g. bans before events sent in other forks. (However events can point to old parts of the DAG, for a variety of reasons, and ideally in that case the -resolved state would closely match the recent state). +resolved state would closely match the recent state). ## Power Level Ordering @@ -190,38 +216,69 @@ a room.) First we define: -* **"State sets"** are the sets of state that the resolution algorithm tries to resolve, i.e. the inputs to the algorithm. -* **"Power events"** are events that have the potential to remove the ability of another user to do something. These are power levels, join rules, bans and kicks. -* The **"unconflicted state map"** is the state where the value of each key exists and is the same in every state set. The **"conflicted state map"** is everything else. (Note that this is subtly different to the definition used in the existing algorithm, which considered the merge of a present event with an absent event to be unconflicted rather than conflicted) -* The "**auth difference"** is calculated by first calculating the full auth chain for each state set and taking every event that doesn't appear in every auth chain. -* The **"full conflicted set"** is the union of the conflicted state map and auth difference. -* The **"reverse topological power ordering"**[^4] of a set of events is an ordering of the given events, plus any events in their auth chains that appear in the auth difference, ordered such that x < y if: +* **"State sets"** are the sets of state that the resolution algorithm tries + to resolve, i.e. the inputs to the algorithm. +* **"Power events"** are events that have the potential to remove the ability + of another user to do something. These are power levels, join rules, bans + and kicks. +* The **"unconflicted state map"** is the state where the value of each key + exists and is the same in every state set. The **"conflicted state map"** is + everything else. (Note that this is subtly different to the definition used + in the existing algorithm, which considered the merge of a present event + with an absent event to be unconflicted rather than conflicted) +* The "**auth difference"** is calculated by first calculating the full auth + chain for each state set and taking every event that doesn't appear in every + auth chain. +* The **"full conflicted set"** is the union of the conflicted state map and + auth difference. +* The **"reverse topological power ordering"**[^4] of a set of events is an + ordering of the given events, plus any events in their auth chains that + appear in the auth difference, ordered such that x < y if: 1. x is in the auth chain of y, or if - 1. x's sender has a greater power level than y (calculated by looking at their respective auth events, or if - 1. x's origin_server_ts is less than y's, or if - 1. x's event_id is lexicographically less than y's + 2. x's sender has a greater power level than y (calculated by looking at + their respective auth events, or if + 3. x's origin_server_ts is less than y's, or if + 4. x's event_id is lexicographically less than y's This is also known as a lexicographical topological sort. -* The **"mainline ordering"** based on a power level event P of a set of events is calculated as follows: - 1. Generate the list of power levels starting at P and recursively take the power level from its auth events. This list is called the mainline, ordered such that P is last. - 1. We say the "closest mainline event" of an event is the first power level event encountered in mainline when iteratively descending through the power level events in the auth events. +* The **"mainline ordering"** based on a power level event P of a set of + events is calculated as follows: + 1. Generate the list of power levels starting at P and recursively take the + power level from its auth events. This list is called the mainline, + ordered such that P is last. + 1. We say the "closest mainline event" of an event is the first power level + event encountered in mainline when iteratively descending through the + power level events in the auth events. 1. Order the set of events such that x < y if: - 1. The closest mainline event of x appears strictly before the closest of y in the mainline list, or if + 1. The closest mainline event of x appears strictly before the closest + of y in the mainline list, or if 1. x's origin_server_ts is less than y's, or if 1. x's event_id lexicographically sorts before y's -* The **"iterative auth checks"** algorithm is where given a sorted list of events, the auth check algorithm is applied to each event in turn. The state events used to auth are built up from previous events that passed the auth checks, starting from a base set of state. If a required auth key doesn't exist in the state, then the one in the event's auth_events is used. (See _Variations_ and _Attack Vectors_ below). +* The **"iterative auth checks"** algorithm is where given a sorted list of + events, the auth check algorithm is applied to each event in turn. The state + events used to auth are built up from previous events that passed the auth + checks, starting from a base set of state. If a required auth key doesn't + exist in the state, then the one in the event's auth_events is used. (See + _Variations_ and _Attack Vectors_ below). The algorithm proceeds as follows: -1. Take all power events and any events in their auth chains that appear in the _full_ _conflicted set_ and order them by the _reverse topological power ordering._ -1. Apply the _iterative auth checks_ algorithm based on the unconflicted state map to get a partial set of resolved state. -1. Take all remaining events that weren't picked in step 1 and order them by the _mainline ordering_ based on the power level in the partially resolved state. -1. Apply the _iterative auth checks algorithm_ based on the partial resolved state. -1. Update the result with the _unconflicted state_ to get the final resolved state[^5]. -(_Note_: this is different from the current algorithm, which considered different event types at distinct stages) +1. Take all power events and any events in their auth chains that appear in the + _full_ _conflicted set_ and order them by the _reverse topological power + ordering._ +1. Apply the _iterative auth checks_ algorithm based on the unconflicted state + map to get a partial set of resolved state. +1. Take all remaining events that weren't picked in step 1 and order them by + the _mainline ordering_ based on the power level in the partially resolved + state. +1. Apply the _iterative auth checks algorithm_ based on the partial resolved + state. +1. Update the result with the _unconflicted state_ to get the final resolved + state[^5]. (_Note_: this is different from the current algorithm, which + considered different event types at distinct stages) An example python implementation can be found on github [here](https://github.com/matrix-org/matrix-test-state-resolution-ideas). @@ -265,12 +322,11 @@ proposed algorithm. The proposed algorithm still has some potentially unexpected behaviour. -One example of this is when Alice sets a topic and then gets banned. If an -event gets created (potentially much later) that points to both before and -after the topic and ban then the proposed algorithm will resolve and apply the -ban before resolving the topic, causing the topic to be denied and dropped from -the resolved state. This will result in no topic being set in the resolved -state. +One example of this is when Alice sets a topic and then gets banned. If an event +gets created (potentially much later) that points to both before and after the +topic and ban then the proposed algorithm will resolve and apply the ban before +resolving the topic, causing the topic to be denied and dropped from the +resolved state. This will result in no topic being set in the resolved state. ### Auth Events @@ -299,9 +355,9 @@ authorization to do an action (and vice versa). For example, in the current model bans (basically) revoke the ability for a particular user from being able to join. If the user later gets unbanned and then rejoins, the join would point to the join rules as the authorization that lets them join, but would not -(necessarily) point to the unban. This has the effect that if a state -resolution happened between the new join and the ban, the unban would not be -included in the resolution and so the join would be rejected. +(necessarily) point to the unban. This has the effect that if a state resolution +happened between the new join and the ban, the unban would not be included in +the resolution and so the join would be rejected. The changes to the current model that would be required to make the above assumptions true would be, for example: @@ -309,8 +365,10 @@ assumptions true would be, for example: 1. By default permissions are closed. -1. Bans would need to be a list in either the join rules event or a separate event type which all membership events pointed to. -1. Bans would only revoke the ability to join, not automatically remove users from the room. +1. Bans would need to be a list in either the join rules event or a separate + event type which all membership events pointed to. +1. Bans would only revoke the ability to join, not automatically remove users + from the room. 1. Change the defaults of join_rules to be closed by default @@ -338,8 +396,11 @@ applying steps 3 and 4 to the state deltas. The properties are: 1. The delta contains no power events -1. The origin_server_ts of all events in state delta are strictly greater than those in the previous state sets -1. Any event that has been removed must not have been used to auth subsequent events (e.g. if we replaced a member event and that user had also set a topic) +1. The origin_server_ts of all events in state delta are strictly greater than + those in the previous state sets +1. Any event that has been removed must not have been used to auth subsequent + events (e.g. if we replaced a member event and that user had also set a + topic) These properties will likely hold true for most state updates that happen in a room, allowing servers to use this more efficient algorithm the majority of the @@ -425,23 +486,24 @@ This gives the resolved state at _Message 3_ to be _Topic 4_. ## Notes -[^1]: - In the current room protocol these are: the create event, power levels, membership, join rules and third party invites. See the [spec](https://matrix.org/docs/spec/server_server/unstable.html#pdu-fields). +[^1]: In the current room protocol these are: the create event, power levels, + membership, join rules and third party invites. See the + [spec](https://matrix.org/docs/spec/server_server/unstable.html#pdu-fields). -[^2]: - In the current protocol these are: power levels, kicks, bans and join rules. +[^2]: In the current protocol these are: power levels, kicks, bans and join + rules. -[^3]: - Future room versions may have a concept of server ban event that works like existing bans, which would also be included +[^3]: Future room versions may have a concept of server ban event that works + like existing bans, which would also be included -[^4]: - The topology being considered here is the auth chain DAG, rather than the room DAG, so this ordering is only applicable to events which appear in the auth chain DAG. +[^4]: The topology being considered here is the auth chain DAG, rather than the + room DAG, so this ordering is only applicable to events which appear in the + auth chain DAG. -[^5]: - We do this so that, if we receive events with misleading auth_events, this ensures that the unconflicted state at least is correct. +[^5]: We do this so that, if we receive events with misleading auth_events, this + ensures that the unconflicted state at least is correct. -[^6]: - This isn't true in the current protocol +[^6]: This isn't true in the current protocol From 9af5ecd080b5ebef7a952c4639774524ec83c7be Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 2 Aug 2018 14:24:25 +0100 Subject: [PATCH 05/73] Fixup definition of reverse topological power ordering --- proposals/1442-state-resolution.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/1442-state-resolution.md b/proposals/1442-state-resolution.md index 4dd0fe7c..61ee4611 100644 --- a/proposals/1442-state-resolution.md +++ b/proposals/1442-state-resolution.md @@ -233,13 +233,13 @@ First we define: auth difference. * The **"reverse topological power ordering"**[^4] of a set of events is an ordering of the given events, plus any events in their auth chains that - appear in the auth difference, ordered such that x < y if: + appear in the auth difference, topologically ordered by their auth chains + with ties broken such that x < y if: - 1. x is in the auth chain of y, or if - 2. x's sender has a greater power level than y (calculated by looking at + 1. x's sender has a greater power level than y (calculated by looking at their respective auth events, or if - 3. x's origin_server_ts is less than y's, or if - 4. x's event_id is lexicographically less than y's + 2. x's origin_server_ts is less than y's, or if + 3. x's event_id is lexicographically less than y's This is also known as a lexicographical topological sort. From fa70e3e486952c942573ac2ce066f1b2e92a3713 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 2 Aug 2018 14:42:36 +0100 Subject: [PATCH 06/73] Mention why auth difference are useful --- proposals/1442-state-resolution.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/proposals/1442-state-resolution.md b/proposals/1442-state-resolution.md index 61ee4611..ce36efa5 100644 --- a/proposals/1442-state-resolution.md +++ b/proposals/1442-state-resolution.md @@ -160,6 +160,14 @@ e.g. bans before events sent in other forks. (However events can point to old parts of the DAG, for a variety of reasons, and ideally in that case the resolved state would closely match the recent state). +Similarly care should be taken when multiple changes to e.g. power levels happen +in a fork. If Alice gives Bob power (A), then Bob gives Charlie power (B) and +then Charlie, say, changes the ban level (C). If you try and resolve two state +sets one of which has A and the other has C, C will not pass auth unless B is +also taken into account. This case can be handled if we also consider the +difference in auth chains between the two sets, which in the previous example +would include B. + ## Power Level Ordering @@ -362,8 +370,6 @@ the resolution and so the join would be rejected. The changes to the current model that would be required to make the above assumptions true would be, for example: - - 1. By default permissions are closed. 1. Bans would need to be a list in either the join rules event or a separate event type which all membership events pointed to. From 472f75d9a596b331f49fd0ab3c2a02ae80dfca9f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 3 Aug 2018 12:18:41 +0100 Subject: [PATCH 07/73] Mention hotel california --- proposals/1442-state-resolution.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proposals/1442-state-resolution.md b/proposals/1442-state-resolution.md index ce36efa5..9fd7da8a 100644 --- a/proposals/1442-state-resolution.md +++ b/proposals/1442-state-resolution.md @@ -168,6 +168,14 @@ also taken into account. This case can be handled if we also consider the difference in auth chains between the two sets, which in the previous example would include B. +(This is also the root cause of the "Hotel California" issue, where left users +get spontaneously rejoined to rooms. This happens when a user has a sequence of +memberships changes of the form: leave (A), join (B) and then another leave (C). +In the current algorithm a resoluton of A and C would pick A, and a resolution +of A and B would then pick B, i.e. the join. This means that a suitably forked +graph can reset the state to B. This is fixed if when resolving A and C we also +consider B, since its in the auth chain of C.) + ## Power Level Ordering From dc499bc440ae38422f2041e4b2534579940c7756 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 3 Aug 2018 14:00:47 +0100 Subject: [PATCH 08/73] Expand on reverse topological power ordering --- proposals/1442-state-resolution.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proposals/1442-state-resolution.md b/proposals/1442-state-resolution.md index 9fd7da8a..1a2e82a3 100644 --- a/proposals/1442-state-resolution.md +++ b/proposals/1442-state-resolution.md @@ -257,7 +257,10 @@ First we define: 2. x's origin_server_ts is less than y's, or if 3. x's event_id is lexicographically less than y's - This is also known as a lexicographical topological sort. + This is also known as a lexicographical topological sort (i.e. this is the + unique topological ordering such that for an entry x all entries after it + must either have x in their auth chain or be greater than x as defined + above). This can be implemented using Kahn's algorithm. * The **"mainline ordering"** based on a power level event P of a set of events is calculated as follows: From 88b35d1be5e016958878af6890286023b31aa272 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 9 Aug 2018 08:30:50 -0600 Subject: [PATCH 09/73] Clarify how third party invites work This adds several diagrams to the Client-Server API about how invites are handled, including what the server is expected to do. This helps implementors know what they are supposed to do in the common cases, and infer where needed to get the more complex cases correct. Although lacking in some areas, this is how third party invites work today. A link to the now-improved client-server documentation for third party invites has been added to the server-server specification. The existing server-server specification needed no further changes on the subject. Fixes https://github.com/matrix-org/matrix-doc/issues/1366 --- api/identity/pubkey.yaml | 2 +- specification/modules/third_party_invites.rst | 205 +++++++++++++----- specification/server_server_api.rst | 4 + 3 files changed, 158 insertions(+), 53 deletions(-) diff --git a/api/identity/pubkey.yaml b/api/identity/pubkey.yaml index 00796975..5ea6341f 100644 --- a/api/identity/pubkey.yaml +++ b/api/identity/pubkey.yaml @@ -56,7 +56,7 @@ paths: get: summary: Check whether a long-term public key is valid. description: |- - Check whether a long-term public key is valid. + Check whether a long-term public key is valid. The request must be idempotent. operationId: isPubKeyValid parameters: - in: query diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index 9ea0eb0b..a9b06258 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -40,6 +40,7 @@ with ``content.membership`` = ``invite``, as well as a ``content.third_party_invite`` property which contains proof that the invitee does indeed own that third party identifier. + Events ------ @@ -55,41 +56,79 @@ A client asks a server to invite a user by their third party identifier. Server behaviour ---------------- -All homeservers MUST verify the signature in the event's -``content.third_party_invite.signed`` object. - -When a homeserver inserts an ``m.room.member`` ``invite`` event into the graph -because of an ``m.room.third_party_invite`` event, -that homesever MUST validate that the public -key used for signing is still valid, by checking ``key_validity_url`` from the ``m.room.third_party_invite``. It does -this by making an HTTP GET request to ``key_validity_url``: - -.. TODO: Link to identity server spec when it exists +Upon receipt of an ``/invite``, the server is expected to look up the third party +identifier with the provided identity server. If the lookup yields a result for +a Matrix User ID then the normal invite process can be initiated. This process +ends up looking like this: + +:: + + +---------+ +-------------+ +-----------------+ + | Client | | Homeserver | | IdentityServer | + +---------+ +-------------+ +-----------------+ + | | | + | POST /invite | | + |------------------------------------>| | + | | | + | | GET /lookup | + | |--------------------------------------------------->| + | | | + | | User ID result | + | |<---------------------------------------------------| + | | | + | | Invite process for the discovered User ID | + | |------------------------------------------ | + | | | | + | |<----------------------------------------- | + | | | + | Complete the /invite request | | + |<------------------------------------| | + | | | + + +However, if the lookup does not yield a bound User ID, the homeserver must store +the invite on the identity server and emit a valid ``m.room.third_party_invite`` +event to the room. This process ends up looking like this: + +:: + + +---------+ +-------------+ +-----------------+ + | Client | | Homeserver | | IdentityServer | + +---------+ +-------------+ +-----------------+ + | | | + | POST /invite | | + |------------------------------------>| | + | | | + | | GET /lookup | + | |-------------------------------------------------------------->| + | | | + | | "no users" result | + | |<--------------------------------------------------------------| + | | | + | | POST /store-invite | + | |-------------------------------------------------------------->| + | | | + | | Information needed for the m.room.third_party_invite | + | |<--------------------------------------------------------------| + | | | + | | Emit m.room.third_party_invite to the room | + | |------------------------------------------- | + | | | | + | |<------------------------------------------ | + | | | + | Complete the /invite request | | + |<------------------------------------| | + | | | -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 - } +All homeservers MUST verify the signature in the event's +``content.third_party_invite.signed`` object. -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. +The third party user will then need to verify their identity, which results in +a call from the Identity Server to the homeserver that bound the third party +identifier to a user. The homeserver then exchanges the ``m.room.third_party_invite`` +event in the room for a complete ``m.room.member`` event for ``membership: invite`` +for the user that has bound the third party identifier. 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 @@ -102,26 +141,85 @@ No other homeservers may reject the joining of the room on the basis of the room. They may, however, indicate to their clients that a member's' membership is questionable. -For example: - -#. Room R has two participating homeservers, H1, H2 - -#. 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, their homeserver H3 - is notified and attempts to issue an ``m.room.member`` event to participate - in the room. - -#. H3 validates the signature given to it by the identity server. - -#. H3 then asks H1 to join it to the room. H1 *must* validate the ``signed`` - property *and* check ``key_validity_url``. - -#. Having validated these things, H1 writes the invite event to the room, and H3 - begins participating in the room. H2 *must* accept this event. +For example, given H1, H2, and H3 as homeservers, UserA as a user of H1, and an +identity server IS, the full sequence for a third party invite would look like +the following. This diagram assumes H1 and H2 are residents of the room while +H3 is attempting to join. + +:: + + +-------+ +-----------------+ +-----+ +-----+ +-----+ +-----+ + | UserA | | ThirdPartyUser | | H1 | | H2 | | H3 | | IS | + +-------+ +-----------------+ +-----+ +-----+ +-----+ +-----+ + | | | | | | + | POST /invite for ThirdPartyUser | | | | + |----------------------------------->| | | | + | | | | | | + | | | GET /lookup | | | + | | |---------------------------------------------------------------------------------------------->| + | | | | | | + | | | | Lookup results (empty object) | + | | |<----------------------------------------------------------------------------------------------| + | | | | | | + | | | POST /store-invite | | | + | | |---------------------------------------------------------------------------------------------->| + | | | | | | + | | | | Token, keys, etc for third party invite | + | | |<----------------------------------------------------------------------------------------------| + | | | | | | + | | | (Federation) Emit m.room.third_party_invite | | | + | | |----------------------------------------------->| | | + | | | | | | + | Complete /invite request | | | | + |<-----------------------------------| | | | + | | | | | | + | | Verify identity | | | | + | |-------------------------------------------------------------------------------------------------------------------->| + | | | | | | + | | | | | POST /3pid/onbind | + | | | | |<---------------------------| + | | | | | | + | | | PUT /exchange_third_party_invite/:roomId | | + | | |<-----------------------------------------------------------------| | + | | | | | | + | | | Verify the request | | | + | | |------------------- | | | + | | | | | | | + | | |<------------------ | | | + | | | | | | + | | | (Federation) Emit m.room.member for invite | | | + | | |----------------------------------------------->| | | + | | | | | | + | | | | Accept event | | + | | | |------------- | | + | | | | | | | + | | | |<------------ | | + | | | | | | + | | | (Federation) Emit the m.room.member event sent to H2 | | + | | |----------------------------------------------------------------->| | + | | | | | | + | | | Complete /exchange_third_party_invite/:roomId request | | + | | |----------------------------------------------------------------->| | + | | | | | | + | | | | | Participate in the room | + | | | | |------------------------ | + | | | | | | | + | | | | |<----------------------- | + | | | | | | + + +Note that when H1 sends the ``m.room.member`` event to H2 and H3 it does not +have to block on either server's receipt of the event. Likewise, H1 may complete +the ``/exchange_third_party_invite/:roomId`` request at the same time as sending +the ``m.room.member`` event to H2 and H3. Additionally, H3 may complete the +``/3pid/onbind`` request it got from IS at any time - the completion is not shown +in the diagram. + +H1 MUST verify the request from H3 to ensure the ``signed`` property is correct +as well as the ``key_validity_url`` as still being valid. This is done by making +a request to the `Identity Server /isvalid`_ endpoint, using the provided URL +rather than constructing a new one. The query string and response for the provided +URL must match the Identity Server specification. 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. @@ -158,3 +256,6 @@ There is some risk of denial of service attacks by flooding homeservers or identity servers with many requests, or much state to store. Defending against these is left to the implementer's discretion. + + +.. _`Identity Server /isvalid`: ../identity_service/unstable.html#get-matrix-identity-api-v1-pubkey-isvalid diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 472aca12..b8df0b47 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -734,6 +734,10 @@ event to other servers in the room. Third-party invites ------------------- +.. NOTE:: + More information about third party invites is available in the `Client-Server API`_ + under the Third Party Invites module. + When an user wants to invite another user in a room but doesn't know the Matrix ID to invite, they can do so using a third-party identifier (e.g. an e-mail or a phone number). From a556e33eb9d606efeea321466678ba1ed2c3c379 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 Aug 2018 16:59:36 -0600 Subject: [PATCH 10/73] Spec /3pid/onbind Fixes https://github.com/matrix-org/matrix-doc/issues/1422 --- api/server-server/third_party_invite.yaml | 123 ++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/api/server-server/third_party_invite.yaml b/api/server-server/third_party_invite.yaml index 754a3282..2225bf1a 100644 --- a/api/server-server/third_party_invite.yaml +++ b/api/server-server/third_party_invite.yaml @@ -190,3 +190,126 @@ paths: type: object description: An empty object example: {} + "/3pid/onbind": + put: + summary: |- + Notifies the server that a third party identifier has been bound to one + of its users. + description: |- + Used by Identity Servers to notify the homeserver that one of its users + has bound a third party identifier successfully, including any pending + room invites the Identity Server has been made aware of. + operationId: onBindThirdPartyIdentifier + parameters: + - in: body + name: body + type: object + required: true + schema: + type: object + properties: + medium: + type: string + description: |- + The type of third party identifier. Currently only "email" is + a possible value. + example: "email" + address: + type: string + description: |- + The third party identifier itself. For example, an email address. + example: "alice@domain.com" + mxid: + type: string + description: The user that is now bound to the third party identifier. + example: "@alice:matrix.org" + invites: + type: array + description: |- + A list of pending invites that the third party identifier has received. + items: + type: object + title: Third Party Invite + properties: + medium: + type: string + description: |- + The type of third party invite issues. Currently only + "email" is used. + example: "email" + address: + type: string + description: |- + The third party identifier that received the invite. + example: "alice@domain.com" + mxid: + type: string + description: The now-bound user ID that received the invite. + example: "@alice:matrix.org" + room_id: + type: string + description: The room ID the invite is valid for. + example: "!somewhere:example.org" + sender: + type: string + description: The user ID that sent the invite. + example: "@bob:matrix.org" + # TODO (TravisR): Make this reusable when doing IS spec changes + # also make sure it isn't lying about anything, like the key version + signed: + type: object + title: Identity Server Signatures + description: |- + Signature from the Identity Server using a long-term private + key. + properties: + mxid: + type: string + description: |- + The user ID that has been bound to the third party + identifier. + example: "@alice:matrix.org" + token: + type: string + # TODO: What is this actually? + description: A token. + example: "Hello World" + signatures: + type: object + title: Identity Server Signature + description: |- + The signature from the identity server. The ``string`` key + is the identity server's domain name, such as vector.im + additionalProperties: + type: object + title: Identity Server Domain Signature + description: The signature for the identity server. + properties: + "ed25519:0": + type: string + description: The signature. + example: "SomeSignatureGoesHere" + required: ['ed25519:0'] + example: { + "vector.im": { + "ed25519:0": "SomeSignatureGoesHere" + } + } + required: ['mxid', 'token', 'signatures'] + required: + - medium + - address + - mxid + - room_id + - sender + - signed + required: ['medium', 'address', 'mxid', 'invites'] + responses: + 200: + description: The homeserver has processed the notification. + examples: + application/json: {} + schema: + type: object + description: An empty object + example: {} From 3de50cbc7f9b9187b426108f810eab9516586560 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Aug 2018 09:49:09 -0600 Subject: [PATCH 11/73] Clarify how /isvalid is meant to always be truthful --- api/identity/pubkey.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/identity/pubkey.yaml b/api/identity/pubkey.yaml index 5ea6341f..1cd7b94c 100644 --- a/api/identity/pubkey.yaml +++ b/api/identity/pubkey.yaml @@ -56,7 +56,8 @@ paths: get: summary: Check whether a long-term public key is valid. description: |- - Check whether a long-term public key is valid. The request must be idempotent. + Check whether a long-term public key is valid. The response should always + be the same, provided the key exists. operationId: isPubKeyValid parameters: - in: query From a803d9d07753d58137369a72aaab00e9535da9e4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 20:39:53 -0600 Subject: [PATCH 12/73] Document `validated_at` and `added_at` on GET /3pid Fixes https://github.com/matrix-org/matrix-doc/issues/661 --- api/client-server/administrative_contact.yaml | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml index 1cf66fe1..1b809c11 100644 --- a/api/client-server/administrative_contact.yaml +++ b/api/client-server/administrative_contact.yaml @@ -47,13 +47,15 @@ paths: description: The lookup was successful. examples: application/json: { - "threepids": [ - { - "medium": "email", - "address": "monkey@banana.island" - } - ] - } + "threepids": [ + { + "medium": "email", + "address": "monkey@banana.island", + "validated_at": 1535176800000, + "added_at": 1535336848756 + } + ] + } schema: type: object properties: @@ -70,6 +72,19 @@ paths: address: type: string description: The third party identifier address. + validated_at: + type: integer + format: int64 + description: |- + The timestamp, in milliseconds, when the identifier was + validated by the identity service. + added_at: + type: integer + format: int64 + description: + The timestamp, in milliseconds, when the homeserver + associated the third party identifier with the user. + required: ['medium', 'address', 'validated_at', 'added_at'] tags: - User data post: From b4be11af4283aad8533f367a0c7a8fc3a98ff1e2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 20:41:34 -0600 Subject: [PATCH 13/73] Document POST /account/3pid/delete Fixes https://github.com/matrix-org/matrix-doc/issues/985 Includes documentation for https://github.com/matrix-org/synapse/pull/3667 Raises https://github.com/matrix-org/matrix-doc/issues/1566 --- api/client-server/administrative_contact.yaml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml index 1b809c11..c1ecf337 100644 --- a/api/client-server/administrative_contact.yaml +++ b/api/client-server/administrative_contact.yaml @@ -148,6 +148,51 @@ paths: "$ref": "definitions/errors/error.yaml" tags: - User data + "/account/3pid/delete": + post: + summary: Deletes a third party identifier from the user's account + description: |- + Removes a third party identifier from the user's account. The homeserver + should attempt to unbind the identifier from the identity service, if the + homeserver is able to reasonably determine the identity service used. + operationId: delete3pidFromAccount + security: + - accessToken: [] + parameters: + - in: body + name: body + schema: + type: object + properties: + medium: + type: string + description: The medium of the third party identifier being removed. + enum: ["email", "msisdn"] + example: "email" + address: + type: string + description: The third party address being removed. + example: "example@domain.com" + required: ['medium', 'address'] + responses: + 200: + description: |- + The homeserver has disassociated the third party identifier from the + user. + schema: + type: object + properties: + id_server_unbind_result: + type: string + description: |- + The result of the homeserver's attempt to unbind the identifier from + the identity service. ``success`` indicates that the homeserver was + able to unbind the identifier while ``no-support`` means the homeserver + was not able to unbind, likely due to the identity service not supporting + the operation. Defaults to ``no-support``. + example: "success" + tags: + - User data "/account/3pid/email/requestToken": post: summary: Requests a validation token be sent to the given email address for the purpose of adding an email address to an account From 3b2bf18c67c6953d8f7552a4d88f96f69afe4996 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 20:54:22 -0600 Subject: [PATCH 14/73] Changelog --- changelogs/client_server/newsfragments/1567.feature | 1 + changelogs/client_server/newsfragments/1567.new | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelogs/client_server/newsfragments/1567.feature create mode 100644 changelogs/client_server/newsfragments/1567.new diff --git a/changelogs/client_server/newsfragments/1567.feature b/changelogs/client_server/newsfragments/1567.feature new file mode 100644 index 00000000..0c19b4be --- /dev/null +++ b/changelogs/client_server/newsfragments/1567.feature @@ -0,0 +1 @@ +Document the ``validated_at`` and ``added_at`` fields on ``GET /acount/3pid``. diff --git a/changelogs/client_server/newsfragments/1567.new b/changelogs/client_server/newsfragments/1567.new new file mode 100644 index 00000000..15e3305b --- /dev/null +++ b/changelogs/client_server/newsfragments/1567.new @@ -0,0 +1 @@ +Add ``POST /account/3pid/delete`` From 1bc0f63bfb26f7f60f034c6bdcf38b901005516b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 21:03:48 -0600 Subject: [PATCH 15/73] Clarify the homeserver's behaviour for user searching The homeserver can implement its own algorithm, but is suggested to ensure the requesting user can reasonably see the other users. The text is intentionally left vague for whether or not the API is local users only to give the homeserver some flexibility in this area. General cleanup is also included in this commit. Fixes https://github.com/matrix-org/matrix-doc/issues/1108 --- api/client-server/users.yaml | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/api/client-server/users.yaml b/api/client-server/users.yaml index a682b435..ef527a23 100644 --- a/api/client-server/users.yaml +++ b/api/client-server/users.yaml @@ -31,8 +31,13 @@ paths: post: summary: Searches the user directory. description: |- - This API performs a server-side search over all users registered on the server. - It searches user ID and displayname case-insensitively for users that you share a room with or that are in public rooms. + Performs a search for users on the homeserver. The homeserver may + determine which subset of users are searched, however the homeserver + is encouraged to only consider users the requesting user shares a + room with and those who reside in public rooms. + + The search is performed case-insensitively on user IDs and display + names. operationId: searchUserDirectory security: - accessToken: [] @@ -48,7 +53,7 @@ paths: example: "foo" limit: type: number - description: The maximum number of results to return (Defaults to 10). + description: The maximum number of results to return. Defaults to 10. example: 10 required: ["search_term"] responses: @@ -56,15 +61,15 @@ paths: description: The results of the search. examples: application/json: { - "results": [ - { - "user_id": "@foo:bar.com", - "display_name": "Foo", - "avatar_url": "mxc://bar.com/foo" - } - ], - "limited": false - } + "results": [ + { + "user_id": "@foo:bar.com", + "display_name": "Foo", + "avatar_url": "mxc://bar.com/foo" + } + ], + "limited": false + } schema: type: object required: ["results", "limited"] From e926b4a84011332a698189ec421993d4d5605388 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 21:05:09 -0600 Subject: [PATCH 16/73] Changelog --- changelogs/client_server/newsfragments/1569.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1569.clarification diff --git a/changelogs/client_server/newsfragments/1569.clarification b/changelogs/client_server/newsfragments/1569.clarification new file mode 100644 index 00000000..83185e02 --- /dev/null +++ b/changelogs/client_server/newsfragments/1569.clarification @@ -0,0 +1 @@ +Clarify the homeserver's behaviour for searching users. From 55c0b968b5aaa86c9c37415dded849b060f47f24 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 22:22:14 -0600 Subject: [PATCH 17/73] Fix event schemas for /sync This commit clarifies the required keys for each type of event that appears in sync, fixes the core event schema not declaring 'content' as required, and includes a mention that events may not have a room_id when appearing in /sync. Fixes https://github.com/matrix-org/matrix-doc/issues/595 Fixes https://github.com/matrix-org/matrix-doc/issues/909 --- .../definitions/event_batch.yaml | 5 +- .../definitions/room_event_batch.yaml | 27 +++++++++ .../definitions/state_event_batch.yaml | 28 +++++++++ .../definitions/timeline_batch.yaml | 7 ++- api/client-server/sync.yaml | 9 +-- .../schema/core-event-schema/event.yaml | 1 + .../schema/core-event-schema/room_event.yaml | 39 ++---------- .../core-event-schema/sync_room_event.yaml | 60 +++++++++++++++++++ .../core-event-schema/sync_state_event.yaml | 35 +++++++++++ 9 files changed, 167 insertions(+), 44 deletions(-) create mode 100644 api/client-server/definitions/room_event_batch.yaml create mode 100644 api/client-server/definitions/state_event_batch.yaml create mode 100644 event-schemas/schema/core-event-schema/sync_room_event.yaml create mode 100644 event-schemas/schema/core-event-schema/sync_state_event.yaml diff --git a/api/client-server/definitions/event_batch.yaml b/api/client-server/definitions/event_batch.yaml index 21377a41..d169c355 100644 --- a/api/client-server/definitions/event_batch.yaml +++ b/api/client-server/definitions/event_batch.yaml @@ -1,4 +1,5 @@ # Copyright 2016 OpenMarket Ltd +# Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ # limitations under the License. properties: events: - description: List of events + description: List of events. items: allOf: - - $ref: event.yaml + - $ref: event-schemas/schema/core-event-schema/event.yaml type: object type: array type: object diff --git a/api/client-server/definitions/room_event_batch.yaml b/api/client-server/definitions/room_event_batch.yaml new file mode 100644 index 00000000..7367198f --- /dev/null +++ b/api/client-server/definitions/room_event_batch.yaml @@ -0,0 +1,27 @@ +# Copyright 2018 New Vector Ltd +# +# 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. +properties: + events: + description: List of events. + items: + allOf: + - $ref: event-schemas/schema/core-event-schema/sync_room_event.yaml + type: object + required: + - event_id + #- room_id - Not in /sync + - sender + - origin_server_ts + type: array +type: object diff --git a/api/client-server/definitions/state_event_batch.yaml b/api/client-server/definitions/state_event_batch.yaml new file mode 100644 index 00000000..db01ecb1 --- /dev/null +++ b/api/client-server/definitions/state_event_batch.yaml @@ -0,0 +1,28 @@ +# Copyright 2018 New Vector Ltd +# +# 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. +properties: + events: + description: List of events. + items: + allOf: + - $ref: event-schemas/schema/core-event-schema/sync_state_event.yaml + type: object + required: + - event_id + #- room_id - Not in /sync + - sender + - origin_server_ts + - state_key + type: array +type: object diff --git a/api/client-server/definitions/timeline_batch.yaml b/api/client-server/definitions/timeline_batch.yaml index ce613ac4..abf93830 100644 --- a/api/client-server/definitions/timeline_batch.yaml +++ b/api/client-server/definitions/timeline_batch.yaml @@ -1,4 +1,5 @@ # Copyright 2016 OpenMarket Ltd +# Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. allOf: -- $ref: event_batch.yaml +- $ref: room_event_batch.yaml properties: limited: description: True if the number of events returned was limited by the ``limit`` - on the filter + on the filter. type: boolean prev_batch: description: A token that can be supplied to the ``from`` parameter of the - rooms/{roomId}/messages endpoint + rooms/{roomId}/messages endpoint. type: string type: object diff --git a/api/client-server/sync.yaml b/api/client-server/sync.yaml index 4b44c20e..6a1d4f60 100644 --- a/api/client-server/sync.yaml +++ b/api/client-server/sync.yaml @@ -134,7 +134,7 @@ paths: ``timeline``, if ``since`` is not given, or ``full_state`` is true). allOf: - - $ref: "definitions/event_batch.yaml" + - $ref: "definitions/state_event_batch.yaml" timeline: title: Timeline type: object @@ -202,7 +202,7 @@ paths: delta against the archived ``state`` not the ``invite_state``. allOf: - - $ref: "definitions/event_batch.yaml" + - $ref: "definitions/state_event_batch.yaml" leave: title: Left rooms type: object @@ -218,7 +218,7 @@ paths: description: |- The state updates for the room up to the start of the timeline. allOf: - - $ref: "definitions/event_batch.yaml" + - $ref: "definitions/state_event_batch.yaml" timeline: title: Timeline type: object @@ -261,6 +261,8 @@ paths: description: |- Information on end-to-end encryption keys, as specified in |device_lists_sync|_. + required: + - next_batch examples: application/json: { "next_batch": "s72595_4483_1934", @@ -312,7 +314,6 @@ paths: { "sender": "@alice:example.com", "type": "m.room.message", - "age": 124524, "txn_id": "1234", "content": { "body": "I am a fish", diff --git a/event-schemas/schema/core-event-schema/event.yaml b/event-schemas/schema/core-event-schema/event.yaml index 7a060283..0fe5ac6c 100644 --- a/event-schemas/schema/core-event-schema/event.yaml +++ b/event-schemas/schema/core-event-schema/event.yaml @@ -10,5 +10,6 @@ properties: type: string required: - type + - content title: Event type: object diff --git a/event-schemas/schema/core-event-schema/room_event.yaml b/event-schemas/schema/core-event-schema/room_event.yaml index a8a23f54..007372a5 100644 --- a/event-schemas/schema/core-event-schema/room_event.yaml +++ b/event-schemas/schema/core-event-schema/room_event.yaml @@ -1,45 +1,14 @@ allOf: -- $ref: event.yaml +- $ref: sync_room_event.yaml description: In addition to the Event fields, Room Events have the following additional fields. properties: - event_id: - description: The globally unique event identifier. - type: string room_id: - description: The ID of the room associated with this event. - type: string - sender: - description: Contains the fully-qualified ID of the user who *sent* - this event. + description: |- + The ID of the room associated with this event. Will not be present on events + that arrive through ``/sync``, despite being required everywhere else. type: string - origin_server_ts: - description: Timestamp in milliseconds on originating homeserver - when this event was sent. - type: number - unsigned: - description: Contains optional extra information about the event. - properties: - age: - description: The time in milliseconds that has elapsed since the event was - sent. This field is generated by the local homeserver, and may be incorrect - if the local time on at least one of the two servers is out of sync, which can - cause the age to either be negative or greater than it actually is. - type: integer - redacted_because: - description: Optional. The event that redacted this event, if any. - title: Event - type: object - transaction_id: - description: The client-supplied transaction ID, if the client being given - the event is the same one which sent it. - type: string - title: UnsignedData - type: object required: -- event_id - room_id -- sender -- origin_server_ts title: Room Event type: object diff --git a/event-schemas/schema/core-event-schema/sync_room_event.yaml b/event-schemas/schema/core-event-schema/sync_room_event.yaml new file mode 100644 index 00000000..300dfb2b --- /dev/null +++ b/event-schemas/schema/core-event-schema/sync_room_event.yaml @@ -0,0 +1,60 @@ +# Copyright 2018 New Vector Ltd +# +# 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. + +# Note: this is technically not a core event schema, however it is included here +# to keep things sane. The short story is that /sync doesn't require a room_id to +# be on events, so we give it a whole event structure as a base for room_event. +# This base doesn't declare a room_id, which instead appears in the room_event. + +allOf: +- $ref: event.yaml +description: In addition to the Event fields, Room Events have the following additional + fields. +properties: + event_id: + description: The globally unique event identifier. + type: string + sender: + description: Contains the fully-qualified ID of the user who *sent* + this event. + type: string + origin_server_ts: + description: Timestamp in milliseconds on originating homeserver + when this event was sent. + type: number + unsigned: + description: Contains optional extra information about the event. + properties: + age: + description: The time in milliseconds that has elapsed since the event was + sent. This field is generated by the local homeserver, and may be incorrect + if the local time on at least one of the two servers is out of sync, which can + cause the age to either be negative or greater than it actually is. + type: integer + redacted_because: + description: Optional. The event that redacted this event, if any. + title: Event + type: object + transaction_id: + description: The client-supplied transaction ID, if the client being given + the event is the same one which sent it. + type: string + title: UnsignedData + type: object +required: +- event_id +- sender +- origin_server_ts +title: Room Event +type: object diff --git a/event-schemas/schema/core-event-schema/sync_state_event.yaml b/event-schemas/schema/core-event-schema/sync_state_event.yaml new file mode 100644 index 00000000..a073caac --- /dev/null +++ b/event-schemas/schema/core-event-schema/sync_state_event.yaml @@ -0,0 +1,35 @@ +# Copyright 2018 New Vector Ltd +# +# 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. + +# See sync_room_event.yaml for why this file is here. + +allOf: +- $ref: sync_room_event.yaml +description: In addition to the Room Event fields, State Events have the following + additional fields. +properties: + prev_content: + description: Optional. The previous ``content`` for this event. If there is no + previous content, this key will be missing. + title: EventContent + type: object + state_key: + description: A unique key which defines the overwriting semantics for this piece + of room state. This value is often a zero-length string. The presence of this + key makes this event a State Event. The key MUST NOT start with '_'. + type: string +required: +- state_key +title: State Event +type: object From fd47184ce37d4b6c387ed5d884c11946e560d20a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 26 Aug 2018 22:23:38 -0600 Subject: [PATCH 18/73] Changelog --- changelogs/client_server/newsfragments/1573.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1573.clarification diff --git a/changelogs/client_server/newsfragments/1573.clarification b/changelogs/client_server/newsfragments/1573.clarification new file mode 100644 index 00000000..74efa28f --- /dev/null +++ b/changelogs/client_server/newsfragments/1573.clarification @@ -0,0 +1 @@ +Clarify the event fields used in the ``/sync`` response. From 80699812525655fa1f251eb527993af3c71df955 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Aug 2018 14:19:10 -0600 Subject: [PATCH 19/73] Add federation /user/* endpoints Fixes https://github.com/matrix-org/matrix-doc/issues/1438 --- api/server-server/user_devices.yaml | 84 +++++++++++++ api/server-server/user_keys.yaml | 188 ++++++++++++++++++++++++++++ specification/server_server_api.rst | 17 ++- 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 api/server-server/user_devices.yaml create mode 100644 api/server-server/user_keys.yaml diff --git a/api/server-server/user_devices.yaml b/api/server-server/user_devices.yaml new file mode 100644 index 00000000..d1644270 --- /dev/null +++ b/api/server-server/user_devices.yaml @@ -0,0 +1,84 @@ +# Copyright 2018 New Vector Ltd +# +# 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. + +swagger: '2.0' +info: + title: "Matrix Federation User Device Management API" + version: "1.0.0" +host: localhost:8448 +schemes: + - https +basePath: /_matrix/federation/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + $ref: definitions/security.yaml +paths: + "/user/devices/{userId}": + get: + summary: Gets all of the user's devices + description: Gets information on all of the user's devices + operationId: getUserDevices + security: + - signedRequest: [] + parameters: + - in: path + name: userId + type: string + required: true + description: |- + The user ID to retrieve devices for. Must be a user local to the + receiving homeserver. + required: true + x-example: "@alice:example.org" + responses: + 200: + description: The user's devices. + schema: + type: object + properties: + user_id: + type: string + description: The user ID devices were requested for. + example: "@alice:example.org" + stream_id: + type: integer + description: |- + An ID the requesting homeserver may use to detect changes in the + device list. This should increase as time goes on, and always + produce the same ``devices`` list if not incremented. + example: 334608 + devices: + type: array + description: The user's devices. May be empty. + items: + type: object + title: User Device + properties: + device_id: + type: string + description: The device ID. + example: "JLAFKJWSCS" + keys: + type: object + description: Identity keys for the device. + $ref: "../client-server/definitions/device_keys.yaml" + device_display_name: + type: string + description: Optional display name for the device. + example: "Alice's Mobile Phone" + required: ['device_id', 'keys'] + required: ['user_id', 'stream_id', 'devices'] \ No newline at end of file diff --git a/api/server-server/user_keys.yaml b/api/server-server/user_keys.yaml new file mode 100644 index 00000000..86993433 --- /dev/null +++ b/api/server-server/user_keys.yaml @@ -0,0 +1,188 @@ +# Copyright 2018 New Vector Ltd +# +# 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. + +swagger: '2.0' +info: + title: "Matrix Federation User Key Management API" + version: "1.0.0" +host: localhost:8448 +schemes: + - https +basePath: /_matrix/federation/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + $ref: definitions/security.yaml +paths: + "/user/keys/claim": + post: + summary: Claims one-time encryption keys for a user. + description: |- + Claims one-time keys for use in pre-key messages. + operationId: claimUserEncryptionKeys + security: + - signedRequest: [] + parameters: + - in: body + name: body + type: object + required: true + schema: + type: object + properties: + one_time_keys: + type: object + description: |- + The keys to be claimed. A map from user ID, to a map from + device ID to algorithm name. + additionalProperties: + type: object + additionalProperties: + type: string + description: algorithm + example: "signed_curve25519" + example: { + "@alice:example.com": { + "JLAFKJWSCS": "signed_curve25519" + } + } + required: + - one_time_keys + responses: + 200: + description: The claimed keys + schema: + type: object + properties: + one_time_keys: + type: object + description: |- + One-time keys for the queried devices. A map from user ID, to a + map from devices to a map from ``:`` to the key object. + additionalProperties: + type: object + additionalProperties: + type: + - string + - object + required: ['one_time_keys'] + examples: + application/json: { + "one_time_keys": { + "@alice:example.com": { + "JLAFKJWSCS": { + "signed_curve25518:AAAAHg": { + "key": "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs", + "signatures": { + "@alice:example.com": { + "ed25519:JLAFKJWSCS": "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw" + } + } + } + } + } + } + } + "/user/keys/query": + post: + summary: Download device identity keys. + description: |- + Returns the current devices and identity keys for the given users. + operationId: queryUserEncryptionKeys + security: + - signedRequest: [] + parameters: + - in: body + name: body + type: object + required: true + schema: + type: object + properties: + device_keys: + type: object + description: |- + The keys to be downloaded. A map from user ID, to a list of + device IDs, or to an empty list to indicate all devices for the + corresponding user. + additionalProperties: + type: array + items: + type: string + description: "Device ID" + example: { + "@alice:example.com": [] + } + responses: + 200: + description: The device information. + schema: + type: object + properties: + device_keys: + type: object + description: |- + Information on the queried devices. A map from user ID, to a + map from device ID to device information. For each device, + the information returned will be the same as uploaded via + ``/keys/upload``, with the addition of an ``unsigned`` + property. + additionalProperties: + type: object + additionalProperties: + allOf: + - $ref: ../client-server/definitions/device_keys.yaml + properties: + unsigned: + title: UnsignedDeviceInfo + type: object + description: |- + Additional data added to the device key information + by intermediate servers, and not covered by the + signatures. + properties: + device_display_name: + type: string + description: + The display name which the user set on the device. + required: ['device_keys'] + examples: + application/json: { + "device_keys": { + "@alice:example.com": { + "JLAFKJWSCS": { + "user_id": "@alice:example.com", + "device_id": "JLAFKJWSCS", + "algorithms": [ + "m.olm.v1.curve25519-aes-sha256", + "m.megolm.v1.aes-sha" + ], + "keys": { + "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", + "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI" + }, + "signatures": { + "@alice:example.com": { + "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" + } + }, + "unsigned": { + "device_display_name": "Alice's mobile phone" + } + } + } + } + } diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index dbde8b10..634b8fd0 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -890,6 +890,20 @@ nothing else. {{openid_ss_http_api}} +End-to-End Encryption +--------------------- + +This section compliments the `End-to-End Encryption module`_ of the Client-Server +API. For detailed information about end-to-end encryption, please see that module. + +The APIs defined here are designed to be able to proxy much of the client's request +through to federation, and have the response also be proxied through to the client. + +{{user_keys_ss_http_api}} + +{{user_devices_ss_http_api}} + + Send-to-device messaging ------------------------ @@ -1077,7 +1091,8 @@ that are too long. .. _`Invitation storage`: ../identity_service/unstable.html#invitation-storage .. _`Identity Service API`: ../identity_service/unstable.html -.. _`Client-Server API`: ../client_server/unstable.html +.. _`Client-Server API`: ../client_server/%CLIENT_RELEASE_LABEL%.html .. _`Inviting to a room`: #inviting-to-a-room .. _`Canonical JSON`: ../appendices.html#canonical-json .. _`Unpadded Base64`: ../appendices.html#unpadded-base64 +.. _`End-to-End Encryption module`: ../client-server/%CLIENT_RELEASE_LABEL%.html#end-to-end-encryption From be97e95dc123080514a07ef6b90a4842616b8921 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Aug 2018 14:49:38 -0600 Subject: [PATCH 20/73] Promote device management to be a dedicated section --- specification/server_server_api.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 634b8fd0..cc23a83d 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -890,6 +890,19 @@ nothing else. {{openid_ss_http_api}} + +Device Management +----------------- + +.. TODO: TravisR - Incorporate https://github.com/matrix-org/matrix-doc/issues/1212 + +A user's devices may need to be queried over federation for different purposes, such +as end-to-end encryption or for utilizing send-to-device messaging. This section is +intended to compliment the `Device Management module`_ of the Client-Server API. + +{{user_devices_ss_http_api}} + + End-to-End Encryption --------------------- @@ -901,8 +914,6 @@ through to federation, and have the response also be proxied through to the clie {{user_keys_ss_http_api}} -{{user_devices_ss_http_api}} - Send-to-device messaging ------------------------ @@ -1095,4 +1106,5 @@ that are too long. .. _`Inviting to a room`: #inviting-to-a-room .. _`Canonical JSON`: ../appendices.html#canonical-json .. _`Unpadded Base64`: ../appendices.html#unpadded-base64 +.. _`Device Management module`: ../client-server/%CLIENT_RELEASE_LABEL%.html#device-management .. _`End-to-End Encryption module`: ../client-server/%CLIENT_RELEASE_LABEL%.html#end-to-end-encryption From bb515d15a40470fc2c9a9290428b8fcf675ec0c1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Aug 2018 18:55:54 -0600 Subject: [PATCH 21/73] Clarify which collation to use for user directory searching --- api/client-server/users.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/client-server/users.yaml b/api/client-server/users.yaml index ef527a23..619263ff 100644 --- a/api/client-server/users.yaml +++ b/api/client-server/users.yaml @@ -37,7 +37,8 @@ paths: room with and those who reside in public rooms. The search is performed case-insensitively on user IDs and display - names. + names preferably using a collation determined based upon the + ``Accept-Language`` header provided in the request, if present. operationId: searchUserDirectory security: - accessToken: [] From f2332d242aeb079e77396bd6bd8c5d7f2a629b7e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Aug 2018 18:58:16 -0600 Subject: [PATCH 22/73] Add some SHOULDs and MUSTs to the user directory requirements --- api/client-server/users.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/client-server/users.yaml b/api/client-server/users.yaml index 619263ff..8a60a7bc 100644 --- a/api/client-server/users.yaml +++ b/api/client-server/users.yaml @@ -33,8 +33,10 @@ paths: description: |- Performs a search for users on the homeserver. The homeserver may determine which subset of users are searched, however the homeserver - is encouraged to only consider users the requesting user shares a - room with and those who reside in public rooms. + MUST at a minimum consider the users the requesting user shares a + room with and those who reside in public rooms (known to the homeserver). + The search MUST consider local users to the homeserver, and SHOULD + query remote users as part of the search. The search is performed case-insensitively on user IDs and display names preferably using a collation determined based upon the From efef3412a0278d1dff714a8fe9c157369e4db2ff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Aug 2018 19:07:32 -0600 Subject: [PATCH 23/73] Take out the underscore restriction from state events It's not needed anymore, and we should remove it while we're in the area. Includes other misc changes to the schema layout. --- .../schema/core-event-schema/state_event.yaml | 14 +------------- .../schema/core-event-schema/sync_room_event.yaml | 3 +-- .../schema/core-event-schema/sync_state_event.yaml | 2 +- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/event-schemas/schema/core-event-schema/state_event.yaml b/event-schemas/schema/core-event-schema/state_event.yaml index 020e9087..37d4426f 100644 --- a/event-schemas/schema/core-event-schema/state_event.yaml +++ b/event-schemas/schema/core-event-schema/state_event.yaml @@ -1,19 +1,7 @@ allOf: - $ref: room_event.yaml +- $ref: sync_state_event.yaml description: In addition to the Room Event fields, State Events have the following additional fields. -properties: - prev_content: - description: Optional. The previous ``content`` for this event. If there is no - previous content, this key will be missing. - title: EventContent - type: object - state_key: - description: A unique key which defines the overwriting semantics for this piece - of room state. This value is often a zero-length string. The presence of this - key makes this event a State Event. The key MUST NOT start with '_'. - type: string -required: -- state_key title: State Event type: object diff --git a/event-schemas/schema/core-event-schema/sync_room_event.yaml b/event-schemas/schema/core-event-schema/sync_room_event.yaml index 300dfb2b..fbbf154f 100644 --- a/event-schemas/schema/core-event-schema/sync_room_event.yaml +++ b/event-schemas/schema/core-event-schema/sync_room_event.yaml @@ -26,8 +26,7 @@ properties: description: The globally unique event identifier. type: string sender: - description: Contains the fully-qualified ID of the user who *sent* - this event. + description: Contains the fully-qualified ID of the user who sent this event. type: string origin_server_ts: description: Timestamp in milliseconds on originating homeserver diff --git a/event-schemas/schema/core-event-schema/sync_state_event.yaml b/event-schemas/schema/core-event-schema/sync_state_event.yaml index a073caac..30af9698 100644 --- a/event-schemas/schema/core-event-schema/sync_state_event.yaml +++ b/event-schemas/schema/core-event-schema/sync_state_event.yaml @@ -27,7 +27,7 @@ properties: state_key: description: A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this - key makes this event a State Event. The key MUST NOT start with '_'. + key makes this event a State Event. type: string required: - state_key From 1051aff108ba814b62feaf01e978da288dc90ee7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Aug 2018 18:36:07 -0600 Subject: [PATCH 24/73] Document the inhibit_login registration option Fixes https://github.com/matrix-org/matrix-doc/issues/1351 --- api/client-server/registration.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/client-server/registration.yaml b/api/client-server/registration.yaml index e4b05629..72ec1fb6 100644 --- a/api/client-server/registration.yaml +++ b/api/client-server/registration.yaml @@ -117,6 +117,13 @@ paths: A display name to assign to the newly-created device. Ignored if ``device_id`` corresponds to a known device. example: Jungle Phone + inhibit_login: + type: boolean + description: |- + If true, an ``access_token`` and ``device_id`` should not be + returned from this call, therefore preventing an automatic + login. Defaults to false. + example: false responses: 200: description: The account has been registered. @@ -141,6 +148,7 @@ paths: description: |- An access token for the account. This access token can then be used to authorize other requests. + Required if the ``inhibit_login`` option is false. home_server: type: string description: |- @@ -155,6 +163,8 @@ paths: description: |- ID of the registered device. Will be the same as the corresponding parameter in the request, if one was specified. + Required if the ``inhibit_login`` option is false. + required: ['user_id'] 400: description: |- Part of the request was invalid. This may include one of the following error codes: From 5d0f77de3abc058a12386e6fef2ff24d105d68a8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Aug 2018 18:38:12 -0600 Subject: [PATCH 25/73] changelog --- changelogs/client_server/newsfragments/1589.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1589.feature diff --git a/changelogs/client_server/newsfragments/1589.feature b/changelogs/client_server/newsfragments/1589.feature new file mode 100644 index 00000000..8c8b1a86 --- /dev/null +++ b/changelogs/client_server/newsfragments/1589.feature @@ -0,0 +1 @@ +Add an ``inhibit_login`` registration option. From 0b7b3bfcb0c2da63ef0eff799e83bbaf00ce40f0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Aug 2018 11:20:33 +0100 Subject: [PATCH 26/73] Reindent auth rules section RST expects sub lists to be indented by three or more spaces. By doing so we can then rely on `#.` for automatic numbering. --- specification/server_server_api.rst | 112 ++++++++++++++-------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 9440f2f2..c67bea3d 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -359,100 +359,100 @@ The rules are as follows: 1. If type is ``m.room.create``, allow if and only if it has no previous events - *i.e.* it is the first event in the room. -2. If type is ``m.room.member``: +#. If type is ``m.room.member``: - a. If ``membership`` is ``join``: + a. If ``membership`` is ``join``: - i. If the only previous event is an ``m.room.create`` - and the ``state_key`` is the creator, allow. + i. If the only previous event is an ``m.room.create`` + and the ``state_key`` is the creator, allow. - #. If the ``sender`` does not match ``state_key``, reject. + #. If the ``sender`` does not match ``state_key``, reject. - #. If the user's current membership state is ``invite`` or ``join``, - allow. + #. If the user's current membership state is ``invite`` or ``join``, + allow. - #. If the ``join_rule`` is ``public``, allow. + #. If the ``join_rule`` is ``public``, allow. - #. Otherwise, reject. + #. Otherwise, reject. - b. If ``membership`` is ``invite``: + #. If ``membership`` is ``invite``: - i. If the ``sender``'s current membership state is not ``join``, reject. + i. If the ``sender``'s current membership state is not ``join``, reject. - #. If *target user*'s current membership state is ``join`` or ``ban``, - reject. + #. If *target user*'s current membership state is ``join`` or ``ban``, + reject. - #. If the ``sender``'s power level is greater than or equal to the *invite - level*, allow. + #. If the ``sender``'s power level is greater than or equal to the *invite + level*, allow. - #. Otherwise, reject. + #. Otherwise, reject. - c. If ``membership`` is ``leave``: + #. If ``membership`` is ``leave``: - i. If the ``sender`` matches ``state_key``, allow if and only if that user's - current membership state is ``invite`` or ``join``. + i. If the ``sender`` matches ``state_key``, allow if and only if that user's + current membership state is ``invite`` or ``join``. - #. If the ``sender``'s current membership state is not ``join``, reject. + #. If the ``sender``'s current membership state is not ``join``, reject. - #. If the *target user*'s current membership state is ``ban``, and the - ``sender``'s power level is less than the *ban level*, reject. + #. If the *target user*'s current membership state is ``ban``, and the + ``sender``'s power level is less than the *ban level*, reject. - #. If the ``sender``'s power level is greater than or equal to the *kick - level*, and the *target user*'s power level is less than the - ``sender``'s power level, allow. + #. If the ``sender``'s power level is greater than or equal to the *kick + level*, and the *target user*'s power level is less than the + ``sender``'s power level, allow. - #. Otherwise, reject. + #. Otherwise, reject. - d. If ``membership`` is ``ban``: + #. If ``membership`` is ``ban``: - i. If the ``sender``'s current membership state is not ``join``, reject. + i. If the ``sender``'s current membership state is not ``join``, reject. - #. If the ``sender``'s power level is greater than or equal to the *ban - level*, and the *target user*'s power level is less than the - ``sender``'s power level, allow. + #. If the ``sender``'s power level is greater than or equal to the *ban + level*, and the *target user*'s power level is less than the + ``sender``'s power level, allow. - #. Otherwise, reject. + #. Otherwise, reject. - e. Otherwise, the membership is unknown. Reject. + #. Otherwise, the membership is unknown. Reject. -3. If the ``sender``'s current membership state is not ``join``, reject. +#. If the ``sender``'s current membership state is not ``join``, reject. -4. If the event type's *required power level* is greater than the ``sender``'s power +#. If the event type's *required power level* is greater than the ``sender``'s power level, reject. -5. If type is ``m.room.power_levels``: +#. If type is ``m.room.power_levels``: - a. If there is no previous ``m.room.power_levels`` event in the room, allow. + a. If there is no previous ``m.room.power_levels`` event in the room, allow. - b. For each of the keys ``users_default``, ``events_default``, - ``state_default``, ``ban``, ``redact``, ``kick``, ``invite``, as well as - each entry being changed under the ``events`` or ``users`` keys: + #. For each of the keys ``users_default``, ``events_default``, + ``state_default``, ``ban``, ``redact``, ``kick``, ``invite``, as well as + each entry being changed under the ``events`` or ``users`` keys: - i. If the current value is higher than the ``sender``'s current power level, - reject. + i. If the current value is higher than the ``sender``'s current power level, + reject. - #. If the new value is higher than the ``sender``'s current power level, - reject. + #. If the new value is higher than the ``sender``'s current power level, + reject. - c. For each entry being changed under the ``users`` key, other than the - ``sender``'s own entry: + #. For each entry being changed under the ``users`` key, other than the + ``sender``'s own entry: - i. If the current value is equal to the ``sender``'s current power level, - reject. + i. If the current value is equal to the ``sender``'s current power level, + reject. - d. Otherwise, allow. + #. Otherwise, allow. -6. If type is ``m.room.redaction``: +#. If type is ``m.room.redaction``: - a. If the ``sender``'s power level is greater than or equal to the *redact - level*, allow. + a. If the ``sender``'s power level is greater than or equal to the *redact + level*, allow. - #. If the ``sender`` of the event being redacted is the same as the - ``sender`` of the ``m.room.redaction``, allow. + #. If the ``sender`` of the event being redacted is the same as the + ``sender`` of the ``m.room.redaction``, allow. - #. Otherwise, reject. + #. Otherwise, reject. -7. Otherwise, allow. +#. Otherwise, allow. .. NOTE:: From 86b5486445cbe5fc8d7092b2aba30486850da9a8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Aug 2018 11:32:14 +0100 Subject: [PATCH 27/73] Add missing clauses to and fix authorization rules They are still missing third party invites. --- specification/server_server_api.rst | 39 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index c67bea3d..e6e4625c 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -356,20 +356,36 @@ the state of the room. The rules are as follows: -1. If type is ``m.room.create``, allow if and only if it has no - previous events - *i.e.* it is the first event in the room. +1. If type is ``m.room.create``: + + a. Reject if it has any previous events + b. Reject if the domain of the ``room_id`` does not match the domain of the + ``sender``. + c. Reject if ``content.room_version`` key is an unrecognized version + d. Otherwise, allow. + +#. Reject if event does not have a ``m.room.create`` in its ``auth_events`` + +#. If type is ``m.room.aliases``: + + a. Reject if event has no ``state_key`` + b. Allow if and only if sender's domain matches ``state_key`` #. If type is ``m.room.member``: - a. If ``membership`` is ``join``: + a. Reject if no ``state_key`` key or ``membership`` key in ``content``. + + #. If ``membership`` is ``join``: i. If the only previous event is an ``m.room.create`` and the ``state_key`` is the creator, allow. #. If the ``sender`` does not match ``state_key``, reject. - #. If the user's current membership state is ``invite`` or ``join``, - allow. + #. If the ``sender`` is banned, reject. + + #. If the ``join_rule`` is ``invite`` then allow if membership state + is ``invite`` or ``join``. #. If the ``join_rule`` is ``public``, allow. @@ -420,9 +436,16 @@ The rules are as follows: #. If the event type's *required power level* is greater than the ``sender``'s power level, reject. +#. If the event has a ``state_key`` that starts with an ``@`` and does not match + the ``sender``, reject. + #. If type is ``m.room.power_levels``: - a. If there is no previous ``m.room.power_levels`` event in the room, allow. + a. If ``users`` key in ``content`` is not a dictionary with keys that are + valid user IDs with values that are integers (or a string that is an + integer), reject. + + #. If there is no previous ``m.room.power_levels`` event in the room, allow. #. For each of the keys ``users_default``, ``events_default``, ``state_default``, ``ban``, ``redact``, ``kick``, ``invite``, as well as @@ -447,8 +470,8 @@ The rules are as follows: a. If the ``sender``'s power level is greater than or equal to the *redact level*, allow. - #. If the ``sender`` of the event being redacted is the same as the - ``sender`` of the ``m.room.redaction``, allow. + #. If the domain of the ``event_id`` of the event being redacted is the same + as the domain of the ``event_id`` of the ``m.room.redaction``, allow. #. Otherwise, reject. From 5d40af7ecff9f862fddb79ec2243e1b4167f1795 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Aug 2018 13:24:02 +0100 Subject: [PATCH 28/73] Update rules on which events to add to `auth_events` --- specification/server_server_api.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 9440f2f2..a2a2274d 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -299,8 +299,15 @@ following subset of the room state: - The ``m.room.create`` event. - The current ``m.room.power_levels`` event, if any. -- The current ``m.room.join_rules`` event, if any. - The sender's current ``m.room.member`` event, if any. +- If type is ``m.room.member``: + + - The target's current ``m.room.member`` event, if any. + - If ``join`` or ``invite`` then the current ``m.room.join_rules`` event, + if any. + - If ``invite`` with ``third_party_invite`` in ``content``, then add + ``m.room.third_party_invite`` with state_key of ``token`` in ``signed`` + field of ``third_party_invite``, if any. {{definition_ss_pdu}} From 073ebb051b84ead1be988c5dec03fb11ceddc356 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Aug 2018 13:08:07 +0100 Subject: [PATCH 29/73] Spec third party invites in auth rules --- specification/server_server_api.rst | 37 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index e6e4625c..f2d14738 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -342,6 +342,7 @@ be inserted. The types of state events that affect authorization are: - ``m.room.member`` - ``m.room.join_rules`` - ``m.room.power_levels`` +- ``m.room.third_party_invite`` Servers should not create new events that reference unauthorized events. However, any event that does reference an unauthorized event is not itself @@ -393,7 +394,33 @@ The rules are as follows: #. If ``membership`` is ``invite``: - i. If the ``sender``'s current membership state is not ``join``, reject. + i. If ``content`` has ``third_party_invite`` key: + + #. Reject if *target user* is banned. + + #. Reject if ``content.third_party_invite`` does not have a + ``signed`` key. + + #. Reject if ``signed`` does not have ``mxid`` and ``token`` keys. + + #. Reject if ``mxid`` does not match ``state_key`` + + #. Reject if no ``m.room.third_party_invite`` event in + current state with ``state_key`` matching ``token``. + + #. Reject if ``sender`` does not match ``sender`` of third party + invite. + + #. If any signature in ``signed`` matches any public key in third + party invite, allow. The public keys are in ``content`` of + third party invite under: + + #. A single public key in ``public_key`` field + #. A list of public keys in ``public_keys`` field + + #. Otherwise, reject. + + #. If the ``sender``'s current membership state is not ``join``, reject. #. If *target user*'s current membership state is ``join`` or ``ban``, reject. @@ -433,6 +460,11 @@ The rules are as follows: #. If the ``sender``'s current membership state is not ``join``, reject. +#. If type is ``m.room.third_party_invite``: + + a. Allow if and only if ``sender``'s current power level is greater than + or equal to the *invite level*. + #. If the event type's *required power level* is greater than the ``sender``'s power level, reject. @@ -489,9 +521,6 @@ The rules are as follows: the kick *and* ban levels, *and* greater than the target user's power level. -.. TODO-spec - - I think there is some magic about 3pid invites too. Retrieving event authorization information ++++++++++++++++++++++++++++++++++++++++++ From d921b81c703d8845dc6ea5894880d47564e90a21 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 29 Aug 2018 14:17:52 +0100 Subject: [PATCH 30/73] Reject events with superfluous auth_events entries --- specification/server_server_api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index f2d14738..fb221357 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -365,6 +365,12 @@ The rules are as follows: c. Reject if ``content.room_version`` key is an unrecognized version d. Otherwise, allow. +#. Reject if event has ``auth_events`` that: + + a. have duplicate entries for a given ``type`` and ``state_key`` pair + #. have entries whose ``type`` and ``state_key`` don't match those + specified by the algorithm described previously. + #. Reject if event does not have a ``m.room.create`` in its ``auth_events`` #. If type is ``m.room.aliases``: From ec20c43220d0683d7f25514cbe0e8a2ddd4a43d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 10:55:34 -0600 Subject: [PATCH 31/73] Specify the minimum CSP for media Fixes https://github.com/matrix-org/matrix-doc/issues/1066 --- specification/modules/content_repo.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specification/modules/content_repo.rst b/specification/modules/content_repo.rst index 0f1a9944..f6da38ff 100644 --- a/specification/modules/content_repo.rst +++ b/specification/modules/content_repo.rst @@ -33,6 +33,11 @@ recipient's local homeserver, which must first transfer the content from the origin homeserver using the same API (unless the origin and destination homeservers are the same). +When serving content, the server MUST provide a ``Content-Security-Policy`` +header. The policy may be more restrictive, however the minimum policy is +``default-src 'none'; script-src 'none'; plugin-types application/pdf; +style-src 'unsafe-inline'; object-src 'self';``. + Client behaviour ---------------- From 348b549f9f7694a39be0c6f83200332b179788e5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 11:19:37 -0600 Subject: [PATCH 32/73] Add the other fields the server is expected to keep on events Fixes https://github.com/matrix-org/matrix-doc/issues/839 Reference: https://github.com/matrix-org/synapse/blob/d69decd5c78c72abef50b597a689e2bc55a39702/synapse/events/utils.py#L44-L91 --- specification/client_server_api.rst | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/specification/client_server_api.rst b/specification/client_server_api.rst index b377cbb8..cbe7d24a 100644 --- a/specification/client_server_api.rst +++ b/specification/client_server_api.rst @@ -1328,16 +1328,30 @@ the following list: - ``state_key`` - ``prev_content`` - ``content`` +- ``hashes`` +- ``signatures`` +- ``depth`` +- ``prev_events`` +- ``prev_state`` +- ``auth_events`` +- ``origin`` +- ``origin_server_ts`` +- ``membership`` + +.. Note: + Some of the keys, such as ``hashes``, will appear on the federation-formatted + event and therefore the client may not be aware of them. The content object should also be stripped of all keys, unless it is one of one of the following event types: -- ``m.room.member`` allows key ``membership`` -- ``m.room.create`` allows key ``creator`` -- ``m.room.join_rules`` allows key ``join_rule`` +- ``m.room.member`` allows key ``membership``. +- ``m.room.create`` allows key ``creator``. +- ``m.room.join_rules`` allows key ``join_rule``. - ``m.room.power_levels`` allows keys ``ban``, ``events``, ``events_default``, ``kick``, ``redact``, ``state_default``, ``users``, ``users_default``. -- ``m.room.aliases`` allows key ``aliases`` +- ``m.room.aliases`` allows key ``aliases``. +- ``m.room.history_visibility`` allows key ``history_visibility``. The server should add the event causing the redaction to the ``unsigned`` property of the redacted event, under the ``redacted_because`` key. When a From 86f616bb31041a6b49f6239adc9193c60cb4b46f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 11:20:51 -0600 Subject: [PATCH 33/73] Changelog --- changelogs/client_server/newsfragments/1602.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1602.clarification diff --git a/changelogs/client_server/newsfragments/1602.clarification b/changelogs/client_server/newsfragments/1602.clarification new file mode 100644 index 00000000..def503cb --- /dev/null +++ b/changelogs/client_server/newsfragments/1602.clarification @@ -0,0 +1 @@ +Add the other keys that redactions are expected to preserve. From 2234e0b0978283f6c26ac4b0da71519a72e1e085 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 12:38:15 -0600 Subject: [PATCH 34/73] Mention that clients should not be producing invalid HTML Fixes https://github.com/matrix-org/matrix-doc/issues/1595 --- specification/modules/instant_messaging.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specification/modules/instant_messaging.rst b/specification/modules/instant_messaging.rst index 079a4801..f3deb220 100644 --- a/specification/modules/instant_messaging.rst +++ b/specification/modules/instant_messaging.rst @@ -106,6 +106,11 @@ of tags they can render, falling back to other representations of the tags where For example, a client may not be able to render tables correctly and instead could fall back to rendering tab-delimited text. +In addition to not rendering unsafe HTML, clients should not emit unsafe HTML in events. +Likewise, clients should not generate HTML that is not needed, such as extra paragraph tags +surrounding text due to Rich Text Editors. HTML included in events should otherwise be valid, +such as having appropriate closing tags, valid attributes, and generally valid structure. + .. Note:: A future iteration of the specification will support more powerful and extensible message formatting options, such as the proposal `MSC1225 `_. From 60b97fcf26158f2ab05af4c98dc03d9df9dd6b30 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 12:39:25 -0600 Subject: [PATCH 35/73] Changelog --- changelogs/client_server/newsfragments/1605.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1605.clarification diff --git a/changelogs/client_server/newsfragments/1605.clarification b/changelogs/client_server/newsfragments/1605.clarification new file mode 100644 index 00000000..ce9f967d --- /dev/null +++ b/changelogs/client_server/newsfragments/1605.clarification @@ -0,0 +1 @@ +Clarify that clients should not be generating invalid HTML for formatted events. From 8950f0b2cc08d96f9d835804f78b59b08273b8c5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 14:36:43 -0600 Subject: [PATCH 36/73] Spelling --- specification/server_server_api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index cc23a83d..9a68f33c 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -804,7 +804,7 @@ identifier. Public Room Directory --------------------- -To compliment the `Client-Server API`_'s room directory, homeservers need a +To complement the `Client-Server API`_'s room directory, homeservers need a way to query the public rooms for another server. This can be done by making a request to the ``/publicRooms`` endpoint for the server the room directory should be retrieved for. @@ -906,7 +906,7 @@ intended to compliment the `Device Management module`_ of the Client-Server API. End-to-End Encryption --------------------- -This section compliments the `End-to-End Encryption module`_ of the Client-Server +This section complements the `End-to-End Encryption module`_ of the Client-Server API. For detailed information about end-to-end encryption, please see that module. The APIs defined here are designed to be able to proxy much of the client's request From f2d02c9559339f51b7ed0f75d2b408132df7f757 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 14:37:30 -0600 Subject: [PATCH 37/73] Take out device management section for now This will be handled by the implementation of https://github.com/matrix-org/matrix-doc/issues/1212 --- api/server-server/user_devices.yaml | 84 ----------------------------- specification/server_server_api.rst | 12 ----- 2 files changed, 96 deletions(-) delete mode 100644 api/server-server/user_devices.yaml diff --git a/api/server-server/user_devices.yaml b/api/server-server/user_devices.yaml deleted file mode 100644 index d1644270..00000000 --- a/api/server-server/user_devices.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2018 New Vector Ltd -# -# 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. - -swagger: '2.0' -info: - title: "Matrix Federation User Device Management API" - version: "1.0.0" -host: localhost:8448 -schemes: - - https -basePath: /_matrix/federation/v1 -consumes: - - application/json -produces: - - application/json -securityDefinitions: - $ref: definitions/security.yaml -paths: - "/user/devices/{userId}": - get: - summary: Gets all of the user's devices - description: Gets information on all of the user's devices - operationId: getUserDevices - security: - - signedRequest: [] - parameters: - - in: path - name: userId - type: string - required: true - description: |- - The user ID to retrieve devices for. Must be a user local to the - receiving homeserver. - required: true - x-example: "@alice:example.org" - responses: - 200: - description: The user's devices. - schema: - type: object - properties: - user_id: - type: string - description: The user ID devices were requested for. - example: "@alice:example.org" - stream_id: - type: integer - description: |- - An ID the requesting homeserver may use to detect changes in the - device list. This should increase as time goes on, and always - produce the same ``devices`` list if not incremented. - example: 334608 - devices: - type: array - description: The user's devices. May be empty. - items: - type: object - title: User Device - properties: - device_id: - type: string - description: The device ID. - example: "JLAFKJWSCS" - keys: - type: object - description: Identity keys for the device. - $ref: "../client-server/definitions/device_keys.yaml" - device_display_name: - type: string - description: Optional display name for the device. - example: "Alice's Mobile Phone" - required: ['device_id', 'keys'] - required: ['user_id', 'stream_id', 'devices'] \ No newline at end of file diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 9a68f33c..558a5f8d 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -891,18 +891,6 @@ nothing else. {{openid_ss_http_api}} -Device Management ------------------ - -.. TODO: TravisR - Incorporate https://github.com/matrix-org/matrix-doc/issues/1212 - -A user's devices may need to be queried over federation for different purposes, such -as end-to-end encryption or for utilizing send-to-device messaging. This section is -intended to compliment the `Device Management module`_ of the Client-Server API. - -{{user_devices_ss_http_api}} - - End-to-End Encryption --------------------- From 9bc14703058adc3558d83f16fadcdf5f92cf8c3a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 26 Aug 2018 20:25:05 +0900 Subject: [PATCH 38/73] Elaborate the structure of m.tag events ...and corresponding structures in tag-related CS API calls Signed-off-by: Alexey Rusakov --- api/client-server/tags.yaml | 27 +++++++++++++++++++++++---- event-schemas/schema/m.tag | 10 +++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/api/client-server/tags.yaml b/api/client-server/tags.yaml index b7bafab6..10809572 100644 --- a/api/client-server/tags.yaml +++ b/api/client-server/tags.yaml @@ -60,13 +60,23 @@ paths: type: object properties: tags: - title: Tags type: object + additionalProperties: + title: Tag + type: object + properties: + order: + type: number + format: float + description: |- + A number in a range ``[0,1]`` describing a relative + position of the room under the given tag. + additionalProperties: true examples: application/json: { "tags": { - "m.favourite": {}, - "u.Work": {"order": "1"}, + "m.favourite": {"order": 0.1}, + "u.Work": {"order": 0.7}, "u.Customers": {} } } @@ -110,8 +120,17 @@ paths: Extra data for the tag, e.g. ordering. schema: type: object + properties: + order: + type: number + format: float + description: |- + A number in a range ``[0,1]`` describing a relative + position of the room under the given tag. + additionalProperties: true example: { - "order": "1"} + "order": 0.25 + } responses: 200: description: diff --git a/event-schemas/schema/m.tag b/event-schemas/schema/m.tag index 80d3f9dd..8da093bd 100644 --- a/event-schemas/schema/m.tag +++ b/event-schemas/schema/m.tag @@ -18,7 +18,15 @@ "description": "The tags on the room and their contents.", "additionalProperties": { "title": "Tag", - "type": "object" + "type": "object", + "properties": { + "order": { + "type": "number", + "format": "float", + "description": + "A number in a range ``[0,1]`` describing a relative position of the room under the given tag." + } + } } } } From c03f2f8d79ac9f589249890382a204ad1158972b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 14:53:55 -0600 Subject: [PATCH 39/73] Minor touchups to the room tagging API Fixes some of https://github.com/matrix-org/matrix-doc/issues/1565 --- api/client-server/tags.yaml | 34 ++++++++++++++++------------------ event-schemas/examples/m.tag | 2 +- specification/modules/tags.rst | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/api/client-server/tags.yaml b/api/client-server/tags.yaml index 10809572..081d8a84 100644 --- a/api/client-server/tags.yaml +++ b/api/client-server/tags.yaml @@ -43,14 +43,14 @@ paths: required: true description: |- The id of the user to get tags for. The access token must be - authorized to make requests for this user id. + authorized to make requests for this user ID. x-example: "@alice:example.com" - in: path type: string name: roomId required: true description: |- - The id of the room to get tags for. + The ID of the room to get tags for. x-example: "!726s6s6q:example.com" responses: 200: @@ -74,12 +74,12 @@ paths: additionalProperties: true examples: application/json: { - "tags": { - "m.favourite": {"order": 0.1}, - "u.Work": {"order": 0.7}, - "u.Customers": {} - } + "tags": { + "m.favourite": {"order": 0.1}, + "u.Work": {"order": 0.7}, + "u.Customers": {} } + } tags: - User data "/user/{userId}/rooms/{roomId}/tags/{tag}": @@ -97,14 +97,14 @@ paths: required: true description: |- The id of the user to add a tag for. The access token must be - authorized to make requests for this user id. + authorized to make requests for this user ID. x-example: "@alice:example.com" - in: path type: string name: roomId required: true description: |- - The id of the room to add a tag to. + The ID of the room to add a tag to. x-example: "!726s6s6q:example.com" - in: path type: string @@ -112,7 +112,7 @@ paths: required: true description: |- The tag to add. - x-example: "work" + x-example: "u.work" - in: body name: body required: true @@ -138,8 +138,7 @@ paths: schema: type: object examples: - application/json: { - } + application/json: {} tags: - User data delete: @@ -156,14 +155,14 @@ paths: required: true description: |- The id of the user to remove a tag for. The access token must be - authorized to make requests for this user id. + authorized to make requests for this user ID. x-example: "@alice:example.com" - in: path type: string name: roomId required: true description: |- - The id of the room to remove a tag from. + The ID of the room to remove a tag from. x-example: "!726s6s6q:example.com" - in: path type: string @@ -171,15 +170,14 @@ paths: required: true description: |- The tag to remove. - x-example: "work" + x-example: "u.work" responses: 200: description: - The tag was successfully removed + The tag was successfully removed. schema: type: object examples: - application/json: { - } + application/json: {} tags: - User data diff --git a/event-schemas/examples/m.tag b/event-schemas/examples/m.tag index 53dbc921..e5564a4b 100644 --- a/event-schemas/examples/m.tag +++ b/event-schemas/examples/m.tag @@ -2,7 +2,7 @@ "type": "m.tag", "content": { "tags": { - "u.work": {"order": 1} + "u.work": {"order": 0.9} } } } diff --git a/specification/modules/tags.rst b/specification/modules/tags.rst index f965c2e8..739ead2c 100644 --- a/specification/modules/tags.rst +++ b/specification/modules/tags.rst @@ -39,7 +39,7 @@ with an ``order`` of ``0.2`` would be displayed before a room with an ``order`` of ``0.7``. If a room has a tag without an ``order`` key then it should appear after the rooms with that tag that have an ``order`` key. -The name of a tag MUST not exceed 255 bytes. +The name of a tag MUST NOT exceed 255 bytes. The tag namespace is defined as follows: From 30ff020ac75d480d3025748fc803546bb784c415 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 14:57:05 -0600 Subject: [PATCH 40/73] Changelog --- changelogs/client_server/newsfragments/1606.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1606.clarification diff --git a/changelogs/client_server/newsfragments/1606.clarification b/changelogs/client_server/newsfragments/1606.clarification new file mode 100644 index 00000000..f65ed257 --- /dev/null +++ b/changelogs/client_server/newsfragments/1606.clarification @@ -0,0 +1 @@ +Clarify the room tag structure (thanks @KitsuneRal!) From bd9348101333894e5086b7218a773c9c8f4c471e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 23:18:10 -0600 Subject: [PATCH 41/73] Reference that transaction IDs may be used to echo Fixes https://github.com/matrix-org/matrix-doc/issues/1462 --- specification/modules/instant_messaging.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/specification/modules/instant_messaging.rst b/specification/modules/instant_messaging.rst index 079a4801..f96a4326 100644 --- a/specification/modules/instant_messaging.rst +++ b/specification/modules/instant_messaging.rst @@ -167,9 +167,14 @@ message which they receive from the event stream. The echo of the same message from the event stream is referred to as "remote echo". Both echoes need to be identified as the same message in order to prevent duplicate messages being displayed. Ideally this pairing would occur transparently to the user: the UI -would not flicker as it transitions from local to remote. Flickering cannot be -fully avoided in the current client-server API. Two scenarios need to be -considered: +would not flicker as it transitions from local to remote. Flickering can be +reduced through clients making use of the transaction ID they used to send +a particular event. The transaction ID used will be included in the event's +``unsigned`` data as ``transaction_id`` when it arrives through the event stream. + +Clients unable to make use of the transaction ID are more likely to experience +flickering due to the following two scenarios, however the effect can be mitigated +to a degree: - The client sends a message and the remote echo arrives on the event stream *after* the request to send the message completes. From 3d99c8b5a8344523af29791e374908227da3692f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Aug 2018 23:19:18 -0600 Subject: [PATCH 42/73] Changelog --- changelogs/client_server/newsfragments/1619.clarification | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/client_server/newsfragments/1619.clarification diff --git a/changelogs/client_server/newsfragments/1619.clarification b/changelogs/client_server/newsfragments/1619.clarification new file mode 100644 index 00000000..fa06e969 --- /dev/null +++ b/changelogs/client_server/newsfragments/1619.clarification @@ -0,0 +1 @@ +Add a note that clients may use the transaction ID to avoid flickering when doing local echo. From 55c4307f1291cb6aab353da8cf2e403eb2bc9a6c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 30 Aug 2018 14:37:24 +0100 Subject: [PATCH 43/73] Rewrite the section on signing events ... for clarity and de-duplication. And to say a bit about validating the signatures. --- api/server-server/definitions/pdu.yaml | 3 +- .../definitions/unsigned_pdu.yaml | 6 +- specification/server_server_api.rst | 185 ++++++++---------- 3 files changed, 85 insertions(+), 109 deletions(-) diff --git a/api/server-server/definitions/pdu.yaml b/api/server-server/definitions/pdu.yaml index bb14ede2..d86b8538 100644 --- a/api/server-server/definitions/pdu.yaml +++ b/api/server-server/definitions/pdu.yaml @@ -23,7 +23,8 @@ allOf: hashes: type: object title: Event Hash - description: Hashes of the PDU, following the algorithm specified in `Signing Events`_. + description: |- + Content hashes of the PDU, following the algorithm specified in `Signing Events`_. example: { "sha256": "thishashcoversallfieldsincasethisisredacted" } diff --git a/api/server-server/definitions/unsigned_pdu.yaml b/api/server-server/definitions/unsigned_pdu.yaml index 64991d22..446973ed 100644 --- a/api/server-server/definitions/unsigned_pdu.yaml +++ b/api/server-server/definitions/unsigned_pdu.yaml @@ -55,8 +55,8 @@ properties: prev_events: type: array description: |- - Event IDs and hashes of the most recent events in the room that the homeserver was aware - of when it made this event. + Event IDs and reference hashes for the most recent events in the room + that the homeserver was aware of when it made this event. items: type: array maxItems: 2 @@ -86,7 +86,7 @@ properties: auth_events: type: array description: |- - An event reference list containing the authorization events that would + Event IDs and reference hashes for the authorization events that would allow this event to be in the room. items: type: array diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 244a8b82..e55b8113 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -112,7 +112,7 @@ Server implementation {{version_ss_http_api}} -Retrieving Server Keys +Retrieving server keys ~~~~~~~~~~~~~~~~~~~~~~ .. NOTE:: @@ -965,142 +965,114 @@ Signing Events Signing events is complicated by the fact that servers can choose to redact non-essential parts of an event. -Before signing the event, the ``unsigned`` and ``signature`` members are -removed, it is encoded as `Canonical JSON`_, and then hashed using SHA-256. The -resulting hash is then stored in the event JSON in a ``hash`` object under a -``sha256`` key. +Adding hashes and signatures to outgoing events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: python - - def hash_event(event_json_object): - - # Keys under "unsigned" can be modified by other servers. - # They are useful for conveying information like the age of an - # event that will change in transit. - # Since they can be modifed we need to exclude them from the hash. - unsigned = event_json_object.pop("unsigned", None) - - # Signatures will depend on the current value of the "hashes" key. - # We cannot add new hashes without invalidating existing signatures. - signatures = event_json_object.pop("signatures", None) +Before signing the event, the *content hash* of the event is calculated as +described below. The hash is encoded using `Unpadded Base64`_ and stored in the +event object, in a ``hashes`` object, under a ``sha256`` key. - # The "hashes" key might contain multiple algorithms if we decide to - # migrate away from SHA-2. We don't want to include an existing hash - # output in our hash so we exclude the "hashes" dict from the hash. - hashes = event_json_object.pop("hashes", {}) - - # Encode the JSON using a canonical encoding so that we get the same - # bytes on every server for the same JSON object. - event_json_bytes = encode_canonical_json(event_json_bytes) +The event object is then *redacted*, following the `redaction +algorithm`_. Finally it is signed as described in `Signing JSON`_, using the +server's signing key (see also `Retrieving server keys`_). - # Add the base64 encoded bytes of the hash to the "hashes" dict. - hashes["sha256"] = encode_base64(sha256(event_json_bytes).digest()) +The signature is then copied back to the original event object. - # Add the "hashes" dict back the event JSON under a "hashes" key. - event_json_object["hashes"] = hashes - if unsigned is not None: - event_json_object["unsigned"] = unsigned - return event_json_object +See `Persistent Data Unit schema`_ for an example of a signed event. -The event is then stripped of all non-essential keys both at the top level and -within the ``content`` object. Any top-level keys not in the following list -MUST be removed: -.. code:: +Validating hashes and signatures on received events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +When a server receives an event over federation from another server, the +receiving server should check the hashes and signatures on that event. - auth_events - depth - event_id - hashes - membership - origin - origin_server_ts - prev_events - prev_state - room_id - sender - signatures - state_key - type - -A new ``content`` object is constructed for the resulting event that contains -only the essential keys of the original ``content`` object. If the original -event lacked a ``content`` object at all, a new empty JSON object is created -for it. - -The keys that are considered essential for the ``content`` object depend on the -the ``type`` of the event. These are: +First the signature is checked. The event is redacted following the `redaction +algorithm`_, and the resultant object is checked for a signature from the +originating server, following the algorithm described in `Checking for a signature`_. +Note that this step should succeed whether we have been sent the full event or +a redacted copy. -.. code:: +If the signature is found to be valid, the expected content hash is calculated +as described below. The content hash in the ``hashes`` property of the received +event is base64-decoded, and the two are compared for equality. - type is "m.room.aliases": - aliases +If the hash check fails, then it is assumed that this is because we have only +been given a redacted version of the event. To enforce this, the receiving +server should use the redacted copy it calculated rather than the full copy it +received. - type is "m.room.create": - creator +Calculating the content hash for an event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - type is "m.room.history_visibility": - history_visibility +The *content hash* of an event covers the complete event including the +*unredacted* contents. It is calculated as follows. - type is "m.room.join_rules": - join_rule +First, any existing ``unsigned``, ``signature``, and ``hashes`` members are +removed. The resulting object is then encoded as `Canonical JSON`_, and the +JSON is hashed using SHA-256. - type is "m.room.member": - membership - type is "m.room.power_levels": - ban - events - events_default - kick - redact - state_default - users - users_default - -The resulting stripped object with the new ``content`` object and the original -``hashes`` key is then signed using the JSON signing algorithm outlined below: +Example code +~~~~~~~~~~~~ .. code:: python - def sign_event(event_json_object, name, key): - - # Make sure the event has a "hashes" key. - if "hashes" not in event_json_object: - event_json_object = hash_event(event_json_object) + def hash_and_sign_event(event_object, signing_key, signing_name): + # First we need to hash the event object. + content_hash = compute_content_hash(event_object) + event_object["hashes"] = {"sha256": encode_unpadded_base64(content_hash)} # Strip all the keys that would be removed if the event was redacted. # The hashes are not stripped and cover all the keys in the event. # This means that we can tell if any of the non-essential keys are # modified or removed. - stripped_json_object = strip_non_essential_keys(event_json_object) + stripped_object = strip_non_essential_keys(event_object) # Sign the stripped JSON object. The signature only covers the # essential keys and the hashes. This means that we can check the # signature even if the event is redacted. - signed_json_object = sign_json(stripped_json_object) + signed_object = sign_json(stripped_object, signing_key, signing_name) # Copy the signatures from the stripped event to the original event. - event_json_object["signatures"] = signed_json_oject["signatures"] - return event_json_object + event_object["signatures"] = signed_object["signatures"] -Servers can then transmit the entire event or the event with the non-essential -keys removed. If the entire event is present, receiving servers can then check -the event by computing the SHA-256 of the event, excluding the ``hash`` object. -If the keys have been redacted, then the ``hash`` object is included when -calculating the SHA-256 hash instead. + def compute_content_hash(event_object): + # take a copy of the event before we remove any keys. + event_object = dict(event_object) -New hash functions can be introduced by adding additional keys to the ``hash`` -object. Since the ``hash`` object cannot be redacted a server shouldn't allow -too many hashes to be listed, otherwise a server might embed illict data within -the ``hash`` object. For similar reasons a server shouldn't allow hash values -that are too long. + # Keys under "unsigned" can be modified by other servers. + # They are useful for conveying information like the age of an + # event that will change in transit. + # Since they can be modifed we need to exclude them from the hash. + event_object.pop("unsigned", None) + + # Signatures will depend on the current value of the "hashes" key. + # We cannot add new hashes without invalidating existing signatures. + event_object.pop("signatures", None) + + # The "hashes" key might contain multiple algorithms if we decide to + # migrate away from SHA-2. We don't want to include an existing hash + # output in our hash so we exclude the "hashes" dict from the hash. + event_object.pop("hashes", None) + + # Encode the JSON using a canonical encoding so that we get the same + # bytes on every server for the same JSON object. + event_json_bytes = encode_canonical_json(event_object) + + return hashlib.sha256(event_json_bytes) .. TODO - [[TODO(markjh): We might want to specify a maximum number of keys for the - ``hash`` and we might want to specify the maximum output size of a hash]] - [[TODO(markjh) We might want to allow the server to omit the output of well - known hash functions like SHA-256 when none of the keys have been redacted]] + + [[TODO(markjh): Since the ``hash`` object cannot be redacted a server + shouldn't allow too many hashes to be listed, otherwise a server might embed + illict data within the ``hash`` object. + + We might want to specify a maximum number of keys for the + ``hash`` and we might want to specify the maximum output size of a hash]] + + [[TODO(markjh) We might want to allow the server to omit the output of well + known hash functions like SHA-256 when none of the keys have been redacted]] + .. |/query/directory| replace:: ``/query/directory`` .. _/query/directory: #get-matrix-federation-v1-query-directory @@ -1111,3 +1083,6 @@ that are too long. .. _`Inviting to a room`: #inviting-to-a-room .. _`Canonical JSON`: ../appendices.html#canonical-json .. _`Unpadded Base64`: ../appendices.html#unpadded-base64 +.. _`redaction algorithm`: ../client_server/unstable.html#redactions +.. _`Signing JSON`: ../appendices.html#signing-json +.. _`Checking for a signature`: ../appendices.html#checking-for-a-signature From 5c2b8e35693c311e77781e78b7b3dbfdc1fae244 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 30 Aug 2018 17:03:16 +0100 Subject: [PATCH 44/73] Explain how to handle rejeceted events --- specification/server_server_api.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 244a8b82..4c40d6dc 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -472,6 +472,24 @@ The rules are as follows: I think there is some magic about 3pid invites too. +Rejection ++++++++++ + +If an event is rejected it should neither be relayed to clients nor be included +as a prev event in any new events generated by the server. Subsequent events +from other servers that reference rejected events should be allowed if they +still pass the auth rules. The state used in the checks should be calculated as +normal, except not updating with the rejected event where it is a state event. + +If an event in an incoming transaction is rejected, this should not cause the +transaction request to be responded to with an error response. + +.. NOTE:: + + This means that events may be included in the room DAG even though they + should be rejected. + + Retrieving event authorization information ++++++++++++++++++++++++++++++++++++++++++ From 440841d1ffd0f3d4398019da26e61e7340cd12ef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 12:13:21 -0600 Subject: [PATCH 45/73] Recommend a CSP rather than require it. --- changelogs/client_server/newsfragments/1600.feature | 1 + specification/modules/content_repo.rst | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 changelogs/client_server/newsfragments/1600.feature diff --git a/changelogs/client_server/newsfragments/1600.feature b/changelogs/client_server/newsfragments/1600.feature new file mode 100644 index 00000000..142a67b2 --- /dev/null +++ b/changelogs/client_server/newsfragments/1600.feature @@ -0,0 +1 @@ +Recommend that servers set a Content Security Policy for the content repository. diff --git a/specification/modules/content_repo.rst b/specification/modules/content_repo.rst index f6da38ff..51cf999a 100644 --- a/specification/modules/content_repo.rst +++ b/specification/modules/content_repo.rst @@ -33,10 +33,9 @@ recipient's local homeserver, which must first transfer the content from the origin homeserver using the same API (unless the origin and destination homeservers are the same). -When serving content, the server MUST provide a ``Content-Security-Policy`` -header. The policy may be more restrictive, however the minimum policy is -``default-src 'none'; script-src 'none'; plugin-types application/pdf; -style-src 'unsafe-inline'; object-src 'self';``. +When serving content, the server SHOULD provide a ``Content-Security-Policy`` +header. The recommended policy is ``default-src 'none'; script-src 'none'; +plugin-types application/pdf; style-src 'unsafe-inline'; object-src 'self';``. Client behaviour ---------------- From b8f2c721dc9bfbd44b6545c96a99755b490e58a7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 12:22:50 -0600 Subject: [PATCH 46/73] Clarify which attributes should be valid --- specification/modules/instant_messaging.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specification/modules/instant_messaging.rst b/specification/modules/instant_messaging.rst index f3deb220..d6cc9f09 100644 --- a/specification/modules/instant_messaging.rst +++ b/specification/modules/instant_messaging.rst @@ -109,7 +109,8 @@ back to rendering tab-delimited text. In addition to not rendering unsafe HTML, clients should not emit unsafe HTML in events. Likewise, clients should not generate HTML that is not needed, such as extra paragraph tags surrounding text due to Rich Text Editors. HTML included in events should otherwise be valid, -such as having appropriate closing tags, valid attributes, and generally valid structure. +such as having appropriate closing tags, appropriate attributes (considering the custom ones +defined in this specification), and generally valid structure. .. Note:: A future iteration of the specification will support more powerful and extensible From 4630c067213c3c30b2a96c8c0383166e6ac8f56f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 11:24:36 -0600 Subject: [PATCH 47/73] Appservice r0.1.0 changelog --- changelogs/application_service.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelogs/application_service.rst b/changelogs/application_service.rst index e69de29b..9d098837 100644 --- a/changelogs/application_service.rst +++ b/changelogs/application_service.rst @@ -0,0 +1,7 @@ +r0.1.0 +====== + +This is the first release of the Application Service specification. It +includes support for application services being able to interact with +homeservers and bridge third party networks, such as IRC, over to Matrix +in a standard and accessible way. From 5e6a2c30a2171366049fbde23c0b55711b2b1b79 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 15:00:14 -0600 Subject: [PATCH 48/73] Comment out the part where identity services can revoke their keys They can't because otherwise 3rd party invites can be rejected by homeservers, as per https://github.com/matrix-org/matrix-doc/issues/1633 --- specification/identity_service_api.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/specification/identity_service_api.rst b/specification/identity_service_api.rst index cb079593..d438f2c7 100644 --- a/specification/identity_service_api.rst +++ b/specification/identity_service_api.rst @@ -80,9 +80,11 @@ in a scheme ``algorithm:identifier``, e.g. ``ed25519:0``. When signing an association, the Matrix standard JSON signing format is used, as specified in the server-server API specification under the heading "Signing Events". -In the event of key compromise, the identity service may revoke any of its keys. -An HTTP API is offered to get public keys, and check whether a particular key is -valid. +.. TODO: Actually allow identity services to revoke all keys + See: https://github.com/matrix-org/matrix-doc/issues/1633 +.. In the event of key compromise, the identity service may revoke any of its keys. + An HTTP API is offered to get public keys, and check whether a particular key is + valid. The identity server may also keep track of some short-term public-private keypairs, which may have different usage and lifetime characteristics than the From 356626845ccadabdb5d9239eb80e010c505bde40 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 15:00:52 -0600 Subject: [PATCH 49/73] Remove unhelpful arrow from the 3rd party invite sequence dance This doesn't add anything in terms of clarity. --- specification/modules/third_party_invites.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index a9b06258..0af81844 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -190,10 +190,6 @@ H3 is attempting to join. | | | (Federation) Emit m.room.member for invite | | | | | |----------------------------------------------->| | | | | | | | | - | | | | Accept event | | - | | | |------------- | | - | | | | | | | - | | | |<------------ | | | | | | | | | | | (Federation) Emit the m.room.member event sent to H2 | | | | |----------------------------------------------------------------->| | From 1f6499d563ab26af66f80a0d946cc0c07aa87176 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 15:01:20 -0600 Subject: [PATCH 50/73] Fix typo --- specification/modules/third_party_invites.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/modules/third_party_invites.rst b/specification/modules/third_party_invites.rst index 0af81844..248b9ba9 100644 --- a/specification/modules/third_party_invites.rst +++ b/specification/modules/third_party_invites.rst @@ -138,7 +138,7 @@ validate that the public key used for signing is still valid, by checking 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. They may, however, indicate to their clients that a member's' +the room. They may, however, indicate to their clients that a member's membership is questionable. For example, given H1, H2, and H3 as homeservers, UserA as a user of H1, and an From 98a445890ced148d6b9dd54e631401d092d4e477 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 13:25:01 -0600 Subject: [PATCH 51/73] Render a warning if the spec is unstable Fixes https://github.com/matrix-org/matrix-doc/issues/1499 This is done by using magic variables in the RST. The magic variables are generated based on the substitutions available, making them available for use at build-time. Magic variables were chosen because it allows people to continue working on the spec and release process without having to worry about removing a chunk of text from the top of the file. Originally, this was attempted by using jinja2 if-statements, however the substitutions are replaced *after* the template is executed, so the condition would never match. The format of the variable is to make the templating happy. Using colons or percent signs results in the templator thinking something else is going on, and then complaining about format. --- scripts/templating/matrix_templates/sections.py | 7 +++++++ scripts/templating/matrix_templates/units.py | 16 ++++++++++++++++ specification/application_service_api.rst | 2 ++ specification/client_server_api.rst | 2 ++ specification/identity_service_api.rst | 2 ++ specification/push_gateway.rst | 2 ++ specification/server_server_api.rst | 2 ++ 7 files changed, 33 insertions(+) diff --git a/scripts/templating/matrix_templates/sections.py b/scripts/templating/matrix_templates/sections.py index 4c07649d..f4c015c7 100644 --- a/scripts/templating/matrix_templates/sections.py +++ b/scripts/templating/matrix_templates/sections.py @@ -207,6 +207,13 @@ class MatrixSections(Sections): apis = self.units.get("apis") return template.render(apis=apis) + def render_unstable_warnings(self): + rendered = {} + blocks = self.units.get("unstable_warnings") + for var, text in blocks.items(): + rendered["unstable_warning_block_" + var] = text + return rendered + def render_swagger_definition(self): rendered = {} template = self.env.get_template("schema-definition.tmpl") diff --git a/scripts/templating/matrix_templates/units.py b/scripts/templating/matrix_templates/units.py index 94b535b5..11a9d441 100644 --- a/scripts/templating/matrix_templates/units.py +++ b/scripts/templating/matrix_templates/units.py @@ -971,6 +971,22 @@ class MatrixUnits(Units): return changelogs + def load_unstable_warnings(self, substitutions): + warning = """ +.. WARNING:: + You are viewing an unstable version of this specification. Unstable + specifications may change at any time without notice. To view the + current specification, please `click here `_. +""" + warnings = {} + for var in substitutions.keys(): + key = var[1:-1] # take off the surrounding %-signs + if substitutions.get(var, "unstable") == "unstable": + warnings[key] = warning + else: + warnings[key] = "" + return warnings + def load_spec_targets(self): with open(TARGETS, "r") as f: diff --git a/specification/application_service_api.rst b/specification/application_service_api.rst index 69d39d21..8af10df8 100644 --- a/specification/application_service_api.rst +++ b/specification/application_service_api.rst @@ -16,6 +16,8 @@ Application Service API ======================= +{{unstable_warning_block_APPSERVICE_RELEASE_LABEL}} + The Matrix client-server API and server-server APIs provide the means to implement a consistent self-contained federated messaging fabric. However, they provide limited means of implementing custom server-side behaviour in Matrix diff --git a/specification/client_server_api.rst b/specification/client_server_api.rst index b377cbb8..070d9de9 100644 --- a/specification/client_server_api.rst +++ b/specification/client_server_api.rst @@ -15,6 +15,8 @@ Client-Server API ================= +{{unstable_warning_block_CLIENT_RELEASE_LABEL}} + The client-server API provides a simple lightweight API to let clients send messages, control rooms and synchronise conversation history. It is designed to support both lightweight clients which store no state and lazy-load data from diff --git a/specification/identity_service_api.rst b/specification/identity_service_api.rst index 81ff0ede..86170bd8 100644 --- a/specification/identity_service_api.rst +++ b/specification/identity_service_api.rst @@ -18,6 +18,8 @@ Identity Service API ==================== +{{unstable_warning_block_IDENTITY_RELEASE_LABEL}} + The Matrix client-server and server-server APIs are largely expressed in Matrix user identifiers. From time to time, it is useful to refer to users by other ("third-party") identifiers, or "3pid"s, e.g. their email address or phone diff --git a/specification/push_gateway.rst b/specification/push_gateway.rst index e4623887..a77d43db 100644 --- a/specification/push_gateway.rst +++ b/specification/push_gateway.rst @@ -16,6 +16,8 @@ Push Gateway API ================ +{{unstable_warning_block_PUSH_GATEWAY_RELEASE_LABEL}} + Clients may want to receive push notifications when events are received at the homeserver. This is managed by a distinct entity called the Push Gateway. diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 3bbfa12c..e059d560 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -17,6 +17,8 @@ Federation API ============== +{{unstable_warning_block_SERVER_RELEASE_LABEL}} + .. WARNING:: This API is unstable and will change without warning or discussion while we work towards a r0 release (scheduled for August 2018). From 390f1c16f73b94e47ee100c12fd88de31a7d2a14 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 15:27:36 -0600 Subject: [PATCH 52/73] Remove mentions of identity services from /3pid/delete This will be covered by https://github.com/matrix-org/matrix-doc/issues/1194 For now, we can accept that homeservers may try to unbind, however clients should not rely on it. --- api/client-server/administrative_contact.yaml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml index c1ecf337..78e5811b 100644 --- a/api/client-server/administrative_contact.yaml +++ b/api/client-server/administrative_contact.yaml @@ -152,9 +152,8 @@ paths: post: summary: Deletes a third party identifier from the user's account description: |- - Removes a third party identifier from the user's account. The homeserver - should attempt to unbind the identifier from the identity service, if the - homeserver is able to reasonably determine the identity service used. + Removes a third party identifier from the user's account. This may not + cause an unbind of the identifier from the identity service. operationId: delete3pidFromAccount security: - accessToken: [] @@ -181,16 +180,6 @@ paths: user. schema: type: object - properties: - id_server_unbind_result: - type: string - description: |- - The result of the homeserver's attempt to unbind the identifier from - the identity service. ``success`` indicates that the homeserver was - able to unbind the identifier while ``no-support`` means the homeserver - was not able to unbind, likely due to the identity service not supporting - the operation. Defaults to ``no-support``. - example: "success" tags: - User data "/account/3pid/email/requestToken": From 8bcd7d26e349fd464f88282324411b9eced6d15d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 15:28:43 -0600 Subject: [PATCH 53/73] /3pid/delete returns an empty object --- api/client-server/administrative_contact.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml index 78e5811b..f0362f25 100644 --- a/api/client-server/administrative_contact.yaml +++ b/api/client-server/administrative_contact.yaml @@ -180,6 +180,7 @@ paths: user. schema: type: object + properties: {} tags: - User data "/account/3pid/email/requestToken": From caaa688e37750fea10788aced24330cfc283e8f7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 16:28:26 -0600 Subject: [PATCH 54/73] Fix links to client-server spec --- specification/server_server_api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index a7124010..76aed91d 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -1110,9 +1110,9 @@ Example code .. _`Inviting to a room`: #inviting-to-a-room .. _`Canonical JSON`: ../appendices.html#canonical-json .. _`Unpadded Base64`: ../appendices.html#unpadded-base64 -.. _`Server ACLs`: ../client_server/unstable.html#module-server-acls -.. _`redaction algorithm`: ../client_server/unstable.html#redactions +.. _`Server ACLs`: ../client_server/%CLIENT_RELEASE_LABEL%.html#module-server-acls +.. _`redaction algorithm`: ../client_server/%CLIENT_RELEASE_LABEL%.html#redactions .. _`Signing JSON`: ../appendices.html#signing-json .. _`Checking for a signature`: ../appendices.html#checking-for-a-signature -.. _`Device Management module`: ../client-server/%CLIENT_RELEASE_LABEL%.html#device-management -.. _`End-to-End Encryption module`: ../client-server/%CLIENT_RELEASE_LABEL%.html#end-to-end-encryption +.. _`Device Management module`: ../client_server/%CLIENT_RELEASE_LABEL%.html#device-management +.. _`End-to-End Encryption module`: ../client_server/%CLIENT_RELEASE_LABEL%.html#end-to-end-encryption From 2a08a308f34f7583534e3e9009f390471d1faf07 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 16:29:24 -0600 Subject: [PATCH 55/73] device_keys are required --- api/server-server/user_keys.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/server-server/user_keys.yaml b/api/server-server/user_keys.yaml index 86993433..63c74d20 100644 --- a/api/server-server/user_keys.yaml +++ b/api/server-server/user_keys.yaml @@ -55,7 +55,7 @@ paths: description: algorithm example: "signed_curve25519" example: { - "@alice:example.com": { + "@alice:example.com": { "JLAFKJWSCS": "signed_curve25519" } } @@ -126,6 +126,7 @@ paths: example: { "@alice:example.com": [] } + required: ['device_keys'] responses: 200: description: The device information. @@ -178,7 +179,7 @@ paths: "@alice:example.com": { "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" } - }, + }, "unsigned": { "device_display_name": "Alice's mobile phone" } From 5da3072eef94129c0da1a216ef34421415c8f242 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Aug 2018 16:32:14 -0600 Subject: [PATCH 56/73] Wording change ('may not' has a special meaning) --- api/client-server/administrative_contact.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml index f0362f25..541df43c 100644 --- a/api/client-server/administrative_contact.yaml +++ b/api/client-server/administrative_contact.yaml @@ -152,7 +152,7 @@ paths: post: summary: Deletes a third party identifier from the user's account description: |- - Removes a third party identifier from the user's account. This may not + Removes a third party identifier from the user's account. This might not cause an unbind of the identifier from the identity service. operationId: delete3pidFromAccount security: From ebb044674f0b83d41ce4c1a323a813162f3821f8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 09:51:17 +0100 Subject: [PATCH 57/73] Remove duplicated explanation about event rejection --- specification/server_server_api.rst | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 4c40d6dc..2677cf3a 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -345,17 +345,6 @@ be inserted. The types of state events that affect authorization are: - ``m.room.join_rules`` - ``m.room.power_levels`` -Servers should not create new events that reference unauthorized events. -However, any event that does reference an unauthorized event is not itself -automatically considered unauthorized. - -Unauthorized events that appear in the event graph do *not* have any effect on -the state of the room. - -.. Note:: This is in contrast to redacted events which can still affect the - state of the room. For example, a redacted ``join`` event will still - result in the user being considered joined. - The rules are as follows: 1. If type is ``m.room.create``, allow if and only if it has no From 6c8a45c165f72884bfc53a90566d77c495529ee0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 09:57:00 +0100 Subject: [PATCH 58/73] Full stops --- specification/server_server_api.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index e6e4625c..a69273eb 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -358,18 +358,18 @@ The rules are as follows: 1. If type is ``m.room.create``: - a. Reject if it has any previous events + a. Reject if it has any previous events. b. Reject if the domain of the ``room_id`` does not match the domain of the ``sender``. - c. Reject if ``content.room_version`` key is an unrecognized version + c. Reject if ``content.room_version`` key is an unrecognized version. d. Otherwise, allow. -#. Reject if event does not have a ``m.room.create`` in its ``auth_events`` +#. Reject if event does not have a ``m.room.create`` in its ``auth_events``. #. If type is ``m.room.aliases``: - a. Reject if event has no ``state_key`` - b. Allow if and only if sender's domain matches ``state_key`` + a. Reject if event has no ``state_key``. + b. Allow if and only if sender's domain matches ``state_key``. #. If type is ``m.room.member``: From f3aea32a243c04e79f52304273ab0050ed967872 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 09:59:45 +0100 Subject: [PATCH 59/73] Be consistent with 'If..., reject' --- specification/server_server_api.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index a69273eb..a72f5dc6 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -358,22 +358,23 @@ The rules are as follows: 1. If type is ``m.room.create``: - a. Reject if it has any previous events. - b. Reject if the domain of the ``room_id`` does not match the domain of the - ``sender``. - c. Reject if ``content.room_version`` key is an unrecognized version. + a. If it has any previous events, reject. + b. If the domain of the ``room_id`` does not match the domain of the + ``sender``, reject. + c. If ``content.room_version`` key is an unrecognised version, reject. d. Otherwise, allow. -#. Reject if event does not have a ``m.room.create`` in its ``auth_events``. +#. If event does not have a ``m.room.create`` in its ``auth_events``, reject. #. If type is ``m.room.aliases``: - a. Reject if event has no ``state_key``. - b. Allow if and only if sender's domain matches ``state_key``. + a. If event has no ``state_key``, reject + b. If sender's domain doesn't matches ``state_key``, reject. + c. Otherwise, allow. #. If type is ``m.room.member``: - a. Reject if no ``state_key`` key or ``membership`` key in ``content``. + a. If no ``state_key`` key or ``membership`` key in ``content``, reject. #. If ``membership`` is ``join``: From f4706c888986e3e20aa9c4a7b4a3bd77d8812c4b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 10:06:26 +0100 Subject: [PATCH 60/73] Require creator field --- specification/server_server_api.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index a72f5dc6..54b6e07c 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -362,7 +362,8 @@ The rules are as follows: b. If the domain of the ``room_id`` does not match the domain of the ``sender``, reject. c. If ``content.room_version`` key is an unrecognised version, reject. - d. Otherwise, allow. + d. If ``content`` has no ``creator`` field, reject. + e. Otherwise, allow. #. If event does not have a ``m.room.create`` in its ``auth_events``, reject. From 59f86ec8f2a3b7b07b742ccf5706b44d30c65767 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 10:07:53 +0100 Subject: [PATCH 61/73] Reindent as RST requires sublists to be three spaces --- specification/server_server_api.rst | 128 ++++++++++++++-------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 54b6e07c..b82901be 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -358,80 +358,80 @@ The rules are as follows: 1. If type is ``m.room.create``: - a. If it has any previous events, reject. - b. If the domain of the ``room_id`` does not match the domain of the - ``sender``, reject. - c. If ``content.room_version`` key is an unrecognised version, reject. - d. If ``content`` has no ``creator`` field, reject. - e. Otherwise, allow. + a. If it has any previous events, reject. + b. If the domain of the ``room_id`` does not match the domain of the + ``sender``, reject. + c. If ``content.room_version`` key is an unrecognised version, reject. + d. If ``content`` has no ``creator`` field, reject. + e. Otherwise, allow. #. If event does not have a ``m.room.create`` in its ``auth_events``, reject. #. If type is ``m.room.aliases``: - a. If event has no ``state_key``, reject - b. If sender's domain doesn't matches ``state_key``, reject. - c. Otherwise, allow. + a. If event has no ``state_key``, reject + b. If sender's domain doesn't matches ``state_key``, reject. + c. Otherwise, allow. #. If type is ``m.room.member``: - a. If no ``state_key`` key or ``membership`` key in ``content``, reject. + a. If no ``state_key`` key or ``membership`` key in ``content``, reject. - #. If ``membership`` is ``join``: + #. If ``membership`` is ``join``: - i. If the only previous event is an ``m.room.create`` - and the ``state_key`` is the creator, allow. + i. If the only previous event is an ``m.room.create`` + and the ``state_key`` is the creator, allow. - #. If the ``sender`` does not match ``state_key``, reject. + #. If the ``sender`` does not match ``state_key``, reject. - #. If the ``sender`` is banned, reject. + #. If the ``sender`` is banned, reject. - #. If the ``join_rule`` is ``invite`` then allow if membership state - is ``invite`` or ``join``. + #. If the ``join_rule`` is ``invite`` then allow if membership state + is ``invite`` or ``join``. - #. If the ``join_rule`` is ``public``, allow. + #. If the ``join_rule`` is ``public``, allow. - #. Otherwise, reject. + #. Otherwise, reject. - #. If ``membership`` is ``invite``: + #. If ``membership`` is ``invite``: - i. If the ``sender``'s current membership state is not ``join``, reject. + i. If the ``sender``'s current membership state is not ``join``, reject. - #. If *target user*'s current membership state is ``join`` or ``ban``, - reject. + #. If *target user*'s current membership state is ``join`` or ``ban``, + reject. - #. If the ``sender``'s power level is greater than or equal to the *invite - level*, allow. + #. If the ``sender``'s power level is greater than or equal to the *invite + level*, allow. - #. Otherwise, reject. + #. Otherwise, reject. - #. If ``membership`` is ``leave``: + #. If ``membership`` is ``leave``: - i. If the ``sender`` matches ``state_key``, allow if and only if that user's - current membership state is ``invite`` or ``join``. + i. If the ``sender`` matches ``state_key``, allow if and only if that user's + current membership state is ``invite`` or ``join``. - #. If the ``sender``'s current membership state is not ``join``, reject. + #. If the ``sender``'s current membership state is not ``join``, reject. - #. If the *target user*'s current membership state is ``ban``, and the - ``sender``'s power level is less than the *ban level*, reject. + #. If the *target user*'s current membership state is ``ban``, and the + ``sender``'s power level is less than the *ban level*, reject. - #. If the ``sender``'s power level is greater than or equal to the *kick - level*, and the *target user*'s power level is less than the - ``sender``'s power level, allow. + #. If the ``sender``'s power level is greater than or equal to the *kick + level*, and the *target user*'s power level is less than the + ``sender``'s power level, allow. - #. Otherwise, reject. + #. Otherwise, reject. - #. If ``membership`` is ``ban``: + #. If ``membership`` is ``ban``: - i. If the ``sender``'s current membership state is not ``join``, reject. + i. If the ``sender``'s current membership state is not ``join``, reject. - #. If the ``sender``'s power level is greater than or equal to the *ban - level*, and the *target user*'s power level is less than the - ``sender``'s power level, allow. + #. If the ``sender``'s power level is greater than or equal to the *ban + level*, and the *target user*'s power level is less than the + ``sender``'s power level, allow. - #. Otherwise, reject. + #. Otherwise, reject. - #. Otherwise, the membership is unknown. Reject. + #. Otherwise, the membership is unknown. Reject. #. If the ``sender``'s current membership state is not ``join``, reject. @@ -443,39 +443,39 @@ The rules are as follows: #. If type is ``m.room.power_levels``: - a. If ``users`` key in ``content`` is not a dictionary with keys that are - valid user IDs with values that are integers (or a string that is an - integer), reject. + a. If ``users`` key in ``content`` is not a dictionary with keys that are + valid user IDs with values that are integers (or a string that is an + integer), reject. - #. If there is no previous ``m.room.power_levels`` event in the room, allow. + #. If there is no previous ``m.room.power_levels`` event in the room, allow. - #. For each of the keys ``users_default``, ``events_default``, - ``state_default``, ``ban``, ``redact``, ``kick``, ``invite``, as well as - each entry being changed under the ``events`` or ``users`` keys: + #. For each of the keys ``users_default``, ``events_default``, + ``state_default``, ``ban``, ``redact``, ``kick``, ``invite``, as well as + each entry being changed under the ``events`` or ``users`` keys: - i. If the current value is higher than the ``sender``'s current power level, - reject. + i. If the current value is higher than the ``sender``'s current power level, + reject. - #. If the new value is higher than the ``sender``'s current power level, - reject. + #. If the new value is higher than the ``sender``'s current power level, + reject. - #. For each entry being changed under the ``users`` key, other than the - ``sender``'s own entry: + #. For each entry being changed under the ``users`` key, other than the + ``sender``'s own entry: - i. If the current value is equal to the ``sender``'s current power level, - reject. + i. If the current value is equal to the ``sender``'s current power level, + reject. - #. Otherwise, allow. + #. Otherwise, allow. #. If type is ``m.room.redaction``: - a. If the ``sender``'s power level is greater than or equal to the *redact - level*, allow. + a. If the ``sender``'s power level is greater than or equal to the *redact + level*, allow. - #. If the domain of the ``event_id`` of the event being redacted is the same - as the domain of the ``event_id`` of the ``m.room.redaction``, allow. + #. If the domain of the ``event_id`` of the event being redacted is the same + as the domain of the ``event_id`` of the ``m.room.redaction``, allow. - #. Otherwise, reject. + #. Otherwise, reject. #. Otherwise, allow. From 1a45bc4a8591365fd0ab8616d0194a49e21a4913 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 10:24:51 +0100 Subject: [PATCH 62/73] Reword --- specification/server_server_api.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index a2a2274d..29d104d9 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -303,11 +303,12 @@ following subset of the room state: - If type is ``m.room.member``: - The target's current ``m.room.member`` event, if any. - - If ``join`` or ``invite`` then the current ``m.room.join_rules`` event, - if any. - - If ``invite`` with ``third_party_invite`` in ``content``, then add - ``m.room.third_party_invite`` with state_key of ``token`` in ``signed`` - field of ``third_party_invite``, if any. + - If membership is ``join`` or ``invite``, the current ``m.room.join_rules`` + event, if any. + - If membership is ``invite`` and ``content`` contains a + ``third_party_invite`` property, the current + ``m.room.third_party_invite`` with state_key matching + ``content.third_party_invite.signed.token``, if any. {{definition_ss_pdu}} From 3c53e1910d87750ef1b8844298dc1e720aba6620 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 10:51:59 +0100 Subject: [PATCH 63/73] Fixup --- specification/server_server_api.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index f2d14738..a9c2b1fb 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -396,27 +396,27 @@ The rules are as follows: i. If ``content`` has ``third_party_invite`` key: - #. Reject if *target user* is banned. + #. If *target user* is banned, reject. - #. Reject if ``content.third_party_invite`` does not have a - ``signed`` key. + #. If ``content.third_party_invite`` does not have a + ``signed`` key, reject. - #. Reject if ``signed`` does not have ``mxid`` and ``token`` keys. + #. If ``signed`` does not have ``mxid`` and ``token`` keys, reject. - #. Reject if ``mxid`` does not match ``state_key`` + #. If ``mxid`` does not match ``state_key``, reject. - #. Reject if no ``m.room.third_party_invite`` event in - current state with ``state_key`` matching ``token``. + #. If no ``m.room.third_party_invite`` event in current state with + ``state_key`` matching ``token``, reject. - #. Reject if ``sender`` does not match ``sender`` of third party - invite. + #. If ``sender`` does not match ``sender`` of third party invite, + reject. - #. If any signature in ``signed`` matches any public key in third - party invite, allow. The public keys are in ``content`` of - third party invite under: + #. If any signature in ``signed`` matches any public key in the + ``m.room.third_party_invite``, allow. The public keys are in + ``content`` of ``m.room.third_party_invite`` as: - #. A single public key in ``public_key`` field - #. A list of public keys in ``public_keys`` field + #. A single public key in the ``public_key`` field. + #. A list of public keys in the ``public_keys`` field. #. Otherwise, reject. From d63184ebacc9ff16a620152fb9b057405fd91326 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 10:59:42 +0100 Subject: [PATCH 64/73] Fixup --- specification/server_server_api.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index b82901be..4b099e11 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -361,7 +361,8 @@ The rules are as follows: a. If it has any previous events, reject. b. If the domain of the ``room_id`` does not match the domain of the ``sender``, reject. - c. If ``content.room_version`` key is an unrecognised version, reject. + c. If ``content.room_version`` is present and is not a recognised version, + reject. d. If ``content`` has no ``creator`` field, reject. e. Otherwise, allow. @@ -369,7 +370,7 @@ The rules are as follows: #. If type is ``m.room.aliases``: - a. If event has no ``state_key``, reject + a. If event has no ``state_key``, reject. b. If sender's domain doesn't matches ``state_key``, reject. c. Otherwise, allow. From a1aedb386a5ce8ca20bbc2bd79de35262306eb88 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 11:04:44 +0100 Subject: [PATCH 65/73] Actually use proper sentences like a proper adult --- specification/server_server_api.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index a9c2b1fb..c225874e 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -405,15 +405,15 @@ The rules are as follows: #. If ``mxid`` does not match ``state_key``, reject. - #. If no ``m.room.third_party_invite`` event in current state with - ``state_key`` matching ``token``, reject. + #. If there is no ``m.room.third_party_invite`` event in the + current room state with ``state_key`` matching ``token``, reject. - #. If ``sender`` does not match ``sender`` of third party invite, - reject. + #. If ``sender`` does not match ``sender`` of the + ``m.room.third_party_invite``, reject. #. If any signature in ``signed`` matches any public key in the - ``m.room.third_party_invite``, allow. The public keys are in - ``content`` of ``m.room.third_party_invite`` as: + ``m.room.third_party_invite`` event, allow. The public keys are + in ``content`` of ``m.room.third_party_invite`` as: #. A single public key in the ``public_key`` field. #. A list of public keys in the ``public_keys`` field. From 4d653748d60abfb631ed1c8fbbd3c5a19eca7721 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 11:17:58 +0100 Subject: [PATCH 66/73] Add note back in --- specification/server_server_api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 2677cf3a..19f0fb78 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -478,6 +478,12 @@ transaction request to be responded to with an error response. This means that events may be included in the room DAG even though they should be rejected. +.. NOTE:: + + This is in contrast to redacted events which can still affect the + state of the room. For example, a redacted ``join`` event will still + result in the user being considered joined. + Retrieving event authorization information ++++++++++++++++++++++++++++++++++++++++++ From d91dc6fbf628dc2e021abb5cc89d92cd6b3a2c89 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 11:20:26 +0100 Subject: [PATCH 67/73] Fixup --- specification/server_server_api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 29d104d9..727ac493 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -303,11 +303,11 @@ following subset of the room state: - If type is ``m.room.member``: - The target's current ``m.room.member`` event, if any. - - If membership is ``join`` or ``invite``, the current ``m.room.join_rules`` - event, if any. + - If ``membership`` is ``join`` or ``invite``, the current + ``m.room.join_rules`` event, if any. - If membership is ``invite`` and ``content`` contains a ``third_party_invite`` property, the current - ``m.room.third_party_invite`` with state_key matching + ``m.room.third_party_invite`` event with ``state_key`` matching ``content.third_party_invite.signed.token``, if any. {{definition_ss_pdu}} From b6ed25e4b4b2810f2a09582d9dce45821f967a33 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 11:32:29 +0100 Subject: [PATCH 68/73] Fix incorrect indent --- specification/server_server_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index fbf48393..5eacba8a 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -434,7 +434,7 @@ The rules are as follows: #. A single public key in the ``public_key`` field. #. A list of public keys in the ``public_keys`` field. - #. Otherwise, reject. + #. Otherwise, reject. #. If the ``sender``'s current membership state is not ``join``, reject. From 080845ac1c50dcc3479c819abced912aa20a8cd5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 31 Aug 2018 11:53:21 +0100 Subject: [PATCH 69/73] Add link to previous section --- specification/server_server_api.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index e5bff698..74704faf 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -308,6 +308,8 @@ creating a new event in this room should populate the new event's | E4 +.. _`auth events selection`: + The ``auth_events`` field of a PDU identifies the set of events which give the sender permission to send the event. The ``auth_events`` for the ``m.room.create`` event in a room is empty; for other events, it should be the @@ -384,7 +386,7 @@ The rules are as follows: a. have duplicate entries for a given ``type`` and ``state_key`` pair #. have entries whose ``type`` and ``state_key`` don't match those - specified by the algorithm described previously. + specified by the `auth events selection`_ algorithm described above. #. If event does not have a ``m.room.create`` in its ``auth_events``, reject. From fc037b3a7210863814444e545aaaa542702a0c7f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Aug 2018 09:33:30 -0600 Subject: [PATCH 70/73] Fix merge --- event-schemas/schema/core-event-schema/sync_room_event.yaml | 3 ++- event-schemas/schema/core-event-schema/sync_state_event.yaml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/event-schemas/schema/core-event-schema/sync_room_event.yaml b/event-schemas/schema/core-event-schema/sync_room_event.yaml index fbbf154f..8006148f 100644 --- a/event-schemas/schema/core-event-schema/sync_room_event.yaml +++ b/event-schemas/schema/core-event-schema/sync_room_event.yaml @@ -31,7 +31,8 @@ properties: origin_server_ts: description: Timestamp in milliseconds on originating homeserver when this event was sent. - type: number + type: integer + format: int64 unsigned: description: Contains optional extra information about the event. properties: diff --git a/event-schemas/schema/core-event-schema/sync_state_event.yaml b/event-schemas/schema/core-event-schema/sync_state_event.yaml index 30af9698..1b6ce9b2 100644 --- a/event-schemas/schema/core-event-schema/sync_state_event.yaml +++ b/event-schemas/schema/core-event-schema/sync_state_event.yaml @@ -28,6 +28,10 @@ properties: description: A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this key makes this event a State Event. + + State keys starting with an ``@`` are reserved for referencing user IDs, such + as room members. With the exception of a few events, state events set with a + given user's ID as the state key MUST only be set by that user. type: string required: - state_key From 2f824df8dd72454909087ccfafcb6bf9c778d1e0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Aug 2018 09:43:29 -0600 Subject: [PATCH 71/73] Define the real event types on the invite_state --- api/client-server/sync.yaml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/api/client-server/sync.yaml b/api/client-server/sync.yaml index 0f096e13..9422abb4 100644 --- a/api/client-server/sync.yaml +++ b/api/client-server/sync.yaml @@ -202,8 +202,29 @@ paths: the room then the current state will be given as a delta against the archived ``state`` not the ``invite_state``. - allOf: - - $ref: "definitions/state_event_batch.yaml" + properties: + events: + description: The StrippedState events that form the invite state. + items: + description: 'A stripped down state event, with only the ``type``, ``state_key`` and ``content`` keys.' + properties: + content: + description: The ``content`` for the event. + title: EventContent + type: object + state_key: + description: The ``state_key`` for the event. + type: string + type: + description: The ``type`` for the event. + type: string + required: + - type + - state_key + - content + title: StrippedState + type: object + type: array leave: title: Left rooms type: object From e3ad253dcaeb184e64147d1f4ecbd4e06b4c7834 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Aug 2018 09:44:29 -0600 Subject: [PATCH 72/73] A sender is also required for the invite_state --- api/client-server/sync.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/client-server/sync.yaml b/api/client-server/sync.yaml index 9422abb4..bb514bbe 100644 --- a/api/client-server/sync.yaml +++ b/api/client-server/sync.yaml @@ -206,7 +206,9 @@ paths: events: description: The StrippedState events that form the invite state. items: - description: 'A stripped down state event, with only the ``type``, ``state_key`` and ``content`` keys.' + description: |- + A stripped down state event, with only the ``type``, ``state_key``, + ``sender``, and ``content`` keys. properties: content: description: The ``content`` for the event. @@ -218,10 +220,14 @@ paths: type: description: The ``type`` for the event. type: string + sender: + description: The ``sender`` for the event. + type: string required: - type - state_key - content + - sender title: StrippedState type: object type: array From ab00630ebc3c6aa73c2d7573e5e8a197e7bc6a28 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Aug 2018 09:49:24 -0600 Subject: [PATCH 73/73] Don't include a second unstable warning in the s2s spec for now --- specification/server_server_api.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index e059d560..3bbfa12c 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -17,8 +17,6 @@ Federation API ============== -{{unstable_warning_block_SERVER_RELEASE_LABEL}} - .. WARNING:: This API is unstable and will change without warning or discussion while we work towards a r0 release (scheduled for August 2018).