Commit 4d2e0391 authored by Richard Pospesel's avatar Richard Pospesel Committed by brizental
Browse files

Bug 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.
parent 483cde3b
Loading
Loading
Loading
Loading
+54 −13
Original line number Diff line number Diff line
@@ -139,6 +139,12 @@ var gIdentityHandler = {
    );
  },

  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";
@@ -723,7 +729,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);
  },

  /**
@@ -734,9 +748,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";
  },

  /**
@@ -744,13 +758,19 @@ var gIdentityHandler = {
   * built-in (returns false) or imported (returns true).
   */
  _hasCustomRoot() {
    if (!this._secInfo) {
      return false;
    }

    let issuerCert = null;
    issuerCert =
      this._secInfo.succeededCertChain[
        this._secInfo.succeededCertChain.length - 1
      ];

    if (issuerCert) {
      return !issuerCert.isBuiltInRoot;
    }
    return false;
  },

  /**
@@ -787,11 +807,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.
@@ -802,17 +828,27 @@ 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"
        );
      } 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) {
@@ -827,6 +863,8 @@ var gIdentityHandler = {
    } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) {
      // Network errors and blocked pages 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";
@@ -853,7 +891,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"
+49 −5
Original line number Diff line number Diff line
@@ -16,6 +16,11 @@ ChromeUtils.defineESModuleGetters(this, {
  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
  PluralForm: "resource://gre/modules/PluralForm.sys.mjs",
});
XPCOMUtils.defineLazyGetter(this, "gTorButtonBundle", function () {
  return Services.strings.createBundle(
    "chrome://torbutton/locale/torbutton.properties"
  );
});

var security = {
  async init(uri, windowInfo) {
@@ -54,6 +59,16 @@ 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;
    var isOnion = false;
    let hostName;
    try {
      hostName = Services.eTLD.getBaseDomain(this.uri);
    } catch (e) {
      hostName = this.windowInfo.hostName;
    }
    if (hostName && hostName.endsWith(".onion")) {
      isOnion = true;
    }

    let retval = {
      cAName: "",
@@ -63,6 +78,7 @@ var security = {
      isBroken,
      isMixed,
      isEV,
      isOnion,
      cert: null,
      certificateTransparency: null,
    };
@@ -100,6 +116,7 @@ var security = {
      isBroken,
      isMixed,
      isEV,
      isOnion,
      cert,
      certChain: certChainArray,
      certificateTransparency: undefined,
@@ -341,13 +358,31 @@ async function securityOnLoad(uri, windowInfo) {
    }
    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
  } else if (info.encryptionStrength > 0) {
    if (!info.isOnion) {
      hdr = pkiBundle.getFormattedString(
        "pageInfo_EncryptionWithBitsAndProtocol",
        [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
      );
    } else {
      try {
        hdr = gTorButtonBundle.formatStringFromName(
          "pageInfo_OnionEncryptionWithBitsAndProtocol",
          [info.encryptionAlgorithm, info.encryptionStrength + "", 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", [
@@ -357,6 +392,15 @@ async function securityOnLoad(uri, windowInfo) {
      msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
    }
    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
  } else {
    try {
      hdr = gTorButtonBundle.GetStringFromName("pageInfo_OnionEncryption");
    } catch (err) {
      hdr = "Connection Encrypted (Onion Service)";
    }

    msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
    msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
  }
  setText("security-technical-shortform", hdr);
  setText("security-technical-longform1", msg1);
+3 −3
Original line number Diff line number Diff line
@@ -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"
                  },
                  "alternate_urls": {
+23 −0
Original line number Diff line number Diff line
@@ -192,6 +192,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);
}
+6 −2
Original line number Diff line number Diff line
@@ -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";
    }

Loading