Verified Commit 21f3283e authored by Richard Pospesel's avatar Richard Pospesel Committed by Pier Angelo Vendrame
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 cec178bf
Loading
Loading
Loading
Loading
+45 −12
Original line number Original line 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() {
  get _isAboutNetErrorPage() {
    let { documentURI } = gBrowser.selectedBrowser;
    let { documentURI } = gBrowser.selectedBrowser;
    return documentURI?.scheme == "about" && documentURI.filePath == "neterror";
    return documentURI?.scheme == "about" && documentURI.filePath == "neterror";
@@ -737,9 +743,9 @@ var gIdentityHandler = {
  get pointerlockFsWarningClassName() {
  get pointerlockFsWarningClassName() {
    // Note that the fullscreen warning does not handle _isSecureInternalUI.
    // Note that the fullscreen warning does not handle _isSecureInternalUI.
    if (this._uriHasHost && this._isSecureConnection) {
    if (this._uriHasHost && this._isSecureConnection) {
      return "verifiedDomain";
      return this._uriIsOnionHost ? "onionVerifiedDomain" : "verifiedDomain";
    }
    }
    return "unknownIdentity";
    return this._uriIsOnionHost ? "onionUnknownIdentity" : "unknownIdentity";
  },
  },


  /**
  /**
@@ -747,12 +753,18 @@ var gIdentityHandler = {
   * built-in (returns false) or imported (returns true).
   * built-in (returns false) or imported (returns true).
   */
   */
  _hasCustomRoot() {
  _hasCustomRoot() {
    if (!this._secInfo) {
      return false;
    }

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

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


  /**
  /**
@@ -789,11 +801,17 @@ var gIdentityHandler = {
        "identity.extension.label",
        "identity.extension.label",
        [extensionName]
        [extensionName]
      );
      );
    } else if (this._uriHasHost && this._isSecureConnection) {
    } else if (this._uriHasHost && this._isSecureConnection && this._secInfo) {
      // This is a secure connection.
      // 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) {
      if (this._isMixedActiveContentBlocked) {
        this._identityBox.classList.add("mixedActiveBlocked");
        this._identityBox.classList.add(
          uriIsOnionHost ? "onionMixedActiveBlocked" : "mixedActiveBlocked"
        );
      }
      }
      if (!this._isCertUserOverridden) {
      if (!this._isCertUserOverridden) {
        // It's a normal cert, verifier is the CA Org.
        // It's a normal cert, verifier is the CA Org.
@@ -804,17 +822,27 @@ var gIdentityHandler = {
      }
      }
    } else if (this._isBrokenConnection) {
    } else if (this._isBrokenConnection) {
      // This is a secure connection, but something is wrong.
      // 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) {
      if (this._isMixedActiveContentLoaded) {
        this._identityBox.classList.add("mixedActiveContent");
        this._identityBox.classList.add(
          uriIsOnionHost ? "onionMixedActiveContent" : "mixedActiveContent"
        );
      } else if (this._isMixedActiveContentBlocked) {
      } else if (this._isMixedActiveContentBlocked) {
        this._identityBox.classList.add(
        this._identityBox.classList.add(
          "mixedDisplayContentLoadedActiveBlocked"
          uriIsOnionHost
            ? "onionMixedDisplayContentLoadedActiveBlocked"
            : "mixedDisplayContentLoadedActiveBlocked"
        );
        );
      } else if (this._isMixedPassiveContentLoaded) {
      } else if (this._isMixedPassiveContentLoaded) {
        this._identityBox.classList.add("mixedDisplayContent");
        this._identityBox.classList.add(
          uriIsOnionHost ? "onionMixedDisplayContent" : "mixedDisplayContent"
        );
      } else {
      } else {
        // TODO: ignore weak https cipher for onionsites?
        this._identityBox.classList.add("weakCipher");
        this._identityBox.classList.add("weakCipher");
      }
      }
    } else if (this._isCertErrorPage) {
    } else if (this._isCertErrorPage) {
@@ -829,6 +857,8 @@ var gIdentityHandler = {
    } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) {
    } else if (this._isAboutNetErrorPage || this._isAboutBlockedPage) {
      // Network errors and blocked pages get a more neutral icon
      // Network errors and blocked pages get a more neutral icon
      this._identityBox.className = "unknownIdentity";
      this._identityBox.className = "unknownIdentity";
    } else if (this._uriIsOnionHost) {
      this._identityBox.className = "onionUnknownIdentity";
    } else if (this._isPotentiallyTrustworthy) {
    } else if (this._isPotentiallyTrustworthy) {
      // This is a local resource (and shouldn't be marked insecure).
      // This is a local resource (and shouldn't be marked insecure).
      this._identityBox.className = "localResource";
      this._identityBox.className = "localResource";
@@ -855,7 +885,10 @@ var gIdentityHandler = {
    }
    }


    if (this._isCertUserOverridden) {
    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.
      // Cert is trusted because of a security exception, verifier is a special string.
      tooltip = gNavigatorBundle.getString(
      tooltip = gNavigatorBundle.getString(
        "identity.identified.verified_by_you"
        "identity.identified.verified_by_you"
+49 −5
Original line number Original line Diff line number Diff line
@@ -22,6 +22,11 @@ ChromeUtils.defineModuleGetter(
  "PluralForm",
  "PluralForm",
  "resource://gre/modules/PluralForm.jsm"
  "resource://gre/modules/PluralForm.jsm"
);
);
XPCOMUtils.defineLazyGetter(this, "gTorButtonBundle", function() {
  return Services.strings.createBundle(
    "chrome://torbutton/locale/torbutton.properties"
  );
});


var security = {
var security = {
  async init(uri, windowInfo) {
  async init(uri, windowInfo) {
@@ -60,6 +65,16 @@ var security = {
      (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
      (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
        Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
        Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
    var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
    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 = {
    let retval = {
      cAName: "",
      cAName: "",
@@ -69,6 +84,7 @@ var security = {
      isBroken,
      isBroken,
      isMixed,
      isMixed,
      isEV,
      isEV,
      isOnion,
      cert: null,
      cert: null,
      certificateTransparency: null,
      certificateTransparency: null,
    };
    };
@@ -107,6 +123,7 @@ var security = {
      isBroken,
      isBroken,
      isMixed,
      isMixed,
      isEV,
      isEV,
      isOnion,
      cert,
      cert,
      certChain: certChainArray,
      certChain: certChainArray,
      certificateTransparency: undefined,
      certificateTransparency: undefined,
@@ -348,13 +365,31 @@ async function securityOnLoad(uri, windowInfo) {
    }
    }
    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
  } else if (info.encryptionStrength > 0) {
  } else if (info.encryptionStrength > 0) {
    if (!info.isOnion) {
      hdr = pkiBundle.getFormattedString(
      hdr = pkiBundle.getFormattedString(
        "pageInfo_EncryptionWithBitsAndProtocol",
        "pageInfo_EncryptionWithBitsAndProtocol",
        [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
        [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");
    msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
    msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
    msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
  } else {
  } else if (!info.isOnion) {
    hdr = pkiBundle.getString("pageInfo_NoEncryption");
    hdr = pkiBundle.getString("pageInfo_NoEncryption");
    if (windowInfo.hostName != null) {
    if (windowInfo.hostName != null) {
      msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
      msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
@@ -364,6 +399,15 @@ async function securityOnLoad(uri, windowInfo) {
      msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
      msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
    }
    }
    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
    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-shortform", hdr);
  setText("security-technical-longform1", msg1);
  setText("security-technical-longform1", msg1);
+3 −3
Original line number Original line Diff line number Diff line
@@ -45,7 +45,7 @@
                  "search_url": {
                  "search_url": {
                    "type": "string",
                    "type": "string",
                    "format": "url",
                    "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"
                    "preprocess": "localize"
                  },
                  },
                  "favicon_url": {
                  "favicon_url": {
@@ -66,7 +66,7 @@
                  "suggest_url": {
                  "suggest_url": {
                    "type": "string",
                    "type": "string",
                    "optional": true,
                    "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"
                    "preprocess": "localize"
                  },
                  },
                  "instant_url": {
                  "instant_url": {
@@ -123,7 +123,7 @@
                    "type": "string",
                    "type": "string",
                    "optional": true,
                    "optional": true,
                    "format": "url",
                    "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"
                    "preprocess": "localize"
                  },
                  },
                  "alternate_urls": {
                  "alternate_urls": {
+19 −0
Original line number Original line Diff line number Diff line
@@ -193,6 +193,25 @@
  list-style-image: url(chrome://global/skin/icons/security-broken.svg);
  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://browser/skin/onion.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://browser/skin/onion-warning.svg);
  visibility: visible;
}

#identity-box[pageproxystate="valid"].onionMixedActiveContent #identity-icon {
  list-style-image: url(chrome://browser/skin/onion-slash.svg);
  visibility: visible;
}

#permissions-granted-icon {
#permissions-granted-icon {
  list-style-image: url(chrome://browser/skin/permissions.svg);
  list-style-image: url(chrome://browser/skin/permissions.svg);
}
}
+8 −1
Original line number Original line Diff line number Diff line
@@ -600,6 +600,9 @@ var NetworkHelper = {


    // The request did not contain any security info.
    // The request did not contain any security info.
    if (!securityInfo) {
    if (!securityInfo) {
      if (httpActivity.hostname && httpActivity.hostname.endsWith(".onion")) {
        info.state = "secure";
      }
      return info;
      return info;
    }
    }


@@ -651,7 +654,11 @@ var NetworkHelper = {
        // schemes other than https and wss are subject to
        // schemes other than https and wss are subject to
        // downgrade/etc at the scheme level and should always be
        // downgrade/etc at the scheme level and should always be
        // considered insecure
        // considered insecure
        if (httpActivity.hostname && httpActivity.hostname.endsWith(".onion")) {
          info.state = "secure";
        } else {
          info.state = "insecure";
          info.state = "insecure";
        }
      } else if (state & wpl.STATE_IS_SECURE) {
      } else if (state & wpl.STATE_IS_SECURE) {
        // The connection is secure if the scheme is sufficient
        // The connection is secure if the scheme is sufficient
        info.state = "secure";
        info.state = "secure";
Loading