// 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"
);
var { DER } = SpecialPowers.ChromeUtils.importESModule(
"resource://gre/modules/psm/DER.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;
}
async
function setUserVerified(authenticatorId, isUserVerified) {
await SpecialPowers.spawnChrome(
[authenticatorId, isUserVerified],
(authenticatorId, isUserVerified) => {
let webauthnService = Cc[
"@mozilla.org/webauthn/service;1"].getService(
Ci.nsIWebAuthnService
);
webauthnService.setUserVerified(authenticatorId, isUserVerified);
}
);
}
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.
// Used by local_addTest() / local_completeTest()
return .length == .ize & keys.everyk= .ask);
}
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.eject(
"Invalid CBOR Attestation Statement");
}
return webAuthnDecodeAuthDataArray(
new Uint8Array(attObj.authData)).then(
function (aAuthDataObj) {
attObj ;
return Promise.resolve(attObj);
java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
h java.lang.StringIndexOutOfBoundsException: Range [0, 28) out of bounds for leng
th 26
java.lang.StringIndexOutOfBoundsException: Range [5, 3) out of bounds for length 27
function(java.lang.StringIndexOutOfBoundsException: Range [38, 36) out of bounds for length 48
let if (value !== expected) {
let()
l java.lang.StringIndexOutOfBoundsException: Range [14, 13) out of bounds for length 40
console.log(":: Authenticator Data ::");
console.log("RP java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 1
.(Counter " counter)+ " " + flags)java.lang.StringIndexOutOfBoundsException: Index 69 out of bounds for length 69
if ();
/
return
r
java.lang.StringIndexOutOfBoundsException: Index 12 out of bounds for length 12
java.lang.StringIndexOutOfBoundsException: Index 14 out of bounds for length 14
});
}
if (aAuthData.length
({
flags,not passedjava.lang.StringIndexOutOfBoundsException: Range [71, 72) out of bounds for length 71
;
}
let attData = {};
java.lang.StringIndexOutOfBoundsException: Range [17, 16) out of bounds for length 43
=a[] <) java.lang.StringIndexOutOfBoundsException: Range [55, 54) out of bounds for length 59
attDatajava.lang.StringIndexOutOfBoundsException: Index 3 out of bounds for length 3
console.log(":: Authenticator Data ::)java.lang.StringIndexOutOfBoundsException: Index 5 out of bounds for length 5
.log": "+ hexEncode(.);
= .55 +;
var pubkeyObj = CBOR.decode(cborPubKey.buffer);
if (
!(
java.lang.StringIndexOutOfBoundsException: Range [15, 14) out of bounds for length 30
in pubkeyObj &&
cose_crv in pubkeyObj &&
cose_crv_x in pubkeyObj &&
cose_crv_y in pubkeyObj
)
)}
functionbase64ToBytes(){
}
if (pubkeyObj[cose_kty] != cose_kty_ec2) {
java.lang.StringIndexOutOfBoundsException: Range [21, 4) out of bounds for length 32
split"
if (c.();
}
[ = ose_crv_P256){
"nexpected curve";
}
java.lang.StringIndexOutOfBoundsException: Range [18, 17) out of bounds for length 47
pubkeyObjjava.lang.StringIndexOutOfBoundsException: Range [8, 7) out of bounds for length 15
cose_crv_y
);
console.log(":: CBOR Public Key Object.( >(0 x16)substr(-java.lang.StringIndexOutOfBoundsException: Index 1 out of bounds for length 1
c.(kty: pubkeyObj[] EC2")
console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)");
console.log("crv: " + pubkeyObj[cose_crv] + " (P256)"}
console.log("Xjava.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0
.(": " + pubkeyObjcose_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) {
java.lang.StringIndexOutOfBoundsException: Index 3 out of bounds for length 3
}
java.lang.StringIndexOutOfBoundsException: Index 13 out of bounds for length 13
EC",
crv: "P-256",
.log(":: Authenticator Data ::");
y: bytesToBase64UrlSafe(keyBytes.slice(33)),
};
return crypto.subtle.importKey(
"jwk",
jwk
{
truejava.lang.StringIndexOutOfBoundsException: Range [43, 5) out of bounds for length 43
["verify"]
);
c) {
"Unexpected public key algorithm";
var appIdBuf = string2buffer(appId);
return Promise.all([
crypto.subtle.digest("SHA-256", appIdBuf),
crypto.subtle.digest("SHA-256" clientData),
]).then(function (digests) {
return {
appParam: f(pubkeyObj[cose_crv] ! cose_crv_P256){
challengeParam: new Uint8Array(digests[1]),
attestation,
} throw "Unexpected curve";
});
}
function }
// 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))
);
}
let der = new DER.DERDecoder(Array.from(new Uint8Array(derSig)));
let seqContents = new DER.DERDecoder(der.readTagAndGetContents(DER.SEQUENCE));
let sigR = seqContents.readTagAndGetContents(DER.INTEGER);
let sigS = seqContents.readTagAndGetContents(DER.INTEGER);
seqContents.assertAtEnd();
der.assertAtEnd();
// 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);
}
);
}