1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/crypto/tests/unit/test_utils_hawk.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,301 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.8 +Cu.import("resource://services-common/utils.js"); 1.9 +Cu.import("resource://services-crypto/utils.js"); 1.10 + 1.11 +function run_test() { 1.12 + initTestLogging(); 1.13 + 1.14 + run_next_test(); 1.15 +} 1.16 + 1.17 +add_test(function test_hawk() { 1.18 + let compute = CryptoUtils.computeHAWK; 1.19 + 1.20 + // vectors copied from the HAWK (node.js) tests 1.21 + let credentials_sha1 = { 1.22 + id: "123456", 1.23 + key: "2983d45yun89q", 1.24 + algorithm: "sha1", 1.25 + }; 1.26 + 1.27 + let method = "POST"; 1.28 + let ts = 1353809207; 1.29 + let nonce = "Ygvqdz"; 1.30 + let result; 1.31 + 1.32 + let uri_http = CommonUtils.makeURI("http://example.net/somewhere/over/the/rainbow"); 1.33 + let sha1_opts = { credentials: credentials_sha1, 1.34 + ext: "Bazinga!", 1.35 + ts: ts, 1.36 + nonce: nonce, 1.37 + payload: "something to write about", 1.38 + }; 1.39 + result = compute(uri_http, method, sha1_opts); 1.40 + 1.41 + // The HAWK spec uses non-urlsafe base64 (+/) for its output MAC string. 1.42 + do_check_eq(result.field, 1.43 + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 1.44 + 'hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", ' + 1.45 + 'mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="' 1.46 + ); 1.47 + do_check_eq(result.artifacts.ts, ts); 1.48 + do_check_eq(result.artifacts.nonce, nonce); 1.49 + do_check_eq(result.artifacts.method, method); 1.50 + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); 1.51 + do_check_eq(result.artifacts.host, "example.net"); 1.52 + do_check_eq(result.artifacts.port, 80); 1.53 + // artifacts.hash is the *payload* hash, not the overall request MAC. 1.54 + do_check_eq(result.artifacts.hash, "bsvY3IfUllw6V5rvk4tStEvpBhE="); 1.55 + do_check_eq(result.artifacts.ext, "Bazinga!"); 1.56 + 1.57 + let credentials_sha256 = { 1.58 + id: "123456", 1.59 + key: "2983d45yun89q", 1.60 + algorithm: "sha256", 1.61 + }; 1.62 + 1.63 + let uri_https = CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"); 1.64 + let sha256_opts = { credentials: credentials_sha256, 1.65 + ext: "Bazinga!", 1.66 + ts: ts, 1.67 + nonce: nonce, 1.68 + payload: "something to write about", 1.69 + contentType: "text/plain", 1.70 + }; 1.71 + 1.72 + result = compute(uri_https, method, sha256_opts); 1.73 + do_check_eq(result.field, 1.74 + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 1.75 + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 1.76 + 'ext="Bazinga!", ' + 1.77 + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 1.78 + ); 1.79 + do_check_eq(result.artifacts.ts, ts); 1.80 + do_check_eq(result.artifacts.nonce, nonce); 1.81 + do_check_eq(result.artifacts.method, method); 1.82 + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); 1.83 + do_check_eq(result.artifacts.host, "example.net"); 1.84 + do_check_eq(result.artifacts.port, 443); 1.85 + do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="); 1.86 + do_check_eq(result.artifacts.ext, "Bazinga!"); 1.87 + 1.88 + let sha256_opts_noext = { credentials: credentials_sha256, 1.89 + ts: ts, 1.90 + nonce: nonce, 1.91 + payload: "something to write about", 1.92 + contentType: "text/plain", 1.93 + }; 1.94 + result = compute(uri_https, method, sha256_opts_noext); 1.95 + do_check_eq(result.field, 1.96 + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 1.97 + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 1.98 + 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="' 1.99 + ); 1.100 + do_check_eq(result.artifacts.ts, ts); 1.101 + do_check_eq(result.artifacts.nonce, nonce); 1.102 + do_check_eq(result.artifacts.method, method); 1.103 + do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow"); 1.104 + do_check_eq(result.artifacts.host, "example.net"); 1.105 + do_check_eq(result.artifacts.port, 443); 1.106 + do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="); 1.107 + 1.108 + /* Leaving optional fields out should work, although of course then we can't 1.109 + * assert much about the resulting hashes. The resulting header should look 1.110 + * roughly like: 1.111 + * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U=" 1.112 + */ 1.113 + 1.114 + result = compute(uri_https, method, { credentials: credentials_sha256 }); 1.115 + let fields = result.field.split(" "); 1.116 + do_check_eq(fields[0], "Hawk"); 1.117 + do_check_eq(fields[1], 'id="123456",'); // from creds.id 1.118 + do_check_true(fields[2].startsWith('ts="')); 1.119 + /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch. 1.120 + * Warning: this test will fail in the year 33658, and for time travellers 1.121 + * who journey earlier than 2001. Please plan accordingly. */ 1.122 + do_check_true(result.artifacts.ts > 1000*1000*1000); 1.123 + do_check_true(result.artifacts.ts < 1000*1000*1000*1000); 1.124 + do_check_true(fields[3].startsWith('nonce="')); 1.125 + do_check_eq(fields[3].length, ('nonce="12345678901=",').length); 1.126 + do_check_eq(result.artifacts.nonce.length, ("12345678901=").length); 1.127 + 1.128 + let result2 = compute(uri_https, method, { credentials: credentials_sha256 }); 1.129 + do_check_neq(result.artifacts.nonce, result2.artifacts.nonce); 1.130 + 1.131 + /* Using an upper-case URI hostname shouldn't affect the hash. */ 1.132 + 1.133 + let uri_https_upper = CommonUtils.makeURI("https://EXAMPLE.NET/somewhere/over/the/rainbow"); 1.134 + result = compute(uri_https_upper, method, sha256_opts); 1.135 + do_check_eq(result.field, 1.136 + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 1.137 + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 1.138 + 'ext="Bazinga!", ' + 1.139 + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 1.140 + ); 1.141 + 1.142 + /* Using a lower-case method name shouldn't affect the hash. */ 1.143 + result = compute(uri_https_upper, method.toLowerCase(), sha256_opts); 1.144 + do_check_eq(result.field, 1.145 + 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 1.146 + 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 1.147 + 'ext="Bazinga!", ' + 1.148 + 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 1.149 + ); 1.150 + 1.151 + /* The localtimeOffsetMsec field should be honored. HAWK uses this to 1.152 + * compensate for clock skew between client and server: if the request is 1.153 + * rejected with a timestamp out-of-range error, the error includes the 1.154 + * server's time, and the client computes its clock offset and tries again. 1.155 + * Clients can remember this offset for a while. 1.156 + */ 1.157 + 1.158 + result = compute(uri_https, method, { credentials: credentials_sha256, 1.159 + now: 1378848968650, 1.160 + }); 1.161 + do_check_eq(result.artifacts.ts, 1378848968); 1.162 + 1.163 + result = compute(uri_https, method, { credentials: credentials_sha256, 1.164 + now: 1378848968650, 1.165 + localtimeOffsetMsec: 1000*1000, 1.166 + }); 1.167 + do_check_eq(result.artifacts.ts, 1378848968 + 1000); 1.168 + 1.169 + /* Search/query-args in URIs should be included in the hash. */ 1.170 + let makeURI = CommonUtils.makeURI; 1.171 + result = compute(makeURI("http://example.net/path"), method, sha256_opts); 1.172 + do_check_eq(result.artifacts.resource, "/path"); 1.173 + do_check_eq(result.artifacts.mac, "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI="); 1.174 + 1.175 + result = compute(makeURI("http://example.net/path/"), method, sha256_opts); 1.176 + do_check_eq(result.artifacts.resource, "/path/"); 1.177 + do_check_eq(result.artifacts.mac, "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw="); 1.178 + 1.179 + result = compute(makeURI("http://example.net/path?query=search"), method, sha256_opts); 1.180 + do_check_eq(result.artifacts.resource, "/path?query=search"); 1.181 + do_check_eq(result.artifacts.mac, "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho="); 1.182 + 1.183 + /* Test handling of the payload, which is supposed to be a bytestring 1.184 + (String with codepoints from U+0000 to U+00FF, pre-encoded). */ 1.185 + 1.186 + result = compute(makeURI("http://example.net/path"), method, 1.187 + { credentials: credentials_sha256, 1.188 + ts: 1353809207, 1.189 + nonce: "Ygvqdz", 1.190 + }); 1.191 + do_check_eq(result.artifacts.hash, undefined); 1.192 + do_check_eq(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="); 1.193 + 1.194 + // Empty payload changes nothing. 1.195 + result = compute(makeURI("http://example.net/path"), method, 1.196 + { credentials: credentials_sha256, 1.197 + ts: 1353809207, 1.198 + nonce: "Ygvqdz", 1.199 + payload: null, 1.200 + }); 1.201 + do_check_eq(result.artifacts.hash, undefined); 1.202 + do_check_eq(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="); 1.203 + 1.204 + result = compute(makeURI("http://example.net/path"), method, 1.205 + { credentials: credentials_sha256, 1.206 + ts: 1353809207, 1.207 + nonce: "Ygvqdz", 1.208 + payload: "hello", 1.209 + }); 1.210 + do_check_eq(result.artifacts.hash, "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA="); 1.211 + do_check_eq(result.artifacts.mac, "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM="); 1.212 + 1.213 + // update, utf-8 payload 1.214 + result = compute(makeURI("http://example.net/path"), method, 1.215 + { credentials: credentials_sha256, 1.216 + ts: 1353809207, 1.217 + nonce: "Ygvqdz", 1.218 + payload: "andré@example.org", // non-ASCII 1.219 + }); 1.220 + do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="); 1.221 + do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="); 1.222 + 1.223 + /* If "hash" is provided, "payload" is ignored. */ 1.224 + result = compute(makeURI("http://example.net/path"), method, 1.225 + { credentials: credentials_sha256, 1.226 + ts: 1353809207, 1.227 + nonce: "Ygvqdz", 1.228 + hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=", 1.229 + payload: "something else", 1.230 + }); 1.231 + do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="); 1.232 + do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="); 1.233 + 1.234 + // the payload "hash" is also non-urlsafe base64 (+/) 1.235 + result = compute(makeURI("http://example.net/path"), method, 1.236 + { credentials: credentials_sha256, 1.237 + ts: 1353809207, 1.238 + nonce: "Ygvqdz", 1.239 + payload: "something else", 1.240 + }); 1.241 + do_check_eq(result.artifacts.hash, "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8="); 1.242 + do_check_eq(result.artifacts.mac, "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ="); 1.243 + 1.244 + /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes 1.245 + * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think 1.246 + * punycode was a bad joke that got out of the lab and into a spec. 1.247 + */ 1.248 + 1.249 + result = compute(makeURI("http://ëxample.net/path"), method, 1.250 + { credentials: credentials_sha256, 1.251 + ts: 1353809207, 1.252 + nonce: "Ygvqdz", 1.253 + }); 1.254 + do_check_eq(result.artifacts.mac, "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0="); 1.255 + do_check_eq(result.artifacts.host, "xn--xample-ova.net"); 1.256 + 1.257 + result = compute(makeURI("http://example.net/path"), method, 1.258 + { credentials: credentials_sha256, 1.259 + ts: 1353809207, 1.260 + nonce: "Ygvqdz", 1.261 + ext: "backslash=\\ quote=\" EOF", 1.262 + }); 1.263 + do_check_eq(result.artifacts.mac, "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="); 1.264 + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="'); 1.265 + 1.266 + result = compute(makeURI("http://example.net:1234/path"), method, 1.267 + { credentials: credentials_sha256, 1.268 + ts: 1353809207, 1.269 + nonce: "Ygvqdz", 1.270 + }); 1.271 + do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="); 1.272 + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'); 1.273 + 1.274 + /* HAWK (the node.js library) uses a URL parser which stores the "port" 1.275 + * field as a string, but makeURI() gives us an integer. So we'll diverge 1.276 + * on ports with a leading zero. This test vector would fail on the node.js 1.277 + * library (HAWK-1.1.1), where they get a MAC of 1.278 + * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be 1.279 + * updated to do what we do here, so port="01234" should get the same hash 1.280 + * as port="1234". 1.281 + */ 1.282 + result = compute(makeURI("http://example.net:01234/path"), method, 1.283 + { credentials: credentials_sha256, 1.284 + ts: 1353809207, 1.285 + nonce: "Ygvqdz", 1.286 + }); 1.287 + do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="); 1.288 + do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'); 1.289 + 1.290 + run_next_test(); 1.291 +}); 1.292 + 1.293 + 1.294 +add_test(function test_strip_header_attributes() { 1.295 + let strip = CryptoUtils.stripHeaderAttributes; 1.296 + 1.297 + do_check_eq(strip(undefined), ""); 1.298 + do_check_eq(strip("text/plain"), "text/plain"); 1.299 + do_check_eq(strip("TEXT/PLAIN"), "text/plain"); 1.300 + do_check_eq(strip(" text/plain "), "text/plain"); 1.301 + do_check_eq(strip("text/plain ; charset=utf-8 "), "text/plain"); 1.302 + 1.303 + run_next_test(); 1.304 +});