summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/tests/u2futil.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/tests/u2futil.js')
-rw-r--r--dom/webauthn/tests/u2futil.js511
1 files changed, 511 insertions, 0 deletions
diff --git a/dom/webauthn/tests/u2futil.js b/dom/webauthn/tests/u2futil.js
new file mode 100644
index 0000000000..e8026e8e59
--- /dev/null
+++ b/dom/webauthn/tests/u2futil.js
@@ -0,0 +1,511 @@
+// 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"
+);
+
+async function addVirtualAuthenticator(
+ protocol = "ctap2_1",
+ transport = "internal",
+ hasResidentKey = true,
+ hasUserVerification = true,
+ isUserConsenting = true,
+ isUserVerified = true
+) {
+ let id = await SpecialPowers.spawnChrome(
+ [
+ protocol,
+ transport,
+ hasResidentKey,
+ hasUserVerification,
+ isUserConsenting,
+ isUserVerified,
+ ],
+ (
+ protocol,
+ transport,
+ hasResidentKey,
+ hasUserVerification,
+ isUserConsenting,
+ isUserVerified
+ ) => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ let id = webauthnService.addVirtualAuthenticator(
+ protocol,
+ transport,
+ hasResidentKey,
+ hasUserVerification,
+ isUserConsenting,
+ isUserVerified
+ );
+ return id;
+ }
+ );
+
+ SimpleTest.registerCleanupFunction(async () => {
+ await SpecialPowers.spawnChrome([id], id => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ webauthnService.removeVirtualAuthenticator(id);
+ });
+ });
+
+ return id;
+}
+
+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 == "packed" &&
+ !(
+ hasOnlyKeys(attObj.attStmt, "alg", "sig") ||
+ hasOnlyKeys(attObj.attStmt, "alg", "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);
+}
+
+async function addCredential(authenticatorId, rpId) {
+ let keyPair = await crypto.subtle.generateKey(
+ {
+ name: "ECDSA",
+ namedCurve: "P-256",
+ },
+ true,
+ ["sign"]
+ );
+
+ let credId = new Uint8Array(32);
+ crypto.getRandomValues(credId);
+ credId = bytesToBase64UrlSafe(credId);
+
+ let privateKey = await crypto.subtle
+ .exportKey("pkcs8", keyPair.privateKey)
+ .then(privateKey => bytesToBase64UrlSafe(privateKey));
+
+ await SpecialPowers.spawnChrome(
+ [authenticatorId, credId, rpId, privateKey],
+ (authenticatorId, credId, rpId, privateKey) => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+
+ webauthnService.addCredential(
+ authenticatorId,
+ credId,
+ true, // resident key
+ rpId,
+ privateKey,
+ "VGVzdCBVc2Vy", // "Test User"
+ 0 // sign count
+ );
+ }
+ );
+
+ return credId;
+}
+
+async function removeCredential(authenticatorId, credId) {
+ await SpecialPowers.spawnChrome(
+ [authenticatorId, credId],
+ (authenticatorId, credId) => {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+
+ webauthnService.removeCredential(authenticatorId, credId);
+ }
+ );
+}