Skip to main content

Authentication

Excalibur uses Secure Remote Password (SRP) protocol combined with Proof-of-Possession (PoP) to ensure secure authentication.

Initial Authentication

Although some of the endpoints of the server is available without authentication, any client who wishes to access their files need to authenticate themselves with the server.

The initial authentication process uses a WebSocket connection to the /api/auth endpoint. The rough process is as follows:

Let us examine the process in more detail.

Message Format

Each message is a JSON object containing the following fields:

  • status: Current authentication status. Can be OK, ERR, or null.
  • binary: Whether the included data is binary data. Can be true or false.
  • data: Message data. If binary is true, this field is a Base64 encoded byte array. Otherwise, it is a ASCII string.

Here are two examples of messages:

{
"status": "OK",
"binary": false,
"data": "U is OK"
}
{
"status": null,
"binary": true,
"data": "AQ=="
}

Specification

Let

  • NN denote the SRP prime according to RFC5054, Appendix A;
  • gg denote the SRP generator according to RFC5054, Appendix A;
  • kk denote the SRP multiplier according to RFC5054, Section 2.6;
  • xx denote the SRP-xx key generated on the client-side; and
  • v=gxmodNv = g^x \mod N be the verifier value.
danger

xx and vv should be treated as secrets. The server should never gain access to xx, and no one other than the server and client should gain access to vv.

The initial authentication process is as follows:

  1. Client sends its username to the server.

  2. Server checks whether it has a record of the requested username.

    • Username not found: sends message with status ERR and data User does not exist. Terminate connection.
  3. Server sends message with status OK and the SRP group size as an ASCII string (e.g., 1024).

  4. Server generates its ephemeral values (private value bb and public value B=(kv+gb)modNB = (kv + g^b) \mod N) and sends the public value BB to the client (setting binary = true).

  5. Client checks received BB.

    • If BB is invalid (e.g., BmodN=0B \mod N = 0), sends message with status ERR (data can be anything). Can choose to terminate, or wait for server to try another BB.
  6. Client generates its ephemeral values (private value aa and public value A=gamodNA = g^a \mod N) and sends the public value AA to the server (setting binary = true) with status OK.

  7. Server checks received AA.

    • If AA is invalid (e.g., AmodN=0A \mod N = 0), sends message with status ERR (data can be anything). Can choose to terminate, or wait for client to try another AA.
  8. Both client and server computes u=SHA1(AB)u = \texttt{SHA1}(A || B) where || refers to concatenation. Note that AA and BB have been padded to SRP group size bits using zero padding.

    • If server detects u0(modN)u \equiv 0 \pmod N, sends message with status ERR and data Shared U value is 0. Terminate connection.
    • If client detects u0(modN)u \equiv 0 \pmod N, can just choose to close connection.
  9. Server sends message with status OK and data U is OK.

  10. Client and server each computes their premaster:

    • Client: preK=(Bkgx)a+uxmodN\texttt{preK} = (B - kg^x)^{a + ux} \mod N
    • Server: preK=(Avu)bmodN\texttt{preK} = (Av^u)^b \mod N

    The master KK is computed by computing SHA3-256 of the premaster, padded to the SRP group size bits using zero padding.

  11. Client computes M1=H((H(N)H(g))    username    salt    A    B    Kclient)M_1 = H((H(N) \oplus H(g)) \; || \; \texttt{username} \; || \; \texttt{salt} \; || \; A \; || \; B \; || \; K_{\texttt{client}}), where \oplus means XOR and HH is SHA3-256. Client sends M1M_1 to the server (setting binary = true) with status OK.

  12. Server computes its own M1M_1 value using the same formula (using KserverK_{\texttt{server}} in place of KclientK_{\texttt{client}}), and checks with the received M1M_1.

    • If they do not match, sends message with status ERR and data M1 values do not match. Terminate connection.
  13. Server computes M2=H(A    M1    Kserver)M_2 = H(A \; || \; M_1 \; || \; K_{\texttt{server}}) and sends it to the client (setting binary = true) with status OK.

  14. Client computes its own M2M_2 value using the same formula (using KclientK_{\texttt{client}} in place of KserverK_{\texttt{server}}), and checks with the received M2M_2.

    • If they do not match, terminate connection.
  15. Client sends message with status OK with no data.

  16. Server prepares authentication token in the form of a JWT. Encrypts it with shared master key KK using AES-GCM-256.

  17. Server sends message with no status and JSON data containing three fields: nonce, token, and tag.

    • nonce: A random value, as a Base64 encoded byte array.
    • token: The encrypted JWT, as a Base64 encoded byte array.
    • tag: The authentication tag, as a Base64 encoded byte array.
  18. Both sides close connection.

note

The full code that implements the server-side checking can be found in the comms.py file.

Authenticating Subsequent Requests

Once this initial authentication process is complete, future requests to secure endpoints will require the use of the authentication token obtained from the server. Do note that the body of the request and response will be encrypted using the Excalibur Encryption Format (ExEF).

Authentication Token

The authentication token is a JWT that contains the following claims:

  • sub: The subject of the token, which is the username.
  • iat: The issued at time of the token, which is the current time.
  • exp: The expiration time of the token.
    • Currently the token expires after one hour.
  • uuid: Comms UUID used in the Proof-of-Possession (PoP) process.

An example of JWT (which has an invalid JWT signature) is as follows:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAgInN1YiI6ICJNeUNvb2xVc2VybmFtZSIsCiAgICAiaWF0IjogMTUxNjIzOTAyMiwKICAgICJleHAiOiAxNTE2MjQyNjIyLAogICAgInV1aWQiOiAiMTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDAwIgp9.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

which has the following data (expressed in JSON):

{
"sub": "MyCoolUsername",
"iat": 1516239022,
"exp": 1516242622,
"uuid": "123e4567-e89b-12d3-a456-426614174000"
}

Proof-of-Possession (PoP)

A proof-of-possession (PoP) value needs to be computed in addition to providing the authentication token. This value is computed using a HMAC of the message <METHOD> <PATH> <TIMESTAMP> <NONCE>, where

  • <METHOD> is the HTTP method (e.g., GET, POST, PUT, DELETE) in ALL CAPS;
  • <PATH> is the path of the request (e.g., /some/path/here);
  • <TIMESTAMP> is the current time in seconds since the Unix epoch; and
  • <NONCE> is a random 16 byte value.
Why Not Include the Body?

The body of the message need not be included in the HMAC calculation since it is already verified by the encryption:

  • If the request does not include a body, then there is already nothing to check;
  • If the request does include a body, the body should be encrypted using AES-GCM, which authenticates the data sent. Since the data sent uses the secret master key, no malicious actor can spoof the data; thus no need to check.

The HMAC's key is the SRP master key KK and the hash algorithm used is SHA-256. The output PoP value is encoded using Base64 for ease of transmission.

A CyberChef demonstration of the PoP generation process can be found here.

Proving Possession

Requests to secure endpoints need both the authentication token and a PoP header, and the request/response body will be encrypted using the SRP master key.

To prove that the user is who they claim to be, their request will need to include two headers:

  • Authorization: The format of the header is Bearer <JWT> where <JWT> is the authentication token.
  • X-SRP-PoP: The format of the header is <TIMESTAMP> <NONCE> <PoP> where <PoP> is the Base64 encoded PoP value computed as described above and <TIMESTAMP> and <NONCE> are the timestamp and nonce used to compute the PoP value, respectively.

Failing to provide both headers will result in a 401 Unauthorized response.