Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] MSC3262 aPAKE authentication #3262

Draft
wants to merge 22 commits into
base: old_master
Choose a base branch
from
Draft
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions proposals/3262-apake_authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# \[WIP]MSC3262: aPAKE authentication

Like most password authentication, matrix's
[login](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login)
requires sending the password in plain text (though usually encrypted in transit with https).
This requirement has as (obvious) downside that a man in the middle attack would allow reading the password,
but also requires that the server has temporary access to the plaintext password
(which will subsequently be hashed before storage).

A Password Authenticated Key Exchange (PAKE) can prevent the need for sending the password in plaintext for login,
and an aPAKE (asymmetric or augmented) allows for safe authentication without the server ever needing
access to the plaintext password. OPAQUE is a modern implementation of an aPAKE that is
[currently an ietf draft](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-opaque-05),
but as it's still pending and doesn't have many open source implementations using it would
require implementing it ourselves. As such choosing to use SRP
([rfc2945](https://datatracker.ietf.org/doc/html/rfc2945)) makes more sense.
SRP has as downside that the salt is transmitted to the client before authentication,
allowing a precomputation attack which would speed up a followup attack after a server compromise.
However, should a server be compromised,
it is probably simpler to generate an access token and impersonate the target that way.

## Proposal

Add support for the SRP 6a login flow, as `"type": "m.login.srp6a"`.

### Registration flow

To allow clients to discover the supported groups (and whether srp6a is supported)
the client sends a GET request to /_matrix/client/r0/register

`GET /_matrix/client/r0/register`

```
{
"auth_types": ["password", "srp6a"]
"srp_groups": [supported groups]
}
```
Here the server sends it's supported authentication types (in this case only password and srp6a)
and if applicable it sends the supported SRP groups, as specified by the
[SRP specification](https://datatracker.ietf.org/doc/html/rfc5054#page-16) or
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
[rfc3526](https://datatracker.ietf.org/doc/html/rfc3526).

The client then chooses an srp group and generates a random salt `s`.
The client then calculates the verifier `v` as:

x = H(s, p)
v = g^x

Here H() is a secure hash function, and p is the user specified password.
Note that all values are calculated modulo N.
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved

This is then sent to the server, otherwise mimicking the password registration, through:

`POST /_matrix/client/r0/register?kind=user`

```
{
"auth": {
"type": "example.type.foo",
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
"session": "xxxxx",
"example_credential": "verypoorsharedsecret"
},
"auth_type": "srp6a",
"username": "cheeky_monkey",
"verifier": v,
"group": [g,N],
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
"salt": s,
"device_id": "GHTYAJCE",
"initial_device_display_name": "Jungle Phone",
"inhibit_login": false
}
```

The server stores the verifier, salt, and group next to the username.

### Convert from password to SRP

Mimicking the flow of register above, first a GET request is sent to check if SRP is
supported and find the supported groups, here we'll reuse the register endpoint
`GET /_matrix/client/r0/register`. *Or we could add a GET endpoint for /_matrix/client/r0/account/password*

To convert to SRP we'll use the change password endpoint with the
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
`"auth_type": "srp6a"` added, and the required `verifier`, `group`, and `salt`.

`POST /_matrix/client/r0/account/password HTTP/1.1`

```
{
"auth_type": "srp6a",
"verifier": v,
"group": [g,N],
"salt": s,
"logout_devices": false,
"auth": {
"type": "example.type.foo",
"session": "xxxxx",
"example_credential": "verypoorsharedsecret"
}
}
```

The server then removes the old password (or old verifier, group, and salt) and stores the new values.

### Login flow

To start the login flow the client sends it's username to obtain the salt and SRP group as:
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved

`POST /_matrix/client/r0/login`

```
{
"type": "m.login.srp6a.init",
"username": "cheeky_monkey"
}
```
The server responds with the salt and SRP group (looked up from the database), and public value `B`:

```
{
"prime": N,
"generator": g,
"salt": s,
"server_value": B,
"auth_id": "12345"
}
```
Here N is the prime and g is the generator of the SRP group. s is the stored salt for the user
as supplied in the `POST`, B is the public server value, and auth_id is the id of this authentication flow,
can by any unique random string, used for the server to keep track of the authentication flow.

the server calculates B as:

B = kv + g^b

where b is a private randomly generated value for this session (server side) and k is given as:

k = H(N, g)

The client then calculates:

A = g^a
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
where a is a private randomly generated value for this session (client side).

Both then calculate:

u = H(A, B)

Next the client calculates:

x = H(s, p)
S = (B - kg^x) ^ (a + ux)
K = H(S)

The server calculates:

S = (Av^u) ^ b
K = H(S)

Resulting in the shared session key K.

To complete the authentication we need to prove to the server that the session key K is the same.
*note that this proof is directly lifted from the [SRP spec](http://srp.stanford.edu/design.html),
another proof can be possible as well.*

The client calculates:

M1 = H(H(N) xor H(g), H(I), s, A, B, K)
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved

The client will then respond with:

`POST /_matrix/client/r0/login`

```
{
"type": "m.login.srp6a.verify",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises an interesting question about how the Matrix user-interactive auth is supposed to work.

For a stage that requires multiple rounds of communication, should the type be the same for every request? ie, here it would be m.login.srp6a for both init and verify, and the details would go in the auth object.

Or should each "round" of communication be its own UIA stage? In that case, it seems like the flows returned by GET /_matrix/client/r0/login should include both stages, ie ["m.login.srp6a.init", "m.login.srp6a.verify"].

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flow only has one GET request at the very start, so returning just m.login.srp6a seems like it's sufficient to me, supporting just init or just verify makes little sense so I would argue that's implied.

If we compare against the webrtc spec (which also has multiple stages) I think using m.login.srp6a.init and m.login.srp6a.verify in POST to /login makes sense. (webRTC in matrix use m.call.invite, m.call.candidates, m.call.answer and m.call.hangup). We defined an auth_id in this MSC because we do need to keep track of the flows, you can't verify before having done the init.

Then again: srp can map fairly well on the UIA, but that's a seperate MSC and I kept that out of scope here because there's no reason either this msc or the UIA msc needs to depend one another.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, I think I was getting confused between the UIA vs normal login. I thought at some point they were going to all use the UIA? Or maybe that was just a proposal that I saw.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too, I may have missed that the proposal got merged?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course this MSC actually needs to support UIA, because authentication doesn't only happen at /login so this is added now. It's more or less a duplicate of /login except all SRP json stuff in put inside an auth object.

"evidence_message": M1,
"client_value": A,
"auth_id": "12345"
}
```
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
Here `M1` is the client evidence message, and A is the public client value.
Upon successful authentication (ie M1 matches) the server will respond with the regular login success status code 200:

To prove the identity of the server to the client we can send back M2 as:

M2 = H(A, M, K)

```
{
"user_id": "@cheeky_monkey:matrix.org",
"access_token": "abc123",
"device_id": "GHTYAJCE",
"evidence_message": M2
"well_known": {
"m.homeserver": {
"base_url": "https://example.org"
},
"m.identity_server": {
"base_url": "https://id.example.org"
}
}
}
```

The client verifies that M2 matches, and is subsequently logged in.


## Potential issues

Adding this authentication method requires client developers to implement this in all matrix clients.

SRP is vulnerable to precomputation attacks and it is incompatible elliptic-curve cryptography.
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved
Matthew Green judges it as
["It’s not ideal, but it’s real." and "not obviously broken"](https://blog.cryptographyengineering.com/2018/10/19/lets-talk-about-pake/)
and it's a fairly old protocol.

## Alternatives
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved

OPAQUE is the more modern protocol, which has the added benefit of not sending the salt in plain text to the client,
but rather uses an 'Oblivious Pseudo-Random Function' and can use elliptic curves.

*Bitwardens scheme can be mentioned here as well, since it does allow auth without the server
learning the plaintext password, but it isn't a PAKE, but rather something along the lines of a hash
of the password before sending the hash to the server for auth, practically making the hash
the new password, and as such it doesn't protect against a mitm.*


## Security considerations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a malicious server starts reporting that it no longer supports SRP or that the client does not have SRP configured? It will take some really good UX design by clients to ensure that the client doesn't fall back to a regular login and leak the password to the server.

Copy link
Author

@mvgorcum mvgorcum Jul 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a big one, for quite a while we won't be able to assume the server has no access to the plaintext password. I would imagine that the long-term solution is to remove the password authentication from the matrix spec, and declare all apps still supporting the old password login as incompatible.

I added some thoughts about this. Do you have any additions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is fine. I don't think there is way to enforce it yet. Maybe add a note that clients can consider indicating when SRP is/isn't used. Maybe that indication would even take a while to be enabled by default but it provides a nice path away from sending the password to the server.

  1. Opportunistic, the client uses SRP if advertised.
  2. Recommended. The client warns if SRP isn't going to be used. (Or shows a "lock" if it is)
  3. Enforced. The client refuses to send the password to the server. (At least without some very explicit disclaimer)

Of course we shouldn't mandate client behaviour, however I think it is often helpful to explain what it could look like.


*Probably loads, though SRP and OPAQUE have had lots of eyes, I would assume.*

This whole scheme only works if the user can trust the client, which may be an issue
in the case of a 'random' javascript hosted matrix client, though this is out of scope for this MSC.

## Unstable prefix
mvgorcum marked this conversation as resolved.
Show resolved Hide resolved