Changes

Jump to: navigation, search

Identity/AttachedServices/KeyServerProtocol

2,064 bytes added, 07:04, 10 July 2013
lots of updates
* define how wrap(kB) is unwrapped to get kB
* confirm this is actually implementable inside Firefox (especially w.r.t. NSS)
 
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.
= Email+Password -> SignToken/ResetToken Creating The Account =
The first interaction with the keyserver takes an email+password pair and receives back (kA, wrap(kB), token). This starts act performed by using key-stretching a user is to transform create the account. They enter email+password into a "masterKey"their browser, which 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 does the keyserver, resulting in three valuesfollowing steps: 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.
[[File* 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:PICL8, p:1}, secondPBKDF:20000}.* randomly choose a 32-IdPAuthbyte mainSalt (this should be unique, but is not secret)* choose the SRP group parameters (fixed: use the 2048-bigpix.png|IdP Auth Big Picture]]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) to the keyserver's createAccount() API
This same protocol is used, with slightly different methods and constantsTo limit abuse, to obtain the createAccount() should also require a fresh "resetTokencreateToken". This should be created by some other API, outside the scope of this document, that perhaps requires a CAPTCHA or something. createAccount() might also require a proof-of-work token, as described below.
The protocol is optimized to minimize roundserver, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-trips and to enable parallelismbyte) strings. As a resultIt stores these, along with all the remaining values, indexed by email, in the two messages it sends (getToken1 and getToken2) each perform multiple jobsaccount table where they can be retrieved by getToken later.
== getToken1 ==After creating the account, the client immediately runs getToken("sign"), as described below, to fetch kA and wrap(kB). It then unwraps wrap(kB) by XORing it with wrapKey to obtain kB.
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.
== ProofEmail+Password -Of-Work => SignToken/ResetToken =
To protect connect a browser to an existing account, we use the server's session table memory following "getSignToken" protocol to transform an email+password pair into (kA, kB, signToken). "kA" and CPU usage for "kB" enable the initial SRP calculationbrowser to encrypt/decrypt synchronized data records, while "signToken" is used to convince the storage server might require clients to perform busy-work before calling getToken1(). The server can control how much work is requiredaccept read and write requests for those records.
The getToken1() call looks for This protocol starts by using key-stretching to transform the email+password into a "X-PiCL-PoW:stretchedPW" HTTP header. Most of the time, clients don't supply then feeds this headerinto an SRP protocol to get a session key. But if the server responds It uses this session key to decrypt a bundle of encrypted data from the getToken1keyserver, resulting in three values: kA, wrap(kB) 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 signToken. The stretchedPW is also used to getToken1derive the key that will decrypt wrap(kB)into the actual kB value.
The server's error message includes two parameters. The first is a "prefix string"[[File: 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 rePICL-IdPAuth-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 againbigpix.png|IdP Auth Big Picture]]
When a server This same protocol is under a DoS attack (either via some manual configuration tool or sensed automatically)used, with slightly different methods and constants, it should start requiring valid unique X-PiCL-PoW headersto obtain the "resetToken". The server should initially require very little work, by using This token allows a threshold hash with just a few leading zero bits. If this is insufficient client to reduce the attack volume, the threshold should be lowered, requiring even more work (from both safely reset the attacker and legitimate clients)account password.
The server should create a prefix string that contains a parseable timestamp protocol is optimized to minimize round-trips and to enable parallelism, to reduce the time it takes to connect a random nonce (e.g. "%d-%d-" % (int(time.time()), b32encode(os.urandom(8)))). The server should also decide on browser to the account to just a cutoff time (perhaps ten minutes ago)few seconds. Each server must then maintain As a table of "old PoW strings" to prevent replay attacks result, the two messages it sends (these do not need to be shared among all servers: an in-RAM cache is finegetToken1 and getToken2)each perform multiple jobs.
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.== getToken1 ==
If As soon as the PoW string makes it past all these checksuser finishes typing in the email address, the server client should add send it in the string "getToken1" message to the "old strings" tablekeyserver. The response will include a set of parameters that are needed for key-stretching (described below), then accept and the common parameters used by both sides of the request (iSRP protocol to follow.eThese are simply looked up in a database entry for the client, along with an account-id. compute It must also include an srpB value and add a allocated session-id table entry for 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 oldTo mitigate DoS abuse, getToken1() may also require a proof-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 -work string, and must minimize the work we do when this occurs)described below.
The server can remove values from the old== Client-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:Side Key Stretching ==
The server"Key Stretching" is the practice of running a password through a computationally-expensive one-side code way function before using it for this can be deferred until we care encryption or authentication. The goal is to have a response to a DoS attack. However the clientmake brute-side code for this must be present from day oneforce dictionary attacks more expensive, otherwise we won't be able to turn on by raising the defense later without fear cost of disabling legitimate old clientstesting each guess.
The 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 should perform . We use the memory-hard "scrypt" function (pronounced "ess-crypt") for this purpose, as little work as possible before rejecting a tokenmotivated 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". Every extra CPU cycle it spends in To provide at least minimal protection against this path is increasing helper, we sandwich the DoS attack amplification factorscrypt step between two PBKDF2 steps.The complete protocol looks like this:
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[[File: 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.gPICL-IdPAuth-stretch-KDF. those we know to be under attack) but not others. == Client-Side Key png|Stretching == 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.KDF]]
[[FileOur 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:PICL-IdPAuth-stretch-KDFlowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).png|Stretching KDF]]
A later version of Since 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 , 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" mainSalt is added *after* the stretching, to enable this parallelism (at a tiny cost in security).
After "masterKeystretchedPW" is derived, a second HKDF call is used to derive "unwrapKeysrpPW" and "srpPWunwrapBKey" 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 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.
After using resetAccount, clients should immediately perform the getToken(sign) protocol. If the old password was forgotten, this is necessary to fetch kA. In either case, a new signToken is required, since old signTokens are revoked by resetAccount. Clients should retain the srpPassword value during this process to avoid needing to run the lengthy key-stretching routine a second time.
 
= Creating the Account =
 
To create the account in the first place, the client starts with email+password, then does the following steps:
 
* decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement)
* decide upon a stretchSalt (remembering this should be unique, but is not secret)
* decide upon SRP parameters (generally fixed)
* perform key-stretching, derive masterKey and srpPW and wrapKey
* create srpVerifier, using srpPW and the SRP parameters
* deliver many values to the keyserver: parameters for stretching and SRP, salts, and the srpVerifier
 
 
The server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit strings. It stores these, along with all the remaining values, in the account table where they can be retrieved by getToken later.
 
After creating the account, the client immediately runs getToken("sign") to fetch kA and wrap(kB). It then unwraps wrap(kB) by XORing it with wrapKey to obtain kB.
= Crypto Notes =
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, stretchSaltmainSalt, 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 stretchSalt 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 stretchSalt mainSalt causes wrapKey to change too, preventing this.
stretchSalt 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.
 
= 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 =
Confirm
471
edits

Navigation menu