tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbuild/pgo/server-locations.txt | 3++-
Mdom/webauthn/WebAuthnHandler.cpp | 8--------
Mdom/webauthn/WebAuthnTransactionParent.cpp | 30++++--------------------------
Mdom/webauthn/WebAuthnUtil.cpp | 34+++++++++++++++++++++++++++++-----
Mdom/webauthn/WebAuthnUtil.h | 4+++-
Mdom/webauthn/tests/browser/browser_webauthn_cert_override.js | 129++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mdom/webauthn/tests/browser/head.js | 7+++++++
Mmodules/libpref/init/StaticPrefList.yaml | 6++++++
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