Changes

Jump to: navigation, search

Identity/AttachedServices/KeyServerProtocol

7,673 bytes added, 04:33, 12 July 2013
update to new protocol (with HTTP endpoint names and keyFetchToken)
= PiCL Key Server / IdP Protocol =
NOTE: This specification is under active development (1011-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).
* randomly choose a 32-byte srpSalt (unique, but not secret)
* create srpVerifier from srpPW and srpSalt (as described below)
* deliver (email, stretchParams, mainSalt, srpParams, srpSalt) to the keyserver's createAccount() "POST /account/create" API
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.
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 "getSignToken" login protocol to transform an email+password pair into (kA, kBkeyFetchToken, signTokensessionToken). "kA" and "kB" enable the browser to encrypt/decrypt synchronized data records, while "signToken" is These tokens will be used to convince in the storage server next section to accept read obtain encryption keys and write requests for those recordssigned 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.
[[File:PICL-IdPAuth-bigpix.png|IdP Auth Big Picture]]
This same protocol is used, with slightly different methods and constants, to obtain the "resetTokenaccountResetToken". 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 /session/auth/start and getToken2/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/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 session-id loginSRPToken that is used to associate this request with the subsequent getToken2 /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() /session/auth/start may also require a proof-of-work string, described below.
== Client-Side Key Stretching ==
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() 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.
=== SRP Server-side Sign-In Flow ===
When the user connects a new device to their account, they use the getToken1() /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 getToken2() /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]]
Later, getToken2() /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 ===
While the client is waiting for the response to getToken1()/session/auth/start, it begins its key-stretching calculations. Everything else must wait until the response to getToken1() /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]]
('''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() /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).
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/session/auth/finish).
The server receives "A" in getToken2/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 kA+wrap(kB)keyFetchToken+token sessionToken as described below, returning the encrypted/MACed bundle in the response to getToken2/session/auth/finish.
Outstanding crypto questions:
* 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 "getToken2()" /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 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 two tokens for this device, : keyFetchToken and sessionToken. It encrypts kA/wrap(kB)/token the tokens with the session key. The server returns a success message with the encrypted bundle.
Future variants (e.gBoth tokens have an associated tokenID, described below. The server needs to fetch maintain a third kind of table that maps the tokenID to the token) might put additional itself, so it can derive other values in from the response to getToken2token 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 /session/auth/finish Response ==
The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
[[File:PICL-IdPAuth-encrypt-session.png|Decrypting the sessionToken and keyFetchToken]]
The respXORkey is used to encrypt the concatenated kAkeyFetchToken/wrap(kB)/token 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]]
 
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]]
The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkeyFinally, then splits it into the separate kA/server-provided wrap(kB)/token values. Since value is simply XORed with the kA/wrappassword-derived wrapKey (kBboth are 32-byte strings)/signToken response is so similar to the kA/wrap(obtain kB)/resetToken response, the same code can be used to check+decrypt both. However remember that the respXORkey/respHMACkey There is derived differently for each no MAC on wrap(using different "context" valueskB).
== Unwrapping "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 Note that /account/keys will not succeed until the password-derived wrapKey (both are 32-byte strings) to obtain kBaccount's email address has been verified. There Also note that each keyFetchToken is no MAC on wrap(kB)single-use.
= Signing Certificates =
The Sign Token sessionToken is used to derive two values:
* tokenID
[[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" requestmany APIs, including /certificate/sign. It 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* GET /session/status* POST /session/destroy* POST /certificate/sign* GET /account/recovery_methods (), it is critical to enable payload does not require 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.* 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 == Changing 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. Password ==
For signCertificate()When the user wishes to change their password, we do not need request confidentiality or response confidentiality, since use an SRP-based protocol to protect both old and new passwords. This puts the client's pubkey and old password through the resulting certificate will both be exposed over same code as /session/auth/start+finish, but uses a similar SSL connection to the storage server laterdifferent pair of API endpoints: /password/change/auth/start and /password/change/auth/finish . And it is sufficient to rely on The value returned by auth/finish contains an "accountResetToken" instead of the response integrity provided by SSLsessionToken, since and the client can verify the returned certificate for itselfresponse is protected with a different set of derived keys.
= Changing This API is only used when the Password =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]]
= Resetting The accountResetToken will be used below to set the Account =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.
resetAccountNote 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:
[[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 both pieces are fixed-length. We generate enough reqXORkey bytes to cover all four 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).
Clients might use resetAccount for two reasonsThe 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/account/reset, clients should immediately perform the getToken(sign) login protocolfrom above. If the old password was forgotten, this is necessary to fetch kA. In either case, a new signToken sessionToken is required, since old signTokens sessions and tokens are revoked by resetAccount/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 =
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 =
Confirm
471
edits

Navigation menu