@ -23,36 +23,48 @@ negative effect on the (particularly client) ecosystem as they need to update th
This was seen in [MSC4291: Room IDs as hashes of the create event](https://github.com/matrix-org/matrix-spec-proposals/pull/4291)
This was seen in [MSC4291: Room IDs as hashes of the create event](https://github.com/matrix-org/matrix-spec-proposals/pull/4291)
which removed the `:domain` part of the room ID. Furthermore, both of those MSCs suffer from scope creep: MSC4014 had per-room per-user keys, and MSC1228 had room keys complete with new `^` and `~` sigils.
which removed the `:domain` part of the room ID. Furthermore, both of those MSCs suffer from scope creep: MSC4014 had per-room per-user keys, and MSC1228 had room keys complete with new `^` and `~` sigils.
Instead, this proposal solely addresses the problem with allowing direct personal data in the user ID and using the server signing key to sign events, taking care to keep the user ID format compatible with the existing ecosystem. This makes the proposal very light, and easier to implement incrementally on top of the existing Matrix ecosystem, whilst leaving room for per-room per-user keys or client-controlled cryptographic keys in the future.
Instead, this proposal solely addresses the problem with allowing direct personal data in the user ID and using the server signing key
to sign events, taking care to keep the user ID format compatible with the existing ecosystem. This makes the proposal very light, and
easier to implement incrementally on top of the existing Matrix ecosystem, whilst leaving room for per-room per-user keys or
client-controlled cryptographic keys in the future.
### Proposal
### Proposal
Starting in room version `vNext`:
Starting in room version `vNext`:
- User ID _localparts_ in rooms are replaced with an ed25519 public key: an "Account Key".
- Each user is identified using an ed25519 key: an "Account Key". A user SHOULD[^perroom] have exactly 1 immutable _account key_ for all rooms they are a part of.
Leaving and rejoining the same room MUST NOT change the _account key_. The _account key_ is encoded as unpadded
- User ID _localparts_ in rooms are replaced with the unpadded urlsafe[^urlsafe] base64 encoded public part of the _account key_.
urlsafe base 64. An example _user ID_ is: `@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:matrix.org`.
Leaving and rejoining the same room MUST NOT change the _account key_.
TODO: MSC1228 versioned this with a `1:` prefix. Should we do the same? How would you even relate the same user across multiple versions anyway?
An example _user ID_ is: `@l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:matrix.org`.
- The private key for this _account key_ signs the event JSON over federation. This means servers do not need to make
- The private key for the `sender`'s _account key_ signs the event JSON over federation. Servers no longer
any network requests to verify the signature on inbound events. Currently servers need to ask for the server keys of
sign events with their [server signing key](https://spec.matrix.org/v1.14/server-server-api/#retrieving-server-keys).[^signing]
the domain directly or via a notary server. If they cannot get the server keys, the event is dropped, causing a split-brain.
Co-signed events (e.g invites) are still co-signed, but with _account keys_ not server signing keys.
This is why this proposal improves the security of the federation protocol. NB: The private key still lives on the server, not clients.
- The domain part of the user ID is kept for compatibility and to provide _routing information_ to other servers.
- The domain part of the user ID is kept for compatibility and to provide _routing information_ to other servers.
Servers still determine which servers are in the room based on the domain of the user ID.
Servers still determine which servers are in the room based on the domain of the user ID.
Signatures on an event follow the same format as today for backwards compatibility with existing server code, but the key ID is now the urlsafe base64 encoded public key:
Signatures on an event follow the same format as today for backwards compatibility with existing server code, but the key ID is now the urlsafe base64 encoded public key:
- Account Key: the ed25519 public key for the user's account.
- Account Key: the ed25519 public key for the user's account.
- Account Name: The human-readable localpart today e.g `alice`.
- Account Name: The human-readable localpart today e.g `alice`.
- Account Name User ID: user IDs as they exist today, formed of an account name and domain e.g `@alice:example.com`
- Account Name User ID: user IDs as they exist today, formed of an account name and domain e.g `@alice:example.com`
- Account Key User ID: user IDs of the form `@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:matrix.org`, formed of an account key and domain.
- Account Key User ID: user IDs of the form `@l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:matrix.org`, formed of an account key and domain.
- Localpart / Domain: segments of a user ID as they are defined today. 'Localpart' is ambiguous and should be qualified as 'account name' or 'account key'.
- Localpart / Domain: segments of a user ID as they are defined today. 'Localpart' is ambiguous and should be qualified as 'account name' or 'account key'.
>[!NOTE]
>[!NOTE]
@ -70,7 +82,7 @@ account key via a new bulk federation endpoint:
POST /_matrix/federation/v1/query/accounts
POST /_matrix/federation/v1/query/accounts
{
{
"account_keys": [
"account_keys": [
"l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ",
"l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ",
"EgdGx+0oy/9IX5k7tCobr0JoiwMvmmQ8sDOVlZODh/o",
"EgdGx+0oy/9IX5k7tCobr0JoiwMvmmQ8sDOVlZODh/o",
"cWm64pdXOGz1DbIXTuH+24szY/+9HjPP7jZwbDjn12s"
"cWm64pdXOGz1DbIXTuH+24szY/+9HjPP7jZwbDjn12s"
]
]
@ -81,7 +93,7 @@ Returns:
200 OK
200 OK
{
{
"account_keys": {
"account_keys": {
"l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ": {
"l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ": {
"account_name": "kegan", // The account name. The 'localpart' of a user ID today.
"account_name": "kegan", // The account name. The 'localpart' of a user ID today.
"domain": "matrix.org", // The 'domain' of a user ID today.
"domain": "matrix.org", // The 'domain' of a user ID today.
"signatures": { ... } // This JSON object is signed with the account key to allow changes in the account name/domain to be detected.
"signatures": { ... } // This JSON object is signed with the account key to allow changes in the account name/domain to be detected.
@ -107,6 +119,7 @@ to resolve it. Like all federation requests, this request _is authenticated_ usi
This creates a bidirectional link:
This creates a bidirectional link:
- By signing event JSON, the account key claims to belong to a particular domain. This is embedded into the DAG.
- By signing event JSON, the account key claims to belong to a particular domain. This is embedded into the DAG.
- By responding to the endpoint with that account key, the domain claims to own that particular account key. This is not embedded into the DAG so not all servers will agree on this.
- By responding to the endpoint with that account key, the domain claims to own that particular account key. This is not embedded into the DAG so not all servers will agree on this.
- Taken together, the two claims prove that the domain owns the key.
>[!NOTE]
>[!NOTE]
> Earlier versions of this proposal just omitted unknown account keys rather than returned an explicit `errcode` for them. This was changed
> Earlier versions of this proposal just omitted unknown account keys rather than returned an explicit `errcode` for them. This was changed
@ -114,10 +127,13 @@ This creates a bidirectional link:
> causing account keys to be unknown by omission.
> causing account keys to be unknown by omission.
The server receiving this response SHOULD persist the mapping in persistent storage. The `account_name` MUST NOT change upon subsequent
The server receiving this response SHOULD persist the mapping in persistent storage. The `account_name` MUST NOT change upon subsequent
requests for the same account key.[^1] When a user is created on a server, the account key SHOULD[^2] be created and SHOULD[^3] be kept immutable for the lifetime of that user. There is a chicken/egg problem for some federation operations e.g invites,
requests for the same account key.[^1] When a user is created on a server, the account key SHOULD[^2] be created and SHOULD be kept immutable
as clients will invite the `account_name` to a room, and will not know the account key yet. Specifically, any federation operation which acts on another server's user needs to talk to that server to discover the account key mapping. To aid this, the following adjustments are made:
for the lifetime of that user. There is a chicken/egg problem for some federation operations e.g invites,
- `/_matrix/federation/v2/invite`: The sender sets the `state_key` of the invite `event` to the account name user ID (as we do today), which the receiver then replaces with an account key user ID when signing the invite event. The sender then signs this JSON, creating the double-signed event.
as clients will invite the `account_name` to a room, and will not know the account key yet. Specifically, any federation operation which acts
- A new endpoint `/_matrix/federation/v2/ban` is created, which is identical to `/invite` but for pre-emptive bans when the account key is not known. Omits the `invite_room_state` field.
on another server's user needs to talk to that server to discover the account key mapping. To aid this, the following adjustments are made:
- `/_matrix/federation/v2/invite`: The sender sets the `state_key` of the invite `event` to the account name user ID (as we do today),
which the receiver then replaces with an account key user ID when signing the invite event. The sender then signs this JSON, creating the double-signed event.
- A new endpoint `/_matrix/federation/v1/ban` is created, which is identical to `/invite` but for pre-emptive bans when the account key is not known. Omits the `invite_room_state` field.
>[!NOTE]
>[!NOTE]
> A few designs were considered here, including having a generic bulk lookup function to map **from account name** to account key.
> A few designs were considered here, including having a generic bulk lookup function to map **from account name** to account key.
@ -146,10 +162,10 @@ account keys into three categories:
- Unknown: the domain is unreachable, returned a non 2xx status, or the server cannot decode the response body.
- Unknown: the domain is unreachable, returned a non 2xx status, or the server cannot decode the response body.
This proposal tries to avoid clients needing to know or care about these account keys. As such, it takes steps to replace the account key
This proposal tries to avoid clients needing to know or care about these account keys. As such, it takes steps to replace the account key
with the account name in the user ID where possible in event JSON sent to clients/bots/bridges/appservices. For a given account key `@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:matrix.org`:
with the account name in the user ID where possible in event JSON sent to clients/bots/bridges/appservices. For a given account key `@l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:matrix.org`:
- The server should replace the account key with the account name in the user ID for verified keys. E.g `@kegan:matrix.org`.
- The server should replace the account key with the account name in the user ID for verified keys. E.g `@kegan:matrix.org`.
- The server should replace the `domain` of the user ID with "invalid" for unverified keys. E.g `@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:invalid`.
- The server should replace the `domain` of the user ID with "invalid" for unverified keys. E.g `@l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:invalid`.
- The server should prefix the account key with `_` when the domain is unreachable. E.g `@_l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:matrix.org`.
- The server should prefix the account key with `_` when the domain is unreachable. E.g `@_l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:matrix.org`.
>[!NOTE]
>[!NOTE]
> We could alternatively filter out these events from being delivered to clients, but this would cause
> We could alternatively filter out these events from being delivered to clients, but this would cause
@ -175,7 +191,7 @@ with the account name in the user ID where possible in event JSON sent to client
> public key (e.g used in a distributed hash table).
> public key (e.g used in a distributed hash table).
>
>
> keys are prefixed with `_` down the CSAPI to provide a temporary namespace to avoid conflicts with _account names_
> keys are prefixed with `_` down the CSAPI to provide a temporary namespace to avoid conflicts with _account names_
> which happen to look like `l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ`. The `_` prefix is used by application services,
> which happen to look like `l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ`. The `_` prefix is used by application services,
> and major server implementations disallow creating users starting with `_`, thus ensuring the namespaces remain separate.
> and major server implementations disallow creating users starting with `_`, thus ensuring the namespaces remain separate.
> This is a temporary measure until clients become account key aware.
> This is a temporary measure until clients become account key aware.
@ -190,25 +206,25 @@ This ensures the member list remains accurate on clients. State events sent by t
#### Gradual compatibility
#### Gradual compatibility
To enable clients to gradually become aware of account keys, servers MUST set the `unsigned.account.key` property of the event JSON to be the account key
To enable clients to gradually become aware of account keys, servers MUST set the `unsigned.sender_account.key` property of the event JSON to be the account key
and the `unsigned.account.name` property of the event JSON to be the account name returned from `/accounts` e.g:
and the `unsigned.sender_account.name` property of the event JSON to be the account name returned from `/accounts` e.g:
Clients can then use the `unsigned.account.key` field as an unchanging identifier for the sender of the event, akin to how they use the `sender` field today.
Clients can then use the `unsigned.sender_account.key` field as an unchanging identifier for the sender of the event, akin to how they use the `sender` field today.
A later room version can then:
A later room version can then:
- Revert the `sender` of the event to be the wire-format over federation and not modify it, meaning the `sender` becomes identical to `unsigned.account.key`.
- Revert the `sender` of the event to be the wire-format over federation and not modify it, meaning the `sender` becomes identical to `unsigned.sender_account.key`.
- Tell clients to form the user ID by replacing the account key with the `unsigned.account.name` if it is present. The absence of a `name` means the
- Tell clients to form the user ID by replacing the account key with the `unsigned.sender_account.name` if it is present. The absence of a `name` means the
key is not verified. Abusive `name` strings can be redacted by the server without breaking user identification.
key is not verified. Abusive `name` strings can be redacted by the server without breaking user identification.
This is slightly more wasteful on bandwidth, but provides much more convenience for clients as the data they need is in the same struct.
This is slightly more wasteful on bandwidth, but provides much more convenience for clients as the data they need is in the same struct.
@ -225,7 +241,9 @@ the remote server is unavailable, in which case E2EE will break anyway.
#### Impacts on restricted rooms
#### Impacts on restricted rooms
Rooms with the `restricted` join rule are impacted because we no longer want to check that the server domain signed the event. Thankfully, the `join_authorised_via_users_server` field is a _user ID_, so we can simply extract the account key from the localpart of the user ID and verify that there is a signature with that key. For clarity, auth rules are modified like so:
Rooms with the `restricted` join rule are impacted because we no longer want to check that the server domain signed the event.
Thankfully, the `join_authorised_via_users_server` field is a _user ID_, so we can simply extract the account key from the localpart of the
user ID and verify that there is a signature with that key. For clarity, auth rules are modified like so:
> If type is `m.room.member`:
> If type is `m.room.member`:
> - [...]
> - [...]
@ -234,7 +252,9 @@ Rooms with the `restricted` join rule are impacted because we no longer want to
#### Impacts on key validity
#### Impacts on key validity
It is critical that all servers agree on which events have valid signatures and which do not. As a result, key validity as a concept is untenable if we wish for all servers to converge because the key validity time can be modified inconsistently for different servers. As a result, this MSC _removes_ the [Signing key validity period](https://spec.matrix.org/v1.15/rooms/v5/#signing-key-validity-period) introduced in room version 5.
It is critical that all servers agree on which events have valid signatures and which do not. As a result, key validity as a concept is untenable
if we wish for all servers to converge because the key validity time can be modified inconsistently for different servers. As a result, this
MSC _removes_ the [Signing key validity period](https://spec.matrix.org/v1.15/rooms/v5/#signing-key-validity-period) introduced in room version 5.
The impact of this is that a compromised private key cannot be cycled by setting an expiry time for it. Instead, the server should:
The impact of this is that a compromised private key cannot be cycled by setting an expiry time for it. Instead, the server should:
- Generate a new account key for this user.
- Generate a new account key for this user.
@ -249,7 +269,7 @@ TODO: if we are serious about this, we should probably introduce some kind of re
### Potential Issues
### Potential Issues
Servers may lie about their domain e.g `foo.com` may join the room as `@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:bar.com`.
Servers may lie about their domain e.g `foo.com` may join the room as `@l8Hft5qXKn1vfHrg3p4-W8gELQVo8N13JkluMfmn2sQ:bar.com`.
This means `foo.com` will not get events in the room routed to them, but a victim server `bar.com` will instead be pushed events as a form of amplification attack.
This means `foo.com` will not get events in the room routed to them, but a victim server `bar.com` will instead be pushed events as a form of amplification attack.
Servers MUST have a global backoff timer per-domain to ensure that attackers cannot repeatedly join users with fake domains to popular rooms to cause amplification attacks.
Servers MUST have a global backoff timer per-domain to ensure that attackers cannot repeatedly join users with fake domains to popular rooms to cause amplification attacks.
@ -264,7 +284,7 @@ Servers MUST have a global backoff timer per-domain to ensure that attackers can
- Servers can masequarade as users on their server, but they could _already_ do this due to the lack of any end-to-end
- Servers can masequarade as users on their server, but they could _already_ do this due to the lack of any end-to-end
cryptographic signing of events.
cryptographic signing of events.
- If another domain gets hold of the private key to an identity, they can manufacture valid events with that key
- If another domain gets hold of the private key to an identity, they can manufacture valid events with that key