Identity/AttachedServices/KeyServerProtocol: Difference between revisions
| Line 198: | Line 198: | ||
[[File:PICL-IdPAuth-resetAccount.png| | [[File:PICL-IdPAuth-encrypt-resetAccount.png|Client encrypts resetAccount request]] | ||
The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values. | The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values. | ||
The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password). | The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password). | ||
Clients might use resetAccount for two reasons: | Clients might use resetAccount for two reasons: | ||
Revision as of 03:08, 12 July 2013
PiCL Key Server / IdP Protocol
NOTE: This specification is under active development (10-Jul-2013). Several pieces are not yet complete. If you write any code based on this design, keep a close eye on this page and/or contact me (warner) on the #picl IRC channel to learn about changes. Eventually this will be nailed down and should serve as a stable spec for the PICL keyserver/IdP protocol.
The server is being developed in https://github.com/mozilla/picl-idp . This repo currently include a demonstration client (node.js CLI).
Remaining TODO items:
- decide on client-side key-stretching parameters
- 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
- confirm this is actually implementable inside Firefox (especially w.r.t. NSS)
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) to the keyserver's createAccount() API
To limit abuse, the createAccount() should also require a fresh "createToken". 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 server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-byte) strings. It stores these, along with all the remaining values, indexed by email, in the account table where they can be retrieved by getToken later.
After creating the account, the client immediately runs getToken("sign"), as described below, to fetch kA and wrap(kB). It then unwraps wrap(kB) by XORing it with wrapKey to obtain kB.
Email+Password -> SignToken/ResetToken
To connect a browser to an existing account, we use the following "getSignToken" protocol to transform an email+password pair into (kA, kB, signToken). "kA" and "kB" enable the browser to encrypt/decrypt synchronized data records, while "signToken" is used to convince the storage server to accept read and write requests for those records.
This protocol starts by using key-stretching to transform the email+password into a "stretchedPW", then feeds this into an SRP protocol to get a session key. It uses this session key to decrypt a bundle of encrypted data from the keyserver, resulting in three values: kA, wrap(kB), and the signToken. The stretchedPW is also used to derive the key that will decrypt wrap(kB) into the actual kB value.
This same protocol is used, with slightly different methods and constants, to obtain the "resetToken". This token allows a client to safely reset the account password.
The protocol is optimized to minimize round-trips and to enable parallelism, to reduce the time it takes to connect a browser to the account to just a few seconds. As a result, the two messages it sends (getToken1 and getToken2) each perform multiple jobs.
getToken1
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.
To mitigate DoS abuse, getToken1() 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:
Our initial parameter choices for this stretching are to use 20000 iterations of PBKDF2 on each side (40k in all), and to run scrypt with N/r/p set to 64k/8/1. This requires roughly 60MB of RAM for about 1.3 seconds on a 1.8GHz Intel Atom linux box (it will run faster, but use the same memory, on more modern CPUs). We are still studying performance on various platforms to decide what parameters to use in V1: lowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).
Since the stretching is expected to take a second or two, the client can optimistically start this process (using default parameters) before receiving the getToken1() response, and then check that it used the right parameters afterwards (repeating the operation if not). (We'll want to build the stretching function with periodic checkpoints so that we don't have to lose all progress if the parameters turn out to be wrong). The "mainSalt is added *after* the stretching, to enable this parallelism (at a tiny cost in security).
After "stretchedPW" is derived, a second HKDF call is used to derive "srpPW" and "unwrapBKey" which will be used later.
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 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): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112
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.
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 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.
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.
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.
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 getToken2() message. The server compares the client's copy of M1 against its own. If they match, the client knew the password and the server can safely respond with the encrypted account data. If they do not match, the client (or a man-in-the-middle attacker) did not know the password, and the client should increment a counter that can trigger defenses against online guessing attacks. The server must then return an error to the client, and not use or reveal srpK (or the correct M1) in any way.
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 examples above were brute-forced to ensure that "srpVerifier", "srpA", "srpB", and "S" all wind up with leading zeros, to exercise the padding code in compatible implementations. If you are having problems getting your code to match these results, add some assertions to test that the stringified integers being put into hashes are exactly 256 bytes long.
The client does its entire SRP calculation in a single step, after receiving the server's "B" value. It creates its "A" value, computes the shared secret S, and the proof-of-knowledge M1. It sends both "A" and "M1" in the same message (getToken2).
The 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.
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.
getToken2
This method has two flavors, one for obtaining "signing tokens", the other for getting "reset tokens". TBD: either we'll have two different method names / API endpoints (getToken2Sign and getToken2Reset), or we'll pass an argument to a single "getToken2" method that indicates either "sign" or "reset". (using different endpoints would make it easier to monitor server load).
The client-side SRP calculation results in two values that are sent to the server in the "getToken2()" message: "srpA" and "srpM1". "A" is the client's contribution to the SRP protocol. "M1" is an output of this protocol, and proves (to the server) that this client knew the right password.
The server feeds "A" into its own SRP calculation and derives (hopefully) the same "S" value as the client did. It can then compute its own copy of M1 and see if it matches. If not, the client (or a man-in-the-middle) did not get the right password, and the server will return an error and increment it's "somebody is trying to guess passwords" counter (which will be used to trigger defenses against online guessing attacks). If it does match, then both sides can derive the same "K" session key.
The server then allocates a token for this device, and encrypts kA/wrap(kB)/token with the session key. The server returns a success message with the encrypted bundle.
Future variants (e.g. to fetch a third kind of token) might put additional values in the response to getToken2.
Decrypting the getToken2 Response
The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.
The respXORkey is used to encrypt the concatenated kA/wrap(kB)/token string, by simply XORing the two. This ciphertext is then protected by a MAC, using HMAC-SHA256, keyed by respHMACkey. The MAC is appended to the ciphertext, and the whole bundle is returned to the client.
The 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.
Since the kA/wrap(kB)/signToken response is so similar to the kA/wrap(kB)/resetToken response, the same code can be used to check+decrypt both. However remember that the respXORkey/respHMACkey is derived differently for each (using different "context" values).
Unwrapping kB
The server-provided wrap(kB) value is simply XORed with the password-derived wrapKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).
Signing Certificates
The Sign Token is used to derive two values:
- tokenID
- request HMAC key
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.
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.
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 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.
Changing the Password
Resetting the Account
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.
So the single-use resetToken is used to derive three values:
- tokenID
- request HMAC key
- request XOR key
The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values.
The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).
Clients might use resetAccount for two reasons:
- 1: Changing their password (i.e. they know the old one). In this case, the resetToken is acquired from getToken("reset"), and they know both kA and kB.
- 2: resetting an account (i.e. they forgot the old password). Here, resetToken was acquired by proving control over the account email address (through a mechanism not described in this protocol). The client does not know kA or kB.
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.
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(SKM=localEntropy+remoteEntropy, salt="", context=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.
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.
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 . The diagrams may lag behind the latest version of that code.
stretch-KDF
email: 616e6472c3a94065 78616d706c652e6f 7267
password: 70c3a4737377c3b6 7264
K1: f84913e3d8e6d624 689d0a3e9678ac8d cc79d2c2f3d96414 88cd9d6ef6cd83dd
K2: 5b82f146a6412692 3e4167a0350bb181 feba61f63cb17140 12b19cb0be0119c5
stretchedPW: c16d46c31bee242c b31f916e9e38d60b 76431d3f5304549c c75ae4bc20c7108c
main-KDF
mainSalt (normally random): 0001020304050607 08090a0b0c0d0e0f 1011121314151617 18191a1b1c1d1e1f
srpPW: 1ea1cdd3ba3bb3a8 b6c46331123f48a6 746f143014f5a389 24e6fea4dc1c1289
unwrapBKey: 94995fc5423827df 42d598076eccd996 656183a309e9fbaf e5026431d338b115
internal x (base 10): 137598577746 20950182695987769526622297924214 84665105182182372602062190994896
internal x (hex): 030ac7c51717e1d5 35d59725cb7c49fb 4936b7db7fcd0f10 17d3f1ffef50e5d0
v (verifier as number) (base 10): 914205 81735470597826353382993806239740 11269014547415710721123623779303 40577934966702279822019429237166 85777414275311785862734740227831 07992726500267571929376592901913 18064022616547283873724124275736 15772870036501836242138681573039 79134196479839195060563726666145 91886023021879280485567507056439 54248700318708010771386921629212 79420638327471362434416724355461 72934544873168338790005382928033 88728242355654245060876357499918 14028809240099280481407481230320 68269674162426430261934197103097 99968937656431136067890224663385 48685839318699687268537410286424 14469699859895598445302042986990 86071206600454059861610331587789
SRP Verifier
k (base 10): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112
srpSalt (normally random): 00f1000000000000 0000000000000000 0000000000000000 0000000000000070
srpVerifier: 00b9648c840be3e4 5ae305640dc24c64 ee3a1fb083bcafe4 0e10b37d04ea5a55 05c7538f6a72a6b9 748c97b2fd6d4dc4 89a2cbee5ae2ea9d cb7f2dbe1ad99518 75029ebb7e2f2bd5 bce1a619038092f4 2ea4ccba99665bc4 fd6c3c393d961b1c a2b8f61da5a81c2c cfdf89d28bc256fb b201b79908f64613 4a41fd1ae451f62f ccb00809b5ef8b05 7f198296e5aad231 baf321487d6abfed 2070556097720d5e f48d45724749c7a8 73768238bbd01123 0d004d5d487cc6ac ee40e6ab13a33f64 bd702d5c754167f2 230bb1d15151c070 7ef25d2787727424 32ea0537e95c1a04 d3006d10d99a1c7d 3318d284dc92460d 84dae38b4b698433 61008de94bd744cd
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 04739431335617585333671378812933
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 0000000000000005
transmitted srpB: 00de219f6b48de47 bb8a20e450d50cef 10c9c9fbc80482c2 9792f89f9bbfd27f a0e082209f919128 e29a8ffadfdcb48b e0000fc447d05afd 59b6032581de5596 8ff5f39034fc1aea d033b246660e4257 44fadcb824e7a14e fa6d2fc57502b9a7 b9cef2935a54c2c8 9d24589f1aa9091b 5f2981096e936592 dbbe0adfcb9b97f2 e677f5cb2112d90b 802af7df98eb29c5 31556f62d473e84e 50f70ac6d89e0503 228fb27eaca19f40 03f28516fb8b46c4 122510a9557c6d24 65bb13579e8ddbe5 7aa842d8ccd956f3 5643f43a4da35920 2485e21a6fede4a9 b3d55ee48eab9572 56f75283aed2c06a 9eb03f3feb29cb3b 6dbf644bcd8088cf 777072eb8b6b870e
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 19946264951520393624479315528231
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 0000000000000a27
transmitted srpA: 00e1cad4005422e9 f1bf68ba186c75ed 73f199ec71c2cdf0 e74baa258f9e594a ab48c2d74f064e45 57cfbf05fa0a1c17 491ae8a0568f8f40 04a8529414c6d124 7bf4fe98d0596792 3997eb362fc0cb47 39fde90abe8623d2 38d80003bb6a66e0 987748a329596d54 a2514d50d426e2ac 00eb4c7939977bc1 535625c19e34db08 0a8742af30f3f975 bf668097d0c303c5 ad6408729dad779e ca0ebcb8688bff01 e7781510504fdd4a 5f3b7806353378b2 e98fa90ab9224d7a 9973becafd8b428a 9d8e64bc79cd505c 2f976adacc9947f3 0b1e819f3b958b18 be8f3d2c84726faa 23bdae8e74ecda07 d279f75b25cb9c33 274f42fe038c3d5b 058b2877eb60841c
SRP key-agreement
u: 802ae3bc2a4f3117 77e24280b08ceda0 99d46ba99a65a750 e771229efca2aea3
S: 00516c2f83d5602c 17547fba0c6f3171 bc2cb6462670ec08 f8a0d7bc46eb015b f40dfc06c5be9492 40628605bf2bf598 3ae15679cc7968eb a15249e96f561d47 f6b7d8b43e9ebaeb 127daa2536d52c05 d412c10ea2485fd0 bb90d716a9d36163 83fd3fbcc7981c3b 769bfb5c42244e83 8cfb9fbaf0f37e6b 3336e2af42c6e615 90a7be399352a2a8 b47d85437ffb0859 9f821993ef05328f d07470ba1db050bc d588cc10a6e8dd68 e61d787b5e4a0634 5d5e90ee60c5ab95 329de9526c5684bf 5dad4c75450885a3 249709d37e0ba85e 67ee644ca01ee1de 9ebe3b730a6dd188 6300abae80ebeb68 f0e141cb71a861c7 d42f50d1723fbe0b 78b46305dffb3b90
M1: f8c82e57d1771a24 229c05858cc03bee ea3a7b73d39939d5 a3a8dbbc9dc474f2
srpK: 94ad3e71e29ceb1f 2ed2b80996314344 6cfa5d8640c271dd b632f094f7eda7c7
getSignToken request
srpK: 94ad3e71e29ceb1f 2ed2b80996314344 6cfa5d8640c271dd b632f094f7eda7c7
respHMACkey: 001d14a524e7e7f0 1de527ad01dddce0 e64f915dca46242a 7795397d98cbbb16
respXORkey: e51958994bf03d02 f0651338ea18a186 7f2bb49089000a88 d367770bd9696b86 99c25804ff3ade0d 08622bd66b5b1332 4ce14f315a6dca6c 20a8b49e3743db31 8fb2670ba4b2d10f 416f61dd4eb7bf53 6a233cae88636a19 078213557e583622
plaintext: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f
ciphertext: c5387aba6fd51b25 d84c3913c6358fa9 4f1a86a3bd353cbf eb5e4d30e55455b9 d9831a47bb7f984a 402b619d27165d7d 1cb01d620e389c3b 78f1eec56b1e856e efd30568c0d7b768 29060bb622dad13c 1a524eddfc161c6e 7ffb692e0225485d
MAC: 40e3dd0d0b299033 a31222ceb3504ad4 7e55fc05f8b94402 2ed9e2be5c4be3e3
response: c5387aba6fd51b25 d84c3913c6358fa9 4f1a86a3bd353cbf eb5e4d30e55455b9 d9831a47bb7f984a 402b619d27165d7d 1cb01d620e389c3b 78f1eec56b1e856e efd30568c0d7b768 29060bb622dad13c 1a524eddfc161c6e 7ffb692e0225485d 40e3dd0d0b299033 a31222ceb3504ad4 7e55fc05f8b94402 2ed9e2be5c4be3e3
signCertificate
signToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f
tokenID: 8b5ff98850a2c98a 8059ee891c15b9a6 1af08356f54d865c 39f95f048d185195
reqHMACkey: 85f9aa22e9b35557 3504cd0e934a6c2b 1837fe6ca70d4932 627ba1c02b9aebc2
resetAccount
resetToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f
tokenID: 52437066aae511d3 3709bf25dc6a682a 7e943d49d94c84b3 4e1e6b11c9913159
reqHMACkey: 7de6c9b102dac62f 81d3a09baa00523d e7170ff17238b3af 8491e4cfb23e1a88
reqXORkey: 82d447f095aa8023 3eb5cb5d6c4eea25 5857809b6326b6bd 55fab2d3498b1cf8 a31bb0e319d7c0dc 2792740a480c1a98 99c1a6328bc2066e 3ecc9e8079ae8af6 046f15f3a586bfb3 b9908de7cd60b504 44fdfacc3cf32e2b efc72fca9063e28d a815989f86223394 b89db34bffdc94bb 68c05a49d1f1f63a 2c463d335a06c007
plaintext: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf e0e1e2e3e4e5e6e7 e8e9eaebecedeeef f0f1f2f3f4f5f6f7 f8f9fafbfcfdfeff
ciphertext: c29505b3d1efc664 76fc81162003a46a 0806d2c83773e0ea 0da3e88815d642a7 03ba1240bd72667b 8f3bdea1e4a1b437 297014813f77b0d9 8675243bc5133449 c4aed73061437974 7159472c01ad7bcb 942c281fe826f8fc 371ef5114cbe3c52 48f47a7c62c7d573 507459a013317a54 9831a8ba250400cd d4bfc7c8a6fb3ef8











