Identity/AttachedServices/KeyServerProtocol: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
m (→‎Creating The Account: srpV is sent to /account/create too)
 
(71 intermediate revisions by 3 users not shown)
Line 1: Line 1:
= PiCL Key Server / IdP Protocol =
= PiCL Key Server / IdP Protocol =


NOTE: This specification is under active development (27-Jun-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 slowly converging on stability (31-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 includes a demonstration client (node.js CLI).
 
Note that all messages are delivered over an HTTPS connection. The client browser may also implement cert-pinning to improve on the certificate validation process. The protections described below are in addition to those provided by TLS.


Remaining TODO items:
Remaining TODO items:


* define client-side key-stretching (PBKDF2+scrypt+PBKDF2)
* decide on client-side key-stretching parameters: http://keywrapping.appspot.com can help
* finalize SRP questions (definition of M1, generation of a/b)
* finalize SRP questions (definition of M1, generation of a/b)
* finalize how getToken2() declares whether a signToken or a resetToken is desired
* provide test vectors for decrypting a resetToken
* finalize proof-of-work/DoS-prevention details
* finalize proof-of-work/DoS-prevention details
* define how wrap(kB) is unwrapped to get kB
* decide how to rate-limit account-creation calls
* confirm this is actually implementable inside Firefox (especially w.r.t. NSS)
* confirm this is actually implementable inside Firefox (especially w.r.t. NSS and Android/Java crypto)
 
= Creating The Account =
 
The first act performed by a user is to create the account. They enter email+password into their browser, which then does the following steps:
 
* decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement). For now we used a fixed {firstPBKDF:20000, scrypt: {N:65536, r:8, p:1}, secondPBKDF:20000}.
* randomly choose a 32-byte mainSalt (this should be unique, but is not secret)
* choose the SRP group parameters (fixed: use the 2048-bit group described below)
* perform key-stretching (as described below), derive stretchedPW and srpPW
* 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, srpVerifier) to the keyserver's "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.
 
== Email Verification ==
 
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 most 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 static 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: Obtaining the authToken =
 
To connect a browser to an existing account, we use the following login protocol to transform an email+password pair into a single-use authToken. We will use this in the next section to create a session, from which we can 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 the authToken.
 
[[File:PICL-IdPAuth-auth-start.png|IdP Auth Protocol]]
 
This authToken can be used (once) to do one of the following:
 
* /session/create: obtain a sessionToken (and keyFetchToken), which enables storage server access
* /password/change: obtain an accountResetToken, to safely reset the account password
* /account/delete: to delete the entire account
 
The protocol is designed to enable parallelism between key-stretching and the initial network messages, to reduce the time it takes to connect a browser to the account. In total, the browser requires five messages in four roundtrips (1: /auth/start, 2: /auth/finish, 3: /session/create, 4: /account/keys and /certificate/sign in parallel) before it is ready to talk to the storage server.
 
== /auth/start ==
 
As soon as the user finishes typing in the email address, the client should send it in the "/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 /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, /auth/start may also require a proof-of-work string, described below.
 
== Client-Side Key Stretching ==
 
"Key Stretching" is the practice of running a password through a computationally-expensive one-way function before using it for encryption or authentication. The goal is to make brute-force dictionary attacks more expensive, by raising the cost of testing each guess.
 
To protect the user's class-B data against compromises of our keyserver, we perform this key stretching on the client, rather than on the server. We use the memory-hard "scrypt" function (pronounced "ess-crypt") for this purpose, as motivated by the attacker-cost studies in [[Identity/CryptoIdeas/01-PBKDF-scrypt]]. Slower (low-RAM) devices can still participate by outsourcing the scrypt step to an "scrypt helper". To provide at least minimal protection against a compromise of this helper, we sandwich the scrypt step between two PBKDF2 steps. The complete protocol looks like this:
 
[[File:PICL-IdPAuth-stretch-KDF.png|Stretching KDF]]
 
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 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.
 
[[File:PICL-IdPAuth-main-KDF.png|masterKey KDF]]
 
== SRP Protocol Details ==
 
The PiCL client uses the SRP protocol (http://srp.stanford.edu/) to prove that it knows the (stretched) account password without revealing the actual password (or information enabling a brute-force attack) to the server or any eavesdroppers.
 
SRP is somewhat underspecified. We use SRP-6a, with SHA256 as the hash, and the 2048-bit modulus defined in RFC 5054 Appendix A. We consistently zero-pad all string values to 256 bytes (2048 bits), and use H(A+B+S) as the key-confirmation message "M1". These details, plus the SRP design papers and RFCs 2945 and 5054, should be enough to build a compatible implementation. The diagrams below and the test vectors at the end of this page can be used to verify compatibility.
 
The server should use Jed's SRP module from https://github.com/jedp/node-srp . The client might use SJCL (http://crypto.stanford.edu/sjcl/) or native code (NSS).
 
The basic idea is that we're using the main-KDF output "srpPW" as a password for the SRP calculation. We use the email address for "identity", and a server-provided string for "salt". (We could safely leave them blank, since equivalent values are already folded into the password-stretching process, but it's less confusing to follow the SRP spec and fill them in with something sensible).
 
Note that SRP-6a uses a "k" value which basically encodes the group being used ("N" and "g"). Since all PICL accounts use the same 2048-bit group, they will all use the same "k" value (not to be confused with the per-session shared-secret "K" key that emerges from the protocol). This group's "k" integer is (as a base-10 number):
2590038599070950300691544216303772122846747035652616593381637186118123578112
 
=== SRP Verifier Calculation ===
 
When the client first creates the account, it must combine the account email address, the stretched password (srpPW), and a randomly-generated srpSalt, to compute the srpVerifier. The server will use this verifier later, to check whether or not the client really knows the password.
 
If the server is compromised and an attacker learns the srpVerifier for a given account, they cannot use this to directly log in (the verifier is not "password-equivalent"), but it does allow them to perform an offline brute-force attack against the user's password. In this respect, it is similar to a traditional hashed password. We make these attacks somewhat more expensive by performing the client-side stretching described above, instead of using the raw user password in the SRP calculation.
 
[[File:PICL-IdPAuth-SRP-Verifier.png|client-side SRP Verifier calculation]]
 
Test vectors are provided at the end of this page. The protocol requires that several values ("a", "b", "srpSalt") are chosen randomly, but for illustrative purposes, the test vectors use carefully-crafted non-random values. For the user's sake, please ensure implementations use proper random values instead of simply copying the test vectors.
 
The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.
 
=== SRP Server-side Sign-In Flow ===
 
When the user connects a new device to their account, they use the /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 /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, /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 /auth/start, it begins its key-stretching calculations. Everything else must wait until the response to /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]]
 
Once the client knows srpSalt, it computes the same "x" integer as it did in the middle of the srpVerifier calculation. It also converts srpB into an integer named "B". Then it creates a random "a" integer, uses it to compute the string "srpA", then combines srpA with the server's srpB to compute the "u" integer. It then combines the static "k", the password-derived "x", the combined "u", and the server's "B", together with some magic math, to derive the "S" integer. If everything went well, the client will compute the same "S" value as the server did. If not (the password was wrong, or the client is talking to a fake server that doesn't really know srpVerifier), then the two "S" values will not match.
 
('''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 /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).
 
=== SRP Notes ===
 
The SRP "g" (generator) and "N" (prime modulus) should use the 2048-bit value from RFC 5054 Appendix A. Clients should not accept arbitrary g/N values (to protect against small primes, non-primes, and non-generators). In the future we might allow alternate parameter sets, in which case the server's first response should indicate which parameter set to use.
 
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 test vectors below 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 (/auth/finish).
 
The server receives "A" in /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 /auth/finish.
 
On the server, it is critical to reject an "A" value that is 0, or some other multiple of N. If the server does not check this, anybody can trivially sign in to any account without knowing the password. Likewise, it is critical for the client to reject a "B" value where B%N==0. If the client does not check this, the server (or an attacker pretending to be the server) will get a value that can be used in an offline brute-force search for the user's password.
 
Outstanding crypto questions:
 
* How exactly should the "a" and "b" integers be generated? The issue is of how much bias SRP can tolerate. Ideally these integers are uniformly distributed from 1 to N-1 (inclusive). The only way to obtain a purely uniform distribution from a source of random bytes is try-try-again: pick a (integral-number-of-bytes) number, compare it to the desired range, try again if it falls outside the range. If you get unlucky, this can take a lot of guesses, depending upon how close the range is to a power of two. ECDSA implementations tend to pick a number twice as long as the modulus and then modulo it down (rand(2^4096) % N), which yields a tiny fraction of a bit of bias. The cheaper approach is to do the same with a number of equal length (rand(2^2048) % N), which imposes more bias. Some SRP implementations appear to be satisfied by rand(2^256). We need more review here.
* 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.
 
== /auth/finish ==
 
The client-side SRP calculation results in two values that are sent to the server in the /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 single-use 32-byte random token named "authToken". It encrypts the token with the session key, and returns a success message with the encrypted bundle.
 
All 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 /auth/finish Response ==
 
The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
 
[[File:PICL-IdPAuth-encrypt-authToken.png|Decrypting the authToken]]
 
The respXORkey is used to encrypt the authToken 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 returns the authToken value.
 
= After Login: Using the authToken =
 
After the authToken is acquired, the client can create a session and fetch the encryption keys. The high-level flow looks like this:
 
[[File:PICL-IdPAuth-session-start.png|Using the authToken]]
 
= Creating a Session =
 
For login, the single-use authToken is spent on a call to /session/create .
This allocates two new (random 32-byte) tokens: a long-lived "sessionToken", and a single-use "keyFetchToken". The /session/create call returns an encrypted bundle containing the two tokens.
 
[[File:PICL-IdPAuth-encrypt-sessionToken.png|Decrypting the sessionToken and keyFetchToken]]
 
For calls which accept an authToken, the client uses authToken to derive three values:
 
* tokenID
* reqHMACkey
* requestKey
 
 
The client uses tokenID and reqHMACkey for a HAWK (https://github.com/hueniverse/hawk/) request to the "POST /session/create" 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.
 
Each authToken-using call then derives additional API-specific values from requestKey. /session/create uses two derived values:
 
* respHMACkey
* respXORkey
 
 
When the server receives a valid /session/create request, it allocates sessionToken and keyFetchToken, concatenates them, encrypts the pair by XORing it with the derived respXORkey, and attaches a MAC generated with respHMACkey. The encrypted MACed 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.
 
Each authToken is single-use: once a successful request has been made with it, the authToken and its corresponding ID is removed from the server's memory, and subsequent attempts to use it will return a "no such token" error. The token is consumed even if the request fails (e.g. the MAC did not match).
 
The authToken can be used (once) by multiple APIs. The server only needs to maintain one table mapping tokenID to authToken, because all these APIs share the same method of deriving a tokenID and reqHMACkey from the authToken.
 
When a HAWK request appears, it should look up the tokenID in this table, retrieve the authToken, recompute reqHMACkey and requestKey, validate the request, delete the table entry, then hand the HTTP request and requestKey to the specific API handler. That handler should derive the API-specific values (respHMACkey, respXORkey, etc) to process the request or construct the response.
 
The server can support multiple sessions per account (typically one per client device, plus perhaps others for account-management portals). There can also be multiple outstanding keyFetchTokens. The sessionToken lasts forever (until revoked by a password change or explicit revocation command), and can be used an unlimited number of times. The keyFetchToken expires after 60 seconds, and is single-use.
 
= Obtaining keys kA and kB =
 
Clients which have exchanged an authToken for either a sessionToken or an accountResetToken will also receive a keyFetchToken. This single-use token allows the client to retrieve kA and wrap(kB), which enables the client to encrypt and decrypt browser data (bookmarks, open-tabs, etc) correctly. As above, the keyFetchToken is used to derive tokenID, reqHMACkey, respHMACkey, and respXORkey, which are used in a HAWK request to the "GET /account/keys" API.
 
The server 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]]
 
Finally, the server-provided wrap(kB) value is simply XORed with the password-derived unwrapBKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).
 
[[File:PICL-IdPAuth-key-unwrap.png|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.
 
Note that /account/keys will not succeed until the account's email address has been verified. Also note that each keyFetchToken is single-use and short-lived. The token is consumed even if the request fails (e.g. the MAC does not match).
 
= Signing Certificates =
 
The sessionToken is used to derive two values:
 
* tokenID
* request HMAC key
 
 
[[File:PICL-IdPAuth-use-session.png|Using the sessionToken, signing certificates]]
 
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.
 
The following keyserver APIs require a HAWK-protected request that uses the sessionToken. In addition, some require that the account be in the "verified" state:
 
* GET /account/devices
* POST /session/destroy
* GET /recovery_email/status
* POST /recovery_email/resend_code
* POST /certificate/sign (requires "verified" account)
 
= Resetting The Account =
 
The account may be reset in two circumstances: when the user changes their password, and when the user forgets their password. In both cases, the client first obtains an "accountResetToken". This token is then used to change the SRP Verifier and either reset or replace the wrap(kB) value.
 
== Changing the Password ==
 
When the user wishes to change their password (i.e. they still know the old password), they first use the /auth/start+/auth/finish SRP protocol safely obtain an "authToken". They they use the "/password/change/start" API to exchange the authToken for an accountResetToken and a keyFetchToken.
 
[[File:PICL-IdPAuth-encrypt-passwordChange.png|Server encrypts passwordChange response]]
 
The accountResetToken will be used below to set the new password safely. The keyFetchToken should be used first, to obtain kB, so the subsequent account reset can replace wrap(kB) with a new value. This allows the password-changing client to retain their class-B data.
 
Requiring an authToken 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.
 
This API is only used when the user knows their old password: if they have forgotten the password, use the "/password/forgot" APIs below.
 
== 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 set wrap(kB) to a string of all zeros, which means the server should generate a new random wrap(kB) (just as it does during account creation).
 
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. Another defensive technique is to require that users click an email link before being given the code: the server is told when the link is clicked, so the code will not be enabled until the email has been read. It remains to be seen whether this will be sufficient.
 
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).


The test vectors included on this page were produced by the python code in https://github.com/warner/picl-spec-crypto . The diagrams may lag behind the latest version of that code.
/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.


= Email+Password -> SignToken/ResetToken =
So the single-use resetToken is used to derive three values:


The first interaction with the keyserver takes an email+password pair and receives back (kA, wrap(kB), token). This starts by using key-stretching to transform the email+password into a "masterKey", 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 (or resetToken). The masterKey is also used to derive the key that will decrypt wrap(kB) into the actual kB value.
* tokenID
* request HMAC key
* request XOR key


[[File:PICL-IdPAuth-bigpix.png|IdP Auth Big Picture]]


This same protocol is used, with slightly different methods and constants, to obtain the "resetToken".
[[File:PICL-IdPAuth-encrypt-resetAccount.png|Client encrypts resetAccount request]]


The protocol is optimized to minimize round-trips and to enable parallelism. As a result, the two messages it sends (getToken1 and getToken2) each perform multiple jobs.
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.


== getToken1 ==
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).


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.
The client submits other values in the same request:


== Proof-Of-Work ==
* stretchParams
* mainKDFSalt
* srpSalt
 
 
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 /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.
 
= Deleting The Account =
 
When the user wishes to completely delete their account, the browser needs to perform two actions:
 
* contact the storage servers and delete all records and collections
* contact the keyserver and delete the account information
 
The user should be prompted for their password as confirmation (i.e. a browser in the normal attached-and-synchronizing state should not be able to erase the account information: it must acquire a new authToken first).
 
The device then obtains an authToken as described above, then spends it on a HAWK-protected request to the /account/destroy endpoint. This request contains no body and returns only a success code.
 
[[File:PICL-IdPAuth-deleteAccount.png|Deleting the Account]]
 
= Crypto Notes =
 
Strong entropy is needed in the following places:
 
* (client) creation of private "a" value inside SRP
* (server) initial creation of kA and wrap(kB)
* (server) creation of private "B" value inside SRP
* (server) creation of signToken and resetToken
 
 
On the server, code should get entropy from /dev/urandom via a function that uses it, like "crypto.randomBytes()" in node.js or "os.urandom()" in python. On the client, code should combine local entropy with some fetched from the keyserver via getEntropy(), to guard against failures in the local entropy pool. Something like HKDF(IKM=localEntropy+remoteEntropy, salt="", info=KW("mergeEntropy")).
 
An HKDF-based stream cipher is used to protect the response for getToken2(), and the request for resetAccount(). HKDF is used to create a number of random bytes equal to the length of the message, then these are XORed with the plaintext to produce the ciphertext. An HMAC is then computed from the ciphertext, to protect the integrity of the message.
 
HKDF, like any KDF, is defined to produce output that is indistinguishable from random data ("The HKDF Scheme", http://eprint.iacr.org/2010/264.pdf , by Hugo Krawczyk, section 3). XORing a plaintext with a random keystream to produce ciphertext is a simple and secure approach to data encryption, epitomized by AES-CTR or a stream cipher (http://cr.yp.to/snuffle/design.pdf). HKDF is not the fastest way to generate such a keystream, but it is safe, easy to specify, and easy to implement (just HMAC and XOR).
 
Each keystream must be unique. SRP is defined to produce a random session key for each run (as long as at least one of the sides provides a random ephemeral key). We define resetToken to be a single-use randomly-generated value. Hence our two HKDF-XOR keystreams will be unique.
 
A slightly more-traditional alternative would be to use AES-CTR (with the same HMAC-SHA256 used here), with a randomly-generated IV. This is equally secure, but requires implementors to obtain an AES library (with CTR mode, which does not seem to be universal). An even more traditional technique would be AES-CBC, which introduces the need for padding and a way to specify the length of the plaintext. The additional specification complexity, plus the library load, leads me to prefer HKDF+XOR.
 
kB is equal to the XOR of wrapKey (which is a deterministic function of the user's email address, password, mainSalt, and the stretching parameters) and the server's randomly-generated wrap(kB) value, making kB a random value too. Using XOR as a wrapping function allows us to avoid sending kB or wrap(kB) in the initial createAccount arguments, which are not protected by SRP, and thus would enable an eavesdropper to mount a dictionary attack on the password (using wrap(kB) as their oracle).
 
Likewise, allowing the server to generate kA avoids exposure during createAccount. The only point of vulnerability is a forgotten-password resetAccount() message, if an eavesdropper can learn the resetToken. In this case, the attacker might either use the resetToken themselves (then use signToken to learn kA and wrap(kB)), or passively decrypt the resetAccount() arguments to retrieve just wrap(kB). Given wrap(kB) and some encrypted browser data, the attacker can guess passwords (and derive kB, etc) until the data decrypts properly.
 
To make this technique safe, any time kB or the password is changed, the mainSalt should be changed too. Otherwise knowledge of both wrap(old-kB) and old-kB would reveal wrapKey, making it easy to deduce the new kB. Changing mainSalt causes wrapKey to change too, preventing this.
 
mainSalt is incorporated at the end of the stretching process to allow it to proceed in parallel with the getToken1() call that retrieves the salt. The inputs to the lengthy stretch come entirely from the user (email and password) or are optimistically (stretching parameters). This speedup seems more important than the minor security benefit of including the salt at the beginning of the stretch.
 
There is no MAC on wrap(kB). If the keyserver chooses to deliver a bogus wrap(kB) or kA, the client will discover the problem a moment later when it talks to a storage server and attempts to retrieve data from an unrecognized collection-ID (since we intend to derive collection-IDs from the key used to encrypt their data, which will be derived from kA or kB as appropriate). It might be useful to add a checksum to kA and wrap(kB) to detect accidental corruption (e.g. store and deliver kA+SHA256(kA)), but this doesn't protect against intentional changes. We omit this checksum for now, assuming that disks will be reliable enough to let us never experience such failures.
 
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 =


To protect the server's session table memory and CPU usage for the initial SRP calculation, the server might require clients to perform busy-work before calling getToken1(). The server can control how much work is required.
To protect the server's session table memory and CPU usage for the initial SRP calculation, the server might require clients to perform busy-work before calling getToken1(). The server can control how much work is required.
Line 59: Line 385:
TBD: Is this worth it? Should the PoW string go into an HTTP header? (I want it to be cheap to extract, and not clutter logs). Should the error response be a distinctive HTTP error code so our monitoring tools can easily count them? We can also use this feature to slow down online guessing attacks (i.e. trigger it either when getToken1 is called too much or when getToken2 produces too many errors). Since getToken1() includes an email address, we could also requires PoWs for some addresses (e.g. those we know to be under attack) but not others.
TBD: Is this worth it? Should the PoW string go into an HTTP header? (I want it to be cheap to extract, and not clutter logs). Should the error response be a distinctive HTTP error code so our monitoring tools can easily count them? We can also use this feature to slow down online guessing attacks (i.e. trigger it either when getToken1 is called too much or when getToken2 produces too many errors). Since getToken1() includes an email address, we could also requires PoWs for some addresses (e.g. those we know to be under attack) but not others.


== Client-Side Key Stretching ==
= Glossary =


The current stub does no stretching. It just performs a single HKDF operation, combining the user's email address, their password, and a "stretchSalt" retrieved from the server's getToken1() response.
This defines some of the jargon we've developed for this protocol.


[[File:PICL-IdPAuth-stretch-KDF.png|Stretching KDF]]
* data classes: each type of browser data (bookmarks, passwords, history, etc) can be assigned, by the user, to either class-A or class-B
* class-A: data assigned to this class can be recovered, even if the user forgets their password, by proving control over an email address and resetting the account. It can also be read by Mozilla (since it runs the keyserver and knows kA), or by the user's IdP (by resetting the account without the user's permission).
* class-B: data in this class cannot be recovered if the password is forgotten. It cannot be read by the IdP. Mozilla (via the keyserver) cannot read this data, but can attempt a brute-force dictionary attack against the password.
* kA: the master key for data stored as "class-A", a 32-byte binary string. Individual encryption keys for different datatypes are derived from kA.
* kB: the master key for data stored as "class-B", a 32-byte binary string.
* wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.
* sessionToken: a long-lived per-device token which allows the device to obtained signed BrowserID certificates for the account's identity (GUID@picl-something.org). This token remains valid until the user revokes it (either by changing their password, or triggering some kind of "revoke a specific device" or "revoke all devices" function).


A later version of the protocol will replace this with the PBKDF2+scrypt+PBKDF2 protocol described in [[Identity/CryptoIdeas/01-PBKDF-scrypt]]. This 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 "stretchSalt" is added *after* the stretching, to enable this parallelism (at a tiny cost in security).
= Test Vectors =


After "masterKey" is derived, a second HKDF call is used to derive "unwrapKey" and "srpPW" which will be used later.
The following example uses a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264).


[[File:PICL-IdPAuth-main-KDF.png|masterKey KDF]]
These test vectors were produced by the python code in https://github.com/warner/picl-spec-crypto (revision aa441c6). The diagrams above may lag behind the latest version of that code.


== SRP Protocol Details ==
== stretch-KDF ==


The PiCL client uses the SRP protocol (http://srp.stanford.edu/) to prove that it knows the account password without revealing the actual password (or information enabling a brute-force attack) to the server or any eavesdroppers.
email:
616e6472c3a94065
78616d706c652e6f
7267


SRP is somewhat underspecified. We use SRP-6a, with SHA256 as the hash, and the 2048-bit modulus defined in RFC 5053 Appendix A. We consistently zero-pad all string values to 256 bytes (2048 bits), and use H(A+B+S) as the key-confirmation message "M1". These details, plus the SRP design papers and RFCs 2945 and 5054, should be enough to build a compatible implementation. The diagrams below are annotated with test vectors to verify compatibility.
password:
70c3a4737377c3b6
7264


The server should use Jed's SRP module from https://github.com/jedp/node-srp . The client might use SJCL (http://crypto.stanford.edu/sjcl/) or native code (NSS).
K1 (scrypt input):
f84913e3d8e6d624
689d0a3e9678ac8d
cc79d2c2f3d96414
88cd9d6ef6cd83dd


The basic idea is that we're using the main-KDF output "srpPW" as a password for the SRP calculation. We use the email address for "identity", and a server-provided string for "salt". (We could safely leave them blank, since equivalent values are already folded into the password-stretching process, but it's less confusing to follow the SRP spec and fill them in with something sensible).
K2 (scrypt output):
5b82f146a6412692
3e4167a0350bb181
feba61f63cb17140
12b19cb0be0119c5


Note that SRP-6a uses a "k" value which basically encodes the group being used ("N" and "g"). Since all PICL accounts use the same 2048-bit group, they will all use the same "k" value (not to be confused with the per-session shared-secret "K" key that emerges from the protocol). This group's "k" integer is (as a base-10 number):
stretchedPW:
259003859907
c16d46c31bee242c
09503006915442163037721228467470
b31f916e9e38d60b
35652616593381637186118123578112
76431d3f5304549c
c75ae4bc20c7108c


The following examples use a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264). Given the password-stretching described earlier, this results in an srpPW of:
== main-KDF ==
5b597db713ef1c05
67f8d053e9dde294
f917a0a838ddb661
a98a67a188bdf491


=== SRP Verifier Calculation ===
mainSalt (normally random):
00f0000000000000
0000000000000000
0000000000000000
000000000000034d


When the client first creates the account, it must combine the account email address, the stretched password (srpPW), and a randomly-generated srpSalt, to compute the srpVerifier. The server will use this verifier later, to check whether or not the client really knows the password.
srpPW:
00f9b71800ab5337
d51177d8fbc682a3
653fa6dae5b87628
eeec43a18af59a9d


If the server is compromised and an attacker learns the srpVerifier for a given account, they cannot use this to directly log in (the verifier is not "password-equivalent"), but it does allow them to perform an offline brute-force attack against the user's password. In this respect, it is similar to a traditional hashed password. We make these attacks somewhat more expensive by performing the client-side stretching described above, instead of using the raw user password in the SRP calculation.
unwrapBKey:
6ea660be9c89ec35
5397f89afb282ea0
bf21095760c8c500
9bbcc894155bbe2a


[[File:PICL-IdPAuth-SRP-Verifier.png|client-side SRP Verifier calculation]]
internal x (base 10):
8192518690918
99580124814080709381476194749939
03899664126296984459627523279550


Given the sample email and password above, the SRP Verifier calculation yields the following. (Note: the srpSalt is normally generated randomly, but for illustrative purposes, here we used fixed pre-calculated values).
internal x (hex):
b5200337cc3f3f92
6cdddae0b2d31029
c069936a844aff58
779a545be89d0abe


* srpSalt (hex string): 00f1000000000000 0000000000000000 0000000000000000 000000000000009b
v (verifier as number) (base 10):
* x (internal, as a hex string): ffd36e11f577d312892334810d55089cb96c39443c255a9d85874bb6df69a537
114649
* x (internal, as decimal integer): 11571334079566
57230405843056840989945621595830
92128319718196619842967585739396
71784395917725741221739574165799
25477265918747447380376082294071
54316134303691657140298181419198
* v (internal, decimal integer): 710597
87853709633756255809680435884948
15947322363168818619231596014948
69849281177012209169281795507853
50266475387982152630667264830126
57610332070005048463659745521969
91363325468391002539838039127254
83218225819721112680718485091921
13731153916626297948231925131054
64608360806562626442477160609654
77620430120387238383382529286340
43167308814558974899899506977051
32606803605961340789655696705692
96721477608178869100211706638584
35971894130915251144385164054999
53875100985456239693728258285562
20023879039952438012163402227132
04889672594983678412848291529879
85297349371740668115032272229446
88548996842770025110751388952323
78351915275352511787735824142082
22170663943486107183421205517476
28132003206595132571178470786998
84831590615660554713667726412525
71417330468650192650539261877568
73641352721966728239512914666806
70781628009053137574167426864838
49625530438034148797508015907639
84981432162129791810924115157063
67594925530663571631035463732161
80745962226827721585324849766449
30193328802116982288883318596822
08876686423788254204401136102193
24427662561738518576134929894589
97367433462254526788221238212661
40913290180513540399852050747986
* srpVerifier (hex string):
00901a4e05a7986c fafe2c80993f6e21
847d38b8b9168065 149480722d008c9a
c5fe418d799d03c2 b1c26db2afcd4513
0a0601d310faa060 cc728888aba130a1
7d855773107ecc92 f31ea3a3838bc727
77fc26420ed59918 298583d15640b965
939dd6967e943bd6 ed846dbbb18885c7
4f6e9370e4eeecc4 c8e2a648850cf2ba
5baab18888b433c4 b0bd8891eeffe16c
c022a098284696bc 3a81e735a1a2a371
62f62b980879bbd4 03ae55548b9feeec
b18bf0740f0d078a 435fedb5324d630e
8a14fed435fbb5ea 4b6e94b8b129799d
2a0991671a67be34 149dc5e94a4a3d05
749fc3b9e1a53282 96b20a15348420be
d2f28d2558cb4099 f30be8a7240c9252


== SRP Verifier ==


The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.
k (base 10):
259003859907
09503006915442163037721228467470
35652616593381637186118123578112


=== SRP Server-side Sign-In Flow ===
srpSalt (normally random):
00f1000000000000
0000000000000000
0000000000000000
0000000000000179


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.'''
srpVerifier:
00173ffa0263e63c cfd6791b8ee2a40f
048ec94cd95aa8a3 125726f9805e0c82
83c658dc0b607fbb 25db68e68e93f265
8483049c68af7e82 14c49fde2712a775
b63e545160d64b00 189a86708c69657d
a7a1678eda0cd79f 86b8560ebdb1ffc2
21db360eab901d64 3a75bf1205070a57
91230ae56466b8c3 c1eb656e19b794f1
ea0d2a077b3a7553 50208ea0118fec8c
4b2ec344a05c66ae 1449b32609ca7189
451c259d65bd15b3 4d8729afdb5faff8
af1f3437bbdc0c3d 0b069a8ab2a959c9
0c5a43d42082c774 90f3afcc10ef5648
625c0605cdaace6c 6fdc9e9a7e6635d6
19f50af773452247 0502cab26a52a198
f5b00a2798589165 07b0b4e9ef9524d6


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


* b (hex integer, normally random but pre-calculated for this example):
private b (normally random) (base 10):
00f3000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000020
* b (as decimal integer):
1198277
1198277
66042000957856349411550091527197
66042000957856349411550091527197
Line 191: Line 538:
00403102624831815596140094933216
00403102624831815596140094933216
29832845626116777080504444704039
29832845626116777080504444704039
04739431335617585333671378812960
04739431335617585333671378812943
* srpB (hex string):
00857f70b197a6f3 f79c4270a41c581d
62c7ec7fc554c797 481d4b4075b06be3
df7f4f189e71fbec 08d1bcff8c5e4f74
65256cba8a78b725 daa0b9bddcbbea43
d916067b12c59aaf 4a9cdad53e08e4a5
770ea72287987302 2c5f5f608eb94795
710a907e1b425080 688d9e7790ce0781
6e6b2cdb9ad2c18f 60a2a5feb91b6da3
92579c5eb1e36f42 5b85c34085b216b9
7c4a3f7ffeb887c8 78ce0152d8be66eb
9c7a51abbae3b3f6 56c6e56d95d3e148
a23af3e9aaa54c72 cde19b58bdcbfb34
b9eb7f6dcbcd86e2 7e6221f6d3da2517
255088f5e7c408b3 7d6765120134b719
86287225d781c49a e5436b89525e17eb
dcb8f3b7eb43163a cfb31c45a51a5267


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.
private b (hex):
 
00f3000000000000 0000000000000000
=== 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.
 
[[File:PICL-IdPAuth-SRP-Client1.png|client-side SRP 1]]
 
Once the client knows srpSalt, it computes the same "x" integer as it did in the middle of the srpVerifier calculation. It also converts srpB into an integer named "B". Then it creates a random "a" integer, uses it to compute the string "srpA", then combines srpA with the server's srpB to compute the "u" integer. It then combines the static "k", the password-derived "x", the combined "u", and the server's "B", together with some magic math, to derive the "S" integer. If everything went well, the client will compute the same "S" value as the server did. If not (the password was wrong, or the client is talking to a fake server that doesn't really know srpVerifier), then the two "S" values will not match.
 
('''Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.''')
 
* a (hex integer, normally random but pre-calculated for this example):
00f2000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
Line 238: Line 556:
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 000000000000115c
0000000000000000 000000000000000f
* a (as decimal integer):
 
transmitted srpB:
0022ce5a7b9d8127 7172caa20b0f1efb
4643b3becc535664 73959b07b790d3c3
f08650d5531c19ad 30ebb67bdb481d1d
9cf61bf272f84398 48fdda58a4e6abc5
abb2ac496da5098d 5cbf90e29b4b110e
4e2c033c70af7392 5fa37457ee13ea3e
8fde4ab516dff1c2 ae8e57a6b264fb9d
b637eeeae9b5e43d faba9b329d3b8770
ce89888709e02627 0e474eef822436e6
397562f284778673 a1a7bc12b6883d1c
21fbc27ffb3dbeb8 5efda279a69a1941
4969113f10451603 065f0a0126666456
51dde44a52f4d8de 113e2131321df1bf
4369d2585364f9e5 36c39a4dce33221b
e57d50ddccb4384e 3612bbfd03a268a3
6e4f7e01de651401 e108cc247db50392
 
== SRP A ==
 
private a (normally random) (base 10):
1193346
1193346
47663227291363113405741243413916
47663227291363113405741243413916
Line 259: Line 598:
05751238004976540634839106888223
05751238004976540634839106888223
63866455314898189520502368799907
63866455314898189520502368799907
19946264951520393624479315530076
19946264951520393624479315579863
* srpA (hex string)
 
00f2a357d7da7132 1be6c070fb3a5928
private a (hex):
8cec951cb13e7645 1f8c466ab373626a
00f2000000000000 0000000000000000
7272dc1484c79ea3 cd1ea32e57fa4665
0000000000000000 0000000000000000
2e6450aa61ac5ee7 eac7a8c06c28ab19
0000000000000000 0000000000000000
5ccbe57500062c50 1a15fbb23a7f71b2
0000000000000000 0000000000000000
35448326af5e51c0 63f167378c782137
0000000000000000 0000000000000000
93dbc54efb32f204 de753d7a6b3d826d
0000000000000000 0000000000000000
aaefc007d17862af 9b6a14e35f17f1eb
0000000000000000 0000000000000000
8b13c7b8ffa1f6f4 7b70d62bd0c351b4
0000000000000000 0000000000000000
7596b0b0abcba95c 2d731869ed6e4ec2
0000000000000000 0000000000000000
4ab90da8cb22e65d 256315ee84d8079b
0000000000000000 0000000000000000
4086d90c4e827b51 bb4e4d2d7b387da0
0000000000000000 0000000000000000
2e6b48904a3ba6d7 648a9bcdf3e9fc60
0000000000000000 0000000000000000
7cfba92f8eacae12 3ac45a79307cf3dd
0000000000000000 0000000000000000
281ed75a96c7de8f cd823f148dcc0634
0000000000000000 0000000000000000
9795f825fb029859 b963ab88320133de
0000000000000000 0000000000000000
* u (hex string)
0000000000000000 000000000000d3d7
610c6df1f495e429 8a2a59a0f5b00d47
 
ea2ed6ce2ccec8f7 ade158314a7bd794
transmitted srpA:
* S (hex string)
007da76cb7e77af5 ab61f334dbd5a958
009cc8da2f7a9501 5bc0091faa36d6ef
513afcdf0f47ab99 271fc5f7860fe213
ff52c33b924353e1 1de1d8e738654d6f
2e5802ca79d2e5c0 64bb80a38ee08771
6a481003acb17cae 2ba2d4ae3fea8431
c98a937696698d87 8d78571568c98a1c
4c940397640fce92 d9153dffb7f3bd29
40cc6e7cb101988a 2f9ba3d65679027d
cbdb49e4ff0d26c4 67061337fd370851
4d9068cb8aad6ebf f0101bab6d52b5fd
4e3039d24cb54dc4 6420426b0daf7724
fa81d2ed48bba119 d4ecdb7f3f478bd2
63fe06eb1521c7b0 96c4eeb6e5f9f739
36d5749f2275e948 4f2d0a9259d05e49
49dcc74bc91baab8 398aff6df6735da2
d78a23dd26c60bfb a04fd346e5146469
c9486a645a20f2d7 d8f455a2bd226f21
a8c3f010a627be81 c58ded1caaef2363
e127f23e202b21fd d4ef64dc1a6740b6
635a45f97ca0d895 cc92ace1d09a99d6
fcd2a6b032fcb393 a2b9d97506b6fb89
beb6b0dc0829535c 857a419e834db128
5585d29173cc0e89 c3b3077ffa31215d
64cd6ee8a843563b 0240520ff0195735
b602b28364f81012 46ee9e8c47b63881
cd9d316842d5d3f8 ef7209a0bb4b54ad
f3f867e67971825d f6a881d1142989ab
7374d73e79be2c39 75632de562c59647
cd4abba9c27ae529 c31be53f69966ccb
0bb27bad79c3e2fc ddf194e1666cb9fc
81f7660f95d5f8fc 45d052df3bcbb761
 
== SRP key-agreement ==
 
u:
b284aa1064e87751 50da6b5e2147b47c
a7df505bed94a6f4 bb2ad873332ad732
 
S:
0092aaf0f527906a a5e8601f5d707907
a03137e1b601e04b 5a1deb02a981f4be
037b39829a27dba5 0f1b27545ff2e287
29c2b79dcbdd32c9 d6b20d340affab91
a626a8075806c26f e39df91d0ad979f9
b2ee8aad1bc783e7 097407b63bfe58d9
118b9b0b2a7c5c4c debaf8e9a460f4bf
6247b0da34b760a5 9fac891757ddedca
f08eed823b090586 c63009b2d740cc9f
5397be89a2c32cdc fe6d6251ce11e44e
6ecbdd9b6d93f30e 90896d2527564c7e
b9ff70aa91acc0ba c1740a11cd184ffb
989554ab58117c21 96b353d70c356160
100ef5f4c28d19f6 e59ea2508e8e8aac
6001497c27f362ed bafb25e0f045bfdf
9fb02db9c908f103 40a639fe84c31b27
 
M1:
27949ec1e0f16256
33436865edb037e2
3eb6bf5cb91873f2
a2729373c2039008
 
srpK:
e68fd0112bfa31dc
ffc8e9c96a1cbadb
4c3145978ff35c73
e5bf8d30bbc7499a
 
== /auth ==
 
srpK:
e68fd0112bfa31dc
ffc8e9c96a1cbadb
4c3145978ff35c73
e5bf8d30bbc7499a
 
respHMACkey:
6584613597ef012f
f1752b7869f01d03
c72547a7b7199681
531d9df1991edf23
 
respXORkey:
455835926ae37a1b
627bd16affbeeab6
27ecc737121826ca
4a2bac2c100bf417
 
authToken:
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f
 
plaintext:
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f
 
ciphertext:
253957f10e861c7c
0a12bb0193d384d9
579db544666d50bd
3252d6576c768a68
 
MAC:
a98c87f5769ab4cc
ca3df863faeb217e
b16ddc29d712b301
12b446324ee806d6
 
response:
253957f10e861c7c
0a12bb0193d384d9
579db544666d50bd
3252d6576c768a68
a98c87f5769ab4cc
ca3df863faeb217e
b16ddc29d712b301
12b446324ee806d6
 
== authtoken ==


[[File:PICL-IdPAuth-SRP-Client2.png|client-side SRP 2]]
authToken:
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f


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.
tokenID (authToken):
9a39818e3bbe6132
38c9d7ff013a1841
1ed2c66c3565c3c4
de03feefecb7d212


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).
reqHMACkey:
4a17cbdd54ee17db
426fcd7baddff587
231d7eadb408c091
ce19ca915b715985
 
requestKey:
9d93978e662bfc6e
8cc203fa4628ef5a
7bf1ddfd7ee54e97
ec5c033257b4fca9
 
== /session ==
 
requestKey:
9d93978e662bfc6e
8cc203fa4628ef5a
7bf1ddfd7ee54e97
ec5c033257b4fca9
 
respHMACkey:
cd3f50403d060b21
76d32f71ca105bd8
7c9b6c4e10e3ebf9
3f5077bec2db24fa
 
respXORkey:
8422c53143dea9c6
044afbe95228f291
74996b830b1794a3
eff132da53174d43
92eeb87ccf8ad7a8
0c432894e066de6b
0ff70658dfbf2f07
b9c7704045edcd54
 
keyFetchToken:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f
 
sessionToken:
a0a1a2a3a4a5a6a7
a8a9aaabacadaeaf
b0b1b2b3b4b5b6b7
b8b9babbbcbdbebf
 
plaintext:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f
a0a1a2a3a4a5a6a7
a8a9aaabacadaeaf
b0b1b2b3b4b5b6b7
b8b9babbbcbdbebf
 
ciphertext:
04a347b2c75b2f41
8cc37162dea57c1e
e408f9109f820234
7768a841cf8ad3dc
324f1adf6b2f710f
a4ea823f4ccb70c4
bf46b4eb6b0a99b0
017ecafbf95073eb
 
MAC:
7973ddbb184b601a
c4df09704028ebfc
754dd50e7d8eebfa
52ce3fd868c69852
 
response:
04a347b2c75b2f41
8cc37162dea57c1e
e408f9109f820234
7768a841cf8ad3dc
324f1adf6b2f710f
a4ea823f4ccb70c4
bf46b4eb6b0a99b0
017ecafbf95073eb
7973ddbb184b601a
c4df09704028ebfc
754dd50e7d8eebfa
52ce3fd868c69852
 
== /account/keys ==
 
keyFetchToken:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f
 
tokenID (keyFetchToken):
3d0a7c02a15a62a2
882f76e39b6494b5
00c022a8816e0486
25a495718998ba60
 
reqHMACkey:
87b8937f61d38d0e
29cd2d5600b3f4da
0aa48ac41de36a0e
fe84bb4a9872ceb7
 
keyRequestKey:
14f338a9e8c6324d
9e102d4e6ee83b20
9796d5c74bb734a4
10e729e014a4a546
 
respHMACkey:
f824d2953aab9faf
51a1cb65ba9e7f9e
5bf91c8d8fd1ac1c
8c2d31853a8a1210


* M1 (hex string):
respXORkey:
182ff26523922c52
ce7d7aa77859b235
559cab3cdfc89a74
9932970bbe2101f2
c986b1d7504ea53d
e80d01faf9191bd5
11d9a204fc54449d
ee52181d2f0b7809
* srpK (hex string):
8281ba8cff392543
78a36d3e0df089e7
3a89f7c3095e0c89
29a98dee3290fc49
900a469d60790c83
64cd6ec96b771d6a
3281c4df1a11c763
bb6efe9181be868b


=== SRP Notes ===
kA:
2021222324252627
28292a2b2c2d2e2f
3031323334353637
38393a3b3c3d3e3f


The SRP "g" (generator) and "N" (prime modulus) should use the 2048-bit value from RFC 5054 Appendix A. Clients should not accept arbitrary g/N values (to protect against small primes, non-primes, and non-generators). In the future we might allow alternate parameter sets, in which case the server's first response should indicate which parameter set to use.
wrapkB:
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f


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.
plaintext:
2021222324252627
28292a2b2c2d2e2f
3031323334353637
38393a3b3c3d3e3f
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f


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).
ciphertext:
ee5c58845c7c9412
b11bbd20920c2fdd
d83c33c9cd2c2de2
d66b222613364636
c2c0f8cfbb7c6304
72c0bd88451342c6
c05b14ce342c5ad4
6ad89e84464c993c


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.
MAC:
3927d30230157d08
17a077eef4b20d97
6f7a97363faf3f06
4c003ada7d01aa70


Outstanding crypto questions:
response:
ee5c58845c7c9412
b11bbd20920c2fdd
d83c33c9cd2c2de2
d66b222613364636
c2c0f8cfbb7c6304
72c0bd88451342c6
c05b14ce342c5ad4
6ad89e84464c993c
3927d30230157d08
17a077eef4b20d97
6f7a97363faf3f06
4c003ada7d01aa70


* How exactly should the "a" and "b" integers be generated? The issue is of how much bias SRP can tolerate. Ideally these integers are uniformly distributed from 1 to N-1 (inclusive). The only way to obtain a purely uniform distribution from a source of random bytes is try-try-again: pick a (integral-number-of-bytes) number, compare it to the desired range, try again if it falls outside the range. If you get unlucky, this can take a lot of guesses, depending upon how close the range is to a power of two. ECDSA implementations tend to pick a number twice as long as the modulus and then modulo it down (rand(2^4096) % N), which yields a tiny fraction of a bit of bias. The cheaper approach is to do the same with a number of equal length (rand(2^2048) % N), which imposes more bias. Some SRP implementations appear to be satisfied by rand(2^256). We need more review here.
wrapkB:
* 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.
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f


== getToken2 ==
unwrapBKey:
6ea660be9c89ec35
5397f89afb282ea0
bf21095760c8c500
9bbcc894155bbe2a


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).
kB:
2ee722fdd8ccaa72
1bdeb2d1b76560ef
ef705b04349d9357
c3e592cf4906e075


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.
== use session (certificate/sign, etc) ==


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.
sessionToken:
a0a1a2a3a4a5a6a7
a8a9aaabacadaeaf
b0b1b2b3b4b5b6b7
b8b9babbbcbdbebf


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.
tokenID (sessionToken):
c0a29dcf46174973
da1378696e4c82ae
10f723cf4f4d9f75
e39f4ae3851595ab


Future variants (e.g. to fetch a third kind of token) might put additional values in the response to getToken2.
reqHMACkey:
9d8f22998ee7f579
8b887042466b72d5
3e56ab0c094388bf
65831f702d2febc0


== Decrypting the getToken2 Response ==
== /password/change ==


The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
requestKey:
9d93978e662bfc6e
8cc203fa4628ef5a
7bf1ddfd7ee54e97
ec5c033257b4fca9


[[File:PICL-IdPAuth-decryptSignToken1.png|Decrypting the signToken 1]]
respHMACkey:
60d01e3d1da53b10
93124a30c26889d7
b2e067e7a09fde14
6f935e3c653614f9


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.
respXORkey:
3de5bd5e80faf84a
dfca5396148123ef
8184cd4bc10a7c8a
db1688495affee67
e07f80d914c5105f
c86d6af24c4be1b1
ef6c9c661422ac43
181b3d29624a0cc2


[[File:PICL-IdPAuth-decryptSignToken2.png|Decrypting the signToken 2]]
keyFetchToken:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f


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.
accountResetToken:
c0c1c2c3c4c5c6c7
c8c9cacbcccdcecf
d0d1d2d3d4d5d6d7
d8d9dadbdcdddedf


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).
plaintext:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f
c0c1c2c3c4c5c6c7
c8c9cacbcccdcecf
d0d1d2d3d4d5d6d7
d8d9dadbdcdddedf


= Signing Certificates =
ciphertext:
bd643fdd047f7ecd
5743d91d980cad60
11155fd8559fea1d
438f12d2c66270f8
20be421ad000d698
00a4a03980862f7e
3fbd4eb5c0f77a94
c0c2e7f2be97d21d


The current stub just submits (cert, signToken), and gets back a signed certificate. This will be replaced soon.
MAC:
804fc4bc30923cc0
d6c07ffea954848e
0076b94f7deee71f
a34db5c106d91980


(TBD, future protocol)
response:
bd643fdd047f7ecd
5743d91d980cad60
11155fd8559fea1d
438f12d2c66270f8
20be421ad000d698
00a4a03980862f7e
3fbd4eb5c0f77a94
c0c2e7f2be97d21d
804fc4bc30923cc0
d6c07ffea954848e
0076b94f7deee71f
a34db5c106d91980


The Sign Token is used to derive two values:
== /account/reset ==


* tokenID
accountResetToken:
* request HMAC key
c0c1c2c3c4c5c6c7
c8c9cacbcccdcecf
d0d1d2d3d4d5d6d7
d8d9dadbdcdddedf


tokenID (accountResetToken):
46ec557e56e531a0
58620e9344ca9c75
afac0d0bcbdd6f8c
3c2f36055d9540cf


[[File:PICL-IdPAuth-signCertificate.png|Deriving Keys to Sign the Certificate]]
reqHMACkey (for HAWK):
716ebc28f5122ef4
8670a48209190a16
05263c3188dfe452
56265929d1c45e48


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.
requestKey:
aa5906d2318c6e54
ecebfa52f10df4c0
36165c230cc78ee8
59f546c66ea3c126


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.
reqHMACkey (for ciphertext):
a0d894a6232f2e78
66a51dda3f84e01e
ae5adb812564f391
6c0d3cb16bdb743c


[[File:PICL-IdPAuth-signRequest.png|Signing the signCertificate Request]]
reqXORkey:
9cbde8fc9df31455 837b881e6c0d7e3c
ca13589bc868c527 95fc00e51f2048ab
d56de37629cda0b0 3f580a9e6c433724
b5df12a735ccf2a1 e232d4f5fef84f86
a1b4fdc47f8d1f73 12a6a230a8742d5b
c144ee9abce25b57 9670b81085064cfb
dcab862d9d57abcc 2142dcdde6682281
d378c89b0dce06ae cd1c1ff68ad6db9a
9cab0b02e160805b 59bb8712c8233056
1b3ded75c430e23c 22338833b6f2ba39
f5015ca7a905d6ee 6ec5b1e3ae5204ba
6f3630ebf30ebbac 1f47329e8fe22770
2a3d61f593328dd4 f0a96b628aa8ffec
181e93d2af8d87ff 2d90d67caaf7f7c9
af024c93cfc79e94 67ba70b3076c20cc
141aa254ff159b25 3125a304441cecf3
4fc1845ce96ee598 21fde83cd24e3209
4d304477bfa2c8ed df236e512560694e


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.
wrapkB:
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f


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.
newSRPv:
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111
1111111111111111


= Resetting the Account =
plaintext:
4041424344454647 48494a4b4c4d4e4f
5051525354555657 58595a5b5c5d5e5f
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111
1111111111111111 1111111111111111


The current stub just submits (newPassword, wrap(kB), resetToken). This will be replaced soon.
ciphertext:
dcfcaabfd9b65212 cb32c25520403073
9a420ac89c3d9370 cda55abe437d16f4
c47cf26738dcb1a1 2e491b8f7d522635
a4ce03b624dde3b0 f323c5e4efe95e97
b0a5ecd56e9c0e62 03b7b321b9653c4a
d055ff8badf34a46 8761a90194175dea
cdba973c8c46badd 3053cdccf7793390
c269d98a1cdf17bf dc0d0ee79bc7ca8b
8dba1a13f071914a 48aa9603d9322147
0a2cfc64d521f32d 33229922a7e3ab28
e4104db6b814c7ff 7fd4a0f2bf4315ab
7e2721fae21faabd 0e56238f9ef33661
3b2c70e482239cc5 e1b87a739bb9eefd
090f82c3be9c96ee 3c81c76dbbe6e6d8
be135d82ded68f85 76ab61a2167d31dd
050bb345ee048a34 2034b215550dfde2
5ed0954df87ff489 30ecf92dc35f2318
5c215566aeb3d9fc ce327f403471785f


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.
MAC:
1d3572fe0b4bdf66
f2b2657cb2ee56fc
80f7a82708cafd82
1952e1f01761cb29


So the single-use resetToken is used to derive three values:
response:
dcfcaabfd9b65212 cb32c25520403073
9a420ac89c3d9370 cda55abe437d16f4
c47cf26738dcb1a1 2e491b8f7d522635
a4ce03b624dde3b0 f323c5e4efe95e97
b0a5ecd56e9c0e62 03b7b321b9653c4a
d055ff8badf34a46 8761a90194175dea
cdba973c8c46badd 3053cdccf7793390
c269d98a1cdf17bf dc0d0ee79bc7ca8b
8dba1a13f071914a 48aa9603d9322147
0a2cfc64d521f32d 33229922a7e3ab28
e4104db6b814c7ff 7fd4a0f2bf4315ab
7e2721fae21faabd 0e56238f9ef33661
3b2c70e482239cc5 e1b87a739bb9eefd
090f82c3be9c96ee 3c81c76dbbe6e6d8
be135d82ded68f85 76ab61a2167d31dd
050bb345ee048a34 2034b215550dfde2
5ed0954df87ff489 30ecf92dc35f2318
5c215566aeb3d9fc ce327f403471785f
1d3572fe0b4bdf66 f2b2657cb2ee56fc
80f7a82708cafd82 1952e1f01761cb29


* tokenID
== /account/destroy ==
* request HMAC key
* request XOR key


authToken:
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f


The request data will contain kA, wrap(kB), and the SRP verifier, concatenated together. The first two pieces are fixed-length. We generate enough reqXORkey bytes to cover all three values.
tokenID (authToken):
9a39818e3bbe6132
38c9d7ff013a1841
1ed2c66c3565c3c4
de03feefecb7d212


[[File:PICL-IdPAuth-resetAccount.png|Deriving the resetAccount Keys]]
reqHMACkey:
4a17cbdd54ee17db
426fcd7baddff587
231d7eadb408c091
ce19ca915b715985


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).
= Keyserver Protocol Summary =


[[File:PICL-IdPAuth-encryptResetAccount.png|Encrypting the resetAccount Request]]
* POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
** creates a user account
* GET /account/devices [sessionToken] () -> list of devices
* GET /account/keys [keyFetchToken,needs-verf] () -> kA/wrap(kB)
** single-use, only if email is verified, encrypted results
* POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
** single-use, does not require email to be verified, revoke all tokens for account, send notification email to user
* POST /account/delete [authToken] () -> ok, account deleted
* POST /auth/start (email) -> srpToken,SRP stuff
* POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
* POST /session/create [authToken] () -> keyFetchToken, sessionToken
* POST /session/destroy [sessionToken] () -> ok
** for detaching a device, destroy all tokens
* POST /recovery_email/status [sessionToken] () -> "verified" status of email
**  use "Accept: text/event-stream" header for server-sent-events; server will send "update" event with the new content of the resource any time it changes.
* POST /recovery_email/resend_code [sessionToken] () -> re-send verification email
* POST /recovery_email/verify_code (code) -> set "verified" flag
** this code will come from a clickable link and is an unauthenticated endpoint
** this could maybe take the recovery method if that would be helpful
** sets verified flag on recovery method
* POST /certificate/sign [sessionToken,needs-verf] (pubkey) -> cert
** only if recovery email is verified
* POST /password/change/start [authToken,needs-verf] () -> accountResetToken, keyFetchToken
* POST /password/forgot/send_code () -> forgotPasswordToken
** sends code to recovery method (email for now, maybe SMS later)
** this is a short code, not a clickable link
* POST /password/forgot/resend_code (forgotPasswordToken) -> re-sends code
* POST /password/forgot/verify_code (forgotPasswordToken, code) -> accountResetToken
** sets verified flag on recovery method
* POST /get_random_bytes


= Creating the Account =


To create the account in the first place, the client starts with email+password, then does the following steps:
== Typical Client Flows ==


* decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement)
Create account
* decide upon a stretchSalt (remembering this should be unique, but is not secret)
* decide upon SRP parameters (generally fixed)
* perform key-stretching, derive masterKey
* create kA and kB, combining entropy from the local OS with more from the keyserver's getEntropy()
* create wrap(kB), using unwrapKey (derived from masterKey)
* create srpVerifier, using srpPW and the SRP parameters
* deliver many values to the keyserver: parameters for stretching and SRP, kA, wrap(kB), and the srpVerifier


* POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
* POST /auth/start (email) -> srpToken,SRP stuff
* POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
* POST /session/create [authed with authToken]() -> keyFetchToken, sessionToken
* GET /recovery_email/status [sessionToken] () -> "verified" status
** (optional, only if user requests resend) POST /recovery_email/resend_code [sessionToken]() -> ok
** POST /recovery_email/verify_code (code) -> ok
* GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
* POST /certificate/sign [sessionToken] (pubkey) -> cert


= Crypto Notes =


Strong entropy is needed in the following places:
Attach to new device


* (client) initial creation of kA and kB
* POST /auth/start (email) -> srpToken,SRP stuff
* (client) creation of private "a" value inside SRP
* POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
* (server) creation of private "B" value inside SRP
* POST /session/create [authToken] () -> keyFetchToken, sessionToken
* (server) creation of signToken and resetToken
* GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
** (if unverified-error, do waitUntilEmailVerified, then try again)
* POST /certificate/sign [sessionToken] (pubkey) -> cert




On the server, code should get entropy from /dev/urandom via a function that uses it, like "crypto.randomBytes()" in node.js or "os.urandom()" in python. On the client, code should combine local entropy with some fetched from the keyserver via getEntropy(), to guard against failures in the local entropy pool. Something like HKDF(SKM=localEntropy+remoteEntropy, salt="", context=KW("mergeEntropy")).
Forgot password


An HKDF-based stream cipher is used to protect the response for getToken2(), and the request for resetAccount(). HKDF is used to create a number of random bytes equal to the length of the message, then these are XORed with the plaintext to produce the ciphertext. An HMAC is then computed from the ciphertext, to protect the integrity of the message.
* POST /password/forgot/send_code (email) -> forgotPasswordToken
* POST /password/forgot/verify_code (forgotPasswordToken, code) ->  accountResetToken
* POST /account/reset [authed+encrypted by accountResetToken] (0000,srpV,srpSalt) -> ok
* GOTO "Attach to new device"


HKDF, like any KDF, is defined to produce output that is indistinguishable from random data ("The HKDF Scheme", http://eprint.iacr.org/2010/264.pdf , by Hugo Krawczyk, section 3). XORing a plaintext with a random keystream to produce ciphertext is a simple and secure approach to data encryption, epitomized by AES-CTR or a stream cipher (http://cr.yp.to/snuffle/design.pdf). HKDF is not the fastest way to generate such a keystream, but it is safe, easy to specify, and easy to implement (just HMAC and XOR).


Each keystream must be unique. SRP is defined to produce a random session key for each run (as long as at least one of the sides provides a random ephemeral key). We define resetToken to be a single-use randomly-generated value. Hence our two HKDF-XOR keystreams will be unique.
Change Password


A slightly more-traditional alternative would be to use AES-CTR (with the same HMAC-SHA256 used here), with a randomly-generated IV. This is equally secure, but requires implementors to obtain an AES library (with CTR mode, which does not seem to be universal). An even more traditional technique would be AES-CBC, which introduces the need for padding and a way to specify the length of the plaintext. The additional specification complexity, plus the library load, leads me to prefer HKDF+XOR.
* POST /auth/start (email) -> srpToken,SRP stuff
* POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
* POST /password/change/start [authToken] () -> accountResetToken, keyFetchToken
* GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
* POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
* GOTO "Attach to new device"

Latest revision as of 18:16, 22 June 2014

PiCL Key Server / IdP Protocol

NOTE: This specification is slowly converging on stability (31-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 includes a demonstration client (node.js CLI).

Note that all messages are delivered over an HTTPS connection. The client browser may also implement cert-pinning to improve on the certificate validation process. The protections described below are in addition to those provided by TLS.

Remaining TODO items:

  • decide on client-side key-stretching parameters: http://keywrapping.appspot.com can help
  • finalize SRP questions (definition of M1, generation of a/b)
  • finalize proof-of-work/DoS-prevention details
  • decide how to rate-limit account-creation calls
  • confirm this is actually implementable inside Firefox (especially w.r.t. NSS and Android/Java crypto)

Creating The Account

The first act performed by a user is to create the account. They enter email+password into their browser, which then does the following steps:

  • decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement). For now we used a fixed {firstPBKDF:20000, scrypt: {N:65536, r:8, p:1}, secondPBKDF:20000}.
  • randomly choose a 32-byte mainSalt (this should be unique, but is not secret)
  • choose the SRP group parameters (fixed: use the 2048-bit group described below)
  • perform key-stretching (as described below), derive stretchedPW and srpPW
  • 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, srpVerifier) to the keyserver's "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.

Email Verification

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 most 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 static 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: Obtaining the authToken

To connect a browser to an existing account, we use the following login protocol to transform an email+password pair into a single-use authToken. We will use this in the next section to create a session, from which we can 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 the authToken.

IdP Auth Protocol

This authToken can be used (once) to do one of the following:

  • /session/create: obtain a sessionToken (and keyFetchToken), which enables storage server access
  • /password/change: obtain an accountResetToken, to safely reset the account password
  • /account/delete: to delete the entire account

The protocol is designed to enable parallelism between key-stretching and the initial network messages, to reduce the time it takes to connect a browser to the account. In total, the browser requires five messages in four roundtrips (1: /auth/start, 2: /auth/finish, 3: /session/create, 4: /account/keys and /certificate/sign in parallel) before it is ready to talk to the storage server.

/auth/start

As soon as the user finishes typing in the email address, the client should send it in the "/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 /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, /auth/start may also require a proof-of-work string, described below.

Client-Side Key Stretching

"Key Stretching" is the practice of running a password through a computationally-expensive one-way function before using it for encryption or authentication. The goal is to make brute-force dictionary attacks more expensive, by raising the cost of testing each guess.

To protect the user's class-B data against compromises of our keyserver, we perform this key stretching on the client, rather than on the server. We use the memory-hard "scrypt" function (pronounced "ess-crypt") for this purpose, as motivated by the attacker-cost studies in Identity/CryptoIdeas/01-PBKDF-scrypt. Slower (low-RAM) devices can still participate by outsourcing the scrypt step to an "scrypt helper". To provide at least minimal protection against a compromise of this helper, we sandwich the scrypt step between two PBKDF2 steps. The complete protocol looks like this:

Stretching KDF

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 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.

masterKey KDF

SRP Protocol Details

The PiCL client uses the SRP protocol (http://srp.stanford.edu/) to prove that it knows the (stretched) account password without revealing the actual password (or information enabling a brute-force attack) to the server or any eavesdroppers.

SRP is somewhat underspecified. We use SRP-6a, with SHA256 as the hash, and the 2048-bit modulus defined in RFC 5054 Appendix A. We consistently zero-pad all string values to 256 bytes (2048 bits), and use H(A+B+S) as the key-confirmation message "M1". These details, plus the SRP design papers and RFCs 2945 and 5054, should be enough to build a compatible implementation. The diagrams below and the test vectors at the end of this page can be used to verify compatibility.

The server should use Jed's SRP module from https://github.com/jedp/node-srp . The client might use SJCL (http://crypto.stanford.edu/sjcl/) or native code (NSS).

The basic idea is that we're using the main-KDF output "srpPW" as a password for the SRP calculation. We use the email address for "identity", and a server-provided string for "salt". (We could safely leave them blank, since equivalent values are already folded into the password-stretching process, but it's less confusing to follow the SRP spec and fill them in with something sensible).

Note that SRP-6a uses a "k" value which basically encodes the group being used ("N" and "g"). Since all PICL accounts use the same 2048-bit group, they will all use the same "k" value (not to be confused with the per-session shared-secret "K" key that emerges from the protocol). This group's "k" integer is (as a base-10 number): 2590038599070950300691544216303772122846747035652616593381637186118123578112

SRP Verifier Calculation

When the client first creates the account, it must combine the account email address, the stretched password (srpPW), and a randomly-generated srpSalt, to compute the srpVerifier. The server will use this verifier later, to check whether or not the client really knows the password.

If the server is compromised and an attacker learns the srpVerifier for a given account, they cannot use this to directly log in (the verifier is not "password-equivalent"), but it does allow them to perform an offline brute-force attack against the user's password. In this respect, it is similar to a traditional hashed password. We make these attacks somewhat more expensive by performing the client-side stretching described above, instead of using the raw user password in the SRP calculation.

client-side SRP Verifier calculation

Test vectors are provided at the end of this page. The protocol requires that several values ("a", "b", "srpSalt") are chosen randomly, but for illustrative purposes, the test vectors use carefully-crafted non-random values. For the user's sake, please ensure implementations use proper random values instead of simply copying the test vectors.

The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.

SRP Server-side Sign-In Flow

When the user connects a new device to their account, they use the /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 /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.

server-side SRP

Later, /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 /auth/start, it begins its key-stretching calculations. Everything else must wait until the response to /auth/start arrives, which includes the key-stretching parameters (which are retroactively confirmed), srpSalt, and the server's generated srpB value.

client-side SRP

Once the client knows srpSalt, it computes the same "x" integer as it did in the middle of the srpVerifier calculation. It also converts srpB into an integer named "B". Then it creates a random "a" integer, uses it to compute the string "srpA", then combines srpA with the server's srpB to compute the "u" integer. It then combines the static "k", the password-derived "x", the combined "u", and the server's "B", together with some magic math, to derive the "S" integer. If everything went well, the client will compute the same "S" value as the server did. If not (the password was wrong, or the client is talking to a fake server that doesn't really know srpVerifier), then the two "S" values will not match.

(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 /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).

SRP Notes

The SRP "g" (generator) and "N" (prime modulus) should use the 2048-bit value from RFC 5054 Appendix A. Clients should not accept arbitrary g/N values (to protect against small primes, non-primes, and non-generators). In the future we might allow alternate parameter sets, in which case the server's first response should indicate which parameter set to use.

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 test vectors below 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 (/auth/finish).

The server receives "A" in /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 /auth/finish.

On the server, it is critical to reject an "A" value that is 0, or some other multiple of N. If the server does not check this, anybody can trivially sign in to any account without knowing the password. Likewise, it is critical for the client to reject a "B" value where B%N==0. If the client does not check this, the server (or an attacker pretending to be the server) will get a value that can be used in an offline brute-force search for the user's password.

Outstanding crypto questions:

  • How exactly should the "a" and "b" integers be generated? The issue is of how much bias SRP can tolerate. Ideally these integers are uniformly distributed from 1 to N-1 (inclusive). The only way to obtain a purely uniform distribution from a source of random bytes is try-try-again: pick a (integral-number-of-bytes) number, compare it to the desired range, try again if it falls outside the range. If you get unlucky, this can take a lot of guesses, depending upon how close the range is to a power of two. ECDSA implementations tend to pick a number twice as long as the modulus and then modulo it down (rand(2^4096) % N), which yields a tiny fraction of a bit of bias. The cheaper approach is to do the same with a number of equal length (rand(2^2048) % N), which imposes more bias. Some SRP implementations appear to be satisfied by rand(2^256). We need more review here.
  • 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.

/auth/finish

The client-side SRP calculation results in two values that are sent to the server in the /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 single-use 32-byte random token named "authToken". It encrypts the token with the session key, and returns a success message with the encrypted bundle.

All 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 /auth/finish Response

The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.

Decrypting the authToken

The respXORkey is used to encrypt the authToken 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 returns the authToken value.

After Login: Using the authToken

After the authToken is acquired, the client can create a session and fetch the encryption keys. The high-level flow looks like this:

Using the authToken

Creating a Session

For login, the single-use authToken is spent on a call to /session/create . This allocates two new (random 32-byte) tokens: a long-lived "sessionToken", and a single-use "keyFetchToken". The /session/create call returns an encrypted bundle containing the two tokens.

Decrypting the sessionToken and keyFetchToken

For calls which accept an authToken, the client uses authToken to derive three values:

  • tokenID
  • reqHMACkey
  • requestKey


The client uses tokenID and reqHMACkey for a HAWK (https://github.com/hueniverse/hawk/) request to the "POST /session/create" 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.

Each authToken-using call then derives additional API-specific values from requestKey. /session/create uses two derived values:

  • respHMACkey
  • respXORkey


When the server receives a valid /session/create request, it allocates sessionToken and keyFetchToken, concatenates them, encrypts the pair by XORing it with the derived respXORkey, and attaches a MAC generated with respHMACkey. The encrypted MACed 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.

Each authToken is single-use: once a successful request has been made with it, the authToken and its corresponding ID is removed from the server's memory, and subsequent attempts to use it will return a "no such token" error. The token is consumed even if the request fails (e.g. the MAC did not match).

The authToken can be used (once) by multiple APIs. The server only needs to maintain one table mapping tokenID to authToken, because all these APIs share the same method of deriving a tokenID and reqHMACkey from the authToken.

When a HAWK request appears, it should look up the tokenID in this table, retrieve the authToken, recompute reqHMACkey and requestKey, validate the request, delete the table entry, then hand the HTTP request and requestKey to the specific API handler. That handler should derive the API-specific values (respHMACkey, respXORkey, etc) to process the request or construct the response.

The server can support multiple sessions per account (typically one per client device, plus perhaps others for account-management portals). There can also be multiple outstanding keyFetchTokens. The sessionToken lasts forever (until revoked by a password change or explicit revocation command), and can be used an unlimited number of times. The keyFetchToken expires after 60 seconds, and is single-use.

Obtaining keys kA and kB

Clients which have exchanged an authToken for either a sessionToken or an accountResetToken will also receive a keyFetchToken. This single-use token allows the client to retrieve kA and wrap(kB), which enables the client to encrypt and decrypt browser data (bookmarks, open-tabs, etc) correctly. As above, the keyFetchToken is used to derive tokenID, reqHMACkey, respHMACkey, and respXORkey, which are used in a HAWK request to the "GET /account/keys" API.

The server 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.

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.

keyFetchToken: client decrypts keys

Finally, the server-provided wrap(kB) value is simply XORed with the password-derived unwrapBKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).

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.

Note that /account/keys will not succeed until the account's email address has been verified. Also note that each keyFetchToken is single-use and short-lived. The token is consumed even if the request fails (e.g. the MAC does not match).

Signing Certificates

The sessionToken is used to derive two values:

  • tokenID
  • request HMAC key


Using the sessionToken, signing certificates

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.

The following keyserver APIs require a HAWK-protected request that uses the sessionToken. In addition, some require that the account be in the "verified" state:

  • GET /account/devices
  • POST /session/destroy
  • GET /recovery_email/status
  • POST /recovery_email/resend_code
  • POST /certificate/sign (requires "verified" account)

Resetting The Account

The account may be reset in two circumstances: when the user changes their password, and when the user forgets their password. In both cases, the client first obtains an "accountResetToken". This token is then used to change the SRP Verifier and either reset or replace the wrap(kB) value.

Changing the Password

When the user wishes to change their password (i.e. they still know the old password), they first use the /auth/start+/auth/finish SRP protocol safely obtain an "authToken". They they use the "/password/change/start" API to exchange the authToken for an accountResetToken and a keyFetchToken.

Server encrypts passwordChange response

The accountResetToken will be used below to set the new password safely. The keyFetchToken should be used first, to obtain kB, so the subsequent account reset can replace wrap(kB) with a new value. This allows the password-changing client to retain their class-B data.

Requiring an authToken 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.

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

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 set wrap(kB) to a string of all zeros, which means the server should generate a new random wrap(kB) (just as it does during account creation).

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. Another defensive technique is to require that users click an email link before being given the code: the server is told when the link is clicked, so the code will not be enabled until the email has been read. It remains to be seen whether this will be sufficient.

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:

  • tokenID
  • request HMAC key
  • request XOR key


Client encrypts resetAccount request

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 client submits other values in the same request:

  • stretchParams
  • mainKDFSalt
  • srpSalt


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 /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.

Deleting The Account

When the user wishes to completely delete their account, the browser needs to perform two actions:

  • contact the storage servers and delete all records and collections
  • contact the keyserver and delete the account information

The user should be prompted for their password as confirmation (i.e. a browser in the normal attached-and-synchronizing state should not be able to erase the account information: it must acquire a new authToken first).

The device then obtains an authToken as described above, then spends it on a HAWK-protected request to the /account/destroy endpoint. This request contains no body and returns only a success code.

Deleting the Account

Crypto Notes

Strong entropy is needed in the following places:

  • (client) creation of private "a" value inside SRP
  • (server) initial creation of kA and wrap(kB)
  • (server) creation of private "B" value inside SRP
  • (server) creation of signToken and resetToken


On the server, code should get entropy from /dev/urandom via a function that uses it, like "crypto.randomBytes()" in node.js or "os.urandom()" in python. On the client, code should combine local entropy with some fetched from the keyserver via getEntropy(), to guard against failures in the local entropy pool. Something like HKDF(IKM=localEntropy+remoteEntropy, salt="", info=KW("mergeEntropy")).

An HKDF-based stream cipher is used to protect the response for getToken2(), and the request for resetAccount(). HKDF is used to create a number of random bytes equal to the length of the message, then these are XORed with the plaintext to produce the ciphertext. An HMAC is then computed from the ciphertext, to protect the integrity of the message.

HKDF, like any KDF, is defined to produce output that is indistinguishable from random data ("The HKDF Scheme", http://eprint.iacr.org/2010/264.pdf , by Hugo Krawczyk, section 3). XORing a plaintext with a random keystream to produce ciphertext is a simple and secure approach to data encryption, epitomized by AES-CTR or a stream cipher (http://cr.yp.to/snuffle/design.pdf). HKDF is not the fastest way to generate such a keystream, but it is safe, easy to specify, and easy to implement (just HMAC and XOR).

Each keystream must be unique. SRP is defined to produce a random session key for each run (as long as at least one of the sides provides a random ephemeral key). We define resetToken to be a single-use randomly-generated value. Hence our two HKDF-XOR keystreams will be unique.

A slightly more-traditional alternative would be to use AES-CTR (with the same HMAC-SHA256 used here), with a randomly-generated IV. This is equally secure, but requires implementors to obtain an AES library (with CTR mode, which does not seem to be universal). An even more traditional technique would be AES-CBC, which introduces the need for padding and a way to specify the length of the plaintext. The additional specification complexity, plus the library load, leads me to prefer HKDF+XOR.

kB is equal to the XOR of wrapKey (which is a deterministic function of the user's email address, password, mainSalt, and the stretching parameters) and the server's randomly-generated wrap(kB) value, making kB a random value too. Using XOR as a wrapping function allows us to avoid sending kB or wrap(kB) in the initial createAccount arguments, which are not protected by SRP, and thus would enable an eavesdropper to mount a dictionary attack on the password (using wrap(kB) as their oracle).

Likewise, allowing the server to generate kA avoids exposure during createAccount. The only point of vulnerability is a forgotten-password resetAccount() message, if an eavesdropper can learn the resetToken. In this case, the attacker might either use the resetToken themselves (then use signToken to learn kA and wrap(kB)), or passively decrypt the resetAccount() arguments to retrieve just wrap(kB). Given wrap(kB) and some encrypted browser data, the attacker can guess passwords (and derive kB, etc) until the data decrypts properly.

To make this technique safe, any time kB or the password is changed, the mainSalt should be changed too. Otherwise knowledge of both wrap(old-kB) and old-kB would reveal wrapKey, making it easy to deduce the new kB. Changing mainSalt causes wrapKey to change too, preventing this.

mainSalt is incorporated at the end of the stretching process to allow it to proceed in parallel with the getToken1() call that retrieves the salt. The inputs to the lengthy stretch come entirely from the user (email and password) or are optimistically (stretching parameters). This speedup seems more important than the minor security benefit of including the salt at the beginning of the stretch.

There is no MAC on wrap(kB). If the keyserver chooses to deliver a bogus wrap(kB) or kA, the client will discover the problem a moment later when it talks to a storage server and attempts to retrieve data from an unrecognized collection-ID (since we intend to derive collection-IDs from the key used to encrypt their data, which will be derived from kA or kB as appropriate). It might be useful to add a checksum to kA and wrap(kB) to detect accidental corruption (e.g. store and deliver kA+SHA256(kA)), but this doesn't protect against intentional changes. We omit this checksum for now, assuming that disks will be reliable enough to let us never experience such failures.

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

To protect the server's session table memory and CPU usage for the initial SRP calculation, the server might require clients to perform busy-work before calling getToken1(). The server can control how much work is required.

The getToken1() call looks for a "X-PiCL-PoW:" HTTP header. Most of the time, clients don't supply this header. But if the server responds to the getToken1() call with an error that indicates PoW is required, clients must create a valid PoW string and include it as the value of an "X-PiCL-PoW:" header in their next call to getToken1().

The server's error message includes two parameters. The first is a "prefix string": the client's PoW string is required to begin with this prefix. The second is a "threshold hash". SHA256(PoWString) is required to be lexicographically earlier than the thresholdHashString (i.e. the numerical value of its hash must be closer to zero than the threshold). The client is expected to concatenate the prefix with a counter, then repeatedly increment the counter and hash the result until they meet the threshold, then re-submit their getToken1() request with the combined prefix+counter string in the header. If the client has spent more than e.g. 10 seconds doing this, the client should probably help the user cancel the operation and try again.

When a server is under a DoS attack (either via some manual configuration tool or sensed automatically), it should start requiring valid unique X-PiCL-PoW headers. The server should initially require very little work, by using a threshold hash with just a few leading zero bits. If this is insufficient to reduce the attack volume, the threshold should be lowered, requiring even more work (from both the attacker and legitimate clients).

The server should create a prefix string that contains a parseable timestamp and a random nonce (e.g. "%d-%d-" % (int(time.time()), b32encode(os.urandom(8)))). The server should also decide on a cutoff time (perhaps ten minutes ago). Each server must then maintain a table of "old PoW strings" to prevent replay attacks (these do not need to be shared among all servers: an in-RAM cache is fine).

When the server receives a proposed PoW string, it first splits off the leading timestamp, and if the timestamp is older than the cutoff time, it rejects the string (either by dropping the connection, or returning a new "PoW required" error if it's feeling nice). Then it hashes the whole string and compares it against the threshold, rejecting those which fail to meet the threshold. Finally, for strings that pass the hash threshold, it checks the "old strings" table, and rejects any that appear on that list.

If the PoW string makes it past all these checks, the server should add the string to the "old strings" table, then accept the request (i.e. compute an srpB value and add a session-id table entry for the request).

The old-strings table check should be optimized to reject present strings quickly (i.e. if we are under attack, we should expect to see lots of duplicates of the same string, and must minimize the work we do when this occurs).

The server can remove values from the old-strings table that have timestamps older than the cutoff time. The server can also discard values at other times (to avoid consuming too much memory), without losing anything but protection against resource consumption.

Other notes:

The server-side code for this can be deferred until we care to have a response to a DoS attack. However the client-side code for this must be present from day one, otherwise we won't be able to turn on the defense later without fear of disabling legitimate old clients.

The server should perform as little work as possible before rejecting a token. Every extra CPU cycle it spends in this path is increasing the DoS attack amplification factor.

The nonce in the prefix string exists to make sure that two successive clients get different prefixes, and thus do not come up with the same counter value (and inadvertently create identical strings, looking like a replay attack). If this proved annoying or expensive, we could instead obligate clients to produce their own nonce.

TBD: Is this worth it? Should the PoW string go into an HTTP header? (I want it to be cheap to extract, and not clutter logs). Should the error response be a distinctive HTTP error code so our monitoring tools can easily count them? We can also use this feature to slow down online guessing attacks (i.e. trigger it either when getToken1 is called too much or when getToken2 produces too many errors). Since getToken1() includes an email address, we could also requires PoWs for some addresses (e.g. those we know to be under attack) but not others.

Glossary

This defines some of the jargon we've developed for this protocol.

  • data classes: each type of browser data (bookmarks, passwords, history, etc) can be assigned, by the user, to either class-A or class-B
  • class-A: data assigned to this class can be recovered, even if the user forgets their password, by proving control over an email address and resetting the account. It can also be read by Mozilla (since it runs the keyserver and knows kA), or by the user's IdP (by resetting the account without the user's permission).
  • class-B: data in this class cannot be recovered if the password is forgotten. It cannot be read by the IdP. Mozilla (via the keyserver) cannot read this data, but can attempt a brute-force dictionary attack against the password.
  • kA: the master key for data stored as "class-A", a 32-byte binary string. Individual encryption keys for different datatypes are derived from kA.
  • kB: the master key for data stored as "class-B", a 32-byte binary string.
  • wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.
  • sessionToken: a long-lived per-device token which allows the device to obtained signed BrowserID certificates for the account's identity (GUID@picl-something.org). This token remains valid until the user revokes it (either by changing their password, or triggering some kind of "revoke a specific device" or "revoke all devices" function).

Test Vectors

The following example uses a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264).

These test vectors were produced by the python code in https://github.com/warner/picl-spec-crypto (revision aa441c6). The diagrams above may lag behind the latest version of that code.

stretch-KDF

email: 616e6472c3a94065 78616d706c652e6f 7267

password: 70c3a4737377c3b6 7264

K1 (scrypt input): f84913e3d8e6d624 689d0a3e9678ac8d cc79d2c2f3d96414 88cd9d6ef6cd83dd

K2 (scrypt output): 5b82f146a6412692 3e4167a0350bb181 feba61f63cb17140 12b19cb0be0119c5

stretchedPW: c16d46c31bee242c b31f916e9e38d60b 76431d3f5304549c c75ae4bc20c7108c

main-KDF

mainSalt (normally random): 00f0000000000000 0000000000000000 0000000000000000 000000000000034d

srpPW: 00f9b71800ab5337 d51177d8fbc682a3 653fa6dae5b87628 eeec43a18af59a9d

unwrapBKey: 6ea660be9c89ec35 5397f89afb282ea0 bf21095760c8c500 9bbcc894155bbe2a

internal x (base 10): 8192518690918 99580124814080709381476194749939 03899664126296984459627523279550

internal x (hex): b5200337cc3f3f92 6cdddae0b2d31029 c069936a844aff58 779a545be89d0abe

v (verifier as number) (base 10): 114649 57230405843056840989945621595830 71784395917725741221739574165799 54316134303691657140298181419198 87853709633756255809680435884948 69849281177012209169281795507853 57610332070005048463659745521969 83218225819721112680718485091921 64608360806562626442477160609654 43167308814558974899899506977051 96721477608178869100211706638584 53875100985456239693728258285562 04889672594983678412848291529879 88548996842770025110751388952323 22170663943486107183421205517476 84831590615660554713667726412525 73641352721966728239512914666806 49625530438034148797508015907639 67594925530663571631035463732161 30193328802116982288883318596822

SRP Verifier

k (base 10): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112

srpSalt (normally random): 00f1000000000000 0000000000000000 0000000000000000 0000000000000179

srpVerifier: 00173ffa0263e63c cfd6791b8ee2a40f 048ec94cd95aa8a3 125726f9805e0c82 83c658dc0b607fbb 25db68e68e93f265 8483049c68af7e82 14c49fde2712a775 b63e545160d64b00 189a86708c69657d a7a1678eda0cd79f 86b8560ebdb1ffc2 21db360eab901d64 3a75bf1205070a57 91230ae56466b8c3 c1eb656e19b794f1 ea0d2a077b3a7553 50208ea0118fec8c 4b2ec344a05c66ae 1449b32609ca7189 451c259d65bd15b3 4d8729afdb5faff8 af1f3437bbdc0c3d 0b069a8ab2a959c9 0c5a43d42082c774 90f3afcc10ef5648 625c0605cdaace6c 6fdc9e9a7e6635d6 19f50af773452247 0502cab26a52a198 f5b00a2798589165 07b0b4e9ef9524d6

SRP B

private b (normally random) (base 10): 1198277 66042000957856349411550091527197 08125226960544768993643095227800 29105361555030352774505625606029 71778328003253459733139844872578 33596964143417216389157582554932 02841499373672188315534280693274 23189198736863575142046053414928 39548731879043135718309539706489 29157321423483527294767988835942 53343430842313006332606344714480 99439808861069316482621424231409 08830704769167700098392968117727 43420990997238759832829219109897 32876428831985487823417312772399 92628295469389578458363237146486 38545526799188280210660508721582 00403102624831815596140094933216 29832845626116777080504444704039 04739431335617585333671378812943

private b (hex): 00f3000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 000000000000000f

transmitted srpB: 0022ce5a7b9d8127 7172caa20b0f1efb 4643b3becc535664 73959b07b790d3c3 f08650d5531c19ad 30ebb67bdb481d1d 9cf61bf272f84398 48fdda58a4e6abc5 abb2ac496da5098d 5cbf90e29b4b110e 4e2c033c70af7392 5fa37457ee13ea3e 8fde4ab516dff1c2 ae8e57a6b264fb9d b637eeeae9b5e43d faba9b329d3b8770 ce89888709e02627 0e474eef822436e6 397562f284778673 a1a7bc12b6883d1c 21fbc27ffb3dbeb8 5efda279a69a1941 4969113f10451603 065f0a0126666456 51dde44a52f4d8de 113e2131321df1bf 4369d2585364f9e5 36c39a4dce33221b e57d50ddccb4384e 3612bbfd03a268a3 6e4f7e01de651401 e108cc247db50392

SRP A

private a (normally random) (base 10): 1193346 47663227291363113405741243413916 43482736314616601220006703889414 28162541137108417166380088052095 43910927476491099816542561560345 50331133015255005622124012256352 06121987030570656676375703406470 63422988042473190059156975005813 46381864669664357382020200036915 26156674010218162984912976536206 14440782978764393137821956464627 16314542157937343986808167341567 89864323268060014089757606109012 50649711198896213496068605039486 22864591676298304745954690086093 75374681084741884719851454277570 80362211874088739962880012800917 05751238004976540634839106888223 63866455314898189520502368799907 19946264951520393624479315579863

private a (hex): 00f2000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 000000000000d3d7

transmitted srpA: 007da76cb7e77af5 ab61f334dbd5a958 513afcdf0f47ab99 271fc5f7860fe213 2e5802ca79d2e5c0 64bb80a38ee08771 c98a937696698d87 8d78571568c98a1c 40cc6e7cb101988a 2f9ba3d65679027d 4d9068cb8aad6ebf f0101bab6d52b5fd fa81d2ed48bba119 d4ecdb7f3f478bd2 36d5749f2275e948 4f2d0a9259d05e49 d78a23dd26c60bfb a04fd346e5146469 a8c3f010a627be81 c58ded1caaef2363 635a45f97ca0d895 cc92ace1d09a99d6 beb6b0dc0829535c 857a419e834db128 64cd6ee8a843563b 0240520ff0195735 cd9d316842d5d3f8 ef7209a0bb4b54ad 7374d73e79be2c39 75632de562c59647 0bb27bad79c3e2fc ddf194e1666cb9fc

SRP key-agreement

u: b284aa1064e87751 50da6b5e2147b47c a7df505bed94a6f4 bb2ad873332ad732

S: 0092aaf0f527906a a5e8601f5d707907 a03137e1b601e04b 5a1deb02a981f4be 037b39829a27dba5 0f1b27545ff2e287 29c2b79dcbdd32c9 d6b20d340affab91 a626a8075806c26f e39df91d0ad979f9 b2ee8aad1bc783e7 097407b63bfe58d9 118b9b0b2a7c5c4c debaf8e9a460f4bf 6247b0da34b760a5 9fac891757ddedca f08eed823b090586 c63009b2d740cc9f 5397be89a2c32cdc fe6d6251ce11e44e 6ecbdd9b6d93f30e 90896d2527564c7e b9ff70aa91acc0ba c1740a11cd184ffb 989554ab58117c21 96b353d70c356160 100ef5f4c28d19f6 e59ea2508e8e8aac 6001497c27f362ed bafb25e0f045bfdf 9fb02db9c908f103 40a639fe84c31b27

M1: 27949ec1e0f16256 33436865edb037e2 3eb6bf5cb91873f2 a2729373c2039008

srpK: e68fd0112bfa31dc ffc8e9c96a1cbadb 4c3145978ff35c73 e5bf8d30bbc7499a

/auth

srpK: e68fd0112bfa31dc ffc8e9c96a1cbadb 4c3145978ff35c73 e5bf8d30bbc7499a

respHMACkey: 6584613597ef012f f1752b7869f01d03 c72547a7b7199681 531d9df1991edf23

respXORkey: 455835926ae37a1b 627bd16affbeeab6 27ecc737121826ca 4a2bac2c100bf417

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

plaintext: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

ciphertext: 253957f10e861c7c 0a12bb0193d384d9 579db544666d50bd 3252d6576c768a68

MAC: a98c87f5769ab4cc ca3df863faeb217e b16ddc29d712b301 12b446324ee806d6

response: 253957f10e861c7c 0a12bb0193d384d9 579db544666d50bd 3252d6576c768a68 a98c87f5769ab4cc ca3df863faeb217e b16ddc29d712b301 12b446324ee806d6

authtoken

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID (authToken): 9a39818e3bbe6132 38c9d7ff013a1841 1ed2c66c3565c3c4 de03feefecb7d212

reqHMACkey: 4a17cbdd54ee17db 426fcd7baddff587 231d7eadb408c091 ce19ca915b715985

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

/session

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

respHMACkey: cd3f50403d060b21 76d32f71ca105bd8 7c9b6c4e10e3ebf9 3f5077bec2db24fa

respXORkey: 8422c53143dea9c6 044afbe95228f291 74996b830b1794a3 eff132da53174d43 92eeb87ccf8ad7a8 0c432894e066de6b 0ff70658dfbf2f07 b9c7704045edcd54

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

sessionToken: a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

plaintext: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

ciphertext: 04a347b2c75b2f41 8cc37162dea57c1e e408f9109f820234 7768a841cf8ad3dc 324f1adf6b2f710f a4ea823f4ccb70c4 bf46b4eb6b0a99b0 017ecafbf95073eb

MAC: 7973ddbb184b601a c4df09704028ebfc 754dd50e7d8eebfa 52ce3fd868c69852

response: 04a347b2c75b2f41 8cc37162dea57c1e e408f9109f820234 7768a841cf8ad3dc 324f1adf6b2f710f a4ea823f4ccb70c4 bf46b4eb6b0a99b0 017ecafbf95073eb 7973ddbb184b601a c4df09704028ebfc 754dd50e7d8eebfa 52ce3fd868c69852

/account/keys

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

tokenID (keyFetchToken): 3d0a7c02a15a62a2 882f76e39b6494b5 00c022a8816e0486 25a495718998ba60

reqHMACkey: 87b8937f61d38d0e 29cd2d5600b3f4da 0aa48ac41de36a0e fe84bb4a9872ceb7

keyRequestKey: 14f338a9e8c6324d 9e102d4e6ee83b20 9796d5c74bb734a4 10e729e014a4a546

respHMACkey: f824d2953aab9faf 51a1cb65ba9e7f9e 5bf91c8d8fd1ac1c 8c2d31853a8a1210

respXORkey: ce7d7aa77859b235 9932970bbe2101f2 e80d01faf9191bd5 ee52181d2f0b7809 8281ba8cff392543 3a89f7c3095e0c89 900a469d60790c83 3281c4df1a11c763

kA: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

plaintext: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

ciphertext: ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 c2c0f8cfbb7c6304 72c0bd88451342c6 c05b14ce342c5ad4 6ad89e84464c993c

MAC: 3927d30230157d08 17a077eef4b20d97 6f7a97363faf3f06 4c003ada7d01aa70

response: ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 c2c0f8cfbb7c6304 72c0bd88451342c6 c05b14ce342c5ad4 6ad89e84464c993c 3927d30230157d08 17a077eef4b20d97 6f7a97363faf3f06 4c003ada7d01aa70

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

unwrapBKey: 6ea660be9c89ec35 5397f89afb282ea0 bf21095760c8c500 9bbcc894155bbe2a

kB: 2ee722fdd8ccaa72 1bdeb2d1b76560ef ef705b04349d9357 c3e592cf4906e075

use session (certificate/sign, etc)

sessionToken: a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf

tokenID (sessionToken): c0a29dcf46174973 da1378696e4c82ae 10f723cf4f4d9f75 e39f4ae3851595ab

reqHMACkey: 9d8f22998ee7f579 8b887042466b72d5 3e56ab0c094388bf 65831f702d2febc0

/password/change

requestKey: 9d93978e662bfc6e 8cc203fa4628ef5a 7bf1ddfd7ee54e97 ec5c033257b4fca9

respHMACkey: 60d01e3d1da53b10 93124a30c26889d7 b2e067e7a09fde14 6f935e3c653614f9

respXORkey: 3de5bd5e80faf84a dfca5396148123ef 8184cd4bc10a7c8a db1688495affee67 e07f80d914c5105f c86d6af24c4be1b1 ef6c9c661422ac43 181b3d29624a0cc2

keyFetchToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

plaintext: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

ciphertext: bd643fdd047f7ecd 5743d91d980cad60 11155fd8559fea1d 438f12d2c66270f8 20be421ad000d698 00a4a03980862f7e 3fbd4eb5c0f77a94 c0c2e7f2be97d21d

MAC: 804fc4bc30923cc0 d6c07ffea954848e 0076b94f7deee71f a34db5c106d91980

response: bd643fdd047f7ecd 5743d91d980cad60 11155fd8559fea1d 438f12d2c66270f8 20be421ad000d698 00a4a03980862f7e 3fbd4eb5c0f77a94 c0c2e7f2be97d21d 804fc4bc30923cc0 d6c07ffea954848e 0076b94f7deee71f a34db5c106d91980

/account/reset

accountResetToken: c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf

tokenID (accountResetToken): 46ec557e56e531a0 58620e9344ca9c75 afac0d0bcbdd6f8c 3c2f36055d9540cf

reqHMACkey (for HAWK): 716ebc28f5122ef4 8670a48209190a16 05263c3188dfe452 56265929d1c45e48

requestKey: aa5906d2318c6e54 ecebfa52f10df4c0 36165c230cc78ee8 59f546c66ea3c126

reqHMACkey (for ciphertext): a0d894a6232f2e78 66a51dda3f84e01e ae5adb812564f391 6c0d3cb16bdb743c

reqXORkey: 9cbde8fc9df31455 837b881e6c0d7e3c ca13589bc868c527 95fc00e51f2048ab d56de37629cda0b0 3f580a9e6c433724 b5df12a735ccf2a1 e232d4f5fef84f86 a1b4fdc47f8d1f73 12a6a230a8742d5b c144ee9abce25b57 9670b81085064cfb dcab862d9d57abcc 2142dcdde6682281 d378c89b0dce06ae cd1c1ff68ad6db9a 9cab0b02e160805b 59bb8712c8233056 1b3ded75c430e23c 22338833b6f2ba39 f5015ca7a905d6ee 6ec5b1e3ae5204ba 6f3630ebf30ebbac 1f47329e8fe22770 2a3d61f593328dd4 f0a96b628aa8ffec 181e93d2af8d87ff 2d90d67caaf7f7c9 af024c93cfc79e94 67ba70b3076c20cc 141aa254ff159b25 3125a304441cecf3 4fc1845ce96ee598 21fde83cd24e3209 4d304477bfa2c8ed df236e512560694e

wrapkB: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f

newSRPv: 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111

plaintext: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111 1111111111111111

ciphertext: dcfcaabfd9b65212 cb32c25520403073 9a420ac89c3d9370 cda55abe437d16f4 c47cf26738dcb1a1 2e491b8f7d522635 a4ce03b624dde3b0 f323c5e4efe95e97 b0a5ecd56e9c0e62 03b7b321b9653c4a d055ff8badf34a46 8761a90194175dea cdba973c8c46badd 3053cdccf7793390 c269d98a1cdf17bf dc0d0ee79bc7ca8b 8dba1a13f071914a 48aa9603d9322147 0a2cfc64d521f32d 33229922a7e3ab28 e4104db6b814c7ff 7fd4a0f2bf4315ab 7e2721fae21faabd 0e56238f9ef33661 3b2c70e482239cc5 e1b87a739bb9eefd 090f82c3be9c96ee 3c81c76dbbe6e6d8 be135d82ded68f85 76ab61a2167d31dd 050bb345ee048a34 2034b215550dfde2 5ed0954df87ff489 30ecf92dc35f2318 5c215566aeb3d9fc ce327f403471785f

MAC: 1d3572fe0b4bdf66 f2b2657cb2ee56fc 80f7a82708cafd82 1952e1f01761cb29

response: dcfcaabfd9b65212 cb32c25520403073 9a420ac89c3d9370 cda55abe437d16f4 c47cf26738dcb1a1 2e491b8f7d522635 a4ce03b624dde3b0 f323c5e4efe95e97 b0a5ecd56e9c0e62 03b7b321b9653c4a d055ff8badf34a46 8761a90194175dea cdba973c8c46badd 3053cdccf7793390 c269d98a1cdf17bf dc0d0ee79bc7ca8b 8dba1a13f071914a 48aa9603d9322147 0a2cfc64d521f32d 33229922a7e3ab28 e4104db6b814c7ff 7fd4a0f2bf4315ab 7e2721fae21faabd 0e56238f9ef33661 3b2c70e482239cc5 e1b87a739bb9eefd 090f82c3be9c96ee 3c81c76dbbe6e6d8 be135d82ded68f85 76ab61a2167d31dd 050bb345ee048a34 2034b215550dfde2 5ed0954df87ff489 30ecf92dc35f2318 5c215566aeb3d9fc ce327f403471785f 1d3572fe0b4bdf66 f2b2657cb2ee56fc 80f7a82708cafd82 1952e1f01761cb29

/account/destroy

authToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID (authToken): 9a39818e3bbe6132 38c9d7ff013a1841 1ed2c66c3565c3c4 de03feefecb7d212

reqHMACkey: 4a17cbdd54ee17db 426fcd7baddff587 231d7eadb408c091 ce19ca915b715985

Keyserver Protocol Summary

  • POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
    • creates a user account
  • GET /account/devices [sessionToken] () -> list of devices
  • GET /account/keys [keyFetchToken,needs-verf] () -> kA/wrap(kB)
    • single-use, only if email is verified, encrypted results
  • POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
    • single-use, does not require email to be verified, revoke all tokens for account, send notification email to user
  • POST /account/delete [authToken] () -> ok, account deleted
  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authToken] () -> keyFetchToken, sessionToken
  • POST /session/destroy [sessionToken] () -> ok
    • for detaching a device, destroy all tokens
  • POST /recovery_email/status [sessionToken] () -> "verified" status of email
    • use "Accept: text/event-stream" header for server-sent-events; server will send "update" event with the new content of the resource any time it changes.
  • POST /recovery_email/resend_code [sessionToken] () -> re-send verification email
  • POST /recovery_email/verify_code (code) -> set "verified" flag
    • this code will come from a clickable link and is an unauthenticated endpoint
    • this could maybe take the recovery method if that would be helpful
    • sets verified flag on recovery method
  • POST /certificate/sign [sessionToken,needs-verf] (pubkey) -> cert
    • only if recovery email is verified
  • POST /password/change/start [authToken,needs-verf] () -> accountResetToken, keyFetchToken
  • POST /password/forgot/send_code () -> forgotPasswordToken
    • sends code to recovery method (email for now, maybe SMS later)
    • this is a short code, not a clickable link
  • POST /password/forgot/resend_code (forgotPasswordToken) -> re-sends code
  • POST /password/forgot/verify_code (forgotPasswordToken, code) -> accountResetToken
    • sets verified flag on recovery method
  • POST /get_random_bytes


Typical Client Flows

Create account

  • POST /account/create (email,srpV,srpSalt) -> ok (server sends verification email)
  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authed with authToken]() -> keyFetchToken, sessionToken
  • GET /recovery_email/status [sessionToken] () -> "verified" status
    • (optional, only if user requests resend) POST /recovery_email/resend_code [sessionToken]() -> ok
    • POST /recovery_email/verify_code (code) -> ok
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
  • POST /certificate/sign [sessionToken] (pubkey) -> cert


Attach to new device

  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /session/create [authToken] () -> keyFetchToken, sessionToken
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
    • (if unverified-error, do waitUntilEmailVerified, then try again)
  • POST /certificate/sign [sessionToken] (pubkey) -> cert


Forgot password

  • POST /password/forgot/send_code (email) -> forgotPasswordToken
  • POST /password/forgot/verify_code (forgotPasswordToken, code) -> accountResetToken
  • POST /account/reset [authed+encrypted by accountResetToken] (0000,srpV,srpSalt) -> ok
  • GOTO "Attach to new device"


Change Password

  • POST /auth/start (email) -> srpToken,SRP stuff
  • POST /auth/finish (srpToken,SRP stuff,deviceInfo) -> authToken
  • POST /password/change/start [authToken] () -> accountResetToken, keyFetchToken
  • GET /account/keys [keyFetchToken] () -> kA/wrap(kB)
  • POST /account/reset [authed+encrypted by accountResetToken] (wrap(kB),srpV,srpSalt) -> ok
  • GOTO "Attach to new device"