tor-browser

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

commit 1b34a13fdffbac49baf9323a9dc854544159d34f
parent f54b06f3c87bc4986ed1f74816a2d7da82c849e8
Author: Richard Pospesel <richard@torproject.org>
Date:   Fri,  8 Jun 2018 13:38:40 -0700

TB 23247: Communicating security expectations for .onion

Encrypting pages hosted on Onion Services with SSL/TLS is redundant
(in terms of hiding content) as all traffic within the Tor network is
already fully encrypted.  Therefore, serving HTTP pages from an Onion
Service is more or less fine.

Prior to this patch, Tor Browser would mostly treat pages delivered
via Onion Services as well as pages delivered in the ordinary fashion
over the internet in the same way.  This created some inconsistencies
in behaviour and misinformation presented to the user relating to the
security of pages delivered via Onion Services:

 - HTTP Onion Service pages did not have any 'lock' icon indicating
   the site was secure
 - HTTP Onion Service pages would be marked as unencrypted in the Page
   Info screen
 - Mixed-mode content restrictions did not apply to HTTP Onion Service
   pages embedding Non-Onion HTTP content

This patch fixes the above issues, and also adds several new 'Onion'
icons to the mix to indicate all of the various permutations of Onion
Services hosted HTTP or HTTPS pages with HTTP or HTTPS content.

Strings for Onion Service Page Info page are pulled from Torbutton's
localization strings.

Diffstat:
Mbrowser/base/content/browser-siteIdentity.js | 63+++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mbrowser/base/content/pageinfo/pageInfo.xhtml | 1+
Mbrowser/base/content/pageinfo/security.js | 50+++++++++++++++++++++++++++++++++++++++++++++-----
Mbrowser/components/extensions/schemas/chrome_settings_overrides.json | 6+++---
Mbrowser/themes/shared/identity-block/identity-block.css | 23+++++++++++++++++++++++
Mdevtools/client/netmonitor/src/components/SecurityState.js | 8++++++--
Mdom/base/nsContentUtils.cpp | 19+++++++++++++++++++
Mdom/base/nsContentUtils.h | 5+++++
Mdom/base/nsGlobalWindowOuter.cpp | 3++-
Mdom/ipc/WindowGlobalActor.cpp | 5++++-
Mdom/ipc/WindowGlobalChild.cpp | 5++++-
Mdom/security/nsMixedContentBlocker.cpp | 16+++++++++++++---
Mdom/websocket/WebSocket.cpp | 8+++++---
Mmobile/shared/modules/geckoview/GeckoViewProgress.sys.mjs | 4++++
Mnetwerk/base/nsNetUtil.cpp | 3++-
Mnetwerk/protocol/http/HttpBaseChannel.cpp | 4++--
Msecurity/certverifier/CertVerifier.cpp | 26+++++++++++++++++++++++---
Msecurity/manager/ssl/NSSSocketControl.cpp | 9+++++++--
Msecurity/manager/ssl/SSLServerCertVerification.cpp | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msecurity/manager/ssl/nsSecureBrowserUI.cpp | 12++++++++++++
Msecurity/nss/lib/mozpkix/include/pkix/Result.h | 2++
Msecurity/nss/lib/mozpkix/include/pkix/pkixnss.h | 1+
22 files changed, 313 insertions(+), 39 deletions(-)

diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js @@ -177,6 +177,12 @@ var gIdentityHandler = { return error === "httpErrorPage" || error === "serverError"; }, + get _uriIsOnionHost() { + return this._uriHasHost + ? this._uri.host.toLowerCase().endsWith(".onion") + : false; + }, + get _isAboutNetErrorPage() { let { documentURI } = gBrowser.selectedBrowser; return documentURI?.scheme == "about" && documentURI.filePath == "neterror"; @@ -793,7 +799,15 @@ var gIdentityHandler = { host = this._uri.specIgnoringRef; } - return host; + // For tor browser we want to shorten onion addresses for the site identity + // panel (gIdentityHandler) to match the circuit display and the onion + // authorization panel. + // See tor-browser#42091 and tor-browser#41600. + // This will also shorten addresses for other consumers of this method, + // which includes the permissions panel (gPermissionPanel) and the + // protections panel (gProtectionsHandler), although the latter is hidden in + // tor browser. + return TorUIUtils.shortenOnionAddress(host); }, /** @@ -804,9 +818,9 @@ var gIdentityHandler = { get pointerlockFsWarningClassName() { // Note that the fullscreen warning does not handle _isSecureInternalUI. if (this._uriHasHost && this._isSecureConnection) { - return "verifiedDomain"; + return this._uriIsOnionHost ? "onionVerifiedDomain" : "verifiedDomain"; } - return "unknownIdentity"; + return this._uriIsOnionHost ? "onionUnknownIdentity" : "unknownIdentity"; }, /** @@ -814,7 +828,11 @@ var gIdentityHandler = { * built-in (returns false) or imported (returns true). */ _hasCustomRoot() { - return !this._secInfo.isBuiltCertChainRootBuiltInRoot; + // HTTP Onion Sites are considered secure, but will not not have _secInfo. + // FIXME: Self-signed HTTPS Onion Sites are also deemed secure, but this + // function will return true for them, creating a warning about an exception + // that cannot be actually removed. + return !!this._secInfo && !this._secInfo.isBuiltCertChainRootBuiltInRoot; }, /** @@ -856,11 +874,17 @@ var gIdentityHandler = { "identity.extension.label", [extensionName] ); - } else if (this._uriHasHost && this._isSecureConnection) { + } else if (this._uriHasHost && this._isSecureConnection && this._secInfo) { // This is a secure connection. - this._identityBox.className = "verifiedDomain"; + // _isSecureConnection implicitly includes onion services, which may not have an SSL certificate + const uriIsOnionHost = this._uriIsOnionHost; + this._identityBox.className = uriIsOnionHost + ? "onionVerifiedDomain" + : "verifiedDomain"; if (this._isMixedActiveContentBlocked) { - this._identityBox.classList.add("mixedActiveBlocked"); + this._identityBox.classList.add( + uriIsOnionHost ? "onionMixedActiveBlocked" : "mixedActiveBlocked" + ); } if (!this._isCertUserOverridden) { // It's a normal cert, verifier is the CA Org. @@ -871,10 +895,15 @@ var gIdentityHandler = { } } else if (this._isBrokenConnection) { // This is a secure connection, but something is wrong. - this._identityBox.className = "unknownIdentity"; + const uriIsOnionHost = this._uriIsOnionHost; + this._identityBox.className = uriIsOnionHost + ? "onionUnknownIdentity" + : "unknownIdentity"; if (this._isMixedActiveContentLoaded) { - this._identityBox.classList.add("mixedActiveContent"); + this._identityBox.classList.add( + uriIsOnionHost ? "onionMixedActiveContent" : "mixedActiveContent" + ); if ( UrlbarPrefs.getScotchBonnetPref("trimHttps") && warnTextOnInsecure @@ -885,11 +914,16 @@ var gIdentityHandler = { } } else if (this._isMixedActiveContentBlocked) { this._identityBox.classList.add( - "mixedDisplayContentLoadedActiveBlocked" + uriIsOnionHost + ? "onionMixedDisplayContentLoadedActiveBlocked" + : "mixedDisplayContentLoadedActiveBlocked" ); } else if (this._isMixedPassiveContentLoaded) { - this._identityBox.classList.add("mixedDisplayContent"); + this._identityBox.classList.add( + uriIsOnionHost ? "onionMixedDisplayContent" : "mixedDisplayContent" + ); } else { + // TODO: ignore weak https cipher for onionsites? this._identityBox.classList.add("weakCipher"); } } else if (this._isCertErrorPage) { @@ -909,6 +943,8 @@ var gIdentityHandler = { // Network errors, blocked pages, and pages associated // with another page get a more neutral icon this._identityBox.className = "unknownIdentity"; + } else if (this._uriIsOnionHost) { + this._identityBox.className = "onionUnknownIdentity"; } else if (this._isPotentiallyTrustworthy) { // This is a local resource (and shouldn't be marked insecure). this._identityBox.className = "localResource"; @@ -924,7 +960,10 @@ var gIdentityHandler = { } if (this._isCertUserOverridden) { - this._identityBox.classList.add("certUserOverridden"); + const uriIsOnionHost = this._uriIsOnionHost; + this._identityBox.classList.add( + uriIsOnionHost ? "onionCertUserOverridden" : "certUserOverridden" + ); // Cert is trusted because of a security exception, verifier is a special string. tooltip = gNavigatorBundle.getString( "identity.identified.verified_by_you" diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml @@ -26,6 +26,7 @@ <html:link rel="stylesheet" href="chrome://browser/skin/pageInfo.css" /> <html:link rel="localization" href="browser/pageInfo.ftl"/> + <html:link rel="localization" href="toolkit/global/tor-browser.ftl"/> </linkset> #ifdef XP_MACOSX #include ../macWindow.inc.xhtml diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js @@ -57,6 +57,15 @@ var security = { (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT | Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT); var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL; + let uriInformation = URL.parse(gDocInfo.documentURIObject.spec); + // If the Onion site could not be loaded, the view-source will be also be + // about:neterror. + if (uriInformation?.protocol == "view-source:") { + uriInformation = URL.parse(uriInformation.pathname); + } + const isOnion = + ["http:", "https:"].includes(uriInformation?.protocol) && + uriInformation?.hostname.endsWith(".onion"); let retval = { cAName: "", @@ -66,6 +75,7 @@ var security = { isBroken, isMixed, isEV, + isOnion, cert: null, certificateTransparency: null, }; @@ -103,6 +113,7 @@ var security = { isBroken, isMixed, isEV, + isOnion, cert, certChain: certChainArray, certificateTransparency: undefined, @@ -344,13 +355,35 @@ async function securityOnLoad(uri, windowInfo) { } msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); } else if (info.encryptionStrength > 0) { - hdr = pkiBundle.getFormattedString( - "pageInfo_EncryptionWithBitsAndProtocol", - [info.encryptionAlgorithm, info.encryptionStrength + "", info.version] - ); + if (!info.isOnion) { + hdr = pkiBundle.getFormattedString( + "pageInfo_EncryptionWithBitsAndProtocol", + [info.encryptionAlgorithm, info.encryptionStrength + "", info.version] + ); + } else { + try { + hdr = await document.l10n.formatValue( + "page-info-onion-site-encryption-with-bits", + { + "encryption-type": info.encryptionAlgorithm, + "encryption-strength": info.encryptionStrength, + "encryption-version": info.version, + } + ); + } catch (err) { + hdr = + "Connection Encrypted (Onion Service, " + + info.encryptionAlgorithm + + ", " + + info.encryptionStrength + + " bit keys, " + + info.version + + ")"; + } + } msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); - } else { + } else if (!info.isOnion) { hdr = pkiBundle.getString("pageInfo_NoEncryption"); if (windowInfo.hostName != null) { msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [ @@ -360,6 +393,13 @@ async function securityOnLoad(uri, windowInfo) { msg1 = pkiBundle.getString("pageInfo_Privacy_None4"); } msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } else { + hdr = await document.l10n.formatValue( + "page-info-onion-site-encryption-plain" + ); + + msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); } setText("security-technical-shortform", hdr); setText("security-technical-longform1", msg1); diff --git a/browser/components/extensions/schemas/chrome_settings_overrides.json b/browser/components/extensions/schemas/chrome_settings_overrides.json @@ -45,7 +45,7 @@ "search_url": { "type": "string", "format": "url", - "pattern": "^(https://|http://(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", + "pattern": "^(https://|http://(.+\\.onion|localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", "preprocess": "localize" }, "favicon_url": { @@ -66,7 +66,7 @@ "suggest_url": { "type": "string", "optional": true, - "pattern": "^$|^(https://|http://(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", + "pattern": "^$|^(https://|http://(.+\\.onion|localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", "preprocess": "localize" }, "instant_url": { @@ -123,7 +123,7 @@ "type": "string", "optional": true, "format": "url", - "pattern": "^(https://|http://(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", + "pattern": "^(https://|http://(.+\\.onion|localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d*)?(/|$)).*$", "preprocess": "localize", "deprecated": "Unsupported on Firefox at this time." }, diff --git a/browser/themes/shared/identity-block/identity-block.css b/browser/themes/shared/identity-block/identity-block.css @@ -214,6 +214,29 @@ list-style-image: url(chrome://global/skin/icons/security-broken.svg); } +#identity-box[pageproxystate="valid"].onionUnknownIdentity #identity-icon, +#identity-box[pageproxystate="valid"].onionVerifiedDomain #identity-icon, +#identity-box[pageproxystate="valid"].onionMixedActiveBlocked #identity-icon { + list-style-image: url(chrome://global/skin/icons/onion-site.svg); + visibility: visible; +} + +#identity-box[pageproxystate="valid"].onionMixedDisplayContent #identity-icon, +#identity-box[pageproxystate="valid"].onionMixedDisplayContentLoadedActiveBlocked #identity-icon, +#identity-box[pageproxystate="valid"].onionCertUserOverridden #identity-icon { + list-style-image: url(chrome://global/skin/icons/onion-warning.svg); + visibility: visible; + /* onion-warning includes another context-stroke color. Here we want it to + * match the context-fill color, which should be currentColor. */ + -moz-context-properties: fill, fill-opacity, stroke; + stroke: currentColor; +} + +#identity-box[pageproxystate="valid"].onionMixedActiveContent #identity-icon { + list-style-image: url(chrome://global/skin/icons/onion-slash.svg); + visibility: visible; +} + #permissions-granted-icon { list-style-image: url(chrome://browser/skin/permissions.svg); } diff --git a/devtools/client/netmonitor/src/components/SecurityState.js b/devtools/client/netmonitor/src/components/SecurityState.js @@ -41,7 +41,7 @@ class SecurityState extends Component { const { securityState, - urlDetails: { isLocal }, + urlDetails: { host, isLocal }, } = item; const iconClassList = ["requests-security-state-icon"]; @@ -50,7 +50,11 @@ class SecurityState extends Component { // Locally delivered files such as http://localhost and file:// paths // are considered to have been delivered securely. - if (isLocal) { + if ( + isLocal || + (host?.endsWith(".onion") && + Services.prefs.getBoolPref("dom.securecontext.allowlist_onions", false)) + ) { realSecurityState = "secure"; } diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp @@ -10944,6 +10944,25 @@ bool nsContentUtils::ComputeIsSecureContext(nsIChannel* aChannel) { return principal->GetIsOriginPotentiallyTrustworthy(); } +/* static */ bool nsContentUtils::DocumentHasOnionURI(Document* aDocument) { + if (!aDocument) { + return false; + } + + nsIURI* uri = aDocument->GetDocumentURI(); + if (!uri) { + return false; + } + + nsAutoCString host; + if (NS_SUCCEEDED(uri->GetHost(host))) { + bool hasOnionURI = StringEndsWith(host, ".onion"_ns); + return hasOnionURI; + } + + return false; +} + /* static */ void nsContentUtils::TryToUpgradeElement(Element* aElement) { NodeInfo* nodeInfo = aElement->NodeInfo(); diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h @@ -3136,6 +3136,11 @@ class nsContentUtils { static bool HttpsStateIsModern(Document* aDocument); /** + * Returns true of the document's URI is a .onion + */ + static bool DocumentHasOnionURI(Document* aDocument); + + /** * Returns true if the channel is for top-level window and is over secure * context. * https://github.com/whatwg/html/issues/4930 tracks the spec side of this. diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp @@ -1879,7 +1879,8 @@ bool nsGlobalWindowOuter::ComputeIsSecureContext(Document* aDocument, return false; } - if (nsContentUtils::HttpsStateIsModern(aDocument)) { + if (nsContentUtils::HttpsStateIsModern(aDocument) || + nsContentUtils::DocumentHasOnionURI(aDocument)) { return true; } diff --git a/dom/ipc/WindowGlobalActor.cpp b/dom/ipc/WindowGlobalActor.cpp @@ -19,6 +19,7 @@ #include "mozilla/dom/WindowContext.h" #include "mozilla/dom/WindowGlobalChild.h" #include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/net/CookieJarSettings.h" #include "nsContentUtils.h" #include "nsGlobalWindowInner.h" @@ -137,7 +138,9 @@ WindowGlobalInit WindowGlobalActor::WindowInitializer( // Init Mixed Content Fields nsCOMPtr<nsIURI> innerDocURI = NS_GetInnermostURI(doc->GetDocumentURI()); fields.Get<Indexes::IDX_IsSecure>() = - innerDocURI && innerDocURI->SchemeIs("https"); + innerDocURI && + (innerDocURI->SchemeIs("https") || + nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI)); nsCOMPtr<nsITransportSecurityInfo> securityInfo; if (nsCOMPtr<nsIChannel> channel = doc->GetChannel()) { diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp @@ -30,6 +30,7 @@ #include "mozilla/dom/WindowContext.h" #include "mozilla/dom/WindowGlobalActorsBinding.h" #include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/ipc/Endpoint.h" #include "nsContentUtils.h" #include "nsDocShell.h" @@ -257,7 +258,9 @@ void WindowGlobalChild::OnNewDocument(Document* aDocument) { nsCOMPtr<nsIURI> innerDocURI = NS_GetInnermostURI(aDocument->GetDocumentURI()); if (innerDocURI) { - txn.SetIsSecure(innerDocURI->SchemeIs("https")); + txn.SetIsSecure( + innerDocURI->SchemeIs("https") || + nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI)); } MOZ_DIAGNOSTIC_ASSERT(mDocumentPrincipal->GetIsLocalIpAddress() == diff --git a/dom/security/nsMixedContentBlocker.cpp b/dom/security/nsMixedContentBlocker.cpp @@ -663,8 +663,8 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect, return NS_OK; } - // Check the parent scheme. If it is not an HTTPS page then mixed content - // restrictions do not apply. + // Check the parent scheme. If it is not an HTTPS or .onion page then mixed + // content restrictions do not apply. nsCOMPtr<nsIURI> innerRequestingLocation = NS_GetInnermostURI(requestingLocation); if (!innerRequestingLocation) { @@ -679,6 +679,17 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect, bool parentIsHttps = innerRequestingLocation->SchemeIs("https"); if (!parentIsHttps) { + bool parentIsOnion = IsPotentiallyTrustworthyOnion(innerRequestingLocation); + if (!parentIsOnion) { + *aDecision = ACCEPT; + return NS_OK; + } + } + + bool isHttpScheme = innerContentLocation->SchemeIs("http"); + // .onion URLs are encrypted and authenticated. Don't treat them as mixed + // content if potentially trustworthy (i.e. whitelisted). + if (isHttpScheme && IsPotentiallyTrustworthyOnion(innerContentLocation)) { *aDecision = ACCEPT; MOZ_LOG(sMCBLog, LogLevel::Verbose, (" -> decision: Request will be allowed because the requesting " @@ -705,7 +716,6 @@ nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect, return NS_OK; } - bool isHttpScheme = innerContentLocation->SchemeIs("http"); if (isHttpScheme && IsPotentiallyTrustworthyOrigin(innerContentLocation)) { *aDecision = ACCEPT; return NS_OK; diff --git a/dom/websocket/WebSocket.cpp b/dom/websocket/WebSocket.cpp @@ -1786,13 +1786,15 @@ nsresult WebSocketImpl::Init(nsIGlobalObject* aWindowGlobal, JSContext* aCx, // Don't allow https:// to open ws:// // Check that we aren't a server side websocket or set to be upgraded to wss // or allowing ws from https or a local websocket - if (!mIsServerSide && !mSecure && + if (!mIsServerSide && !mSecure && aIsSecure && !Preferences::GetBool("network.websocket.allowInsecureFromHTTPS", false) && !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( mAsciiHost)) { - // If aIsSecure is true then disallow loading ws - if (aIsSecure) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURI); + NS_ENSURE_SUCCESS(rv, rv); + if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(uri)) { return NS_ERROR_DOM_SECURITY_ERR; } diff --git a/mobile/shared/modules/geckoview/GeckoViewProgress.sys.mjs b/mobile/shared/modules/geckoview/GeckoViewProgress.sys.mjs @@ -138,6 +138,10 @@ var IdentityHandler = { result.host = uri.host; } + if (!aBrowser.securityUI.secInfo) { + return result; + } + const cert = aBrowser.securityUI.secInfo.serverCert; result.certificate = diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp @@ -3111,7 +3111,8 @@ nsresult NS_ShouldSecureUpgrade( } // If it is a mixed content trustworthy loopback, then we shouldn't upgrade // it. - if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aURI)) { + if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aURI) || + nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aURI)) { aShouldUpgrade = false; return NS_OK; } diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -325,6 +325,7 @@ void HttpBaseChannel::AddClassificationFlags(uint32_t aClassificationFlags, static bool isSecureOrTrustworthyURL(nsIURI* aURI) { return aURI->SchemeIs("https") || + nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aURI) || (StaticPrefs::network_http_encoding_trustworthy_is_https() && nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aURI)); } @@ -349,7 +350,6 @@ nsresult HttpBaseChannel::Init(nsIURI* aURI, uint32_t aCaps, // Construct connection info object nsAutoCString host; int32_t port = -1; - bool isHTTPS = isSecureOrTrustworthyURL(mURI); nsresult rv = mURI->GetAsciiHost(host); if (NS_FAILED(rv)) return rv; @@ -403,7 +403,7 @@ nsresult HttpBaseChannel::Init(nsIURI* aURI, uint32_t aCaps, : EmptyCString(); rv = gHttpHandler->AddStandardRequestHeaders( - &mRequestHead, aURI, isHTTPS, contentPolicyType, + &mRequestHead, aURI, isSecureOrTrustworthyURL(mURI), contentPolicyType, nsContentUtils::ShouldResistFingerprinting(this, RFPTarget::HttpUserAgent), languageOverride); diff --git a/security/certverifier/CertVerifier.cpp b/security/certverifier/CertVerifier.cpp @@ -891,6 +891,9 @@ Result CertVerifier::VerifySSLServerCert( if (rv != Success) { return rv; } + + bool errOnionWithSelfSignedCert = false; + bool isBuiltChainRootBuiltInRootLocal; rv = VerifyCert( peerCertBytes, VerifyUsage::TLSServer, time, pinarg, @@ -913,9 +916,21 @@ Result CertVerifier::VerifySSLServerCert( // In this case we didn't find any issuer for the certificate, or we did // find other certificates with the same subject but different keys, and // the certificate is self-signed. - return Result::ERROR_SELF_SIGNED_CERT; + if (StringEndsWith(hostname, ".onion"_ns)) { + // Self signed cert over onion is deemed secure in some cases, as the + // onion service provides encryption. + // Firefox treats some errors as self-signed certificates and it allows + // to override them. For Onion services, we prefer being stricter, and + // we return the original errors. + // Moreover, we need also to determine if there are other legitimate + // certificate errors (such as expired, wrong domain) that we would like + // to surface to the user. + errOnionWithSelfSignedCert = rv == Result::ERROR_UNKNOWN_ISSUER; + } else { + return Result::ERROR_SELF_SIGNED_CERT; + } } - if (rv == Result::ERROR_UNKNOWN_ISSUER) { + if (rv == Result::ERROR_UNKNOWN_ISSUER && !errOnionWithSelfSignedCert) { // In this case we didn't get any valid path for the cert. Let's see if // the issuer is the same as the issuer for our canary probe. If yes, this // connection is connecting via a misconfigured proxy. @@ -951,7 +966,9 @@ Result CertVerifier::VerifySSLServerCert( return hostnameResult; } } - return rv; + if (!errOnionWithSelfSignedCert) { + return rv; + } } if (dcInfo) { @@ -990,6 +1007,9 @@ Result CertVerifier::VerifySSLServerCert( return rv; } + if (errOnionWithSelfSignedCert) { + return Result::ERROR_ONION_WITH_SELF_SIGNED_CERT; + } return Success; } diff --git a/security/manager/ssl/NSSSocketControl.cpp b/security/manager/ssl/NSSSocketControl.cpp @@ -411,7 +411,11 @@ void NSSSocketControl::SetCertVerificationResult(PRErrorCode errorCode) { "Invalid state transition to AfterCertVerification"); if (mFd) { - SECStatus rv = SSL_AuthCertificateComplete(mFd, errorCode); + PRErrorCode passCode = errorCode; + if (errorCode == MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT) { + passCode = 0; + } + SECStatus rv = SSL_AuthCertificateComplete(mFd, passCode); // Only replace errorCode if there was originally no error. // SSL_AuthCertificateComplete will return SECFailure with the error code // set to PR_WOULD_BLOCK_ERROR if there is a pending event to select a @@ -426,7 +430,8 @@ void NSSSocketControl::SetCertVerificationResult(PRErrorCode errorCode) { } } - if (errorCode) { + if (errorCode && + errorCode != MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT) { mFailedVerification = true; SetCanceled(errorCode); } diff --git a/security/manager/ssl/SSLServerCertVerification.cpp b/security/manager/ssl/SSLServerCertVerification.cpp @@ -284,6 +284,7 @@ CategorizeCertificateError(PRErrorCode certificateError) { case mozilla::pkix::MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE: case mozilla::pkix::MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT: case mozilla::pkix::MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA: + case mozilla::pkix::MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT: return Some( nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TRUST); @@ -659,6 +660,78 @@ Result AuthCertificate( return rv; } +/** + * Check if the self-signed onion certificate error can be overridden by another + * error. + * + * Basically, this function restore part of the old functionalities of + * DetermineCertOverrideErrors, before it was changed in Bug 1781104. + */ +static PRErrorCode OverrideOnionSelfSignedError( + const nsCOMPtr<nsIX509Cert>& aCert, const nsACString& aHostName, + mozilla::pkix::Time aNow, PRErrorCode aCertVerificationError) { + nsTArray<uint8_t> certDER; + if (NS_FAILED(aCert->GetRawDER(certDER))) { + return SEC_ERROR_LIBRARY_FAILURE; + } + mozilla::pkix::Input certInput; + if (certInput.Init(certDER.Elements(), certDER.Length()) != Success) { + return SEC_ERROR_BAD_DER; + } + + // First, check the hostname. + { + Input hostnameInput; + Result result = hostnameInput.Init( + BitwiseCast<const uint8_t*, const char*>(aHostName.BeginReading()), + aHostName.Length()); + if (result != Success) { + return SEC_ERROR_INVALID_ARGS; + } + result = CheckCertHostname(certInput, hostnameInput); + if (result == Result::ERROR_BAD_DER || + result == Result::ERROR_BAD_CERT_DOMAIN) { + aCertVerificationError = SSL_ERROR_BAD_CERT_DOMAIN; + } else if (IsFatalError(result)) { + // This should be then mapped to a fatal error by + // CategorizeCertificateError. + return MapResultToPRErrorCode(result); + } + } + + // Then, check if the certificate has expired. + { + mozilla::pkix::BackCert backCert( + certInput, mozilla::pkix::EndEntityOrCA::MustBeEndEntity, nullptr); + Result rv = backCert.Init(); + if (rv != Success) { + PR_SetError(MapResultToPRErrorCode(rv), 0); + return SECFailure; + } + mozilla::pkix::Time notBefore(mozilla::pkix::Time::uninitialized); + mozilla::pkix::Time notAfter(mozilla::pkix::Time::uninitialized); + // If the validity can't be parsed, ParseValidity will return + // Result::ERROR_INVALID_DER_TIME. + rv = mozilla::pkix::ParseValidity(backCert.GetValidity(), &notBefore, + &notAfter); + if (rv != Success) { + return MapResultToPRErrorCode(rv); + } + // If `now` is outside of the certificate's validity period, + // CheckValidity will return Result::ERROR_NOT_YET_VALID_CERTIFICATE or + // Result::ERROR_EXPIRED_CERTIFICATE, as appropriate, and Success + // otherwise. + rv = mozilla::pkix::CheckValidity(aNow, notBefore, notAfter); + if (rv != Success) { + return MapResultToPRErrorCode(rv); + } + } + + // If we arrive here, the cert is okay, just self-signed, so return the + // original error. + return aCertVerificationError; +} + PRErrorCode AuthCertificateParseResults( uint64_t aPtrForLog, const nsACString& aHostName, int32_t aPort, const OriginAttributes& aOriginAttributes, @@ -670,6 +743,12 @@ PRErrorCode AuthCertificateParseResults( uint32_t probeValue = MapCertErrorToProbeValue(aCertVerificationError); glean::ssl::cert_verification_errors.AccumulateSingleSample(probeValue); + if (aCertVerificationError == + mozilla::pkix::MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT) { + aCertVerificationError = OverrideOnionSelfSignedError( + aCert, aHostName, aTime, aCertVerificationError); + } + Maybe<nsITransportSecurityInfo::OverridableErrorCategory> maybeOverridableErrorCategory = CategorizeCertificateError(aCertVerificationError); diff --git a/security/manager/ssl/nsSecureBrowserUI.cpp b/security/manager/ssl/nsSecureBrowserUI.cpp @@ -8,6 +8,7 @@ #include "mozilla/Assertions.h" #include "mozilla/Logging.h" #include "mozilla/dom/Document.h" +#include "mozilla/dom/nsMixedContentBlocker.h" #include "nsContentUtils.h" #include "nsIChannel.h" #include "nsDocShell.h" @@ -83,6 +84,17 @@ void nsSecureBrowserUI::RecomputeSecurityFlags() { } } } + + // any protocol routed over tor is secure + if (!(mState & nsIWebProgressListener::STATE_IS_SECURE)) { + nsCOMPtr<nsIURI> innerDocURI = NS_GetInnermostURI(win->GetDocumentURI()); + if (innerDocURI && + nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(innerDocURI)) { + MOZ_LOG(gSecureBrowserUILog, LogLevel::Debug, (" is onion")); + mState = (mState & ~nsIWebProgressListener::STATE_IS_INSECURE) | + nsIWebProgressListener::STATE_IS_SECURE; + } + } } // Add upgraded-state flags when request has been diff --git a/security/nss/lib/mozpkix/include/pkix/Result.h b/security/nss/lib/mozpkix/include/pkix/Result.h @@ -192,6 +192,8 @@ static const unsigned int FATAL_ERROR_FLAG = 0x800; SEC_ERROR_LIBRARY_FAILURE) \ MOZILLA_PKIX_MAP(FATAL_ERROR_NO_MEMORY, FATAL_ERROR_FLAG | 4, \ SEC_ERROR_NO_MEMORY) \ + MOZILLA_PKIX_MAP(ERROR_ONION_WITH_SELF_SIGNED_CERT, 155, \ + MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT) \ /* nothing here */ enum class Result { diff --git a/security/nss/lib/mozpkix/include/pkix/pkixnss.h b/security/nss/lib/mozpkix/include/pkix/pkixnss.h @@ -96,6 +96,7 @@ enum ErrorCode { MOZILLA_PKIX_ERROR_MITM_DETECTED = ERROR_BASE + 15, MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY = ERROR_BASE + 16, MOZILLA_PKIX_ERROR_ISSUER_NO_LONGER_TRUSTED = ERROR_BASE + 17, + MOZILLA_PKIX_ERROR_ONION_WITH_SELF_SIGNED_CERT = ERROR_BASE + 100, END_OF_LIST };