Bug 1020859 Part 1 - Make HawkClient return all the response details for a request, and make deriveHawkCredentials common code. r=jparsons

This commit is contained in:
Mark Banner 2014-06-18 10:42:15 +01:00
parent d31f4e8d14
commit 19c7270342
8 changed files with 95 additions and 86 deletions

View File

@ -176,7 +176,7 @@ this.HawkClient.prototype = {
* An object that can be encodable as JSON as the payload of the * An object that can be encodable as JSON as the payload of the
* request * request
* @return Promise * @return Promise
* Returns a promise that resolves to the text response of the API call, * Returns a promise that resolves to the response of the API call,
* or is rejected with an error. If the server response can be parsed * or is rejected with an error. If the server response can be parsed
* as JSON and contains an 'error' property, the promise will be * as JSON and contains an 'error' property, the promise will be
* rejected with this JSON-parsed response. * rejected with this JSON-parsed response.
@ -239,8 +239,8 @@ this.HawkClient.prototype = {
return deferred.reject(self._constructError(restResponse, "Request failed")); return deferred.reject(self._constructError(restResponse, "Request failed"));
} }
// It's up to the caller to know how to decode the response. // It's up to the caller to know how to decode the response.
// We just return the raw text. // We just return the whole response.
deferred.resolve(this.response.body); deferred.resolve(this.response);
}; };
function onComplete(error) { function onComplete(error) {

View File

@ -8,6 +8,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [ this.EXPORTED_SYMBOLS = [
"HAWKAuthenticatedRESTRequest", "HAWKAuthenticatedRESTRequest",
"deriveHawkCredentials"
]; ];
Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Preferences.jsm");
@ -15,6 +16,8 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js"); Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/Credentials.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
"resource://services-crypto/utils.js"); "resource://services-crypto/utils.js");
@ -90,6 +93,46 @@ HAWKAuthenticatedRESTRequest.prototype = {
} }
}; };
/**
* Generic function to derive Hawk credentials.
*
* Hawk credentials are derived using shared secrets, which depend on the token
* in use.
*
* @param tokenHex
* The current session token encoded in hex
* @param context
* A context for the credentials. A protocol version will be prepended
* to the context, see Credentials.keyWord for more information.
* @param size
* The size in bytes of the expected derived buffer,
* defaults to 3 * 32.
* @return credentials
* Returns an object:
* {
* algorithm: sha256
* id: the Hawk id (from the first 32 bytes derived)
* key: the Hawk key (from bytes 32 to 64)
* extra: size - 64 extra bytes (if size > 64)
* }
*/
function deriveHawkCredentials(tokenHex, context, size=96) {
let token = CommonUtils.hexToBytes(tokenHex);
let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size);
let result = {
algorithm: "sha256",
key: out.slice(32, 64),
id: CommonUtils.bytesAsHex(out.slice(0, 32))
};
if (size > 64) {
result.extra = out.slice(64);
}
return result;
}
// With hawk request, we send the user's accepted-languages with each request. // With hawk request, we send the user's accepted-languages with each request.
// To keep the number of times we read this pref at a minimum, maintain the // To keep the number of times we read this pref at a minimum, maintain the
// preference in a stateful object that notices and updates itself when the // preference in a stateful object that notices and updates itself when the

View File

@ -59,7 +59,7 @@ add_task(function test_authenticated_get_request() {
let client = new HawkClient(server.baseURI); let client = new HawkClient(server.baseURI);
let response = yield client.request("/foo", method, TEST_CREDS); let response = yield client.request("/foo", method, TEST_CREDS);
let result = JSON.parse(response); let result = JSON.parse(response.body);
do_check_eq("Great Success!", result.msg); do_check_eq("Great Success!", result.msg);
@ -81,7 +81,7 @@ add_task(function test_authenticated_post_request() {
let client = new HawkClient(server.baseURI); let client = new HawkClient(server.baseURI);
let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"}); let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"});
let result = JSON.parse(response); let result = JSON.parse(response.body);
do_check_eq("bar", result.foo); do_check_eq("bar", result.foo);
@ -103,7 +103,7 @@ add_task(function test_credentials_optional() {
let client = new HawkClient(server.baseURI); let client = new HawkClient(server.baseURI);
let result = yield client.request("/foo", method); // credentials undefined let result = yield client.request("/foo", method); // credentials undefined
do_check_eq(JSON.parse(result).msg, "you're in the friend zone"); do_check_eq(JSON.parse(result.body).msg, "you're in the friend zone");
yield deferredStop(server); yield deferredStop(server);
}); });
@ -242,7 +242,7 @@ add_task(function test_2xx_success() {
let response = yield client.request("/foo", method, credentials); let response = yield client.request("/foo", method, credentials);
// Shouldn't be any content in a 202 // Shouldn't be any content in a 202
do_check_eq(response, ""); do_check_eq(response.body, "");
yield deferredStop(server); yield deferredStop(server);
}); });
@ -297,7 +297,7 @@ add_task(function test_retry_request_on_fail() {
// Request will have bad timestamp; client will retry once // Request will have bad timestamp; client will retry once
let response = yield client.request("/maybe", method, credentials); let response = yield client.request("/maybe", method, credentials);
do_check_eq(response, "i love you!!!"); do_check_eq(response.body, "i love you!!!");
yield deferredStop(server); yield deferredStop(server);
}); });

View File

@ -7,6 +7,18 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawkrequest.js"); Cu.import("resource://services-common/hawkrequest.js");
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc
let SESSION_KEYS = {
sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf"+
"b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"),
tokenID: h("c0a29dcf46174973 da1378696e4c82ae"+
"10f723cf4f4d9f75 e39f4ae3851595ab"),
reqHMACkey: h("9d8f22998ee7f579 8b887042466b72d5"+
"3e56ab0c094388bf 65831f702d2febc0"),
};
function do_register_cleanup() { function do_register_cleanup() {
Services.prefs.resetUserPrefs(); Services.prefs.resetUserPrefs();
@ -201,3 +213,16 @@ add_test(function test_hawk_language_pref_changed() {
} }
}); });
add_task(function test_deriveHawkCredentials() {
let credentials = deriveHawkCredentials(
SESSION_KEYS.sessionToken, "sessionToken");
do_check_eq(credentials.algorithm, "sha256");
do_check_eq(credentials.id, SESSION_KEYS.tokenID);
do_check_eq(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey);
});
// turn formatted test vectors into normal hex strings
function h(hexStr) {
return hexStr.replace(/\s+/g, "");
}

View File

@ -11,6 +11,7 @@ Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawkclient.js"); Cu.import("resource://services-common/hawkclient.js");
Cu.import("resource://services-common/hawkrequest.js");
Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/Credentials.jsm"); Cu.import("resource://gre/modules/Credentials.jsm");
@ -157,7 +158,7 @@ this.FxAccountsClient.prototype = {
*/ */
signOut: function (sessionTokenHex) { signOut: function (sessionTokenHex) {
return this._request("/session/destroy", "POST", return this._request("/session/destroy", "POST",
this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); deriveHawkCredentials(sessionTokenHex, "sessionToken"));
}, },
/** /**
@ -169,7 +170,7 @@ this.FxAccountsClient.prototype = {
*/ */
recoveryEmailStatus: function (sessionTokenHex) { recoveryEmailStatus: function (sessionTokenHex) {
return this._request("/recovery_email/status", "GET", return this._request("/recovery_email/status", "GET",
this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); deriveHawkCredentials(sessionTokenHex, "sessionToken"));
}, },
/** /**
@ -181,7 +182,7 @@ this.FxAccountsClient.prototype = {
*/ */
resendVerificationEmail: function(sessionTokenHex) { resendVerificationEmail: function(sessionTokenHex) {
return this._request("/recovery_email/resend_code", "POST", return this._request("/recovery_email/resend_code", "POST",
this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); deriveHawkCredentials(sessionTokenHex, "sessionToken"));
}, },
/** /**
@ -198,7 +199,7 @@ this.FxAccountsClient.prototype = {
* } * }
*/ */
accountKeys: function (keyFetchTokenHex) { accountKeys: function (keyFetchTokenHex) {
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
let keyRequestKey = creds.extra.slice(0, 32); let keyRequestKey = creds.extra.slice(0, 32);
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined, let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
Credentials.keyWord("account/keys"), 3 * 32); Credentials.keyWord("account/keys"), 3 * 32);
@ -247,7 +248,7 @@ this.FxAccountsClient.prototype = {
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12 * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
*/ */
signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) { signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken"); let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { publicKey: serializedPublicKey, let body = { publicKey: serializedPublicKey,
duration: lifetime }; duration: lifetime };
@ -309,38 +310,6 @@ this.FxAccountsClient.prototype = {
); );
}, },
/**
* The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
* Hawk credentials are derived using shared secrets, which depend on the context
* (e.g. sessionToken vs. keyFetchToken).
*
* @param tokenHex
* The current session token encoded in hex
* @param context
* A context for the credentials
* @param size
* The size in bytes of the expected derived buffer
* @return credentials
* Returns an object:
* {
* algorithm: sha256
* id: the Hawk id (from the first 32 bytes derived)
* key: the Hawk key (from bytes 32 to 64)
* extra: size - 64 extra bytes
* }
*/
_deriveHawkCredentials: function (tokenHex, context, size) {
let token = CommonUtils.hexToBytes(tokenHex);
let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size || 3 * 32);
return {
algorithm: "sha256",
key: out.slice(32, 64),
extra: out.slice(64),
id: CommonUtils.bytesAsHex(out.slice(0, 32))
};
},
_clearBackoff: function() { _clearBackoff: function() {
this.backoffError = null; this.backoffError = null;
}, },
@ -379,12 +348,12 @@ this.FxAccountsClient.prototype = {
} }
this.hawk.request(path, method, credentials, jsonPayload).then( this.hawk.request(path, method, credentials, jsonPayload).then(
(responseText) => { (response) => {
try { try {
let response = JSON.parse(responseText); let responseObj = JSON.parse(response.body);
deferred.resolve(response); deferred.resolve(responseObj);
} catch (err) { } catch (err) {
log.error("json parse error on response: " + responseText); log.error("json parse error on response: " + response.body);
deferred.reject({error: err}); deferred.reject({error: err});
} }
}, },

View File

@ -6,6 +6,7 @@
Cu.import("resource://gre/modules/FxAccountsClient.jsm"); Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawkrequest.js");
Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://services-crypto/utils.js");
const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"; const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
@ -33,18 +34,6 @@ let ACCOUNT_KEYS = {
"5051525354555657 58595a5b5c5d5e5f"), "5051525354555657 58595a5b5c5d5e5f"),
}; };
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc
let SESSION_KEYS = {
sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf"+
"b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"),
tokenID: h("c0a29dcf46174973 da1378696e4c82ae"+
"10f723cf4f4d9f75 e39f4ae3851595ab"),
reqHMACkey: h("9d8f22998ee7f579 8b887042466b72d5"+
"3e56ab0c094388bf 65831f702d2febc0"),
};
function deferredStop(server) { function deferredStop(server) {
let deferred = Promise.defer(); let deferred = Promise.defer();
server.stop(deferred.resolve); server.stop(deferred.resolve);
@ -670,17 +659,6 @@ add_task(function test_email_case() {
yield deferredStop(server); yield deferredStop(server);
}); });
add_task(function test__deriveHawkCredentials() {
let client = new FxAccountsClient("https://example.org");
let credentials = client._deriveHawkCredentials(
SESSION_KEYS.sessionToken, "sessionToken");
do_check_eq(credentials.algorithm, "sha256");
do_check_eq(credentials.id, SESSION_KEYS.tokenID);
do_check_eq(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey);
});
// turn formatted test vectors into normal hex strings // turn formatted test vectors into normal hex strings
function h(hexStr) { function h(hexStr) {
return hexStr.replace(/\s+/g, ""); return hexStr.replace(/\s+/g, "");

View File

@ -12,6 +12,7 @@ this.EXPORTED_SYMBOLS = ["MobileIdentityClient"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-common/hawkclient.js"); Cu.import("resource://services-common/hawkclient.js");
Cu.import("resource://services-common/hawkrequest.js");
Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm"); Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
@ -104,15 +105,8 @@ this.MobileIdentityClient.prototype = {
* } * }
*/ */
_deriveHawkCredentials: function(aSessionToken) { _deriveHawkCredentials: function(aSessionToken) {
let token = CommonUtils.hexToBytes(aSessionToken); return deriveHawkCredentials(aSessionToken, CREDENTIALS_DERIVATION_INFO,
let out = CryptoUtils.hkdf(token, undefined, CREDENTIALS_DERIVATION_SIZE);
CREDENTIALS_DERIVATION_INFO,
CREDENTIALS_DERIVATION_SIZE);
return {
algorithm: "sha256",
key: CommonUtils.bytesAsHex(out.slice(32, 64)),
id: CommonUtils.bytesAsHex(out.slice(0, 32))
};
}, },
/** /**
@ -136,11 +130,11 @@ this.MobileIdentityClient.prototype = {
let deferred = Promise.defer(); let deferred = Promise.defer();
this.hawk.request(path, method, credentials, jsonPayload).then( this.hawk.request(path, method, credentials, jsonPayload).then(
(responseText) => { (response) => {
log.debug("MobileIdentityClient -> responseText " + responseText); log.debug("MobileIdentityClient -> response.body " + response.body);
try { try {
let response = JSON.parse(responseText); let responseObj = JSON.parse(response.body);
deferred.resolve(response); deferred.resolve(responseObj);
} catch (err) { } catch (err) {
deferred.reject({error: err}); deferred.reject({error: err});
} }

View File

@ -52,7 +52,7 @@ this.UNREGISTER = "/unregister";
// Server consts. // Server consts.
this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri"); this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri");
this.CREDENTIALS_DERIVATION_INFO = "identity.mozilla.com/picl/v1/sessionToken"; this.CREDENTIALS_DERIVATION_INFO = "sessionToken";
this.CREDENTIALS_DERIVATION_SIZE = 2 * 32; this.CREDENTIALS_DERIVATION_SIZE = 2 * 32;
this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received"; this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received";