Commit 0e1c61dc authored by Richard Pospesel's avatar Richard Pospesel
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 113d625c
Loading
Loading
Loading
Loading
+25 −14
Original line number Diff line number Diff line
@@ -142,6 +142,10 @@ var gIdentityHandler = {
    );
  },

  get _uriIsOnionHost() {
    return this._uriHasHost ? this._uri.host.toLowerCase().endsWith(".onion") : false;
  },

  get _isAboutNetErrorPage() {
    return (
      gBrowser.selectedBrowser.documentURI &&
@@ -745,9 +749,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";
  },

  /**
@@ -755,6 +759,10 @@ 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
@@ -797,11 +805,13 @@ 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.
@@ -812,17 +822,17 @@ 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"
        );
        this._identityBox.classList.add(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._isAboutCertErrorPage) {
@@ -835,8 +845,8 @@ var gIdentityHandler = {
      // Network errors and blocked pages get a more neutral icon
      this._identityBox.className = "unknownIdentity";
    } else if (this._isPotentiallyTrustworthy) {
      // This is a local resource (and shouldn't be marked insecure).
      this._identityBox.className = "localResource";
      // This is a local resource or an onion site (and shouldn't be marked insecure).
      this._identityBox.className = this._uriIsOnionHost ? "onionUnknownIdentity" : "localResource";
    } else {
      // This is an insecure connection.
      let warnOnInsecure =
@@ -860,7 +870,8 @@ 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"
+53 −11
Original line number Diff line number Diff line
@@ -22,6 +22,13 @@ ChromeUtils.defineModuleGetter(
  "PluralForm",
  "resource://gre/modules/PluralForm.jsm"
);
XPCOMUtils.defineLazyGetter(
  this,
  "gTorButtonBundle",
  function() {
    return Services.strings.createBundle("chrome://torbutton/locale/torbutton.properties");
  }
);

var security = {
  async init(uri, windowInfo) {
@@ -60,6 +67,11 @@ 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;
    const hostName = this.windowInfo.hostName;
    if (hostName && hostName.endsWith(".onion")) {
      isOnion = true;
    }

    let retval = {
      cAName: "",
@@ -69,6 +81,7 @@ var security = {
      isBroken,
      isMixed,
      isEV,
      isOnion,
      cert: null,
      certificateTransparency: null,
    };
@@ -107,6 +120,7 @@ var security = {
      isBroken,
      isMixed,
      isEV,
      isOnion,
      cert,
      certChain: certChainArray,
      certificateTransparency: undefined,
@@ -348,13 +362,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 {
    if (!info.isOnion) {
      hdr = pkiBundle.getString("pageInfo_NoEncryption");
      if (windowInfo.hostName != null) {
        msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
@@ -364,6 +396,16 @@ 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);
+19 −0
Original line number Diff line number Diff line
@@ -203,6 +203,25 @@
  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 {
  list-style-image: url(chrome://browser/skin/permissions.svg);
}
+19 −0
Original line number Diff line number Diff line
@@ -9325,6 +9325,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();
+5 −0
Original line number Diff line number Diff line
@@ -2999,6 +2999,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.
Loading