Bug 729659 - Implement HTTP MAC authorization signing API; r=rnewman, dchan
authorGregory Szorc <gps@mozilla.com>
Mon, 27 Feb 2012 17:52:48 -0800
changeset 88461 54f2d811e79aea6fa09777e0e8e3a42f7d50552f
parent 88460 ab7d7c53d6182b5099303d5eb16eb88032814ae7
child 88462 b4b8a60097b01b07e5288338c7c6779a5d3f1dab
push id157
push userMs2ger@gmail.com
push dateWed, 07 Mar 2012 19:27:10 +0000
reviewersrnewman, dchan
bugs729659
milestone13.0a1
Bug 729659 - Implement HTTP MAC authorization signing API; r=rnewman, dchan
services/sync/modules/util.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_utils_httpmac.js
services/sync/tests/unit/xpcshell.ini
--- 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]