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:
Tim Taubert 2018-03-13 08:16:52 +01:00
parent faeeb9d479
commit 141cb3849c
5 changed files with 128 additions and 156 deletions

View File

@ -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

View File

@ -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.

View File

@ -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);

View File

@ -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>

View File

@ -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);