u2futil.js (14045B)
1 // Used by local_addTest() / local_completeTest() 2 var _countCompletions = 0; 3 var _expectedCompletions = 0; 4 5 const flag_TUP = 0x01; 6 const flag_UV = 0x04; 7 const flag_AT = 0x40; 8 9 const cose_kty = 1; 10 const cose_kty_ec2 = 2; 11 const cose_alg = 3; 12 const cose_alg_ECDSA_w_SHA256 = -7; 13 const cose_alg_ECDSA_w_SHA512 = -36; 14 const cose_crv = -1; 15 const cose_crv_P256 = 1; 16 const cose_crv_x = -2; 17 const cose_crv_y = -3; 18 19 var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( 20 "resource://gre/modules/AppConstants.sys.mjs" 21 ); 22 23 async function addVirtualAuthenticator( 24 protocol = "ctap2_1", 25 transport = "internal", 26 hasResidentKey = true, 27 hasUserVerification = true, 28 isUserConsenting = true, 29 isUserVerified = true 30 ) { 31 let id = await SpecialPowers.spawnChrome( 32 [ 33 protocol, 34 transport, 35 hasResidentKey, 36 hasUserVerification, 37 isUserConsenting, 38 isUserVerified, 39 ], 40 ( 41 protocol, 42 transport, 43 hasResidentKey, 44 hasUserVerification, 45 isUserConsenting, 46 isUserVerified 47 ) => { 48 let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( 49 Ci.nsIWebAuthnService 50 ); 51 let id = webauthnService.addVirtualAuthenticator( 52 protocol, 53 transport, 54 hasResidentKey, 55 hasUserVerification, 56 isUserConsenting, 57 isUserVerified 58 ); 59 return id; 60 } 61 ); 62 63 SimpleTest.registerCleanupFunction(async () => { 64 await SpecialPowers.spawnChrome([id], id => { 65 let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( 66 Ci.nsIWebAuthnService 67 ); 68 webauthnService.removeVirtualAuthenticator(id); 69 }); 70 }); 71 72 return id; 73 } 74 75 async function setUserVerified(authenticatorId, isUserVerified) { 76 await SpecialPowers.spawnChrome( 77 [authenticatorId, isUserVerified], 78 (authenticatorId, isUserVerified) => { 79 let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( 80 Ci.nsIWebAuthnService 81 ); 82 webauthnService.setUserVerified(authenticatorId, isUserVerified); 83 } 84 ); 85 } 86 87 function handleEventMessage(event) { 88 if ("test" in event.data) { 89 let summary = event.data.test + ": " + event.data.msg; 90 log(event.data.status + ": " + summary); 91 ok(event.data.status, summary); 92 } else if ("done" in event.data) { 93 SimpleTest.finish(); 94 } else { 95 ok(false, "Unexpected message in the test harness: " + event.data); 96 } 97 } 98 99 function log(msg) { 100 console.log(msg); 101 let logBox = document.getElementById("log"); 102 if (logBox) { 103 logBox.textContent += "\n" + msg; 104 } 105 } 106 107 function local_is(value, expected, message) { 108 if (value === expected) { 109 local_ok(true, message); 110 } else { 111 local_ok(false, message + " unexpectedly: " + value + " !== " + expected); 112 } 113 } 114 115 function local_isnot(value, expected, message) { 116 if (value !== expected) { 117 local_ok(true, message); 118 } else { 119 local_ok(false, message + " unexpectedly: " + value + " === " + expected); 120 } 121 } 122 123 function local_ok(expression, message) { 124 let body = { test: this.location.pathname, status: expression, msg: message }; 125 parent.postMessage(body, "http://mochi.test:8888"); 126 } 127 128 function local_doesThrow(fn, name) { 129 let gotException = false; 130 try { 131 fn(); 132 } catch (ex) { 133 gotException = true; 134 } 135 local_ok(gotException, name); 136 } 137 138 function local_expectThisManyTests(count) { 139 if (_expectedCompletions > 0) { 140 local_ok( 141 false, 142 "Error: local_expectThisManyTests should only be called once." 143 ); 144 } 145 _expectedCompletions = count; 146 } 147 148 function local_completeTest() { 149 _countCompletions += 1; 150 if (_countCompletions == _expectedCompletions) { 151 log("All tests completed."); 152 local_finished(); 153 } 154 if (_countCompletions > _expectedCompletions) { 155 local_ok( 156 false, 157 "Error: local_completeTest called more than local_addTest." 158 ); 159 } 160 } 161 162 function local_finished() { 163 parent.postMessage({ done: true }, "http://mochi.test:8888"); 164 } 165 166 function string2buffer(str) { 167 return new Uint8Array(str.length).map((x, i) => str.charCodeAt(i)); 168 } 169 170 function buffer2string(buf) { 171 let str = ""; 172 if (!(buf.constructor === Uint8Array)) { 173 buf = new Uint8Array(buf); 174 } 175 buf.map(function (x) { 176 return (str += String.fromCharCode(x)); 177 }); 178 return str; 179 } 180 181 function bytesToBase64(u8a) { 182 let CHUNK_SZ = 0x8000; 183 let c = []; 184 let array = new Uint8Array(u8a); 185 for (let i = 0; i < array.length; i += CHUNK_SZ) { 186 c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ))); 187 } 188 return window.btoa(c.join("")); 189 } 190 191 function base64ToBytes(b64encoded) { 192 return new Uint8Array( 193 window 194 .atob(b64encoded) 195 .split("") 196 .map(function (c) { 197 return c.charCodeAt(0); 198 }) 199 ); 200 } 201 202 function bytesToBase64UrlSafe(buf) { 203 return bytesToBase64(buf) 204 .replace(/\+/g, "-") 205 .replace(/\//g, "_") 206 .replace(/=/g, ""); 207 } 208 209 function base64ToBytesUrlSafe(str) { 210 if (str.length % 4 == 1) { 211 throw "Improper b64 string"; 212 } 213 214 var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/"); 215 while (b64.length % 4 != 0) { 216 b64 += "="; 217 } 218 return base64ToBytes(b64); 219 } 220 221 function hexEncode(buf) { 222 return Array.from(buf) 223 .map(x => ("0" + x.toString(16)).substr(-2)) 224 .join(""); 225 } 226 227 function hexDecode(str) { 228 return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16))); 229 } 230 231 function hasOnlyKeys(obj, ...keys) { 232 let okeys = new Set(Object.keys(obj)); 233 return keys.length == okeys.size && keys.every(k => okeys.has(k)); 234 } 235 236 function webAuthnDecodeCBORAttestation(aCborAttBuf) { 237 let attObj = CBOR.decode(aCborAttBuf); 238 console.log(":: Attestation CBOR Object ::"); 239 if (!hasOnlyKeys(attObj, "authData", "fmt", "attStmt")) { 240 return Promise.reject("Invalid CBOR Attestation Object"); 241 } 242 if (attObj.fmt == "fido-u2f" && !hasOnlyKeys(attObj.attStmt, "sig", "x5c")) { 243 return Promise.reject("Invalid CBOR Attestation Statement"); 244 } 245 if ( 246 attObj.fmt == "packed" && 247 !( 248 hasOnlyKeys(attObj.attStmt, "alg", "sig") || 249 hasOnlyKeys(attObj.attStmt, "alg", "sig", "x5c") 250 ) 251 ) { 252 return Promise.reject("Invalid CBOR Attestation Statement"); 253 } 254 if (attObj.fmt == "none" && Object.keys(attObj.attStmt).length) { 255 return Promise.reject("Invalid CBOR Attestation Statement"); 256 } 257 258 return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData)).then( 259 function (aAuthDataObj) { 260 attObj.authDataObj = aAuthDataObj; 261 return Promise.resolve(attObj); 262 } 263 ); 264 } 265 266 function webAuthnDecodeAuthDataArray(aAuthData) { 267 let rpIdHash = aAuthData.slice(0, 32); 268 let flags = aAuthData.slice(32, 33); 269 let counter = aAuthData.slice(33, 37); 270 271 console.log(":: Authenticator Data ::"); 272 console.log("RP ID Hash: " + hexEncode(rpIdHash)); 273 console.log("Counter: " + hexEncode(counter) + " Flags: " + flags); 274 275 if ((flags & flag_AT) == 0x00) { 276 // No Attestation Data, so we're done. 277 return Promise.resolve({ 278 rpIdHash, 279 flags, 280 counter, 281 }); 282 } 283 284 if (aAuthData.length < 38) { 285 return Promise.reject( 286 "Authenticator Data flag was set, but not enough data passed in!" 287 ); 288 } 289 290 let attData = {}; 291 attData.aaguid = aAuthData.slice(37, 53); 292 attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54]; 293 attData.credId = aAuthData.slice(55, 55 + attData.credIdLen); 294 295 console.log(":: Authenticator Data ::"); 296 console.log("AAGUID: " + hexEncode(attData.aaguid)); 297 298 let cborPubKey = aAuthData.slice(55 + attData.credIdLen); 299 var pubkeyObj = CBOR.decode(cborPubKey.buffer); 300 if ( 301 !( 302 cose_kty in pubkeyObj && 303 cose_alg in pubkeyObj && 304 cose_crv in pubkeyObj && 305 cose_crv_x in pubkeyObj && 306 cose_crv_y in pubkeyObj 307 ) 308 ) { 309 throw "Invalid CBOR Public Key Object"; 310 } 311 if (pubkeyObj[cose_kty] != cose_kty_ec2) { 312 throw "Unexpected key type"; 313 } 314 if (pubkeyObj[cose_alg] != cose_alg_ECDSA_w_SHA256) { 315 throw "Unexpected public key algorithm"; 316 } 317 if (pubkeyObj[cose_crv] != cose_crv_P256) { 318 throw "Unexpected curve"; 319 } 320 321 let pubKeyBytes = assemblePublicKeyBytesData( 322 pubkeyObj[cose_crv_x], 323 pubkeyObj[cose_crv_y] 324 ); 325 console.log(":: CBOR Public Key Object Data ::"); 326 console.log("kty: " + pubkeyObj[cose_kty] + " (EC2)"); 327 console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)"); 328 console.log("crv: " + pubkeyObj[cose_crv] + " (P256)"); 329 console.log("X: " + pubkeyObj[cose_crv_x]); 330 console.log("Y: " + pubkeyObj[cose_crv_y]); 331 console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes)); 332 333 return importPublicKey(pubKeyBytes).then(function (aKeyHandle) { 334 return Promise.resolve({ 335 rpIdHash, 336 flags, 337 counter, 338 attestationAuthData: attData, 339 publicKeyBytes: pubKeyBytes, 340 publicKeyHandle: aKeyHandle, 341 }); 342 }); 343 } 344 345 function importPublicKey(keyBytes) { 346 if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) { 347 throw "Bad public key octet string"; 348 } 349 var jwk = { 350 kty: "EC", 351 crv: "P-256", 352 x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)), 353 y: bytesToBase64UrlSafe(keyBytes.slice(33)), 354 }; 355 return crypto.subtle.importKey( 356 "jwk", 357 jwk, 358 { name: "ECDSA", namedCurve: "P-256" }, 359 true, 360 ["verify"] 361 ); 362 } 363 364 function deriveAppAndChallengeParam(appId, clientData, attestation) { 365 var appIdBuf = string2buffer(appId); 366 return Promise.all([ 367 crypto.subtle.digest("SHA-256", appIdBuf), 368 crypto.subtle.digest("SHA-256", clientData), 369 ]).then(function (digests) { 370 return { 371 appParam: new Uint8Array(digests[0]), 372 challengeParam: new Uint8Array(digests[1]), 373 attestation, 374 }; 375 }); 376 } 377 378 function assemblePublicKeyBytesData(xCoord, yCoord) { 379 // Produce an uncompressed EC key point. These start with 0x04, and then 380 // two 32-byte numbers denoting X and Y. 381 if (xCoord.length != 32 || yCoord.length != 32) { 382 throw "Coordinates must be 32 bytes long"; 383 } 384 let keyBytes = new Uint8Array(65); 385 keyBytes[0] = 0x04; 386 xCoord.map((x, i) => (keyBytes[1 + i] = x)); 387 yCoord.map((x, i) => (keyBytes[33 + i] = x)); 388 return keyBytes; 389 } 390 391 function assembleSignedData(appParam, flags, counter, challengeParam) { 392 let signedData = new Uint8Array(32 + 1 + 4 + 32); 393 new Uint8Array(appParam).map((x, i) => (signedData[0 + i] = x)); 394 signedData[32] = new Uint8Array(flags)[0]; 395 new Uint8Array(counter).map((x, i) => (signedData[33 + i] = x)); 396 new Uint8Array(challengeParam).map((x, i) => (signedData[37 + i] = x)); 397 return signedData; 398 } 399 400 function assembleRegistrationSignedData( 401 appParam, 402 challengeParam, 403 keyHandle, 404 pubKey 405 ) { 406 let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65); 407 signedData[0] = 0x00; 408 new Uint8Array(appParam).map((x, i) => (signedData[1 + i] = x)); 409 new Uint8Array(challengeParam).map((x, i) => (signedData[33 + i] = x)); 410 new Uint8Array(keyHandle).map((x, i) => (signedData[65 + i] = x)); 411 new Uint8Array(pubKey).map( 412 (x, i) => (signedData[65 + keyHandle.length + i] = x) 413 ); 414 return signedData; 415 } 416 417 function sanitizeSigArray(arr) { 418 // ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so 419 // this method strips leading padding bytes, if added, and also appends 420 // padding zeros, if needed. 421 if (arr.length > 32) { 422 arr = arr.slice(arr.length - 32); 423 } 424 let ret = new Uint8Array(32); 425 ret.set(arr, ret.length - arr.length); 426 return ret; 427 } 428 429 function verifySignature(key, data, derSig) { 430 if (derSig.byteLength < 68) { 431 return Promise.reject( 432 "Invalid signature (length=" + 433 derSig.byteLength + 434 "): " + 435 hexEncode(new Uint8Array(derSig)) 436 ); 437 } 438 439 // Copy signature data into the current context. 440 let derSigCopy = new ArrayBuffer(derSig.byteLength); 441 new Uint8Array(derSigCopy).set(new Uint8Array(derSig)); 442 443 let sigAsn1 = org.pkijs.fromBER(derSigCopy); 444 445 // pkijs.asn1 seems to erroneously set an error code when calling some 446 // internal function. The test suite doesn't like dangling globals. 447 delete window.error; 448 449 let sigR = new Uint8Array( 450 sigAsn1.result.value_block.value[0].value_block.value_hex 451 ); 452 let sigS = new Uint8Array( 453 sigAsn1.result.value_block.value[1].value_block.value_hex 454 ); 455 456 // The resulting R and S values from the ASN.1 Sequence must be fit into 32 457 // bytes. Sometimes they have leading zeros, sometimes they're too short, it 458 // all depends on what lib generated the signature. 459 let R = sanitizeSigArray(sigR); 460 let S = sanitizeSigArray(sigS); 461 462 console.log("Verifying these bytes: " + bytesToBase64UrlSafe(data)); 463 464 let sigData = new Uint8Array(R.length + S.length); 465 sigData.set(R); 466 sigData.set(S, R.length); 467 468 let alg = { name: "ECDSA", hash: "SHA-256" }; 469 return crypto.subtle.verify(alg, key, sigData, data); 470 } 471 472 async function addCredential(authenticatorId, rpId) { 473 let keyPair = await crypto.subtle.generateKey( 474 { 475 name: "ECDSA", 476 namedCurve: "P-256", 477 }, 478 true, 479 ["sign"] 480 ); 481 482 let credId = new Uint8Array(32); 483 crypto.getRandomValues(credId); 484 credId = bytesToBase64UrlSafe(credId); 485 486 let privateKey = await crypto.subtle 487 .exportKey("pkcs8", keyPair.privateKey) 488 .then(privateKey => bytesToBase64UrlSafe(privateKey)); 489 490 await SpecialPowers.spawnChrome( 491 [authenticatorId, credId, rpId, privateKey], 492 (authenticatorId, credId, rpId, privateKey) => { 493 let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( 494 Ci.nsIWebAuthnService 495 ); 496 497 webauthnService.addCredential( 498 authenticatorId, 499 credId, 500 true, // resident key 501 rpId, 502 privateKey, 503 "VGVzdCBVc2Vy", // "Test User" 504 0 // sign count 505 ); 506 } 507 ); 508 509 return credId; 510 } 511 512 async function removeCredential(authenticatorId, credId) { 513 await SpecialPowers.spawnChrome( 514 [authenticatorId, credId], 515 (authenticatorId, credId) => { 516 let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( 517 Ci.nsIWebAuthnService 518 ); 519 520 webauthnService.removeCredential(authenticatorId, credId); 521 } 522 ); 523 }