commit 3fc322f731ab810388d3e195a80fc47017644c14
parent 4113b2a28522a0a1483a1a451e9b2d71fc7fa980
Author: John M. Schanck <jschanck@mozilla.com>
Date: Fri, 5 Dec 2025 17:32:16 +0000
Bug 1977284 - allow webauthn with certificate overrides on localhost. r=keeler
This also adds a pref, `security.webauthn.allow_with_certificate_override`, that can be set to true to allow webauthn when any certificate override is in place. This is primarily for users with local network devices that use self-signed certificates.
Differential Revision: https://phabricator.services.mozilla.com/D275108
Diffstat:
8 files changed, 117 insertions(+), 104 deletions(-)
diff --git a/build/pgo/server-locations.txt b/build/pgo/server-locations.txt
@@ -354,8 +354,9 @@ https://tls13.example.com:443 privileged,tls1,tls1_3
# Hosts for youtube rewrite tests
https://mochitest.youtube.com:443
-# Host for U2F localhost tests
+# Hosts for WebAuthn localhost tests
https://localhost:443
+https://badcertdomain.localhost:443 cert=badCertDomain
# Bug 1402530
http://localhost:80 privileged
diff --git a/dom/webauthn/WebAuthnHandler.cpp b/dom/webauthn/WebAuthnHandler.cpp
@@ -144,10 +144,6 @@ already_AddRefed<Promise> WebAuthnHandler::MakeCredential(
}
nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
- if (!IsWebAuthnAllowedForPrincipal(principal)) {
- promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
- return promise.forget();
- }
nsCString rpId;
if (aOptions.mRp.mId.WasPassed()) {
@@ -463,10 +459,6 @@ already_AddRefed<Promise> WebAuthnHandler::GetAssertion(
}
nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
- if (!IsWebAuthnAllowedForPrincipal(principal)) {
- promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
- return promise.forget();
- }
nsCString rpId;
if (aOptions.mRpId.WasPassed()) {
diff --git a/dom/webauthn/WebAuthnTransactionParent.cpp b/dom/webauthn/WebAuthnTransactionParent.cpp
@@ -145,24 +145,13 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
mTransactionId = Some(aTransactionId);
WindowGlobalParent* manager = static_cast<WindowGlobalParent*>(Manager());
- nsIPrincipal* principal = manager->DocumentPrincipal();
-
- WindowGlobalParent* windowContext = manager;
- while (windowContext) {
- nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo();
- if (securityInfo &&
- !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) {
- aResolver(NS_ERROR_DOM_SECURITY_ERR);
- return IPC_OK();
- }
- windowContext = windowContext->GetParentWindowContext();
- }
- if (!IsWebAuthnAllowedForPrincipal(principal)) {
+ if (!IsWebAuthnAllowedInContext(manager)) {
aResolver(NS_ERROR_DOM_SECURITY_ERR);
return IPC_OK();
}
+ nsIPrincipal* principal = manager->DocumentPrincipal();
if (!IsValidRpId(principal, aTransactionInfo.RpId())) {
aResolver(NS_ERROR_DOM_SECURITY_ERR);
return IPC_OK();
@@ -354,24 +343,13 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
mTransactionId = Some(transactionId);
WindowGlobalParent* manager = static_cast<WindowGlobalParent*>(Manager());
- nsIPrincipal* principal = manager->DocumentPrincipal();
-
- WindowGlobalParent* windowContext = manager;
- while (windowContext) {
- nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo();
- if (securityInfo &&
- !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) {
- aResolver(NS_ERROR_DOM_SECURITY_ERR);
- return IPC_OK();
- }
- windowContext = windowContext->GetParentWindowContext();
- }
- if (!IsWebAuthnAllowedForPrincipal(principal)) {
+ if (!IsWebAuthnAllowedInContext(manager)) {
aResolver(NS_ERROR_DOM_SECURITY_ERR);
return IPC_OK();
}
+ nsIPrincipal* principal = manager->DocumentPrincipal();
if (!IsValidRpId(principal, aTransactionInfo.RpId())) {
aResolver(NS_ERROR_DOM_SECURITY_ERR);
return IPC_OK();
diff --git a/dom/webauthn/WebAuthnUtil.cpp b/dom/webauthn/WebAuthnUtil.cpp
@@ -8,6 +8,8 @@
#include "hasht.h"
#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/dom/WindowGlobalParent.h"
#include "mozpkix/pkixutil.h"
#include "nsComponentManagerUtils.h"
#include "nsHTMLDocument.h"
@@ -117,19 +119,41 @@ bool IsWebAuthnAllowedInDocument(const nsCOMPtr<Document>& aDoc) {
return aDoc->IsHTMLOrXHTML();
}
-bool IsWebAuthnAllowedForPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal) {
- MOZ_ASSERT(aPrincipal);
- if (aPrincipal->GetIsNullPrincipal()) {
+bool IsWebAuthnAllowedInContext(WindowGlobalParent* aContext) {
+ nsIPrincipal* principal = aContext->DocumentPrincipal();
+ MOZ_ASSERT(principal);
+
+ if (principal->GetIsNullPrincipal()) {
return false;
}
- if (aPrincipal->GetIsIpAddress()) {
+
+ if (principal->GetIsIpAddress()) {
return false;
}
// This next test is not strictly necessary since CredentialsContainer is
// [SecureContext] in our webidl.
- if (!aPrincipal->GetIsOriginPotentiallyTrustworthy()) {
+ if (!principal->GetIsOriginPotentiallyTrustworthy()) {
return false;
}
+
+ if (principal->GetIsLoopbackHost()) {
+ return true;
+ }
+
+ if (StaticPrefs::security_webauthn_allow_with_certificate_override()) {
+ return true;
+ }
+
+ WindowGlobalParent* windowContext = aContext;
+ while (windowContext) {
+ nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo();
+ if (securityInfo &&
+ !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) {
+ return false;
+ }
+ windowContext = windowContext->GetParentWindowContext();
+ }
+
return true;
}
diff --git a/dom/webauthn/WebAuthnUtil.h b/dom/webauthn/WebAuthnUtil.h
@@ -12,12 +12,14 @@
namespace mozilla::dom {
+class WindowGlobalParent;
+
bool IsValidAppId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
const nsCString& aAppId);
bool IsWebAuthnAllowedInDocument(const nsCOMPtr<Document>& aDoc);
-bool IsWebAuthnAllowedForPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal);
+bool IsWebAuthnAllowedInContext(WindowGlobalParent* aContext);
bool IsWebAuthnAllowedForTransportSecurityInfo(
nsITransportSecurityInfo* aSecurityInfo);
diff --git a/dom/webauthn/tests/browser/browser_webauthn_cert_override.js b/dom/webauthn/tests/browser/browser_webauthn_cert_override.js
@@ -4,15 +4,16 @@
"use strict";
-add_virtual_authenticator();
-
let expectSecurityError = expectError("Security");
async function test_webauthn_with_cert_override({
aTestDomain,
aExpectSecurityError = false,
aFeltPrivacyV1 = false,
+ aAllowCertificateOverrideByPref = false,
}) {
+ let authenticatorId = add_virtual_authenticator(/*autoremove*/ false);
+
let certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
@@ -20,6 +21,11 @@ async function test_webauthn_with_cert_override({
"security.certerrors.felt-privacy-v1",
aFeltPrivacyV1
);
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+ Services.prefs.setBoolPref(
+ "security.webauthn.allow_with_certificate_override",
+ aAllowCertificateOverrideByPref
+ );
let testURL = "https://" + aTestDomain;
let certErrorLoaded;
let tab = await BrowserTestUtils.openNewForegroundTab(
@@ -85,22 +91,16 @@ async function test_webauthn_with_cert_override({
);
});
- if (!aFeltPrivacyV1) {
- let makeCredPromise = promiseWebAuthnMakeCredential(
- tab,
- "none",
- "preferred"
+ let makeCredPromise = promiseWebAuthnMakeCredential(tab, "none", "preferred");
+ if (aExpectSecurityError) {
+ await makeCredPromise.then(arrivingHereIsBad).catch(expectSecurityError);
+ ok(
+ true,
+ "Calling navigator.credentials.create() results in a security error"
);
- if (aExpectSecurityError) {
- await makeCredPromise.then(arrivingHereIsBad).catch(expectSecurityError);
- ok(
- true,
- "Calling navigator.credentials.create() results in a security error"
- );
- } else {
- await makeCredPromise.catch(arrivingHereIsBad);
- ok(true, "Calling navigator.credentials.create() is allowed");
- }
+ } else {
+ await makeCredPromise.catch(arrivingHereIsBad);
+ ok(true, "Calling navigator.credentials.create() is allowed");
}
let getAssertionPromise = promiseWebAuthnGetAssertionDiscoverable(tab);
@@ -121,52 +121,55 @@ async function test_webauthn_with_cert_override({
await loaded;
BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ remove_virtual_authenticator(authenticatorId);
+
+ Services.prefs.clearUserPref(
+ "security.webauthn.allow_with_certificate_override"
+ );
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
Services.prefs.clearUserPref("security.certerrors.felt-privacy-v1");
}
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "expired.example.com",
- aExpectSecurityError: false,
- aFeltPrivacyV1: false,
- })
-);
-
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "untrusted.example.com",
- aExpectSecurityError: true,
- aFeltPrivacyV1: false,
- })
-);
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "no-subject-alt-name.example.com",
- aExpectSecurityError: true,
- aFeltPrivacyV1: false,
- })
-);
-
-/* Testing for felt-privacy-v1 enabled reuses the same
- * webauthn certificates created in the first three tests. */
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "expired.example.com",
- aExpectSecurityError: false,
- aFeltPrivacyV1: true,
- })
-);
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "untrusted.example.com",
- aExpectSecurityError: true,
- aFeltPrivacyV1: true,
- })
-);
-add_task(() =>
- test_webauthn_with_cert_override({
- aTestDomain: "no-subject-alt-name.example.com",
- aExpectSecurityError: true,
- aFeltPrivacyV1: false,
- })
-);
+for (let feltPrivacyV1 of [false, true]) {
+ add_task(() =>
+ test_webauthn_with_cert_override({
+ aTestDomain: "expired.example.com",
+ aExpectSecurityError: false,
+ aFeltPrivacyV1: feltPrivacyV1,
+ aAllowCertificateOverrideByPref: false,
+ })
+ );
+ add_task(() =>
+ test_webauthn_with_cert_override({
+ aTestDomain: "untrusted.example.com",
+ aExpectSecurityError: true,
+ aFeltPrivacyV1: feltPrivacyV1,
+ aAllowCertificateOverrideByPref: false,
+ })
+ );
+ add_task(() =>
+ test_webauthn_with_cert_override({
+ aTestDomain: "no-subject-alt-name.example.com",
+ aExpectSecurityError: true,
+ aFeltPrivacyV1: feltPrivacyV1,
+ aAllowCertificateOverrideByPref: false,
+ })
+ );
+ add_task(() =>
+ test_webauthn_with_cert_override({
+ aTestDomain: "badcertdomain.localhost",
+ aExpectSecurityError: false,
+ aFeltPrivacyV1: feltPrivacyV1,
+ aAllowCertificateOverrideByPref: false,
+ })
+ );
+ add_task(() =>
+ test_webauthn_with_cert_override({
+ aTestDomain: "untrusted.example.com",
+ aExpectSecurityError: false,
+ aFeltPrivacyV1: feltPrivacyV1,
+ aAllowCertificateOverrideByPref: true,
+ })
+ );
+}
diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js
@@ -42,6 +42,13 @@ function add_virtual_authenticator(autoremove = true) {
return id;
}
+function remove_virtual_authenticator(authenticatorId) {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ webauthnService.removeVirtualAuthenticator(authenticatorId);
+}
+
async function addCredential(authenticatorId, rpId) {
let keyPair = await crypto.subtle.generateKey(
{
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
@@ -17360,6 +17360,12 @@
value: false
mirror: always
+# Allow WebAuthn when a certificate override is in place
+- name: security.webauthn.allow_with_certificate_override
+ type: RelaxedAtomicBool
+ value: false
+ mirror: always
+
# Block Worker/SharedWorker scripts with wrong MIME type.
- name: security.block_Worker_with_wrong_mime
type: bool