Bug 1409259 - Add a console warning for soon-to-be-distrusted roots r=keeler r?ttaubert draft
authorJ.C. Jones <jjones@mozilla.com>
Wed, 18 Oct 2017 22:29:42 -0700
changeset 691848 f41a3b606c60b507b9965d8ae3501dea5b08efe8
parent 691847 498996125ad31211710d8eeb574c83aaaaff1fc6
child 691849 4f416c918d61f630832594b8cbe10fd6126be113
push id87327
push userbmo:jjones@mozilla.com
push dateThu, 02 Nov 2017 04:02:42 +0000
reviewerskeeler, ttaubert
bugs1409259
milestone58.0a1
Bug 1409259 - Add a console warning for soon-to-be-distrusted roots r=keeler r?ttaubert This patch adds a new diagnostic status flag to nsIWebProgressListener, STATE_CERT_DISTRUST_IMMINENT, which indicates that the certificate chain is going to change validity due to an upcoming distrust event. The first of these events is this bug, affecting various roots from Symantec. The STATE_CERT_DISTRUST_IMMINENT flag is set by nsNSSCallbacks and passed, via nsSecureBrowserUIImpl, to browser.js where it is used to alert the console. Adding this sort of diagnostic printing to be accessible to browser.js is a long-desired goal, as future functionality can start doing more decision-making there. We may, for example, also want to degrade the lock icon, which will be straightforward with this flag. This commit does not implement the IsCertificateDistrustImminent method. That is follow-on work. MozReview-Commit-ID: 75IOdc24XIV
browser/base/content/browser.js
browser/locales/en-US/chrome/browser/browser.properties
security/manager/ssl/nsNSSCallbacks.cpp
security/manager/ssl/nsSecureBrowserUIImpl.cpp
uriloader/base/nsIWebProgressListener.idl
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7194,16 +7194,20 @@ var gIdentityHandler = {
   get _isMixedPassiveContentLoaded() {
     return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
   },
 
   get _isCertUserOverridden() {
     return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN;
   },
 
+  get _isCertDistrustImminent() {
+    return this._state & Ci.nsIWebProgressListener.STATE_CERT_DISTRUST_IMMINENT;
+  },
+
   get _hasInsecureLoginForms() {
     // checks if the page has been flagged for an insecure login. Also checks
     // if the pref to degrade the UI is set to true
     return LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser) &&
            Services.prefs.getBoolPref("security.insecure_password.ui.enabled");
   },
 
   // smart getters
@@ -7445,16 +7449,29 @@ var gIdentityHandler = {
     if (shouldHidePopup) {
       this._identityPopup.hidePopup();
     }
 
     // NOTE: We do NOT update the identity popup (the control center) when
     // we receive a new security state on the existing page (i.e. from a
     // subframe). If the user opened the popup and looks at the provided
     // information we don't want to suddenly change the panel contents.
+
+    // Finally, if there are warnings to issue, issue them
+    if (this._isCertDistrustImminent) {
+      let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+      let windowId = gBrowser.selectedBrowser.innerWindowID;
+      let message = gBrowserBundle.GetStringFromName("certImminentDistrust.message");
+      // Use uri.prePath instead of initWithSourceURI() so that these can be
+      // de-duplicated on the scheme+host+port combination.
+      consoleMsg.initWithWindowID(message, uri.prePath, null, 0, 0,
+                                  Ci.nsIScriptError.warningFlag, "SSL",
+                                  windowId);
+      Services.console.logMessage(consoleMsg);
+    }
   },
 
   /**
    * This is called asynchronously when requested by the Logins module, after
    * the insecure login forms state for the page has been updated.
    */
   refreshForInsecureLoginForms() {
     // Check this._uri because we don't want to refresh the user interface if
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -910,8 +910,13 @@ permissions.remove.tooltip = Clear this 
 
 # LOCALIZATION NOTE (aboutDialog.architecture.*):
 # The sixtyFourBit and thirtyTwoBit strings describe the architecture of the
 # current Firefox build: 32-bit or 64-bit. These strings are used in parentheses
 # between the Firefox version and the "What's new" link in the About dialog,
 # e.g.: "48.0.2 (32-bit) <What's new>" or "51.0a1 (2016-09-05) (64-bit)".
 aboutDialog.architecture.sixtyFourBit = 64-bit
 aboutDialog.architecture.thirtyTwoBit = 32-bit
+
+# LOCALIZATION NOTE (certImminentDistrust.message):
+# Shown in the browser console when visiting a website that is trusted today,
+# but won't be in the future unless the site operator makes a change.
+certImminentDistrust.message = The security certificate in use on this web site will no longer be trusted in a future release of Firefox. For more information, visit https://wiki.mozilla.org/CA/Upcoming_Distrust_Actions
\ No newline at end of file
--- a/security/manager/ssl/nsNSSCallbacks.cpp
+++ b/security/manager/ssl/nsNSSCallbacks.cpp
@@ -29,16 +29,21 @@
 #include "nsNSSIOLayer.h"
 #include "nsNetUtil.h"
 #include "nsProtectedAuthThread.h"
 #include "nsProxyRelease.h"
 #include "pkix/pkixtypes.h"
 #include "ssl.h"
 #include "sslproto.h"
 
+#include "TrustOverrideUtils.h"
+#include "TrustOverride-SymantecData.inc"
+#include "TrustOverride-AppleGoogleData.inc"
+
+
 using namespace mozilla;
 using namespace mozilla::psm;
 
 extern LazyLogModule gPIPNSSLog;
 
 static void AccumulateCipherSuite(Telemetry::HistogramID probe,
                                   const SSLChannelInfo& channelInfo);
 
@@ -1254,16 +1259,82 @@ DetermineEVAndCTStatusAndSetNewCert(RefP
   }
 
   if (rv == Success) {
     sslStatus->SetCertificateTransparencyInfo(certificateTransparencyInfo);
     sslStatus->SetSucceededCertChain(Move(builtChain));
   }
 }
 
+static nsresult
+IsCertificateDistrustImminent(nsIX509CertList* aCertList,
+                              /* out */ bool& aResult) {
+  if (!aCertList) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  nsCOMPtr<nsIX509Cert> rootCert;
+  nsCOMPtr<nsIX509CertList> intCerts;
+  nsCOMPtr<nsIX509Cert> eeCert;
+
+  RefPtr<nsNSSCertList> certList = aCertList->GetCertList();
+  nsresult rv = certList->SegmentCertificateChain(rootCert, intCerts, eeCert);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // We need to verify the age of the end entity
+  nsCOMPtr<nsIX509CertValidity> validity;
+  rv = eeCert->GetValidity(getter_AddRefs(validity));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  PRTime notBefore;
+  rv = validity->GetNotBefore(&notBefore);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // PRTime is microseconds since the epoch, whereas JS time is milliseconds.
+  // (new Date("2016-06-01T00:00:00Z")).getTime() * 1000
+  static const PRTime JUNE_1_2016 = 1464739200000000;
+
+  // If the end entity's notBefore date is after 2016-06-01, this algorithm
+  // doesn't apply, so exit false before we do any iterating
+  if (notBefore >= JUNE_1_2016) {
+    aResult = false;
+    return NS_OK;
+  }
+
+  // If the root is not one of the Symantec roots, exit false
+  if (!CertDNIsInList(rootCert->GetCert(), RootSymantecDNs)) {
+    aResult = false;
+    return NS_OK;
+  }
+
+  // Look for one of the intermediates to be in the whitelist
+  bool foundInWhitelist = false;
+  RefPtr<nsNSSCertList> intCertList = intCerts->GetCertList();
+
+  intCertList->ForEachCertificateInChain(
+    [&foundInWhitelist] (nsCOMPtr<nsIX509Cert> aCert, bool aHasMore,
+                         /* out */ bool& aContinue) {
+      if (CertDNIsInList(aCert->GetCert(), RootAppleAndGoogleDNs)) {
+        foundInWhitelist = true;
+        aContinue = false;
+      }
+      return NS_OK;
+  });
+
+  // If this chain did not match the whitelist, exit true
+  aResult = !foundInWhitelist;
+  return NS_OK;
+}
+
 void HandshakeCallback(PRFileDesc* fd, void* client_data) {
   nsNSSShutDownPreventionLock locker;
   SECStatus rv;
 
   nsNSSSocketInfo* infoObject = (nsNSSSocketInfo*) fd->higher->secret;
 
   // Do the bookkeeping that needs to be done after the
   // server's ServerHello...ServerHelloDone have been processed, but that doesn't
@@ -1405,16 +1476,28 @@ void HandshakeCallback(PRFileDesc* fd, v
 
   if (status->HasServerCert()) {
     MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
            ("HandshakeCallback KEEPING existing cert\n"));
   } else {
     DetermineEVAndCTStatusAndSetNewCert(status, fd, infoObject);
   }
 
+  nsCOMPtr<nsIX509CertList> succeededCertChain;
+  // This always returns NS_OK, but the list could be empty. This is a
+  // best-effort check for now. Bug 731478 will reduce the incidence of empty
+  // succeeded cert chains through better caching.
+  Unused << status->GetSucceededCertChain(getter_AddRefs(succeededCertChain));
+  bool distrustImminent;
+  nsresult srv = IsCertificateDistrustImminent(succeededCertChain,
+                                               distrustImminent);
+  if (NS_SUCCEEDED(srv) && distrustImminent) {
+    state |= nsIWebProgressListener::STATE_CERT_DISTRUST_IMMINENT;
+  }
+
   bool domainMismatch;
   bool untrusted;
   bool notValidAtThisTime;
   // These all return NS_OK, so don't even bother checking the return values.
   Unused << status->GetIsDomainMismatch(&domainMismatch);
   Unused << status->GetIsUntrusted(&untrusted);
   Unused << status->GetIsNotValidAtThisTime(&notValidAtThisTime);
   // If we're here, the TLS handshake has succeeded. Thus if any of these
--- a/security/manager/ssl/nsSecureBrowserUIImpl.cpp
+++ b/security/manager/ssl/nsSecureBrowserUIImpl.cpp
@@ -264,16 +264,22 @@ nsSecureBrowserUIImpl::MapInternalToExte
 
   // Has Tracking Content been Blocked?
   if (docShell->GetHasTrackingContentBlocked())
     *aState |= nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT;
 
   if (docShell->GetHasTrackingContentLoaded())
     *aState |= nsIWebProgressListener::STATE_LOADED_TRACKING_CONTENT;
 
+  // Copy forward any diagnostic flags for downstream use (e.g., warnings)
+  if (mNewToplevelSecurityStateKnown &&
+      mNewToplevelSecurityState & STATE_CERT_DISTRUST_IMMINENT) {
+    *aState |= nsIWebProgressListener::STATE_CERT_DISTRUST_IMMINENT;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSecureBrowserUIImpl::SetDocShell(nsIDocShell* aDocShell)
 {
   MOZ_ASSERT(NS_IsMainThread());
   nsresult rv;
--- a/uriloader/base/nsIWebProgressListener.idl
+++ b/uriloader/base/nsIWebProgressListener.idl
@@ -219,16 +219,28 @@ interface nsIWebProgressListener : nsISu
    * STATE_BLOCKED_UNSAFE_CONTENT
    *   Content which againts SafeBrowsing list has been blocked from loading.
    */
   const unsigned long STATE_BLOCKED_TRACKING_CONTENT         = 0x00001000;
   const unsigned long STATE_LOADED_TRACKING_CONTENT          = 0x00002000;
   const unsigned long STATE_BLOCKED_UNSAFE_CONTENT           = 0x00004000;
 
   /**
+   * Diagnostic flags
+   *
+   * May be set in addition to other security state flags to indicate that
+   * some state is countered that deserves a warning or error, but does not
+   * change the top level security state of the connection.
+   *
+   * STATE_CERT_DISTRUST_IMMINENT
+   *   The certificate in use will be distrusted in the near future.
+   */
+  const unsigned long STATE_CERT_DISTRUST_IMMINENT    = 0x00008000;
+
+  /**
    * Security Strength Flags
    *
    * These flags describe the security strength and accompany STATE_IS_SECURE
    * in a call to the onSecurityChange method.  These flags are mutually
    * exclusive.
    *
    * These flags are not meant to provide a precise description of data
    * transfer security.  These are instead intended as a rough indicator that