Identity/AttachedServices/KeyServerProtocol: Difference between revisions

update to new protocol (with HTTP endpoint names and keyFetchToken)
(update to new protocol (with HTTP endpoint names and keyFetchToken))
Line 1: Line 1:
= PiCL Key Server / IdP Protocol =
= PiCL Key Server / IdP Protocol =


NOTE: This specification is under active development (10-Jul-2013). Several pieces are not yet complete. If you write any code based on this design, keep a close eye on this page and/or contact me (warner) on the #picl IRC channel to learn about changes. Eventually this will be nailed down and should serve as a stable spec for the PICL keyserver/IdP protocol.
NOTE: This specification is under active development (11-Jul-2013). Several pieces are not yet complete. If you write any code based on this design, keep a close eye on this page and/or contact me (warner) on the #picl IRC channel to learn about changes. Eventually this will be nailed down and should serve as a stable spec for the PICL keyserver/IdP protocol.


The server is being developed in https://github.com/mozilla/picl-idp . This repo currently include a demonstration client (node.js CLI).
The server is being developed in https://github.com/mozilla/picl-idp . This repo currently include a demonstration client (node.js CLI).
Line 24: Line 24:
* randomly choose a 32-byte srpSalt (unique, but not secret)
* randomly choose a 32-byte srpSalt (unique, but not secret)
* create srpVerifier from srpPW and srpSalt (as described below)
* create srpVerifier from srpPW and srpSalt (as described below)
* deliver (email, stretchParams, mainSalt, srpParams, srpSalt) to the keyserver's createAccount() API
* deliver (email, stretchParams, mainSalt, srpParams, srpSalt) to the keyserver's "POST /account/create" API




Line 31: Line 31:
The server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-byte) strings. It stores these, along with all the remaining values, indexed by email, in the account table where they can be retrieved by getToken later.
The server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-byte) strings. It stores these, along with all the remaining values, indexed by email, in the account table where they can be retrieved by getToken later.


After creating the account, the client immediately runs getToken("sign"), as described below, to fetch kA and wrap(kB). It then unwraps wrap(kB) by XORing it with wrapKey to obtain kB.
== Email Verification ==


= Email+Password -> SignToken/ResetToken =
To prevent fixation attacks, we require new accounts to verify their configured recovery email address before letting them learn the generated keys or obtain a signed certificate. Nevertheless, we wish clients to forget the user's password while they wait for email verification to complete. To achieve this, clients can obtain a sessionToken before verification, but all APIs that require it will raise errors until verification is finished.


To connect a browser to an existing account, we use the following "getSignToken" protocol to transform an email+password pair into (kA, kB, signToken). "kA" and "kB" enable the browser to encrypt/decrypt synchronized data records, while "signToken" is used to convince the storage server to accept read and write requests for those records.
The server will send email with a URL that contains a long random "verification code" in the "fragment" hash. This URL points to a page with some javascript that submits the code to the "POST /account/recovery_methods/verify_code" API. The URL can be clicked by any browser (it is not bound to anything), and when the API is hit, the account is marked as verified.
 
After the client submits /account/create, it performs the "/session/auth" login sequence below to obtain a sessionToken. It then polls the "GET /account/recovery_methods" (which requires a sessionToken but not account verification) until the user clicks the email link and the API reports verification is complete. Then the client uses "GET /account/keys" and "POST /certificate/sign", described below, to obtain kA, kB, and a signed certificate to talk to the storage server.
 
= Login: /session/auth =
 
To connect a browser to an existing account, we use the following login protocol to transform an email+password pair into (keyFetchToken, sessionToken). These tokens will be used in the next section to obtain encryption keys and signed certificates.


This protocol starts by using key-stretching to transform the email+password into a "stretchedPW", then feeds this into an SRP protocol to get a session key. It uses this session key to decrypt a bundle of encrypted data from the keyserver, resulting in three values: kA, wrap(kB), and the signToken. The stretchedPW is also used to derive the key that will decrypt wrap(kB) into the actual kB value.
This protocol starts by using key-stretching to transform the email+password into a "stretchedPW", then feeds this into an SRP protocol to get a session key. It uses this session key to decrypt a bundle of encrypted data from the keyserver, resulting in three values: kA, wrap(kB), and the signToken. The stretchedPW is also used to derive the key that will decrypt wrap(kB) into the actual kB value.
Line 41: Line 47:
[[File:PICL-IdPAuth-bigpix.png|IdP Auth Big Picture]]
[[File:PICL-IdPAuth-bigpix.png|IdP Auth Big Picture]]


This same protocol is used, with slightly different methods and constants, to obtain the "resetToken". This token allows a client to safely reset the account password.
This same protocol is used, with slightly different methods and constants, to obtain the "accountResetToken". This token allows a client to safely reset the account password.


The protocol is optimized to minimize round-trips and to enable parallelism, to reduce the time it takes to connect a browser to the account to just a few seconds. As a result, the two messages it sends (getToken1 and getToken2) each perform multiple jobs.
The protocol is optimized to minimize round-trips and to enable parallelism, to reduce the time it takes to connect a browser to the account to just a few seconds. As a result, the two messages it sends (/session/auth/start and /session/auth/finish) each perform multiple jobs.


== getToken1 ==
== auth/start ==


As soon as the user finishes typing in the email address, the client should send it in the "getToken1" message to the keyserver. The response will include a set of parameters that are needed for key-stretching (described below), and the common parameters used by both sides of the SRP protocol to follow. These are simply looked up in a database entry for the client, along with an account-id. It must also include an allocated session-id that is used to associate this request with the subsequent getToken2 request. Finally, the response also includes the server's contribution to the SRP protocol ("srpB"), which is calculated on the server based upon a random value that it remembers in the session.
As soon as the user finishes typing in the email address, the client should send it in the "/session/auth/start" message to the keyserver. The response will include a set of parameters that are needed for key-stretching (described below), and the common parameters used by both sides of the SRP protocol to follow. These are simply looked up in a database entry for the client, along with an account-id. It must also include an allocated loginSRPToken that is used to associate this request with the subsequent /session/auth/finish request. Finally, the response also includes the server's contribution to the SRP protocol ("srpB"), which is calculated on the server based upon a random value that it remembers in the session.


To mitigate DoS abuse, getToken1() may also require a proof-of-work string, described below.
To mitigate DoS abuse, /session/auth/start may also require a proof-of-work string, described below.


== Client-Side Key Stretching ==
== Client-Side Key Stretching ==
Line 61: Line 67:
Our initial parameter choices for this stretching are to use 20000 iterations of PBKDF2 on each side (40k in all), and to run scrypt with N/r/p set to 64k/8/1. This requires roughly 60MB of RAM for about 1.3 seconds on a 1.8GHz Intel Atom linux box (it will run faster, but use the same memory, on more modern CPUs). We are still studying performance on various platforms to decide what parameters to use in V1: lowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).
Our initial parameter choices for this stretching are to use 20000 iterations of PBKDF2 on each side (40k in all), and to run scrypt with N/r/p set to 64k/8/1. This requires roughly 60MB of RAM for about 1.3 seconds on a 1.8GHz Intel Atom linux box (it will run faster, but use the same memory, on more modern CPUs). We are still studying performance on various platforms to decide what parameters to use in V1: lowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).


Since the stretching is expected to take a second or two, the client can optimistically start this process (using default parameters) before receiving the getToken1() response, and then check that it used the right parameters afterwards (repeating the operation if not). (We'll want to build the stretching function with periodic checkpoints so that we don't have to lose all progress if the parameters turn out to be wrong). The "mainSalt is added *after* the stretching, to enable this parallelism (at a tiny cost in security).
Since the stretching is expected to take a second or two, the client can optimistically start this process (using default parameters) before receiving the auth/start response, and then check that it used the right parameters afterwards (repeating the operation if not). (We'll want to build the stretching function with periodic checkpoints so that we don't have to lose all progress if the parameters turn out to be wrong). The "mainSalt is added *after* the stretching, to enable this parallelism (at a tiny cost in security).


After "stretchedPW" is derived, a second HKDF call is used to derive "srpPW" and "unwrapBKey" which will be used later.
After "stretchedPW" is derived, a second HKDF call is used to derive "srpPW" and "unwrapBKey" which will be used later.
Line 96: Line 102:
=== SRP Server-side Sign-In Flow ===
=== SRP Server-side Sign-In Flow ===


When the user connects a new device to their account, they use the getToken1() API to start the SRP protocol. This sends the account email address to the server. The server looks up the stored srpVerifier for this account, creates a random 'b' integer, performs some math to compute the "B" number, then converts B into a string known as "srpB". "srpB" is returned to the client, along with srpSalt and the key-stretching parameters. "b" and "srpB" are retained for the subsequent getToken2() call. '''Note that it is critical that the "b" integer remain secret on the server.'''
When the user connects a new device to their account, they use the /session/auth/start API to start the SRP protocol. This sends the account email address to the server. The server looks up the stored srpVerifier for this account, creates a random 'b' integer, performs some math to compute the "B" number, then converts B into a string known as "srpB". "srpB" is returned to the client, along with srpSalt and the key-stretching parameters. "b" and "srpB" are retained for the subsequent /session/auth/finish call. '''Note that it is critical that the "b" integer remain secret on the server.''' The server also allocates a random loginSRPToken to connect the two calls.


[[File:PICL-IdPAuth-SRP-Server.png|server--side SRP]]
[[File:PICL-IdPAuth-SRP-Server.png|server--side SRP]]


Later, getToken2() will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.
Later, /session/auth/start will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.


=== SRP Client Calculation ===
=== SRP Client Calculation ===


While the client is waiting for the response to getToken1(), it begins its key-stretching calculations. Everything else must wait until the response to getToken1() arrives, which includes the key-stretching parameters (which are retroactively confirmed), srpSalt, and the server's generated srpB value.
While the client is waiting for the response to /session/auth/start, it begins its key-stretching calculations. Everything else must wait until the response to /session/auth/start arrives, which includes the key-stretching parameters (which are retroactively confirmed), srpSalt, and the server's generated srpB value.


[[File:PICL-IdPAuth-SRP-Client.png|client-side SRP]]
[[File:PICL-IdPAuth-SRP-Client.png|client-side SRP]]
Line 112: Line 118:
('''Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.''')
('''Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.''')


To safely tell if the "S" values match, both client and server combine srpA, srpB, and their (independently) generated "S" strings to form a string named "M1". The client sends M1 (along with srpA) in the getToken2() message. The server compares the client's copy of M1 against its own. If they match, the client knew the password and the server can safely respond with the encrypted account data. If they do not match, the client (or a man-in-the-middle attacker) did not know the password, and the client should increment a counter that can trigger defenses against online guessing attacks. The server must then return an error to the client, and '''not''' use or reveal srpK (or the correct M1) in any way.
To safely tell if the "S" values match, both client and server combine srpA, srpB, and their (independently) generated "S" strings to form a string named "M1". The client sends M1 (along with srpA) in the /session/auth/finish message. The server compares the client's copy of M1 against its own. If they match, the client knew the password and the server can safely respond with the encrypted account data. If they do not match, the client (or a man-in-the-middle attacker) did not know the password, and the client should increment a counter that can trigger defenses against online guessing attacks. The server must then return an error to the client, and '''not''' use or reveal srpK (or the correct M1) in any way.


Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).
Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).
Line 122: Line 128:
There are several places in SRP where integers (usually in the range 1..N-1) are converted into bytestrings, either for transmission over a wire, or to be passed into a hash function. The SRP spec is somewhat ambiguous about padding here: if the integer happens to be less than 2^2040, the simplest toString() approach will yield a '''255''' byte string, not a 256 byte string. PiCL consistently uses padding, so compatible implementations must prepend one or more NUL bytes to these short strings before transmission or hashing. The examples above were brute-forced to ensure that "srpVerifier", "srpA", "srpB", and "S" all wind up with leading zeros, to exercise the padding code in compatible implementations. If you are having problems getting your code to match these results, add some assertions to test that the stringified integers being put into hashes are exactly 256 bytes long.
There are several places in SRP where integers (usually in the range 1..N-1) are converted into bytestrings, either for transmission over a wire, or to be passed into a hash function. The SRP spec is somewhat ambiguous about padding here: if the integer happens to be less than 2^2040, the simplest toString() approach will yield a '''255''' byte string, not a 256 byte string. PiCL consistently uses padding, so compatible implementations must prepend one or more NUL bytes to these short strings before transmission or hashing. The examples above were brute-forced to ensure that "srpVerifier", "srpA", "srpB", and "S" all wind up with leading zeros, to exercise the padding code in compatible implementations. If you are having problems getting your code to match these results, add some assertions to test that the stringified integers being put into hashes are exactly 256 bytes long.


The client does its entire SRP calculation in a single step, after receiving the server's "B" value. It creates its "A" value, computes the shared secret S, and the proof-of-knowledge M1. It sends both "A" and "M1" in the same message (getToken2).
The client does its entire SRP calculation in a single step, after receiving the server's "B" value. It creates its "A" value, computes the shared secret S, and the proof-of-knowledge M1. It sends both "A" and "M1" in the same message (/session/auth/finish).


The server receives "A" in getToken2, computes the shared secret "S", computes M1, checks that the client's M1 is correct, then derives the shared session key K. It then allocates a token (of the requested type) and encrypts kA+wrap(kB)+token as described below, returning the encrypted/MACed bundle in the response to getToken2.
The server receives "A" in /session/auth/finish, computes the shared secret "S", computes M1, checks that the client's M1 is correct, then derives the shared session key K. It then allocates a token (of the requested type) and encrypts keyFetchToken+sessionToken as described below, returning the encrypted/MACed bundle in the response to /session/auth/finish.


Outstanding crypto questions:
Outstanding crypto questions:
Line 131: Line 137:
* The original SRP papers defined M1=H(A+B+S) as we use here, but other implementation (in particular RFC2945) uses a curious construct that includes the username, the salt, and an odd XOR combination of N and g. We need to decide what to use for M1.
* The original SRP papers defined M1=H(A+B+S) as we use here, but other implementation (in particular RFC2945) uses a curious construct that includes the username, the salt, and an odd XOR combination of N and g. We need to decide what to use for M1.


== getToken2 ==
== /session/auth/finish ==


This method has two flavors, one for obtaining "signing tokens", the other for getting "reset tokens". TBD: either we'll have two different method names / API endpoints (getToken2Sign and getToken2Reset), or we'll pass an argument to a single "getToken2" method that indicates either "sign" or "reset". (using different endpoints would make it easier to monitor server load).
The client-side SRP calculation results in two values that are sent to the server in the /session/auth/finish message: "srpA" and "srpM1". "A" is the client's contribution to the SRP protocol. "M1" is an output of this protocol, and proves (to the server) that this client knew the right password.
 
The client-side SRP calculation results in two values that are sent to the server in the "getToken2()" message: "srpA" and "srpM1". "A" is the client's contribution to the SRP protocol. "M1" is an output of this protocol, and proves (to the server) that this client knew the right password.


The server feeds "A" into its own SRP calculation and derives (hopefully) the same "S" value as the client did. It can then compute its own copy of M1 and see if it matches. If not, the client (or a man-in-the-middle) did not get the right password, and the server will return an error and increment it's "somebody is trying to guess passwords" counter (which will be used to trigger defenses against online guessing attacks). If it does match, then both sides can derive the same "K" session key.
The server feeds "A" into its own SRP calculation and derives (hopefully) the same "S" value as the client did. It can then compute its own copy of M1 and see if it matches. If not, the client (or a man-in-the-middle) did not get the right password, and the server will return an error and increment it's "somebody is trying to guess passwords" counter (which will be used to trigger defenses against online guessing attacks). If it does match, then both sides can derive the same "K" session key.


The server then allocates a token for this device, and encrypts kA/wrap(kB)/token with the session key. The server returns a success message with the encrypted bundle.
The server then allocates two tokens for this device: keyFetchToken and sessionToken. It encrypts the tokens with the session key. The server returns a success message with the encrypted bundle.


Future variants (e.g. to fetch a third kind of token) might put additional values in the response to getToken2.
Both tokens have an associated tokenID, described below. The server needs to maintain a table that maps the tokenID to the token itself, so it can derive other values from the token later. The tokens are also associated with a specific account, so later API requests do not specify an email address or account ID.


== Decrypting the getToken2 Response ==
== Decrypting the /session/auth/finish Response ==


The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
Line 149: Line 153:
[[File:PICL-IdPAuth-encrypt-session.png|Decrypting the sessionToken and keyFetchToken]]
[[File:PICL-IdPAuth-encrypt-session.png|Decrypting the sessionToken and keyFetchToken]]


The respXORkey is used to encrypt the concatenated kA/wrap(kB)/token string, by simply XORing the two. This ciphertext is then protected by a MAC, using HMAC-SHA256, keyed by respHMACkey. The MAC is appended to the ciphertext, and the whole bundle is returned to the client.
The respXORkey is used to encrypt the concatenated keyFetchToken/sessionToken string, by simply XORing the two. This ciphertext is then protected by a MAC, using HMAC-SHA256, keyed by respHMACkey. The MAC is appended to the ciphertext, and the whole bundle is returned to the client.
 
The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate keyFetchToken and sessionToken values.
 
= Obtaining keys kA and kB =
 
Clients which have successfully proven knowledge of the account password will receive a keyFetchToken. This single-use token allows the client to retrieve kA and wrap(kB). The token is used to derive several values:
 
* tokenID
* reqHMACkey
* respHMACkey
* respXORkey
 
 
The client uses tokenID and reqHMACkey for a HAWK  (https://github.com/hueniverse/hawk/) request to the "GET /account/keys" API, using tokenID as "credentials.id" and reqHMACkey as "credentials.key". The server uses tokenID to look up the corresponding token, then derives reqHMACkey to validate the request. The server then pulls kA and wrap(kB) from the account table, concatenates them, encrypts the pair by XORing it with the derived respXORkey, and attaches a MAC generated with respHMACkey.


[[File:PICL-IdPAuth-keys-server.png|keyFetchToken: server encrypts keys]]
[[File:PICL-IdPAuth-keys-server.png|keyFetchToken: server encrypts keys]]
The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate kA and wrap(kB) values.


[[File:PICL-IdPAuth-keys-client.png|keyFetchToken: client decrypts keys]]
[[File:PICL-IdPAuth-keys-client.png|keyFetchToken: client decrypts keys]]


The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate kA/wrap(kB)/token values.
Finally, the server-provided wrap(kB) value is simply XORed with the password-derived wrapKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).
 
Since the kA/wrap(kB)/signToken response is so similar to the kA/wrap(kB)/resetToken response, the same code can be used to check+decrypt both. However remember that the respXORkey/respHMACkey is derived differently for each (using different "context" values).


== Unwrapping kB ==
"kA" and "kB" enable the browser to encrypt/decrypt synchronized data records. They will be used to derive separate encryption and HMAC keys for each data collection (bookmarks, form-fill data, saved-password, open-tabs, etc). This will allow the user to share some data, but not everything, with a third party. The client may intentionally forget kA and kB (only retaining the derived keys) to reduce the power available to someone who steals their device.


The server-provided wrap(kB) value is simply XORed with the password-derived wrapKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).
Note that /account/keys will not succeed until the account's email address has been verified. Also note that each keyFetchToken is single-use.


= Signing Certificates =
= Signing Certificates =


The Sign Token is used to derive two values:
The sessionToken is used to derive two values:


* tokenID
* tokenID
Line 173: Line 191:
[[File:PICL-IdPAuth-use-session.png|Using the sessionToken, signing certificates]]
[[File:PICL-IdPAuth-use-session.png|Using the sessionToken, signing certificates]]


The requestHMACkey is used in a HAWK (https://github.com/hueniverse/hawk/) request to provide integrity over the "signCertificate" request. It is used as credentials.key, while tokenID is used as credentials.id . HAWK includes the URL and the HTTP method ("POST") in the HMAC-protected data, and will optionally include the HTTP request body (payload) if requested.
The requestHMACkey is used in a HAWK request to provide integrity over many APIs, including /certificate/sign. requestHMACkey is used as credentials.key, while tokenID is used as credentials.id . HAWK includes the URL and the HTTP method ("POST") in the HMAC-protected data, and will optionally include the HTTP request body (payload) if requested.
 
For /certificate/sign, it is critical to enable payload verification by setting options.payload=true (on both client and server). Otherwise a man-in-the-middle could submit their own public key, get it signed, and then delete the user's data on the storage servers.
 
Most keyserver APIs require a HAWK-protected request that uses the sessionToken. In addition, most (but not all) require that the account be in the "verified" state:


For signCertificate(), it is critical to enable payload verification by setting options.payload=true (on both client and server). Otherwise a man-in-the-middle could submit their own public key, get it signed, and then delete the user's data on the storage servers.
* GET /session/status
* POST /session/destroy
* POST /certificate/sign
* GET /account/recovery_methods (does not require verification)
* POST /account/recovery_methods/send_code
* GET /account/devices
* POST /password/change/auth/start
* POST /password/change/auth/finish


= Resetting The Account =


The account may be reset in two circumstances: when the user changes their password, and when the user forgets their password.


HAWK provides one thing: integrity/authentication for the request contents (URL, method, and optionally the body). It does not provide confidentiality of the request, or integrity of the response, or confidentiality of the response.
== Changing the Password ==


For signCertificate(), we do not need request confidentiality or response confidentiality, since the client's pubkey and the resulting certificate will both be exposed over a similar SSL connection to the storage server later. And it is sufficient to rely on the response integrity provided by SSL, since the client can verify the returned certificate for itself.
When the user wishes to change their password, we use an SRP-based protocol to protect both old and new passwords. This puts the old password through the same code as /session/auth/start+finish, but uses a different pair of API endpoints: /password/change/auth/start and /password/change/auth/finish . The value returned by auth/finish contains an "accountResetToken" instead of the sessionToken, and the response is protected with a different set of derived keys.


= Changing the Password =
This API is only used when the user knows their old password: if they have forgotten the password, use the "/password/forgot" API below.


[[File:PICL-IdPAuth-encrypt-passwordChange.png|Server encrypts passwordChange response]]
[[File:PICL-IdPAuth-encrypt-passwordChange.png|Server encrypts passwordChange response]]


= Resetting the Account =
The accountResetToken will be used below to set the new password safely. This token proves that the user has provided the correct account password recently. When the account is reset, all active sessions and tokens will be cancelled (disconnecting all devices from the account). The client should immediately establish a new session as described above.


resetAccount() needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.
Note that this API requires a sessionToken, so the client must have previously logged in.
 
== Handling a Forgotten Password ==
 
When the user has forgotten their password, they can use one of their "recovery methods" to obtain an accountResetToken. For now, this means we send a random code to the email address associated with their account. The user must copy this code from the email into their client, whereupon the client will get an accountResetToken that can be delivered to the API below.
 
Note that, since the forgotten-password client never learns kB, any class-B data will be lost. This is necessary to protect class-B data from attackers who can read the users's email but do not know the account password (including those who compromise the IdP and the keyserver itself). When using /account/reset below, they will supply a new random wrap(kB).
 
The /password/forgot/send_code API is used to ask the server to send a recovery code. This takes a recovery method, which for now is just an email address. This API is unauthenticated (after all, the user who has forgotten their password knows nothing but their email address). The server marks the corresponding account as "pending recovery", allocates a random forgotPasswordToken for the account, creates a recovery code, and sends the code (with instructions) via email. The API returns forgotPasswordToken to the client.
 
The user must copy the recovery code into the same browser where they started the process. The client then submits the code to /password/forgot/verify_code along with the forgotPasswordToken they received. If they match, the server allocates a accountResetToken and returns it to the client. If they do not match, the server increments a counter (which is used to decide if an online guessing attack is happening).
 
forgotPasswordToken can be used three times before it is exhausted. If the user guesses incorrectly this often, the client must call send_code again to get a new token and code. Each account has at most one token+code active at a time.
 
The recovery code is initially a random 8-digit decimal number. If an attacker tries to sign in as someone else, hits the "forgot my password" button, then submits a guess to /password/forgot/verify_code, they will have a 1-in-100-million chance of success. If the server detects too many wrong guesses, it should increase the length of new codes.
 
The exact thresholds are TBD, but a nominal goal is to keep the chances of any attack succeeding to below 1-in-a-million per year. To achieve this, we can tolerate 100 verify_code failures in a single year before we must increase the length of the code.
 
== Using accountResetToken ==
 
An accountResetToken, obtained through either of the methods above, is used to invoke the /account/reset API. If the request is accepted, the server replaces the account password with a new one, updates the wrap(kB) value, cancels all active sessions and tokens (disconnecting all devices from the account), and sends a "your password has been changed" email to the user.
 
If the client knew the old password, it can supply a new wrap(kB) value that will yield the same kB key as before, so no data will be lost. If the client did *not* know the old password, then it will supply a wrap(kB) of all-zeros, which tells the server to generate a new random wrap(kB) (just like during account creation), which means the class-B data will be lost.
 
The client puts their new password through the same stretching procedure as described in the new-account section above, resulting in a new srpVerifier and unwrapBKey. If they knew the old kB, they XOR it with the new unwrapBKey to obtain a new wrap(kB).
 
/account/reset needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.


So the single-use resetToken is used to derive three values:
So the single-use resetToken is used to derive three values:
Line 200: Line 257:
[[File:PICL-IdPAuth-encrypt-resetAccount.png|Client encrypts resetAccount request]]
[[File:PICL-IdPAuth-encrypt-resetAccount.png|Client encrypts resetAccount request]]


The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values.
The request data will contain wrap(kB) and the new SRP verifier, concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, both pieces are fixed-length. We generate enough reqXORkey bytes to cover both values.


The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).
The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).


Clients might use resetAccount for two reasons:
The client submits other values in the same request:
 
* stretchParams
* mainSalt
* srpSalt


* 1: Changing their password (i.e. they know the old one). In this case, the resetToken is acquired from getToken("reset"), and they know both kA and kB.
* 2: resetting an account (i.e. they forgot the old password). Here, resetToken was acquired by proving control over the account email address (through a mechanism not described in this protocol). The client does not know kA or kB.


These values do not require confidentiality, so are not included in the encrypted bundle. They are still protected by the HAWK integrity check. Note that the server should assert that both salts are different than the previously stored values.


After using resetAccount, clients should immediately perform the getToken(sign) protocol. If the old password was forgotten, this is necessary to fetch kA. In either case, a new signToken is required, since old signTokens are revoked by resetAccount. Clients should retain the srpPassword value during this process to avoid needing to run the lengthy key-stretching routine a second time.
After using /account/reset, clients should immediately perform the login protocol from above. If the old password was forgotten, this is necessary to fetch kA. In either case, a new sessionToken is required, since old sessions and tokens are revoked by /account/reset. Clients should retain the new srpPassword value during this process to avoid needing to run the lengthy key-stretching routine a second time.


= Crypto Notes =
= Crypto Notes =
Line 243: Line 303:


We use scrypt without a per-user salt. This is safe because the "password" input to scrypt is already diversified by the user's email address. The intention is to allow the scrypt-helper to run on anonymous data, so that an attacker who compromises this helper cannot easily learn the email addresses of the partially-stretched K1 values that it is receiving, confounding their attack.
We use scrypt without a per-user salt. This is safe because the "password" input to scrypt is already diversified by the user's email address. The intention is to allow the scrypt-helper to run on anonymous data, so that an attacker who compromises this helper cannot easily learn the email addresses of the partially-stretched K1 values that it is receiving, confounding their attack.
HAWK provides one thing: integrity/authentication for the request contents (URL, method, and optionally the body). It does not provide confidentiality of the request, or integrity of the response, or confidentiality of the response.
For /certificate/sign, we do not need request confidentiality or response confidentiality, since the client's pubkey and the resulting certificate will both be exposed over a similar SSL connection to the storage server later. And it is sufficient to rely on the response integrity provided by SSL, since the client can verify the returned certificate for itself. For the other keyserver APIs protected by HAWK, these properties are either unnecessary, or are provided by additional mechanisms.


= Proof-Of-Work =
= Proof-Of-Work =
Confirmed users
471

edits