Skip to content

Commit

Permalink
Bug 1616675 - Prohibit IP addresses for WebAuthn operations r=keeler
Browse files Browse the repository at this point in the history
Refactor to reduce duplication of the promiseWebAuth* methods

Differential Revision: https://phabricator.services.mozilla.com/D63572

--HG--
extra : moz-landing-system : lando
  • Loading branch information
jcjones committed Feb 26, 2020
1 parent 56fa739 commit 36d2616
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 233 deletions.
4 changes: 4 additions & 0 deletions dom/webauthn/WebAuthnManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ nsresult GetOrigin(nsPIDOMWindowInner* aParent,
return NS_ERROR_FAILURE;
}

if (principal->GetIsIpAddress()) {
return NS_ERROR_DOM_SECURITY_ERR;
}

if (aOrigin.EqualsLiteral("null")) {
// 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
// DOMException whose name is "NotAllowedError", and terminate this
Expand Down
1 change: 1 addition & 0 deletions dom/webauthn/tests/browser/browser.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ skip-if = !e10s
[browser_fido_appid_extension.js]
[browser_webauthn_prompts.js]
[browser_webauthn_telemetry.js]
[browser_webauthn_ipaddress.js]
100 changes: 9 additions & 91 deletions dom/webauthn/tests/browser/browser_fido_appid_extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,17 @@

const TEST_URL = "https://example.com/";

function arrivingHereIsBad(aResult) {
ok(false, "Bad result! Received a: " + aResult);
}

function expectError(aType) {
let expected = `${aType}Error`;
return function(aResult) {
is(
aResult.slice(0, expected.length),
expected,
`Expecting a ${aType}Error`
);
};
}

let expectNotSupportedError = expectError("NotSupported");
let expectInvalidStateError = expectError("InvalidState");
let expectSecurityError = expectError("Security");

/* eslint-disable no-shadow */
function promiseU2FRegister(tab, app_id) {
let challenge = crypto.getRandomValues(new Uint8Array(16));
challenge = bytesToBase64UrlSafe(challenge);
function promiseU2FRegister(tab, app_id_) {
let challenge_ = crypto.getRandomValues(new Uint8Array(16));
challenge_ = bytesToBase64UrlSafe(challenge_);

return SpecialPowers.spawn(
tab.linkedBrowser,
[[app_id, challenge]],
[[app_id_, challenge_]],
function([app_id, challenge]) {
return new Promise(resolve => {
content.u2f.register(
Expand All @@ -51,72 +35,6 @@ function promiseU2FRegister(tab, app_id) {
});
}

function promiseWebAuthnRegister(tab, appid) {
return ContentTask.spawn(tab.linkedBrowser, appid, appid => {
const cose_alg_ECDSA_w_SHA256 = -7;

let challenge = content.crypto.getRandomValues(new Uint8Array(16));

let pubKeyCredParams = [
{
type: "public-key",
alg: cose_alg_ECDSA_w_SHA256,
},
];

let publicKey = {
rp: { id: content.document.domain, name: "none", icon: "none" },
user: {
id: new Uint8Array(),
name: "none",
icon: "none",
displayName: "none",
},
pubKeyCredParams,
extensions: { appid },
challenge,
};

return content.navigator.credentials
.create({ publicKey })
.then(res => res.rawId);
});
}

function promiseWebAuthnSign(tab, key_handle, extensions = {}) {
return ContentTask.spawn(
tab.linkedBrowser,
[key_handle, extensions],
([key_handle, extensions]) => {
let challenge = content.crypto.getRandomValues(new Uint8Array(16));

let credential = {
id: key_handle,
type: "public-key",
transports: ["usb"],
};

let publicKey = {
challenge,
extensions,
rpId: content.document.domain,
allowCredentials: [credential],
};

return content.navigator.credentials
.get({ publicKey })
.then(credential => {
return {
authenticatorData: credential.response.authenticatorData,
clientDataJSON: credential.response.clientDataJSON,
extensions: credential.getClientExtensionResults(),
};
});
}
);
}
/* eslint-enable no-shadow */

add_task(function test_setup() {
Services.prefs.setBoolPref("security.webauth.u2f", true);
Services.prefs.setBoolPref("security.webauth.webauthn", true);
Expand All @@ -143,35 +61,35 @@ add_task(async function test_appid() {
let keyHandle = await promiseU2FRegister(tab, appid);

// The FIDO AppId extension can't be used for MakeCredential.
await promiseWebAuthnRegister(tab, appid)
await promiseWebAuthnMakeCredential(tab, "none", { appid })
.then(arrivingHereIsBad)
.catch(expectNotSupportedError);

// Using the keyHandle shouldn't work without the FIDO AppId extension.
// This will be an invalid state, because the softtoken will consent without
// having the correct "RP ID" via the FIDO extension.
await promiseWebAuthnSign(tab, keyHandle)
await promiseWebAuthnGetAssertion(tab, keyHandle)
.then(arrivingHereIsBad)
.catch(expectInvalidStateError);

// Invalid app IDs (for the current origin) must be rejected.
await promiseWebAuthnSign(tab, keyHandle, {
await promiseWebAuthnGetAssertion(tab, keyHandle, {
appid: "https://bogus.com/appId",
})
.then(arrivingHereIsBad)
.catch(expectSecurityError);

// Non-matching app IDs must be rejected. Even when the user/softtoken
// consents, leading to an invalid state.
await promiseWebAuthnSign(tab, keyHandle, { appid: appid + "2" })
await promiseWebAuthnGetAssertion(tab, keyHandle, { appid: appid + "2" })
.then(arrivingHereIsBad)
.catch(expectInvalidStateError);

let rpId = new TextEncoder("utf-8").encode(appid);
let rpIdHash = await crypto.subtle.digest("SHA-256", rpId);

// Succeed with the right fallback rpId.
await promiseWebAuthnSign(tab, keyHandle, { appid }).then(
await promiseWebAuthnGetAssertion(tab, keyHandle, { appid }).then(
({ authenticatorData, clientDataJSON, extensions }) => {
is(extensions.appid, true, "appid extension was acted upon");

Expand Down
49 changes: 49 additions & 0 deletions dom/webauthn/tests/browser/browser_webauthn_ipaddress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

let expectSecurityError = expectError("Security");

add_task(function test_setup() {
Services.prefs.setBoolPref("security.webauth.u2f", true);
Services.prefs.setBoolPref("security.webauth.webauthn", true);
Services.prefs.setBoolPref(
"security.webauth.webauthn_enable_android_fido2",
false
);
Services.prefs.setBoolPref(
"security.webauth.webauthn_enable_softtoken",
true
);
Services.prefs.setBoolPref(
"security.webauth.webauthn_enable_usbtoken",
false
);
});

registerCleanupFunction(async function() {
Services.prefs.clearUserPref("security.webauth.u2f");
Services.prefs.clearUserPref("security.webauth.webauthn");
Services.prefs.clearUserPref(
"security.webauth.webauthn_enable_android_fido2"
);
Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
});

add_task(async function test_appid() {
// 127.0.0.1 triggers special cases in ssltunnel, so let's use .2!
const TEST_URL = "https://127.0.0.2/";

// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);

await promiseWebAuthnMakeCredential(tab, "none", {})
.then(arrivingHereIsBad)
.catch(expectSecurityError);

// Close tab.
BrowserTestUtils.removeTab(tab);
});
104 changes: 20 additions & 84 deletions dom/webauthn/tests/browser/browser_webauthn_prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,88 +19,24 @@ function promiseNotification(id) {
});
}

function arrivingHereIsBad(aResult) {
ok(false, "Bad result! Received a: " + aResult);
}

function expectAbortError(aResult) {
let expected = "AbortError";
is(aResult.slice(0, expected.length), expected, `Expecting a ${expected}`);
}

function verifyAnonymizedCertificate(attestationObject) {
return webAuthnDecodeCBORAttestation(attestationObject).then(
({ fmt, attStmt }) => {
is("none", fmt, "Is a None Attestation");
is("object", typeof attStmt, "attStmt is a map");
is(0, Object.keys(attStmt).length, "attStmt is empty");
}
);
}

function verifyDirectCertificate(attestationObject) {
return webAuthnDecodeCBORAttestation(attestationObject).then(
({ fmt, attStmt }) => {
is("fido-u2f", fmt, "Is a FIDO U2F Attestation");
is("object", typeof attStmt, "attStmt is a map");
ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists");
ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists");
}
);
}

function promiseWebAuthnRegister(tab, attestation = "indirect") {
/* eslint-disable no-shadow */
return ContentTask.spawn(tab.linkedBrowser, attestation, attestation => {
const cose_alg_ECDSA_w_SHA256 = -7;

let challenge = content.crypto.getRandomValues(new Uint8Array(16));

let pubKeyCredParams = [
{
type: "public-key",
alg: cose_alg_ECDSA_w_SHA256,
},
];

let publicKey = {
rp: { id: content.document.domain, name: "none", icon: "none" },
user: {
id: new Uint8Array(),
name: "none",
icon: "none",
displayName: "none",
},
pubKeyCredParams,
attestation,
challenge,
};

return content.navigator.credentials
.create({ publicKey })
.then(cred => cred.response.attestationObject);
let expectAbortError = expectError("Abort");

function verifyAnonymizedCertificate(result) {
let { attObj, rawId } = result;
return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => {
is("none", fmt, "Is a None Attestation");
is("object", typeof attStmt, "attStmt is a map");
is(0, Object.keys(attStmt).length, "attStmt is empty");
});
/* eslint-enable no-shadow */
}

function promiseWebAuthnSign(tab) {
return ContentTask.spawn(tab.linkedBrowser, null, () => {
let challenge = content.crypto.getRandomValues(new Uint8Array(16));
let key_handle = content.crypto.getRandomValues(new Uint8Array(16));

let credential = {
id: key_handle,
type: "public-key",
transports: ["usb"],
};

let publicKey = {
challenge,
rpId: content.document.domain,
allowCredentials: [credential],
};

return content.navigator.credentials.get({ publicKey });
function verifyDirectCertificate(result) {
let { attObj, rawId } = result;
return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => {
is("fido-u2f", fmt, "Is a FIDO U2F Attestation");
is("object", typeof attStmt, "attStmt is a map");
ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists");
ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists");
});
}

Expand All @@ -122,7 +58,7 @@ add_task(async function test_register() {

// Request a new credential and wait for the prompt.
let active = true;
let request = promiseWebAuthnRegister(tab)
let request = promiseWebAuthnMakeCredential(tab, "indirect", {})
.then(arrivingHereIsBad)
.catch(expectAbortError)
.then(() => (active = false));
Expand All @@ -143,7 +79,7 @@ add_task(async function test_sign() {

// Request a new assertion and wait for the prompt.
let active = true;
let request = promiseWebAuthnSign(tab)
let request = promiseWebAuthnGetAssertion(tab)
.then(arrivingHereIsBad)
.catch(expectAbortError)
.then(() => (active = false));
Expand All @@ -164,7 +100,7 @@ add_task(async function test_register_direct_cancel() {

// Request a new credential with direct attestation and wait for the prompt.
let active = true;
let promise = promiseWebAuthnRegister(tab, "direct")
let promise = promiseWebAuthnMakeCredential(tab, "direct", {})
.then(arrivingHereIsBad)
.catch(expectAbortError)
.then(() => (active = false));
Expand Down Expand Up @@ -195,7 +131,7 @@ add_task(async function test_register_direct_proceed() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);

// Request a new credential with direct attestation and wait for the prompt.
let request = promiseWebAuthnRegister(tab, "direct");
let request = promiseWebAuthnMakeCredential(tab, "direct", {});
await promiseNotification("webauthn-prompt-register-direct");

// Proceed.
Expand All @@ -213,7 +149,7 @@ add_task(async function test_register_direct_proceed_anon() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);

// Request a new credential with direct attestation and wait for the prompt.
let request = promiseWebAuthnRegister(tab, "direct");
let request = promiseWebAuthnMakeCredential(tab, "direct", {});
await promiseNotification("webauthn-prompt-register-direct");

// Check "anonymize anyway" and proceed.
Expand Down
Loading

0 comments on commit 36d2616

Please sign in to comment.