mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-26 12:20:56 +00:00
Bug 1444756 - Rewrite browser_webauthn_telemetry.js r=jcj
Reviewers: jcj Reviewed By: jcj Bug #: 1444756 Differential Revision: https://phabricator.services.mozilla.com/D703 --HG-- extra : amend_source : 991801bfb48a6e3262c1d2a77c8734defae2406d
This commit is contained in:
parent
faeeb9d479
commit
141cb3849c
@ -2,7 +2,6 @@
|
||||
support-files =
|
||||
head.js
|
||||
tab_webauthn_result.html
|
||||
tab_webauthn_success.html
|
||||
../pkijs/*
|
||||
../cbor.js
|
||||
../u2futil.js
|
||||
@ -12,4 +11,3 @@ skip-if = !e10s
|
||||
[browser_fido_appid_extension.js]
|
||||
[browser_webauthn_prompts.js]
|
||||
[browser_webauthn_telemetry.js]
|
||||
skip-if = verify
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URL = "https://example.com/";
|
||||
|
||||
// Return the scalars from the parent-process.
|
||||
function getParentProcessScalars(aChannel, aKeyed = false, aClear = false) {
|
||||
const scalars = aKeyed ?
|
||||
@ -37,27 +39,68 @@ function validateHistogramEntryCount(aHistogramName, aExpectedCount) {
|
||||
aHistogramName);
|
||||
}
|
||||
|
||||
// Loads a page, and expects to have an element ID "result" added to the DOM when the page is done.
|
||||
async function executeTestPage(aUri) {
|
||||
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, aUri);
|
||||
try {
|
||||
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
function promiseMakeCredentialRequest(tab) {
|
||||
return ContentTask.spawn(tab.linkedBrowser, null, () => {
|
||||
const cose_alg_ECDSA_w_SHA256 = -7;
|
||||
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
|
||||
let condition = () => content.document.getElementById("result");
|
||||
await ContentTaskUtils.waitForCondition(condition,
|
||||
"Waited too long for operation to finish on "
|
||||
+ content.document.location);
|
||||
let publicKey = {
|
||||
rp: {id: content.document.domain, name: "none", icon: "none"},
|
||||
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
|
||||
challenge: content.crypto.getRandomValues(new Uint8Array(16)),
|
||||
timeout: 5000, // the minimum timeout is actually 15 seconds
|
||||
pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
|
||||
attestation: "direct"
|
||||
};
|
||||
|
||||
return content.navigator.credentials.create({publicKey}).then(cred => {
|
||||
return {
|
||||
attObj: cred.response.attestationObject,
|
||||
rawId: cred.rawId
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promiseGetAssertionRequest(tab, rawId) {
|
||||
return ContentTask.spawn(tab.linkedBrowser, [rawId], ([rawId]) => {
|
||||
let newCredential = {
|
||||
type: "public-key",
|
||||
transports: ["usb"],
|
||||
id: rawId,
|
||||
};
|
||||
|
||||
let publicKey = {
|
||||
challenge: content.crypto.getRandomValues(new Uint8Array(16)),
|
||||
timeout: 5000, // the minimum timeout is actually 15 seconds
|
||||
rpId: content.document.domain,
|
||||
allowCredentials: [newCredential]
|
||||
};
|
||||
|
||||
return content.navigator.credentials.get({publicKey}).then(assertion => {
|
||||
return {
|
||||
clientDataJSON: assertion.response.clientDataJSON,
|
||||
authData: assertion.response.authenticatorData,
|
||||
signature: assertion.response.signature
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkRpIdHash(rpIdHash) {
|
||||
return crypto.subtle.digest("SHA-256", string2buffer("example.com"))
|
||||
.then(calculatedRpIdHash => {
|
||||
let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash));
|
||||
let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash));
|
||||
|
||||
if (calcHashStr != providedHashStr) {
|
||||
throw new Error("Calculated RP ID hash doesn't match.");
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
ok(false, "Exception thrown executing test page: " + e);
|
||||
} finally {
|
||||
// Remove all the extra windows and tabs.
|
||||
return BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function test_setup() {
|
||||
cleanupTelemetry();
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
"set": [
|
||||
["security.webauth.webauthn", true],
|
||||
@ -68,25 +111,54 @@ add_task(async function test_setup() {
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_loopback() {
|
||||
add_task(async function test() {
|
||||
// These tests can't run simultaneously as the preference changes will race.
|
||||
// So let's run them sequentially here, but in an async function so we can
|
||||
// use await.
|
||||
const testPage = "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_success.html";
|
||||
{
|
||||
cleanupTelemetry();
|
||||
await executeTestPage(testPage);
|
||||
|
||||
let webauthn_used = getTelemetryForScalar("security.webauthn_used");
|
||||
ok(webauthn_used, "Scalar keys are set: " + Object.keys(webauthn_used).join(", "));
|
||||
is(webauthn_used["U2FRegisterFinish"], 1, "webauthn_used U2FRegisterFinish scalar should be 1");
|
||||
is(webauthn_used["U2FSignFinish"], 1, "webauthn_used U2FSignFinish scalar should be 1");
|
||||
is(webauthn_used["U2FSignAbort"], undefined, "webauthn_used U2FSignAbort scalar must be unset");
|
||||
is(webauthn_used["U2FRegisterAbort"], undefined, "webauthn_used U2FRegisterAbort scalar must be unset");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
|
||||
|
||||
validateHistogramEntryCount("WEBAUTHN_CREATE_CREDENTIAL_MS", 1);
|
||||
validateHistogramEntryCount("WEBAUTHN_GET_ASSERTION_MS", 1);
|
||||
}
|
||||
// Create a new credential.
|
||||
let {attObj, rawId} = await promiseMakeCredentialRequest(tab);
|
||||
let {authDataObj} = await webAuthnDecodeCBORAttestation(attObj);
|
||||
|
||||
// Make sure the RP ID hash matches what we calculate.
|
||||
await checkRpIdHash(authDataObj.rpIdHash);
|
||||
|
||||
// Get a new assertion.
|
||||
let {clientDataJSON, authData, signature} =
|
||||
await promiseGetAssertionRequest(tab, rawId);
|
||||
|
||||
// Check the we can parse clientDataJSON.
|
||||
JSON.parse(buffer2string(clientDataJSON));
|
||||
|
||||
// Check auth data.
|
||||
let attestation = await webAuthnDecodeAuthDataArray(new Uint8Array(authData));
|
||||
is(attestation.flags, flag_TUP, "Assertion's user presence byte set correctly");
|
||||
|
||||
// Verify the signature.
|
||||
let params = await deriveAppAndChallengeParam("example.com",
|
||||
clientDataJSON,
|
||||
attestation);
|
||||
let signedData = await assembleSignedData(params.appParam,
|
||||
params.attestation.flags,
|
||||
params.attestation.counter,
|
||||
params.challengeParam);
|
||||
let valid = await verifySignature(authDataObj.publicKeyHandle, signedData, signature)
|
||||
ok(valid, "signature is valid");
|
||||
|
||||
// Check telemetry data.
|
||||
let webauthn_used = getTelemetryForScalar("security.webauthn_used");
|
||||
ok(webauthn_used, "Scalar keys are set: " + Object.keys(webauthn_used).join(", "));
|
||||
is(webauthn_used["U2FRegisterFinish"], 1, "webauthn_used U2FRegisterFinish scalar should be 1");
|
||||
is(webauthn_used["U2FSignFinish"], 1, "webauthn_used U2FSignFinish scalar should be 1");
|
||||
is(webauthn_used["U2FSignAbort"], undefined, "webauthn_used U2FSignAbort scalar must be unset");
|
||||
is(webauthn_used["U2FRegisterAbort"], undefined, "webauthn_used U2FRegisterAbort scalar must be unset");
|
||||
|
||||
validateHistogramEntryCount("WEBAUTHN_CREATE_CREDENTIAL_MS", 1);
|
||||
validateHistogramEntryCount("WEBAUTHN_GET_ASSERTION_MS", 1);
|
||||
|
||||
await BrowserTestUtils.removeTab(tab);
|
||||
|
||||
// There aren't tests for register succeeding and sign failing, as I don't see an easy way to prompt
|
||||
// the soft token to fail that way _and_ trigger the Abort telemetry.
|
||||
|
@ -4,12 +4,22 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/dom/webauthn/tests/browser/cbor.js",
|
||||
this);
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/dom/webauthn/tests/browser/u2futil.js",
|
||||
this);
|
||||
let exports = this;
|
||||
|
||||
const scripts = [
|
||||
"pkijs/common.js",
|
||||
"pkijs/asn1.js",
|
||||
"pkijs/x509_schema.js",
|
||||
"pkijs/x509_simpl.js",
|
||||
"browser/cbor.js",
|
||||
"browser/u2futil.js",
|
||||
];
|
||||
|
||||
for (let script of scripts) {
|
||||
Services.scriptloader.loadSubScript(
|
||||
`chrome://mochitests/content/browser/dom/webauthn/tests/${script}`,
|
||||
this, "utf-8");
|
||||
}
|
||||
|
||||
function memcmp(x, y) {
|
||||
let xb = new Uint8Array(x);
|
||||
|
@ -1,117 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<head>
|
||||
<title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title>
|
||||
<script type="text/javascript" src="u2futil.js"></script>
|
||||
<script type="text/javascript" src="../pkijs/common.js"></script>
|
||||
<script type="text/javascript" src="../pkijs/asn1.js"></script>
|
||||
<script type="text/javascript" src="../pkijs/x509_schema.js"></script>
|
||||
<script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
|
||||
<script type="text/javascript" src="cbor.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265472">Mozilla Bug 1265472</a>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
let gCredentialChallenge = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(gCredentialChallenge);
|
||||
let gAssertionChallenge = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(gAssertionChallenge);
|
||||
|
||||
// the WebAuthn browser chrome tests watch for an element 'result' to appear in the DOM
|
||||
function signalCompletion(aText) {
|
||||
console.log(aText)
|
||||
let result = document.createElement('h1');
|
||||
result.id = "result";
|
||||
result.textContent = aText;
|
||||
document.body.append(result);
|
||||
}
|
||||
|
||||
let gState = {};
|
||||
let makeCredentialOptions = {
|
||||
rp: {id: document.domain, name: "none", icon: "none"},
|
||||
user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
|
||||
challenge: gCredentialChallenge,
|
||||
timeout: 5000, // the minimum timeout is actually 15 seconds
|
||||
pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
|
||||
attestation: "direct"
|
||||
};
|
||||
|
||||
navigator.credentials.create({publicKey: makeCredentialOptions})
|
||||
.then(function (aNewCredentialInfo) {
|
||||
gState.credential = aNewCredentialInfo;
|
||||
|
||||
return webAuthnDecodeCBORAttestation(aNewCredentialInfo.response.attestationObject);
|
||||
})
|
||||
.then(function testAssertion(aCredInfo) {
|
||||
gState.authDataObj = aCredInfo.authDataObj;
|
||||
gState.publicKeyHandle = aCredInfo.authDataObj.publicKeyHandle;
|
||||
|
||||
let newCredential = {
|
||||
type: "public-key",
|
||||
id: new Uint8Array(gState.credential.rawId),
|
||||
transports: ["usb"],
|
||||
}
|
||||
|
||||
let publicKeyCredentialRequestOptions = {
|
||||
challenge: gAssertionChallenge,
|
||||
timeout: 5000, // the minimum timeout is actually 15 seconds
|
||||
rpId: document.domain,
|
||||
allowCredentials: [newCredential]
|
||||
};
|
||||
|
||||
// Make sure the RP ID hash matches what we calculate.
|
||||
return crypto.subtle.digest("SHA-256", string2buffer(document.domain))
|
||||
.then(function(calculatedRpIdHash) {
|
||||
let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash));
|
||||
let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(gState.authDataObj.rpIdHash));
|
||||
|
||||
if (calcHashStr != providedHashStr) {
|
||||
return Promise.reject("Calculated RP ID hash must match what the browser derived.");
|
||||
}
|
||||
|
||||
return navigator.credentials.get({publicKey: publicKeyCredentialRequestOptions});
|
||||
});
|
||||
})
|
||||
.then(function(aAssertion) {
|
||||
let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON));
|
||||
|
||||
gState.assertion = aAssertion;
|
||||
|
||||
return webAuthnDecodeAuthDataArray(new Uint8Array(aAssertion.response.authenticatorData));
|
||||
})
|
||||
.then(function(aAttestation) {
|
||||
if (new Uint8Array(aAttestation.flags) != flag_TUP) {
|
||||
return Promise.reject("Assertion's user presence byte not set correctly.");
|
||||
}
|
||||
|
||||
let clientDataJSON = gState.assertion.response.clientDataJSON;
|
||||
return deriveAppAndChallengeParam(document.domain, clientDataJSON, aAttestation);
|
||||
})
|
||||
.then(function(aParams) {
|
||||
return assembleSignedData(aParams.appParam, aParams.attestation.flags,
|
||||
aParams.attestation.counter, aParams.challengeParam);
|
||||
})
|
||||
.then(function(aSignedData) {
|
||||
let signature = gState.assertion.response.signature;
|
||||
console.log(gState.publicKeyHandle, aSignedData, signature);
|
||||
return verifySignature(gState.publicKeyHandle, aSignedData, signature);
|
||||
})
|
||||
.then(function(aSigVerifyResult) {
|
||||
signalCompletion("Signing signature verified: " + aSigVerifyResult);
|
||||
gState = {};
|
||||
})
|
||||
.catch(function(aReason) {
|
||||
signalCompletion("Failure: " + aReason);
|
||||
gState = {};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -318,7 +318,16 @@ function verifySignature(key, data, derSig) {
|
||||
"): " + hexEncode(new Uint8Array(derSig)));
|
||||
}
|
||||
|
||||
let sigAsn1 = org.pkijs.fromBER(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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user