encryption keys and the oauth flow
Ryan Kelly
rfkelly at mozilla.com
Wed Dec 17 02:44:11 PST 2014
Hi All,
A limitation of the current FxA OAuth flow is that OAuth reliers cannot
get access to the user's encryption keys.
This means that we could not have build new-sync atop our OAuth
infrastructure. I've also heard from at least two potential new
services that would like to do encryption of user data.
Let's try to figure out a way to enable this.
Below is a concrete proposal based on discussions I've had with a few
folks. It's intended as a starting point for discussion. I don't think
we can realistically ship anything like this before Q2, but it would be
good to reach a rough consensus on approach so that people can starting
hacking around with the ideas.
It's pretty long, so I recommend stopping here if you're not interested
OAuth or encryption keys...
A brief recap of the current situation:
=======================================
Your Firefox Account comes with a pair of encryption keys, "kA" and
"kB". The first is known to the server and designed for encrypting data
that must be recoverable even after a password reset. The second is
derived from the user's password and hence secret even from Mozilla, but
will not survive a password reset event.
Sync currently encrypts your stored data using a derivative of kB.
To obtain these keys, you must know the user's account password and you
must perform some special extra steps at login time. There is no way to
fetch either key without having the account password.
Since OAuth reliers never get to see the user's password, there is
currently no way for them to take advantage of these keys.
Starting premises:
* OAuth reliers should be able to obtain encryption keys.
* OAuth reliers must not be able to access the raw values of kA or kB,
but may obtain derivatives of them with user consent.
* FxA servers must never learn kB or anything derived from it.
* OAuth relier servers should be able to avoid learning anything
derived from kB (although we cannot prevent them from deliberately
sending the keys from client to server).
* And of course, OAuth reliers must never have access to the user's
account password, or anything that can be used to guess it.
Any amendments or additions to this list?
A proposal for scoped encryption keys:
======================================
There will be two different types of derived encryption key in our
service ecosystem.
1) Relier-specific keys
Each OAuth relier should be able to obtain a pair of derived keys
kAr/kBr that are private to that relier.
kAr = HKDF(kA, "identity.mozilla.com/picl/v1/keys/relier/<client-id>")
kBr = HKDF(kB, "identity.mozilla.com/picl/v1/keys/relier/<client-id>")
The use-case here is an external service that wants to do encryption of
its own private data, such as an FxA/daybed.io integration.
2) Service-specific keys
Each OAuth service provider should be able to have a derived key that is
specific to that service, and is provided to any relier that is granted
access to the service.
As a concrete example, suppose that the readinglist service wants to
encrypt some fields in the data stored on its server. As a matter of
policy, we decide that we can tolerate loss of such data in the event
of a password reset, so we define the key for scope "readinglist" as:
kS = HKDF(kB, "identity.mozilla.com/picl/v1/keys/service/readinglist")
Any relier that is granted access to scope "readinglist" should also be
able to get this derived key, in order to properly access the data
stored in the service.
These two types completely define the encryption keys available to a
relier. During a key-enabled oauth flow they should somehow receive:
* the relier-specific kAr and kBr
* any scope-specific keys defined for the scopes they are granted
Any use-cases not covered by one of these two types of derived key?
A key-fetching OAuth dance:
===========================
The real trick, of course, is safely deriving these keys and delivering
them to the relier. I posit that this must be done as part of the OAuth
dance. The content-server is the only thing that can prompt the user
for their password in order to derive the keys, and the content-server
only gets involved during the OAuth dance.
We have a couple of options here, but I'll sketch out the one I like
best (large chunks of which are thanks to Alexis):
1) When initiating the OAuth dance, the relier generates a
public/private keypair, stores the private key in its application state,
and includes the public key in the OAuth authorization request:
GET /v1/authorization?client_id=AAA&scope=readinglist&keyfetch=PUBKEY
2) We're redirected to the content-server OAuth page, which does its
usual solicitation of user permission, prompts for the user's password
if necessary to obtain kA and kB, and derives the appropriate scoped
encryption keys.
3) The content-server encrypts the derived keys using the supplied
pubkey and includes them when provisioning the OAuth token:
POST /v1/authorization
==>
{
"client_id": "AAA",
"scope": "readinglist",
"keys": {
"relier": [encrypt(kAr, pubkey), encrypt(kBr, pubkey)],
"readinglist": encrypt(kS, pubkey)
}
}
<==
{
"code": "XXX"
}
4) We're redirected back to the relier's registered redirect_uri, where
they retrieve the granted token and encrypted encryption keys:
POST /v1/token
==>
{
"code": "XXX"
}
<==
{
"access_token": "blahblah",
"token_type": "bearer",
"scope": "readinglist",
"keys" {
"relier": [encrypt(kAr, pubkey), encrypt(kBr, pubkey)],
"readinglist": encrypt(kS, pubkey)
}
}
The relier can use its private key to decrypt the encryption keys and
use them as it sees fit.
5) Profit.
Points to note:
* The FxA servers never see any of the keys, because they transit
the server in encrypted form.
* The relier can generate and manage its keyfetch keypair in
client-side code, to avoid learning the keys server-side.
* This flow does not depend on persistent state on the client, all
state can be tunneled through URL params in the OAuth flow.
* There's a whole lot of potential for crypto to be done wrong
here! It's pretty scary stuff and smells kinda fragile.
I'd love to avoid public-key crypto here...perhaps there's a way for the
content-server to generate a symmetric encryption secret and communicate
it directly back to the relier without it transiting our servers?
Chris also suggested that the encryption keys may not need to transit
the server at all, but could instead be communicated from content-server
to relier via a client-side postMessage API. I don't know much about
postMessage but it sounds worth exploring.
Details aside, will this sort of enhanced OAuth dance give us what we want?
Does this sound appropriately terrifying to everyone?
Cheers,
Ryan
More information about the Dev-fxacct
mailing list