test_utils_hawk.js (10934B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { CryptoUtils } = ChromeUtils.importESModule( 5 "moz-src:///services/crypto/modules/utils.sys.mjs" 6 ); 7 8 function run_test() { 9 initTestLogging(); 10 11 run_next_test(); 12 } 13 14 add_task(async function test_hawk() { 15 let compute = CryptoUtils.computeHAWK; 16 17 let method = "POST"; 18 let ts = 1353809207; 19 let nonce = "Ygvqdz"; 20 21 let credentials = { 22 id: "123456", 23 key: "2983d45yun89q", 24 }; 25 26 let uri_https = CommonUtils.makeURI( 27 "https://example.net/somewhere/over/the/rainbow" 28 ); 29 let opts = { 30 credentials, 31 ext: "Bazinga!", 32 ts, 33 nonce, 34 payload: "something to write about", 35 contentType: "text/plain", 36 }; 37 38 let result = await compute(uri_https, method, opts); 39 Assert.equal( 40 result.field, 41 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 42 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 43 'ext="Bazinga!", ' + 44 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 45 ); 46 Assert.equal(result.artifacts.ts, ts); 47 Assert.equal(result.artifacts.nonce, nonce); 48 Assert.equal(result.artifacts.method, method); 49 Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow"); 50 Assert.equal(result.artifacts.host, "example.net"); 51 Assert.equal(result.artifacts.port, 443); 52 Assert.equal( 53 result.artifacts.hash, 54 "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=" 55 ); 56 Assert.equal(result.artifacts.ext, "Bazinga!"); 57 58 let opts_noext = { 59 credentials, 60 ts, 61 nonce, 62 payload: "something to write about", 63 contentType: "text/plain", 64 }; 65 result = await compute(uri_https, method, opts_noext); 66 Assert.equal( 67 result.field, 68 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 69 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 70 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="' 71 ); 72 Assert.equal(result.artifacts.ts, ts); 73 Assert.equal(result.artifacts.nonce, nonce); 74 Assert.equal(result.artifacts.method, method); 75 Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow"); 76 Assert.equal(result.artifacts.host, "example.net"); 77 Assert.equal(result.artifacts.port, 443); 78 Assert.equal( 79 result.artifacts.hash, 80 "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=" 81 ); 82 83 /* Leaving optional fields out should work, although of course then we can't 84 * assert much about the resulting hashes. The resulting header should look 85 * roughly like: 86 * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U=" 87 */ 88 89 result = await compute(uri_https, method, { credentials }); 90 let fields = result.field.split(" "); 91 Assert.equal(fields[0], "Hawk"); 92 Assert.equal(fields[1], 'id="123456",'); // from creds.id 93 Assert.ok(fields[2].startsWith('ts="')); 94 /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch. 95 * Warning: this test will fail in the year 33658, and for time travellers 96 * who journey earlier than 2001. Please plan accordingly. */ 97 Assert.greater(result.artifacts.ts, 1000 * 1000 * 1000); 98 Assert.less(result.artifacts.ts, 1000 * 1000 * 1000 * 1000); 99 Assert.ok(fields[3].startsWith('nonce="')); 100 Assert.equal(fields[3].length, 'nonce="12345678901=",'.length); 101 Assert.equal(result.artifacts.nonce.length, "12345678901=".length); 102 103 let result2 = await compute(uri_https, method, { credentials }); 104 Assert.notEqual(result.artifacts.nonce, result2.artifacts.nonce); 105 106 /* Using an upper-case URI hostname shouldn't affect the hash. */ 107 108 let uri_https_upper = CommonUtils.makeURI( 109 "https://EXAMPLE.NET/somewhere/over/the/rainbow" 110 ); 111 result = await compute(uri_https_upper, method, opts); 112 Assert.equal( 113 result.field, 114 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 115 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 116 'ext="Bazinga!", ' + 117 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 118 ); 119 120 /* Using a lower-case method name shouldn't affect the hash. */ 121 result = await compute(uri_https_upper, method.toLowerCase(), opts); 122 Assert.equal( 123 result.field, 124 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' + 125 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' + 126 'ext="Bazinga!", ' + 127 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="' 128 ); 129 130 /* The localtimeOffsetMsec field should be honored. HAWK uses this to 131 * compensate for clock skew between client and server: if the request is 132 * rejected with a timestamp out-of-range error, the error includes the 133 * server's time, and the client computes its clock offset and tries again. 134 * Clients can remember this offset for a while. 135 */ 136 137 result = await compute(uri_https, method, { 138 credentials, 139 now: 1378848968650, 140 }); 141 Assert.equal(result.artifacts.ts, 1378848968); 142 143 result = await compute(uri_https, method, { 144 credentials, 145 now: 1378848968650, 146 localtimeOffsetMsec: 1000 * 1000, 147 }); 148 Assert.equal(result.artifacts.ts, 1378848968 + 1000); 149 150 /* Search/query-args in URIs should be included in the hash. */ 151 let makeURI = CommonUtils.makeURI; 152 result = await compute(makeURI("http://example.net/path"), method, opts); 153 Assert.equal(result.artifacts.resource, "/path"); 154 Assert.equal( 155 result.artifacts.mac, 156 "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI=" 157 ); 158 159 result = await compute(makeURI("http://example.net/path/"), method, opts); 160 Assert.equal(result.artifacts.resource, "/path/"); 161 Assert.equal( 162 result.artifacts.mac, 163 "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw=" 164 ); 165 166 result = await compute( 167 makeURI("http://example.net/path?query=search"), 168 method, 169 opts 170 ); 171 Assert.equal(result.artifacts.resource, "/path?query=search"); 172 Assert.equal( 173 result.artifacts.mac, 174 "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho=" 175 ); 176 177 /* Test handling of the payload, which is supposed to be a bytestring 178 (String with codepoints from U+0000 to U+00FF, pre-encoded). */ 179 180 result = await compute(makeURI("http://example.net/path"), method, { 181 credentials, 182 ts: 1353809207, 183 nonce: "Ygvqdz", 184 }); 185 Assert.equal(result.artifacts.hash, undefined); 186 Assert.equal( 187 result.artifacts.mac, 188 "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=" 189 ); 190 191 // Empty payload changes nothing. 192 result = await compute(makeURI("http://example.net/path"), method, { 193 credentials, 194 ts: 1353809207, 195 nonce: "Ygvqdz", 196 payload: null, 197 }); 198 Assert.equal(result.artifacts.hash, undefined); 199 Assert.equal( 200 result.artifacts.mac, 201 "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=" 202 ); 203 204 result = await compute(makeURI("http://example.net/path"), method, { 205 credentials, 206 ts: 1353809207, 207 nonce: "Ygvqdz", 208 payload: "hello", 209 }); 210 Assert.equal( 211 result.artifacts.hash, 212 "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA=" 213 ); 214 Assert.equal( 215 result.artifacts.mac, 216 "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM=" 217 ); 218 219 // update, utf-8 payload 220 result = await compute(makeURI("http://example.net/path"), method, { 221 credentials, 222 ts: 1353809207, 223 nonce: "Ygvqdz", 224 payload: "andré@example.org", // non-ASCII 225 }); 226 Assert.equal( 227 result.artifacts.hash, 228 "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=" 229 ); 230 Assert.equal( 231 result.artifacts.mac, 232 "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=" 233 ); 234 235 /* If "hash" is provided, "payload" is ignored. */ 236 result = await compute(makeURI("http://example.net/path"), method, { 237 credentials, 238 ts: 1353809207, 239 nonce: "Ygvqdz", 240 hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=", 241 payload: "something else", 242 }); 243 Assert.equal( 244 result.artifacts.hash, 245 "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=" 246 ); 247 Assert.equal( 248 result.artifacts.mac, 249 "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=" 250 ); 251 252 // the payload "hash" is also non-urlsafe base64 (+/) 253 result = await compute(makeURI("http://example.net/path"), method, { 254 credentials, 255 ts: 1353809207, 256 nonce: "Ygvqdz", 257 payload: "something else", 258 }); 259 Assert.equal( 260 result.artifacts.hash, 261 "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8=" 262 ); 263 Assert.equal( 264 result.artifacts.mac, 265 "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ=" 266 ); 267 268 /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes 269 * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think 270 * punycode was a bad joke that got out of the lab and into a spec. 271 */ 272 273 result = await compute(makeURI("http://ëxample.net/path"), method, { 274 credentials, 275 ts: 1353809207, 276 nonce: "Ygvqdz", 277 }); 278 Assert.equal( 279 result.artifacts.mac, 280 "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0=" 281 ); 282 Assert.equal(result.artifacts.host, "xn--xample-ova.net"); 283 284 result = await compute(makeURI("http://example.net/path"), method, { 285 credentials, 286 ts: 1353809207, 287 nonce: "Ygvqdz", 288 ext: 'backslash=\\ quote=" EOF', 289 }); 290 Assert.equal( 291 result.artifacts.mac, 292 "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc=" 293 ); 294 Assert.equal( 295 result.field, 296 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="' 297 ); 298 299 result = await compute(makeURI("http://example.net:1234/path"), method, { 300 credentials, 301 ts: 1353809207, 302 nonce: "Ygvqdz", 303 }); 304 Assert.equal( 305 result.artifacts.mac, 306 "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=" 307 ); 308 Assert.equal( 309 result.field, 310 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="' 311 ); 312 313 /* HAWK (the node.js library) uses a URL parser which stores the "port" 314 * field as a string, but makeURI() gives us an integer. So we'll diverge 315 * on ports with a leading zero. This test vector would fail on the node.js 316 * library (HAWK-1.1.1), where they get a MAC of 317 * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be 318 * updated to do what we do here, so port="01234" should get the same hash 319 * as port="1234". 320 */ 321 result = await compute(makeURI("http://example.net:01234/path"), method, { 322 credentials, 323 ts: 1353809207, 324 nonce: "Ygvqdz", 325 }); 326 Assert.equal( 327 result.artifacts.mac, 328 "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=" 329 ); 330 Assert.equal( 331 result.field, 332 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="' 333 ); 334 }); 335 336 add_test(function test_strip_header_attributes() { 337 let strip = CryptoUtils.stripHeaderAttributes; 338 339 Assert.equal(strip(undefined), ""); 340 Assert.equal(strip("text/plain"), "text/plain"); 341 Assert.equal(strip("TEXT/PLAIN"), "text/plain"); 342 Assert.equal(strip(" text/plain "), "text/plain"); 343 Assert.equal(strip("text/plain ; charset=utf-8 "), "text/plain"); 344 345 run_next_test(); 346 });