Bug 729659 - Implement HTTP MAC authorization signing API; r=rnewman, dchan
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -750,16 +750,140 @@ let Utils = {
* Take a base64-encoded 128-bit AES key, returning it as five groups of five
* uppercase alphanumeric characters, separated by hyphens.
* A.K.A. base64-to-base32 encoding.
*/
presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) {
return Utils.encodeKeyBase32(atob(encodedKey));
},
+ /**
+ * Compute the HTTP MAC SHA-1 for an HTTP request.
+ *
+ * @param identifier
+ * (string) MAC Key Identifier.
+ * @param key
+ * (string) MAC Key.
+ * @param method
+ * (string) HTTP request method.
+ * @param URI
+ * (nsIURI) HTTP request URI.
+ * @param extra
+ * (object) Optional extra parameters. Valid keys are:
+ * nonce_bytes - How many bytes the nonce should be. This defaults
+ * to 8. Note that this many bytes are Base64 encoded, so the
+ * string length of the nonce will be longer than this value.
+ * ts - Timestamp to use. Should only be defined for testing.
+ * nonce - String nonce. Should only be defined for testing as this
+ * function will generate a cryptographically secure random one
+ * if not defined.
+ * ext - Extra string to be included in MAC. Per the HTTP MAC spec,
+ * the format is undefined and thus application specific.
+ * @returns
+ * (object) Contains results of operation and input arguments (for
+ * symmetry). The object has the following keys:
+ *
+ * identifier - (string) MAC Key Identifier (from arguments).
+ * key - (string) MAC Key (from arguments).
+ * method - (string) HTTP request method (from arguments).
+ * hostname - (string) HTTP hostname used (derived from arguments).
+ * port - (string) HTTP port number used (derived from arguments).
+ * mac - (string) Raw HMAC digest bytes.
+ * getHeader - (function) Call to obtain the string Authorization
+ * header value for this invocation.
+ * nonce - (string) Nonce value used.
+ * ts - (number) Integer seconds since Unix epoch that was used.
+ */
+ computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method,
+ uri, extra) {
+ let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000);
+ let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8;
+
+ // We are allowed to use more than the Base64 alphabet if we want.
+ let nonce = (extra && extra.nonce)
+ ? extra.nonce
+ : btoa(Utils.generateRandomBytes(nonce_bytes));
+
+ let host = uri.asciiHost;
+ let port;
+ let usedMethod = method.toUpperCase();
+
+ if (uri.port != -1) {
+ port = uri.port;
+ } else if (uri.scheme == "http") {
+ port = "80";
+ } else if (uri.scheme == "https") {
+ port = "443";
+ } else {
+ throw new Error("Unsupported URI scheme: " + uri.scheme);
+ }
+
+ let ext = (extra && extra.ext) ? extra.ext : "";
+
+ let requestString = ts.toString(10) + "\n" +
+ nonce + "\n" +
+ usedMethod + "\n" +
+ uri.path + "\n" +
+ host + "\n" +
+ port + "\n" +
+ ext + "\n";
+
+ let hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1,
+ Utils.makeHMACKey(key));
+ let mac = Utils.digestBytes(requestString, hasher);
+
+ function getHeader() {
+ return Utils.getHTTPMACSHA1Header(this.identifier, this.ts, this.nonce,
+ this.mac, this.ext);
+ }
+
+ return {
+ identifier: identifier,
+ key: key,
+ method: usedMethod,
+ hostname: host,
+ port: port,
+ mac: mac,
+ nonce: nonce,
+ ts: ts,
+ ext: ext,
+ getHeader: getHeader
+ };
+ },
+
+ /**
+ * Obtain the HTTP MAC Authorization header value from fields.
+ *
+ * @param identifier
+ * (string) MAC key identifier.
+ * @param ts
+ * (number) Integer seconds since Unix epoch.
+ * @param nonce
+ * (string) Nonce value.
+ * @param mac
+ * (string) Computed HMAC digest (raw bytes).
+ * @param ext
+ * (optional) (string) Extra string content.
+ * @returns
+ * (string) Value to put in Authorization header.
+ */
+ getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce,
+ mac, ext) {
+ let header ='MAC id="' + identifier + '", ' +
+ 'ts="' + ts + '", ' +
+ 'nonce="' + nonce + '", ' +
+ 'mac="' + btoa(mac) + '"';
+
+ if (!ext) {
+ return header;
+ }
+
+ return header += ', ext="' + ext +'"';
+ },
+
makeURI: function Weave_makeURI(URIString) {
if (!URIString)
return null;
try {
return Services.io.newURI(URIString, null, null);
} catch (e) {
let log = Log4Moz.repository.getLogger("Sync.Utils");
log.debug("Could not create URI: " + Utils.exceptionStr(e));
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,16 +1,17 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/engines.js");
-var btoa;
+let btoa;
+let atob;
let provider = {
getFile: function(prop, persistent) {
persistent.value = true;
switch (prop) {
case "ExtPrefDL":
return [Services.dirsvc.get("CurProcD", Ci.nsIFile)];
default:
@@ -34,16 +35,17 @@ function waitForZeroTimer(callback) {
return;
}
callback();
}
timer = Utils.namedTimer(wait, 150, {}, "timer");
}
btoa = Cu.import("resource://services-sync/log4moz.js").btoa;
+atob = Cu.import("resource://services-sync/log4moz.js").atob;
function getTestLogger(component) {
return Log4Moz.repository.getLogger("Testing");
}
function initTestLogging(level) {
function LogStats() {
this.errorsLogged = 0;
}
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_utils_httpmac.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-sync/util.js");
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_test(function test_sha1() {
+ _("Ensure HTTP MAC SHA1 generation works as expected.");
+
+ let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7";
+ let key = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz";
+ let ts = 1329181221;
+ let method = "GET";
+ let nonce = "wGX71";
+ let uri = Utils.makeURI("http://10.250.2.176/alias/");
+
+ let result = Utils.computeHTTPMACSHA1(id, key, method, uri, {ts: ts,
+ nonce: nonce});
+
+ do_check_eq(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck=");
+
+ do_check_eq(result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="');
+
+ let ext = "EXTRA DATA; foo,bar=1";
+
+ let result = Utils.computeHTTPMACSHA1(id, key, method, uri, {ts: ts,
+ nonce: nonce,
+ ext: ext});
+ do_check_eq(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68=");
+ do_check_eq(result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' +
+ 'ext="EXTRA DATA; foo,bar=1"');
+
+ run_next_test();
+});
+
+add_test(function test_nonce_length() {
+ _("Ensure custom nonce lengths are honoured.");
+
+ function get_mac(length) {
+ let uri = Utils.makeURI("http://example.com/");
+ return Utils.computeHTTPMACSHA1("foo", "bar", "GET", uri, {
+ nonce_bytes: length
+ });
+ }
+
+ let result = get_mac(12);
+ do_check_eq(12, atob(result.nonce).length);
+
+ let result = get_mac(2);
+ do_check_eq(2, atob(result.nonce).length);
+
+ let result = get_mac(0);
+ do_check_eq(8, atob(result.nonce).length);
+
+ let result = get_mac(-1);
+ do_check_eq(8, atob(result.nonce).length);
+
+ run_next_test();
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -110,16 +110,17 @@ skip-if = os == "android"
[test_utils_deepEquals.js]
[test_utils_deferGetSet.js]
[test_utils_deriveKey.js]
[test_utils_encodeBase32.js]
[test_utils_ensureOneOpen.js]
[test_utils_getErrorString.js]
[test_utils_getIcon.js]
[test_utils_hkdfExpand.js]
+[test_utils_httpmac.js]
[test_utils_json.js]
[test_utils_lazyStrings.js]
[test_utils_lock.js]
[test_utils_makeGUID.js]
[test_utils_makeURI.js]
[test_utils_namedTimer.js]
[test_utils_notify.js]
[test_utils_passphrase.js]