// Used by local_addTest() / local_completeTest() var _countCompletions = 0; var _expectedCompletions = 0; const flag_TUP = 0x01; const flag_UV = 0x04; const flag_AT = 0x40; const cose_kty = 1; const cose_kty_ec2 = 2; const cose_alg = 3; const cose_alg_ECDSA_w_SHA256 = -7; const cose_alg_ECDSA_w_SHA512 = -36; const cose_crv = -1; const cose_crv_P256 = 1; const cose_crv_x = -2; const cose_crv_y = -3; var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); function handleEventMessage(event) { if ("test" in event.data) { let summary = event.data.test + ": " + event.data.msg; log(event.data.status + ": " + summary); ok(event.data.status, summary); } else if ("done" in event.data) { SimpleTest.finish(); } else { ok(false, "Unexpected message in the test harness: " + event.data); } } function log(msg) { console.log(msg); let logBox = document.getElementById("log"); if (logBox) { logBox.textContent += "\n" + msg; } } function local_is(value, expected, message) { if (value === expected) { local_ok(true, message); } else { local_ok(false, message + " unexpectedly: " + value + " !== " + expected); } } function local_isnot(value, expected, message) { if (value !== expected) { local_ok(true, message); } else { local_ok(false, message + " unexpectedly: " + value + " === " + expected); } } function local_ok(expression, message) { let body = { test: this.location.pathname, status: expression, msg: message }; parent.postMessage(body, "http://mochi.test:8888"); } function local_doesThrow(fn, name) { let gotException = false; try { fn(); } catch (ex) { gotException = true; } local_ok(gotException, name); } function local_expectThisManyTests(count) { if (_expectedCompletions > 0) { local_ok( false, "Error: local_expectThisManyTests should only be called once." ); } _expectedCompletions = count; } function local_completeTest() { _countCompletions += 1; if (_countCompletions == _expectedCompletions) { log("All tests completed."); local_finished(); } if (_countCompletions > _expectedCompletions) { local_ok( false, "Error: local_completeTest called more than local_addTest." ); } } function local_finished() { parent.postMessage({ done: true }, "http://mochi.test:8888"); } function string2buffer(str) { return new Uint8Array(str.length).map((x, i) => str.charCodeAt(i)); } function buffer2string(buf) { let str = ""; if (!(buf.constructor === Uint8Array)) { buf = new Uint8Array(buf); } buf.map(function (x) { return (str += String.fromCharCode(x)); }); return str; } function bytesToBase64(u8a) { let CHUNK_SZ = 0x8000; let c = []; let array = new Uint8Array(u8a); for (let i = 0; i < array.length; i += CHUNK_SZ) { c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ))); } return window.btoa(c.join("")); } function base64ToBytes(b64encoded) { return new Uint8Array( window .atob(b64encoded) .split("") .map(function (c) { return c.charCodeAt(0); }) ); } function bytesToBase64UrlSafe(buf) { return bytesToBase64(buf) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } function base64ToBytesUrlSafe(str) { if (str.length % 4 == 1) { throw "Improper b64 string"; } var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/"); while (b64.length % 4 != 0) { b64 += "="; } return base64ToBytes(b64); } function hexEncode(buf) { return Array.from(buf) .map(x => ("0" + x.toString(16)).substr(-2)) .join(""); } function hexDecode(str) { return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16))); } function hasOnlyKeys(obj, ...keys) { let okeys = new Set(Object.keys(obj)); return keys.length == okeys.size && keys.every(k => okeys.has(k)); } function webAuthnDecodeCBORAttestation(aCborAttBuf) { let attObj = CBOR.decode(aCborAttBuf); console.log(":: Attestation CBOR Object ::"); if (!hasOnlyKeys(attObj, "authData", "fmt", "attStmt")) { return Promise.reject("Invalid CBOR Attestation Object"); } if (attObj.fmt == "fido-u2f" && !hasOnlyKeys(attObj.attStmt, "sig", "x5c")) { return Promise.reject("Invalid CBOR Attestation Statement"); } if (attObj.fmt == "none" && Object.keys(attObj.attStmt).length) { return Promise.reject("Invalid CBOR Attestation Statement"); } return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData)).then( function (aAuthDataObj) { attObj.authDataObj = aAuthDataObj; return Promise.resolve(attObj); } ); } function webAuthnDecodeAuthDataArray(aAuthData) { let rpIdHash = aAuthData.slice(0, 32); let flags = aAuthData.slice(32, 33); let counter = aAuthData.slice(33, 37); console.log(":: Authenticator Data ::"); console.log("RP ID Hash: " + hexEncode(rpIdHash)); console.log("Counter: " + hexEncode(counter) + " Flags: " + flags); if ((flags & flag_AT) == 0x00) { // No Attestation Data, so we're done. return Promise.resolve({ rpIdHash, flags, counter, }); } if (aAuthData.length < 38) { return Promise.reject( "Authenticator Data flag was set, but not enough data passed in!" ); } let attData = {}; attData.aaguid = aAuthData.slice(37, 53); attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54]; attData.credId = aAuthData.slice(55, 55 + attData.credIdLen); console.log(":: Authenticator Data ::"); console.log("AAGUID: " + hexEncode(attData.aaguid)); let cborPubKey = aAuthData.slice(55 + attData.credIdLen); var pubkeyObj = CBOR.decode(cborPubKey.buffer); if ( !( cose_kty in pubkeyObj && cose_alg in pubkeyObj && cose_crv in pubkeyObj && cose_crv_x in pubkeyObj && cose_crv_y in pubkeyObj ) ) { throw "Invalid CBOR Public Key Object"; } if (pubkeyObj[cose_kty] != cose_kty_ec2) { throw "Unexpected key type"; } if (pubkeyObj[cose_alg] != cose_alg_ECDSA_w_SHA256) { throw "Unexpected public key algorithm"; } if (pubkeyObj[cose_crv] != cose_crv_P256) { throw "Unexpected curve"; } let pubKeyBytes = assemblePublicKeyBytesData( pubkeyObj[cose_crv_x], pubkeyObj[cose_crv_y] ); console.log(":: CBOR Public Key Object Data ::"); console.log("kty: " + pubkeyObj[cose_kty] + " (EC2)"); console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)"); console.log("crv: " + pubkeyObj[cose_crv] + " (P256)"); console.log("X: " + pubkeyObj[cose_crv_x]); console.log("Y: " + pubkeyObj[cose_crv_y]); console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes)); return importPublicKey(pubKeyBytes).then(function (aKeyHandle) { return Promise.resolve({ rpIdHash, flags, counter, attestationAuthData: attData, publicKeyBytes: pubKeyBytes, publicKeyHandle: aKeyHandle, }); }); } function importPublicKey(keyBytes) { if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) { throw "Bad public key octet string"; } var jwk = { kty: "EC", crv: "P-256", x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)), y: bytesToBase64UrlSafe(keyBytes.slice(33)), }; return crypto.subtle.importKey( "jwk", jwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"] ); } function deriveAppAndChallengeParam(appId, clientData, attestation) { var appIdBuf = string2buffer(appId); return Promise.all([ crypto.subtle.digest("SHA-256", appIdBuf), crypto.subtle.digest("SHA-256", clientData), ]).then(function (digests) { return { appParam: new Uint8Array(digests[0]), challengeParam: new Uint8Array(digests[1]), attestation, }; }); } function assemblePublicKeyBytesData(xCoord, yCoord) { // Produce an uncompressed EC key point. These start with 0x04, and then // two 32-byte numbers denoting X and Y. if (xCoord.length != 32 || yCoord.length != 32) { throw "Coordinates must be 32 bytes long"; } let keyBytes = new Uint8Array(65); keyBytes[0] = 0x04; xCoord.map((x, i) => (keyBytes[1 + i] = x)); yCoord.map((x, i) => (keyBytes[33 + i] = x)); return keyBytes; } function assembleSignedData(appParam, flags, counter, challengeParam) { let signedData = new Uint8Array(32 + 1 + 4 + 32); new Uint8Array(appParam).map((x, i) => (signedData[0 + i] = x)); signedData[32] = new Uint8Array(flags)[0]; new Uint8Array(counter).map((x, i) => (signedData[33 + i] = x)); new Uint8Array(challengeParam).map((x, i) => (signedData[37 + i] = x)); return signedData; } function assembleRegistrationSignedData( appParam, challengeParam, keyHandle, pubKey ) { let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65); signedData[0] = 0x00; new Uint8Array(appParam).map((x, i) => (signedData[1 + i] = x)); new Uint8Array(challengeParam).map((x, i) => (signedData[33 + i] = x)); new Uint8Array(keyHandle).map((x, i) => (signedData[65 + i] = x)); new Uint8Array(pubKey).map( (x, i) => (signedData[65 + keyHandle.length + i] = x) ); return signedData; } function sanitizeSigArray(arr) { // ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so // this method strips leading padding bytes, if added, and also appends // padding zeros, if needed. if (arr.length > 32) { arr = arr.slice(arr.length - 32); } let ret = new Uint8Array(32); ret.set(arr, ret.length - arr.length); return ret; } function verifySignature(key, data, derSig) { if (derSig.byteLength < 68) { return Promise.reject( "Invalid signature (length=" + derSig.byteLength + "): " + hexEncode(new Uint8Array(derSig)) ); } // Copy signature data into the current context. let derSigCopy = new ArrayBuffer(derSig.byteLength); new Uint8Array(derSigCopy).set(new Uint8Array(derSig)); let sigAsn1 = org.pkijs.fromBER(derSigCopy); // pkijs.asn1 seems to erroneously set an error code when calling some // internal function. The test suite doesn't like dangling globals. delete window.error; let sigR = new Uint8Array( sigAsn1.result.value_block.value[0].value_block.value_hex ); let sigS = new Uint8Array( sigAsn1.result.value_block.value[1].value_block.value_hex ); // The resulting R and S values from the ASN.1 Sequence must be fit into 32 // bytes. Sometimes they have leading zeros, sometimes they're too short, it // all depends on what lib generated the signature. let R = sanitizeSigArray(sigR); let S = sanitizeSigArray(sigS); console.log("Verifying these bytes: " + bytesToBase64UrlSafe(data)); let sigData = new Uint8Array(R.length + S.length); sigData.set(R); sigData.set(S, R.length); let alg = { name: "ECDSA", hash: "SHA-256" }; return crypto.subtle.verify(alg, key, sigData, data); }