Skip to content

Latest commit

 

History

History
479 lines (362 loc) · 14.2 KB

3262-apake_authentication.md

File metadata and controls

479 lines (362 loc) · 14.2 KB

[WIP]MSC3262: aPAKE authentication

Like most password authentication, matrix's 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, 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) 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

Registration follows the UIA flow, which means the client starts with sending a POST with the requested username to `` this allow clients to discover the supported groups (and whether srp6a is supported).

POST /_matrix/client/r0/register

{
  username: "cheeky_monkey"
}

To which the server reponds with:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "flows": [
    {
      "stages": [ "m.login.srp6a.register"]
    }
  ],
  "session": "xxxxx",
  "params": {
    "m.login.srp6a.register": {
      "groups": [supported groups],
      "passwordhash": [supported hashes], 
      "hash": [supported hashes] 
	  }
    }		
}

Here the server sends it's supported authentication types (in this example only password and srp6a) and if applicable it sends the supported SRP groups, as specified by the SRP specification or rfc3526. The groups are referenced by name as:

["2048","3072","4096","6144","8192","1536MODP","2048MODP","3072MODP","4096MODP","6144MODP","8192MODP"]

Note that the "1024" and "1536" are explicitly excluded as they are now too small to provide practical security.

The supported password hashes are a list of supported password hashes by the server, referred to as PH() throughout this MSC. Initially for this MSC we suggest supporting pbkdf2 and bcrypt, though this may change over time. The supported hashes are the hashfunctions used for all other hash calculations, referred to as H() throughout this MSC. Initially we suggest at least supporting SHA256 and SHA512.

The client then chooses a supported srp group from the SRP specification, and a hash function from the supported list and generates a random salt s. The client then calculates the verifier v as:

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

Here PH() is the chosen secure password hash function, p is the user specified password, and g is the generator of the selected srp group.
Note that all values are calculated modulo N (of the selected srp group).

This is then sent (base64 encoded) to the server, otherwise mimicking the password registration, through:

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

{
  "auth": {
    "type": "m.login.srp6a",
    "session": "xxxxx",
    "example_credential": "verypoorsharedsecret"
  },
  "auth_type": "srp6a",
  "username": "cheeky_monkey",
  "verifier": v,
  "salt": s,
  "params": {
    "group": "selected group",
    "passwordhash": "PH",
    "hash_iterations": i,
    "hash": "H"
    },
  "device_id": "GHTYAJCE",
  "initial_device_display_name": "Jungle Phone",
  "inhibit_login": false
}

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

Convert from password to SRP

To convert to SRP we'll use the change password endpoint. This uses the UIA flow to authenticate which as the first step return the flows, with srp support as defined in register above.

After the initial UIA the last step is to send the new credentials to be stored with "auth_type": "m.login.srp6a" added, and the required verifier, group, and salt.

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

{
  "auth_type": "m.login.srp6a",
  "verifier": v,
  "salt": s,
  "params": {
    "group": "selected group",
    "passwordhash": "PH",
    "hash_iterations": i,
    "hash": "H"
    },
  "logout_devices": false,
  "auth": {
    "type": "m.login.password",
    "session": "xxxxx",
    "example_credential": "verypoorsharedsecret"
  }
}

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

Login flow

To start the login flow the client sends a GET request to /_matrix/client/r0/login, the server responds with:

{
  "flows": [
    {
      "type": "m.login.srp6a"
    }
  ]
}

(and any other supported login flows)

Next the client sends its username to obtain the salt and SRP group as:

POST /_matrix/client/r0/login

{
  "type": "m.login.srp6a.init",
  "username": "cheeky_monkey"
}

If the user hasn't registered with SRP the server responds with:

Status code: 403

{
  "errcode": "M_UNAUTHORIZED",
  "error": "User has not registered with SRP."
}

The server responds with the salt and SRP group (looked up from the database), and public value B:

{
  "params": {
    "group": "selected group",
    "passwordhash": "PH",
    "hash_iterations": i,
    "hash": "H"
    },
  "salt": s,
  "server_value": B,
  "session": "12345"
}

The client looks up N (the prime) and g (the generator) of the selected SRP group as sent by the server, PH is the hash algorithm used with i iterations. s is the stored salt for the user as supplied during registration, B is the public server value, and session is the session id of this authentication flow, can be 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) 

Here H() is a secure hash function The client then calculates:

A = g^a 

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 = PH(p, s)  
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.

The client calculates:

M1 = H(H(N) xor H(g), H(I), s, A, B, K)

The client will then respond with:

POST /_matrix/client/r0/login

{
  "type": "m.login.srp6a.verify",
  "evidence_message": M1,
  "client_value": A,
  "session": "12345"
}

Here M1 is the (base64 encoded) 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 (base64 encoded) as:

M2 = H(A, M1, 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.

UIA flow

The client starts the UIA flow by sending a post without the auth object, to which the server reponds with:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "flows": [
    {
      "stages": [ "m.login.srp6a.init", "m.login.srp6a.verify" ]
    }
  ],
  "session": "12345"
}

The client then sends its username to obtain the salt and SRP group as:

POST

auth: {
  "type": "m.login.srp6a.init",
  "username": "cheeky_monkey"
  "session": "12345"
  }

If the user hasn't registered with SRP the server responds with:

Status code: 403

auth: {
  "errcode": "M_UNAUTHORIZED",
  "error": "User has not registered with SRP."
  }

The server responds with the salt and SRP group (looked up from the database), and public value B:

auth: {
  "salt": s,
  "params": {
    "group": "selected group",
    "passwordhash": "PH",
    "hash_iterations": i,
    "hash": "H"
    },
  "server_value": B,
  "session": "12345"
  }

The client looks up N (the prime) and g (the generator) of the selected SRP group as sent by the server, H is the has algorithm used with i iterations. s is the stored salt for the user as supplied during registration, B is the public server value, and session is the session id of this authentication flow, can be 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 

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 = PH(p, s)  
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.

The client calculates:

M1 = H(H(N) xor H(g), H(I), s, A, B, K)

The client will then respond with:

POST

auth: {
  "type": "m.login.srp6a.verify",
  "evidence_message": M1,
  "client_value": A,
  "session": "12345"
  }

Here M1 is the (base64 encoded) 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 (base64 encoded) as:

M2 = H(A, M1, K)
{
  "evidence_message": M2
}

Along with any other data expected returned by the API endpoint.

The client verifies that M2 matches, and is subsequently authenticated.

Potential issues

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

As long as the plain password login remains available it is not trivial to allow clients to assume the server never learns the password, since an evil server can say to a new client that it doesn't support SRP and learn the password that way. And with the client not being authenticated before login there's no easy way for the client to know whether the user expects SRP to be used. The long-term solution to this is to declare the old password authentication as deprecated and have all matrix clients refuse to login with m.password. This may take a long time; "the best moment to plant a tree is 10 years ago, the second best is today." The short term solution requires a good UX in clients to notify the user that they expect to login with SRP but the server doesn't support this.

SRP is vulnerable to precomputation attacks and it is incompatible with elliptic-curve cryptography. Matthew Green judges it as "It’s not ideal, but it’s real." and "not obviously broken" and it's a fairly old protocol.

Alternatives

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.

SCRAM serves a similar purpose and is used by (amongst others) XMPP. SRP seems superior here because it does not store enough information server-side to make a valid authentication against the server in case the database is somehow leaked without the server being otherwise compromised.

The UIA /login flow as proposed by MSC2835 would basically remove the whole /login part of this MSC, making it a lot simpler.

Security considerations

SRP uses the discrete logarithm problem, deemed unsolved, though Shor's algorithm can become an issue when quantum computers get scaled up. Work seems to have been done to create a quantum-safe SRP version, see here, though I lack the credentials to determine whether or not this holds water.

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.

Outlook

This MSC allows for authentication without the server learning the password, and authenticating both the server and the client to each other. This may prevent a few security issues, such as a CDN learning the users password. The main reason for this MSC is that later this may allow the reuse of the same password for both authentication and SSSS encryption while remaining secure.

Phasing out the old plain password login will take time, as older clients may still immediately POST the plaintext password to /login invisibly to the end user, without any checks. The server should then just respond with a HTTP Status code 403:

{  
  "errcode": "M_UNAUTHORIZED",  
  "error": "Password authentication is not supported."  
}

But then the server still has access to the plain text password.

Unstable prefix

The following mapping will be used for identifiers in this MSC during development:

Proposed final identifier Purpose Development identifier
m.login.srp6a.* login type com.vgorcum.msc3262.login.srp6a.*