From b5bcf200910e3f69323cf68af8b5258d3e2e7c4d Mon Sep 17 00:00:00 2001 From: David Keeler Date: Fri, 24 Aug 2012 14:17:27 -0700 Subject: [PATCH] Bug 760307 - Preloaded strict-transport-security site list. r=mayhemer, bsmith --- security/manager/boot/src/nsSTSPreloadList.inc | 135 +++++++ .../boot/src/nsStrictTransportSecurityService.cpp | 390 +++++++++++---------- .../boot/src/nsStrictTransportSecurityService.h | 49 ++- .../manager/ssl/tests/unit/test_sts_preloadlist.js | 180 ++++++++++ security/manager/ssl/tests/unit/xpcshell.ini | 1 + security/manager/tools/getHSTSPreloadList.py | 115 ++++++ 6 files changed, 683 insertions(+), 187 deletions(-) create mode 100644 security/manager/boot/src/nsSTSPreloadList.inc create mode 100644 security/manager/ssl/tests/unit/test_sts_preloadlist.js create mode 100644 security/manager/tools/getHSTSPreloadList.py diff --git a/security/manager/boot/src/nsSTSPreloadList.inc b/security/manager/boot/src/nsSTSPreloadList.inc new file mode 100644 index 000000000000..39d5d10b47e6 --- /dev/null +++ b/security/manager/boot/src/nsSTSPreloadList.inc @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*****************************************************************************/ +/* This is an automatically generated file. If you're not */ +/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */ +/*****************************************************************************/ + +#include + +class nsSTSPreload +{ + public: + const char *mHost; + const bool mIncludeSubdomains; +}; + +static const nsSTSPreload kSTSPreloadList[] = { + { "accounts.google.com", true }, + { "aladdinschools.appspot.com", false }, + { "alpha.irccloud.com", false }, + { "api.recurly.com", true }, + { "apis.google.com", true }, + { "app.recurly.com", true }, + { "appengine.google.com", false }, + { "arivo.com.br", true }, + { "betnet.fr", true }, + { "bigshinylock.minazo.net", true }, + { "blog.torproject.org", true }, + { "braintreegateway.com", true }, + { "braintreepayments.com", false }, + { "browserid.org", true }, + { "business.medbank.com.mt", true }, + { "cert.se", true }, + { "check.torproject.org", true }, + { "checkout.google.com", true }, + { "chrome.google.com", true }, + { "chromiumcodereview.appspot.com", true }, + { "cloudsecurityalliance.org", true }, + { "codereview.appspot.com", true }, + { "crate.io", true }, + { "crypto.cat", true }, + { "crypto.is", true }, + { "developer.mydigipass.com", false }, + { "docs.google.com", true }, + { "download.jitsi.org", false }, + { "drive.google.com", true }, + { "dropcam.com", false }, + { "ebanking.indovinabank.com.vn", true }, + { "emailprivacytester.com", false }, + { "encrypted.google.com", true }, + { "entropia.de", false }, + { "epoxate.com", false }, + { "factor.cc", false }, + { "gmail.com", false }, + { "googlemail.com", false }, + { "googleplex.com", true }, + { "greplin.com", false }, + { "grepular.com", true }, + { "groups.google.com", true }, + { "health.google.com", true }, + { "hostedtalkgadget.google.com", true }, + { "howrandom.org", true }, + { "id.mayfirst.org", false }, + { "irccloud.com", false }, + { "jitsi.org", false }, + { "jottit.com", true }, + { "keyerror.com", true }, + { "kyps.net", false }, + { "lastpass.com", false }, + { "ledgerscope.net", false }, + { "linx.net", true }, + { "lists.mayfirst.org", false }, + { "logentries.com", false }, + { "login.persona.org", true }, + { "login.sapo.pt", true }, + { "luneta.nearbuysystems.com", true }, + { "mail.google.com", true }, + { "market.android.com", true }, + { "mattmccutchen.net", true }, + { "members.mayfirst.org", false }, + { "mydigipass.com", false }, + { "neg9.org", false }, + { "neonisi.com", false }, + { "ottospora.nl", true }, + { "passwd.io", true }, + { "piratenlogin.de", true }, + { "pixi.me", true }, + { "plus.google.com", true }, + { "profiles.google.com", true }, + { "riseup.net", true }, + { "romab.com", true }, + { "sandbox.mydigipass.com", false }, + { "script.google.com", true }, + { "shops.neonisi.com", true }, + { "simon.butcher.name", true }, + { "sites.google.com", true }, + { "sol.io", true }, + { "spreadsheets.google.com", true }, + { "squareup.com", false }, + { "ssl.google-analytics.com", true }, + { "stripe.com", true }, + { "sunshinepress.org", true }, + { "support.mayfirst.org", false }, + { "talk.google.com", true }, + { "talkgadget.google.com", true }, + { "torproject.org", false }, + { "ubertt.org", true }, + { "uprotect.it", true }, + { "www.apollo-auto.com", true }, + { "www.braintreepayments.com", false }, + { "www.cueup.com", true }, + { "www.developer.mydigipass.com", false }, + { "www.dropcam.com", false }, + { "www.elanex.biz", false }, + { "www.entropia.de", false }, + { "www.gmail.com", false }, + { "www.googlemail.com", false }, + { "www.greplin.com", false }, + { "www.irccloud.com", false }, + { "www.jitsi.org", false }, + { "www.kyps.net", false }, + { "www.lastpass.com", false }, + { "www.ledgerscope.net", false }, + { "www.logentries.com", false }, + { "www.moneybookers.com", true }, + { "www.mydigipass.com", false }, + { "www.neonisi.com", true }, + { "www.noisebridge.net", false }, + { "www.paycheckrecords.com", false }, + { "www.paypal.com", false }, + { "www.sandbox.mydigipass.com", false }, + { "www.torproject.org", true }, +}; diff --git a/security/manager/boot/src/nsStrictTransportSecurityService.cpp b/security/manager/boot/src/nsStrictTransportSecurityService.cpp index f82c46a26fe8..ce02d442e667 100644 --- a/security/manager/boot/src/nsStrictTransportSecurityService.cpp +++ b/security/manager/boot/src/nsStrictTransportSecurityService.cpp @@ -17,6 +17,20 @@ #include "nsStringGlue.h" #include "nsIScriptSecurityManager.h" +// A note about the preload list: +// When a site specifically disables sts by sending a header with +// 'max-age: 0', we keep a "knockout" value that means "we have no information +// regarding the sts state of this host" (any ancestor of "this host" can still +// influence its sts status via include subdomains, however). +// This prevents the preload list from overriding the site's current +// desired sts status. Knockout values are indicated by permission values of +// STS_KNOCKOUT. +#include "nsSTSPreloadList.inc" + +#define STS_SET (nsIPermissionManager::ALLOW_ACTION) +#define STS_UNSET (nsIPermissionManager::UNKNOWN_ACTION) +#define STS_KNOCKOUT (nsIPermissionManager::DENY_ACTION) + #if defined(PR_LOGGING) PRLogModuleInfo *gSTSLog = PR_NewLogModule("nsSTSService"); #endif @@ -29,34 +43,13 @@ PRLogModuleInfo *gSTSLog = PR_NewLogModule("nsSTSService"); return NS_ERROR_FAILURE; \ } -namespace { - -/** - * Returns a principal (aPrincipal) corresponding to aURI. - * This is used to interact with the permission manager. - */ -nsresult -GetPrincipalForURI(nsIURI* aURI, nsIPrincipal** aPrincipal) -{ - // The permission manager wants a principal but don't actually check a - // permission but a data we saved in the permission manager so we are good by - // creating a no-app codebase principal and send it to the permission manager. - nsresult rv; - nsCOMPtr securityManager = - do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - - return securityManager->GetNoAppCodebasePrincipal(aURI, aPrincipal); -} - -} // anonymous namespace - //////////////////////////////////////////////////////////////////////////////// nsSTSHostEntry::nsSTSHostEntry(const char* aHost) : mHost(aHost) , mExpireTime(0) - , mDeleted(false) + , mExpired(false) + , mStsPermission(STS_UNSET) , mIncludeSubdomains(false) { } @@ -64,7 +57,8 @@ nsSTSHostEntry::nsSTSHostEntry(const char* aHost) nsSTSHostEntry::nsSTSHostEntry(const nsSTSHostEntry& toCopy) : mHost(toCopy.mHost) , mExpireTime(toCopy.mExpireTime) - , mDeleted(toCopy.mDeleted) + , mExpired(toCopy.mExpired) + , mStsPermission(toCopy.mStsPermission) , mIncludeSubdomains(toCopy.mIncludeSubdomains) { } @@ -124,6 +118,29 @@ nsStrictTransportSecurityService::GetHost(nsIURI *aURI, nsACString &aResult) } nsresult +nsStrictTransportSecurityService::GetPrincipalForURI(nsIURI* aURI, + nsIPrincipal** aPrincipal) +{ + nsresult rv; + nsCOMPtr securityManager = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // We have to normalize the scheme of the URIs we're using, so just use https. + // HSTS information is shared across all ports for a given host. + nsCAutoString host; + rv = GetHost(aURI, host); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + host); + NS_ENSURE_SUCCESS(rv, rv); + + // We want all apps to share HSTS state, so this is one of the few places + // where we do not silo persistent state by extended origin. + return securityManager->GetNoAppCodebasePrincipal(uri, aPrincipal); +} + +nsresult nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI, int64_t maxage, bool includeSubdomains) @@ -135,13 +152,14 @@ nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI, // Expire time is millis from now. Since STS max-age is in seconds, and // PR_Now() is in micros, must equalize the units at milliseconds. - int64_t expiretime = (PR_Now() / 1000) + (maxage * 1000); + int64_t expiretime = (PR_Now() / PR_USEC_PER_MSEC) + + (maxage * PR_MSEC_PER_SEC); // record entry for this host with max-age in the permissions manager STSLOG(("STS: maxage permission SET, adding permission\n")); nsresult rv = AddPermission(aSourceURI, STS_PERMISSION, - (uint32_t) nsIPermissionManager::ALLOW_ACTION, + (uint32_t) STS_SET, (uint32_t) nsIPermissionManager::EXPIRE_TIME, expiretime); NS_ENSURE_SUCCESS(rv, rv); @@ -151,7 +169,7 @@ nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI, STSLOG(("STS: subdomains permission SET, adding permission\n")); rv = AddPermission(aSourceURI, STS_SUBDOMAIN_PERMISSION, - (uint32_t) nsIPermissionManager::ALLOW_ACTION, + (uint32_t) STS_SET, (uint32_t) nsIPermissionManager::EXPIRE_TIME, expiretime); NS_ENSURE_SUCCESS(rv, rv); @@ -319,6 +337,26 @@ nsStrictTransportSecurityService::IsStsHost(const char* aHost, bool* aResult) return IsStsURI(uri, aResult); } +int STSPreloadCompare(const void *key, const void *entry) +{ + const char *keyStr = (const char *)key; + const nsSTSPreload *preloadEntry = (const nsSTSPreload *)entry; + return strcmp(keyStr, preloadEntry->mHost); +} + +// Returns the preload list entry for the given host, if it exists. +// Only does exact host matching - the user must decide how to use the returned +// data. May return null. +const nsSTSPreload * +nsStrictTransportSecurityService::GetPreloadListEntry(const char *aHost) +{ + return (const nsSTSPreload *) bsearch(aHost, + kSTSPreloadList, + PR_ARRAY_SIZE(kSTSPreloadList), + sizeof(nsSTSPreload), + STSPreloadCompare); +} + NS_IMETHODIMP nsStrictTransportSecurityService::IsStsURI(nsIURI* aURI, bool* aResult) { @@ -326,19 +364,134 @@ nsStrictTransportSecurityService::IsStsURI(nsIURI* aURI, bool* aResult) // manager is used and it's not threadsafe. NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); - nsresult rv; - uint32_t permExact, permGeneral; - // If this domain has the forcehttps permission, this is an STS host. - rv = TestPermission(aURI, STS_PERMISSION, &permExact, true); + // set default in case if we can't find any STS information + *aResult = false; + + nsCAutoString host; + nsresult rv = GetHost(aURI, host); + NS_ENSURE_SUCCESS(rv, rv); + + const nsSTSPreload *preload = nullptr; + nsSTSHostEntry *pbEntry = nullptr; + + if (mInPrivateMode) { + pbEntry = mPrivateModeHostTable.GetEntry(host.get()); + } + + nsCOMPtr principal; + rv = GetPrincipalForURI(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); - // If any super-domain has the includeSubdomains permission, this is an - // STS host. - rv = TestPermission(aURI, STS_SUBDOMAIN_PERMISSION, &permGeneral, false); + PRUint32 permMgrPermission; + rv = mPermMgr->TestExactPermissionFromPrincipal(principal, STS_PERMISSION, + &permMgrPermission); NS_ENSURE_SUCCESS(rv, rv); - *aResult = ((permExact == nsIPermissionManager::ALLOW_ACTION) || - (permGeneral == nsIPermissionManager::ALLOW_ACTION)); + // First check the exact host. This involves first checking for an entry in + // the private browsing table. If that entry exists, we don't want to check + // in either the permission manager or the preload list. We only want to use + // the stored permission if it is not a knockout entry, however. + // Additionally, if it is a knockout entry, we want to stop looking for data + // on the host, because the knockout entry indicates "we have no information + // regarding the sts status of this host". + if (pbEntry && pbEntry->mStsPermission != STS_UNSET) { + STSLOG(("Found private browsing table entry for %s", host.get())); + if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) { + *aResult = true; + return NS_OK; + } + } + // Next we look in the permission manager. Same story here regarding + // knockout entries. + else if (permMgrPermission != STS_UNSET) { + STSLOG(("Found permission manager entry for %s", host.get())); + if (permMgrPermission == STS_SET) { + *aResult = true; + return NS_OK; + } + } + // Finally look in the preloaded list. This is the exact host, + // so if an entry exists at all, this host is sts. + else if (GetPreloadListEntry(host.get())) { + STSLOG(("%s is a preloaded STS host", host.get())); + *aResult = true; + return NS_OK; + } + + // Used for testing permissions as we walk up the domain tree. + nsCOMPtr domainWalkURI; + nsCOMPtr domainWalkPrincipal; + const char *subdomain; + + STSLOG(("no HSTS data for %s found, walking up domain", host.get())); + PRUint32 offset = 0; + for (offset = host.FindChar('.', offset) + 1; + offset > 0; + offset = host.FindChar('.', offset) + 1) { + + subdomain = host.get() + offset; + + // If we get an empty string, don't continue. + if (strlen(subdomain) < 1) { + break; + } + + if (mInPrivateMode) { + pbEntry = mPrivateModeHostTable.GetEntry(subdomain); + } + + // normalize all URIs with https:// + rv = NS_NewURI(getter_AddRefs(domainWalkURI), + NS_LITERAL_CSTRING("https://") + Substring(host, offset)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(domainWalkPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal, + STS_PERMISSION, + &permMgrPermission); + NS_ENSURE_SUCCESS(rv, rv); + + // Do the same thing as with the exact host, except now we're looking at + // ancestor domains of the original host. So, we have to look at the + // include subdomains permissions (although we still have to check for the + // STS_PERMISSION first to check that this is an sts host and not a + // knockout entry - and again, if it is a knockout entry, we stop looking + // for data on it and skip to the next higher up ancestor domain). + if (pbEntry && pbEntry->mStsPermission != STS_UNSET) { + STSLOG(("Found private browsing table entry for %s", subdomain)); + if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) { + *aResult = pbEntry->mIncludeSubdomains; + break; + } + } + else if (permMgrPermission != STS_UNSET) { + STSLOG(("Found permission manager entry for %s", subdomain)); + if (permMgrPermission == STS_SET) { + PRUint32 subdomainPermission; + rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal, + STS_SUBDOMAIN_PERMISSION, + &subdomainPermission); + NS_ENSURE_SUCCESS(rv, rv); + *aResult = (subdomainPermission == STS_SET); + break; + } + } + // This is an ancestor, so if we get a match, we have to check if the + // preloaded entry includes subdomains. + else if ((preload = GetPreloadListEntry(subdomain)) != nullptr) { + if (preload->mIncludeSubdomains) { + STSLOG(("%s is a preloaded STS host", subdomain)); + *aResult = true; + break; + } + } + + STSLOG(("no HSTS data for %s found, walking up domain", subdomain)); + } + + // Use whatever we ended up with, which defaults to false. return NS_OK; } @@ -429,7 +582,7 @@ nsStrictTransportSecurityService::AddPermission(nsIURI *aURI, nsCAutoString host; nsresult rv = GetHost(aURI, host); NS_ENSURE_SUCCESS(rv, rv); - STSLOG(("AddPermission for entry for for %s", host.get())); + STSLOG(("AddPermission for entry for %s", host.get())); // Update in mPrivateModeHostTable only, so any changes will be rolled // back when exiting private mode. @@ -444,7 +597,10 @@ nsStrictTransportSecurityService::AddPermission(nsIURI *aURI, // PutEntry returns an existing entry if there already is one, or it // creates a new one if there isn't. nsSTSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get()); - STSLOG(("Created private mode entry for for %s", host.get())); + if (!entry) { + return NS_ERROR_OUT_OF_MEMORY; + } + STSLOG(("Created private mode entry for %s", host.get())); // AddPermission() will be called twice if the STS header encountered has // includeSubdomains (first for the main permission and second for the @@ -454,14 +610,13 @@ nsStrictTransportSecurityService::AddPermission(nsIURI *aURI, if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) { entry->mIncludeSubdomains = true; } - // for the case where PutEntry() returned an existing host entry, make - // sure it's not set as deleted (which might have happened in the past). - entry->mDeleted = false; + else if (strcmp(aType, STS_PERMISSION) == 0) { + entry->mStsPermission = aPermission; + } // Also refresh the expiration time. - entry->mExpireTime = aExpireTime; + entry->SetExpireTime(aExpireTime); return NS_OK; - } nsresult @@ -469,9 +624,10 @@ nsStrictTransportSecurityService::RemovePermission(const nsCString &aHost, const char *aType) { // Build up a principal for use with the permission manager. + // normalize all URIs with https:// nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), - NS_LITERAL_CSTRING("http://") + aHost); + NS_LITERAL_CSTRING("https://") + aHost); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr principal; @@ -480,149 +636,31 @@ nsStrictTransportSecurityService::RemovePermission(const nsCString &aHost, if (!mInPrivateMode) { // Not in private mode: remove permissions persistently. - return mPermMgr->RemoveFromPrincipal(principal, aType); + // This means setting the permission to STS_KNOCKOUT in case + // this host is on the preload list (so we can override it). + return mPermMgr->AddFromPrincipal(principal, aType, + STS_KNOCKOUT, + nsIPermissionManager::EXPIRE_NEVER, 0); } // Make changes in mPrivateModeHostTable only, so any changes will be // rolled back when exiting private mode. nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get()); - // Check to see if there's STS data stored for this host in the - // permission manager (probably set outside private mode). - uint32_t permmgrValue; - rv = mPermMgr->TestExactPermissionFromPrincipal(principal, aType, - &permmgrValue); - NS_ENSURE_SUCCESS(rv, rv); - - // If there is STS data in the permission manager, store a "deleted" mask - // for the permission in mPrivateModeHostTable (either update - // mPrivateModeHostTable to have the deleted mask, or add one). - // This is because we don't want removals that happen in private mode to - // be reflected when private mode is exited -- but while in private mode - // we still want the effect of the removal. - if (permmgrValue != nsIPermissionManager::UNKNOWN_ACTION) { - // if there's no entry in mPrivateModeHostTable, we have to make one. + if (!entry) { + entry = mPrivateModeHostTable.PutEntry(aHost.get()); if (!entry) { - entry = mPrivateModeHostTable.PutEntry(aHost.get()); - STSLOG(("Created private mode deleted mask for for %s", aHost.get())); + return NS_ERROR_OUT_OF_MEMORY; } - entry->mDeleted = true; - entry->mIncludeSubdomains = false; - return NS_OK; + STSLOG(("Created private mode deleted mask for %s", aHost.get())); } - // Otherwise, permission doesn't exist in the real permission manager, so - // there's nothing to "pretend" to delete. I'ts ok to delete any copy in - // mPrivateModeHostTable. - if (entry) mPrivateModeHostTable.RawRemoveEntry(entry); - return NS_OK; -} - -nsresult -nsStrictTransportSecurityService::TestPermission(nsIURI *aURI, - const char *aType, - uint32_t *aPermission, - bool testExact) -{ - // set default for if we can't find any STS information - *aPermission = nsIPermissionManager::UNKNOWN_ACTION; - - if (!mInPrivateMode) { - // if not in private mode, just delegate to the permission manager. - nsCOMPtr principal; - nsresult rv = GetPrincipalForURI(aURI, getter_AddRefs(principal)); - NS_ENSURE_SUCCESS(rv, rv); - - if (testExact) - return mPermMgr->TestExactPermissionFromPrincipal(principal, aType, aPermission); - else - return mPermMgr->TestPermissionFromPrincipal(principal, aType, aPermission); + if (strcmp(aType, STS_PERMISSION) == 0) { + entry->mStsPermission = STS_KNOCKOUT; + } + else if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) { + entry->mIncludeSubdomains = false; } - nsCAutoString host; - nsresult rv = GetHost(aURI, host); - if (NS_FAILED(rv)) return NS_OK; - - nsSTSHostEntry *entry; - uint32_t actualExactPermission; - uint32_t offset = 0; - int64_t now = PR_Now() / 1000; - - // Used for testing permissions as we walk up the domain tree. - nsCOMPtr domainWalkURI; - - // In parallel, loop over private mode cache and also the real permission - // manager--ignoring any masked as "deleted" in the local cache. We have - // to do this here since the most specific permission in *either* the - // permission manager or mPrivateModeHostTable should be used. - do { - entry = mPrivateModeHostTable.GetEntry(host.get() + offset); - STSLOG(("Checking PM Table entry and permmgr for %s", host.get()+offset)); - - // flag as deleted any entries encountered that have expired. We only - // flag the nsSTSHostEntry because there could be some data in the - // permission manager that -- if not in private mode -- would have been - // overwritten by newly encountered STS data. - if (entry && (now > entry->mExpireTime)) { - STSLOG(("Deleting expired PM Table entry for %s", host.get()+offset)); - entry->mDeleted = true; - entry->mIncludeSubdomains = false; - } - - rv = NS_NewURI(getter_AddRefs(domainWalkURI), - NS_LITERAL_CSTRING("http://") + Substring(host, offset)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr principal; - nsresult rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(principal)); - NS_ENSURE_SUCCESS(rv, rv); - - rv = mPermMgr->TestExactPermissionFromPrincipal(principal, aType, - &actualExactPermission); - NS_ENSURE_SUCCESS(rv, rv); - - // There are three cases as we walk up the hostname testing - // permissions: - // 1. There's no entry in mPrivateModeHostTable for this host; rely - // on data in the permission manager - if (!entry) { - if (actualExactPermission != nsIPermissionManager::UNKNOWN_ACTION) { - // no cached data but a permission in the permission manager so use - // it and stop looking. - *aPermission = actualExactPermission; - STSLOG(("no PM Table entry for %s, using permmgr", host.get()+offset)); - break; - } - } - // 2. There's a "deleted" mask in mPrivateModeHostTable for this host - // or we're looking for includeSubdomain information and it's not set: - // any data in the permission manager must be ignored, since the - // permission would have been deleted if not in private mode. - else if (entry->mDeleted || (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0 - && !entry->mIncludeSubdomains)) { - STSLOG(("no entry at all for %s, walking up", host.get()+offset)); - // keep looking - } - // 3. There's a non-deleted entry in mPrivateModeHostTable for this - // host, so it should be used. - else { - // All STS permissions' values are ALLOW_ACTION or they are not - // known (as in, not set or turned off). - *aPermission = nsIPermissionManager::ALLOW_ACTION; - STSLOG(("PM Table entry for %s: forcing", host.get()+offset)); - break; - } - - // Don't continue walking up the host segments if the test was for an - // exact match only. - if (testExact) break; - - STSLOG(("no PM Table entry or permmgr data for %s, walking up domain", - host.get()+offset)); - // walk up the host segments - offset = host.FindChar('.', offset) + 1; - } while (offset > 0); - - // Use whatever we ended up with, which defaults to UNKNOWN_ACTION. return NS_OK; } diff --git a/security/manager/boot/src/nsStrictTransportSecurityService.h b/security/manager/boot/src/nsStrictTransportSecurityService.h index 3c9689200299..6df96e09fe8c 100644 --- a/security/manager/boot/src/nsStrictTransportSecurityService.h +++ b/security/manager/boot/src/nsStrictTransportSecurityService.h @@ -29,12 +29,15 @@ // permissions. // // Each nsSTSHostEntry contains: -// - Expiry time -// - Deleted flag (boolean, default false) -// - Subdomains flag (boolean, default false) +// - Expiry time (PRTime, milliseconds) +// - Expired flag (bool, default false) +// - STS permission (uint32_t, default STS_UNSET) +// - Include subdomains flag (bool, default false) +// +// Note: the subdomains flag has no meaning if the STS permission is STS_UNSET. // // The existence of the nsSTSHostEntry implies STS state is set for the given -// host -- unless the deleted flag is set, in which case not only is the STS +// host -- unless the expired flag is set, in which case not only is the STS // state not set for the host, but any permission actually present in the // permission manager should be ignored. // @@ -58,9 +61,10 @@ class nsSTSHostEntry : public PLDHashEntryHdr explicit nsSTSHostEntry(const nsSTSHostEntry& toCopy); nsCString mHost; - int64_t mExpireTime; - bool mDeleted; - bool mIncludeSubdomains; + PRTime mExpireTime; + uint32_t mStsPermission; + bool mExpired; + bool mIncludeSubdomains; // Hash methods typedef const char* KeyType; @@ -86,11 +90,36 @@ class nsSTSHostEntry : public PLDHashEntryHdr return PL_DHashStringKey(nullptr, aKey); } + void SetExpireTime(PRTime aExpireTime) + { + mExpireTime = aExpireTime; + mExpired = false; + } + + bool IsExpired() + { + // If mExpireTime is 0, this entry never expires (this is the case for + // knockout entries). + // If we've already expired or we never expire, return early. + if (mExpired || mExpireTime == 0) { + return mExpired; + } + + PRTime now = PR_Now() / PR_USEC_PER_MSEC; + if (now > mExpireTime) { + mExpired = true; + } + + return mExpired; + } + // force the hashtable to use the copy constructor. enum { ALLOW_MEMMOVE = false }; }; //////////////////////////////////////////////////////////////////////////////// +class nsSTSPreload; + class nsStrictTransportSecurityService : public nsIStrictTransportSecurityService , public nsIObserver { @@ -105,8 +134,10 @@ public: private: nsresult GetHost(nsIURI *aURI, nsACString &aResult); + nsresult GetPrincipalForURI(nsIURI *aURI, nsIPrincipal **aPrincipal); nsresult SetStsState(nsIURI* aSourceURI, int64_t maxage, bool includeSubdomains); nsresult ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader); + const nsSTSPreload *GetPreloadListEntry(const char *aHost); // private-mode-preserving permission manager overlay functions nsresult AddPermission(nsIURI *aURI, @@ -116,10 +147,6 @@ private: int64_t aExpireTime); nsresult RemovePermission(const nsCString &aHost, const char *aType); - nsresult TestPermission(nsIURI *aURI, - const char *aType, - uint32_t *aPermission, - bool testExact); // cached services nsCOMPtr mPermMgr; diff --git a/security/manager/ssl/tests/unit/test_sts_preloadlist.js b/security/manager/ssl/tests/unit/test_sts_preloadlist.js new file mode 100644 index 000000000000..848483c6bced --- /dev/null +++ b/security/manager/ssl/tests/unit/test_sts_preloadlist.js @@ -0,0 +1,180 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gPBService = Cc["@mozilla.org/privatebrowsing;1"] + .getService(Ci.nsIPrivateBrowsingService); +var gSTSService = Cc["@mozilla.org/stsservice;1"] + .getService(Ci.nsIStrictTransportSecurityService); + +function Observer() {} +Observer.prototype = { + observe: function(subject, topic, data) { + run_next_test(); + } +}; + +var gObserver = new Observer(); + +// This is a list of every host we call processStsHeader with +// (we have to remove any state added to the sts service so as to not muck +// with other tests). +var hosts = ["http://keyerror.com", "http://subdomain.kyps.net", + "http://subdomain.cert.se", "http://crypto.cat", + "http://www.logentries.com"]; + +function cleanup() { + Services.obs.removeObserver(gObserver, "private-browsing-transition-complete"); + gPBService.privateBrowsingEnabled = false; + for (var host of hosts) { + var uri = Services.io.newURI(host, null, null); + gSTSService.removeStsState(uri); + } +} + +function run_test() { + do_register_cleanup(cleanup); + Services.obs.addObserver(gObserver, "private-browsing-transition-complete", false); + Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session", true); + + add_test(test_part1); + add_test(test_private_browsing1); + add_test(test_private_browsing2); + + run_next_test(); +} + +function test_part1() { + // check that a host not in the list is not identified as an sts host + do_check_false(gSTSService.isStsHost("nonexistent.mozilla.com")); + + // check that an ancestor domain is not identified as an sts host + do_check_false(gSTSService.isStsHost("com")); + + // Note: the following were taken from the STS preload list + // as of June 2012. If the list changes, this test will need to be modified. + // check that an entry at the beginning of the list is an sts host + do_check_true(gSTSService.isStsHost("health.google.com")); + + // check that a subdomain is an sts host (includeSubdomains is set) + do_check_true(gSTSService.isStsHost("subdomain.health.google.com")); + + // check that another subdomain is an sts host (includeSubdomains is set) + do_check_true(gSTSService.isStsHost("a.b.c.subdomain.health.google.com")); + + // check that an entry in the middle of the list is an sts host + do_check_true(gSTSService.isStsHost("epoxate.com")); + + // check that a subdomain is not an sts host (includeSubdomains is not set) + do_check_false(gSTSService.isStsHost("subdomain.epoxate.com")); + + // check that an entry at the end of the list is an sts host + do_check_true(gSTSService.isStsHost("www.googlemail.com")); + + // check that a subdomain is not an sts host (includeSubdomains is not set) + do_check_false(gSTSService.isStsHost("a.subdomain.www.googlemail.com")); + + // check that a host with a dot on the end won't break anything + do_check_false(gSTSService.isStsHost("notsts.nonexistent.mozilla.com.")); + + // check that processing a header with max-age: 0 will remove a preloaded + // site from the list + var uri = Services.io.newURI("http://keyerror.com", null, null); + gSTSService.processStsHeader(uri, "max-age=0"); + do_check_false(gSTSService.isStsHost("keyerror.com")); + do_check_false(gSTSService.isStsHost("subdomain.keyerror.com")); + // check that processing another header (with max-age non-zero) will + // re-enable a site's sts status + gSTSService.processStsHeader(uri, "max-age=1000"); + do_check_true(gSTSService.isStsHost("keyerror.com")); + // but this time include subdomains was not set, so test for that + do_check_false(gSTSService.isStsHost("subdomain.keyerror.com")); + + // check that processing a header with max-age: 0 from a subdomain of a site + // will not remove that (ancestor) site from the list + var uri = Services.io.newURI("http://subdomain.kyps.net", null, null); + gSTSService.processStsHeader(uri, "max-age=0"); + do_check_true(gSTSService.isStsHost("kyps.net")); + do_check_false(gSTSService.isStsHost("subdomain.kyps.net")); + + var uri = Services.io.newURI("http://subdomain.cert.se", null, null); + gSTSService.processStsHeader(uri, "max-age=0"); + // we received a header with "max-age=0", so we have "no information" + // regarding the sts state of subdomain.cert.se specifically, but + // it is actually still an STS host, because of the preloaded cert.se + // including subdomains. + // Here's a drawing: + // |-- cert.se (in preload list, includes subdomains) IS sts host + // |-- subdomain.cert.se IS sts host + // | `-- another.subdomain.cert.se IS sts host + // `-- sibling.cert.se IS sts host + do_check_true(gSTSService.isStsHost("subdomain.cert.se")); + do_check_true(gSTSService.isStsHost("sibling.cert.se")); + do_check_true(gSTSService.isStsHost("another.subdomain.cert.se")); + + gSTSService.processStsHeader(uri, "max-age=1000"); + // Here's what we have now: + // |-- cert.se (in preload list, includes subdomains) IS sts host + // |-- subdomain.cert.se (include subdomains is false) IS sts host + // | `-- another.subdomain.cert.se IS NOT sts host + // `-- sibling.cert.se IS sts host + do_check_true(gSTSService.isStsHost("subdomain.cert.se")); + do_check_true(gSTSService.isStsHost("sibling.cert.se")); + do_check_false(gSTSService.isStsHost("another.subdomain.cert.se")); + + // test private browsing correctly interacts with removing preloaded sites + gPBService.privateBrowsingEnabled = true; +} + +function test_private_browsing1() { + // sanity - crypto.cat is preloaded, includeSubdomains set + do_check_true(gSTSService.isStsHost("crypto.cat")); + do_check_true(gSTSService.isStsHost("a.b.c.subdomain.crypto.cat")); + + var uri = Services.io.newURI("http://crypto.cat", null, null); + gSTSService.processStsHeader(uri, "max-age=0"); + do_check_false(gSTSService.isStsHost("crypto.cat")); + do_check_false(gSTSService.isStsHost("a.b.subdomain.crypto.cat")); + + // check adding it back in + gSTSService.processStsHeader(uri, "max-age=1000"); + do_check_true(gSTSService.isStsHost("crypto.cat")); + // but no includeSubdomains this time + do_check_false(gSTSService.isStsHost("b.subdomain.crypto.cat")); + + // do the hokey-pokey... + gSTSService.processStsHeader(uri, "max-age=0"); + do_check_false(gSTSService.isStsHost("crypto.cat")); + do_check_false(gSTSService.isStsHost("subdomain.crypto.cat")); + + // TODO unfortunately we don't have a good way to know when an entry + // has expired in the permission manager, so we can't yet extend this test + // to that case. + // Test that an expired private browsing entry results in correctly + // identifying a host that is on the preload list as no longer sts. + // (This happens when we're in private browsing mode, we get a header from + // a site on the preload list, and that header later expires. We need to + // then treat that host as no longer an sts host.) + // (sanity check first - this should be in the preload list) + do_check_true(gSTSService.isStsHost("www.logentries.com")); + var uri = Services.io.newURI("http://www.logentries.com", null, null); + // according to the rfc, max-age can't be negative, but this is a great + // way to test an expired entry + gSTSService.processStsHeader(uri, "max-age=-1000"); + do_check_false(gSTSService.isStsHost("www.logentries.com")); + + gPBService.privateBrowsingEnabled = false; +} + +function test_private_browsing2() { + do_check_true(gSTSService.isStsHost("crypto.cat")); + // the crypto.cat entry has includeSubdomains set + do_check_true(gSTSService.isStsHost("subdomain.crypto.cat")); + + // Now that we're out of private browsing mode, we need to make sure + // we've "forgotten" that we "forgot" this site's sts status. + do_check_true(gSTSService.isStsHost("www.logentries.com")); + + run_next_test(); +} diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index 0dc7801435c3..fd27f266e294 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -11,3 +11,4 @@ skip-if = os == "android" [test_hmac.js] # Bug 676972: test hangs consistently on Android skip-if = os == "android" +[test_sts_preloadlist.js] diff --git a/security/manager/tools/getHSTSPreloadList.py b/security/manager/tools/getHSTSPreloadList.py new file mode 100644 index 000000000000..87d6921a1976 --- /dev/null +++ b/security/manager/tools/getHSTSPreloadList.py @@ -0,0 +1,115 @@ +#!/usr/bin/python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import sys, subprocess, json, argparse + +SOURCE = "https://src.chromium.org/viewvc/chrome/trunk/src/net/base/transport_security_state_static.json" +OUTPUT = "nsSTSPreloadList.inc" +PREFIX = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*****************************************************************************/ +/* This is an automatically generated file. If you're not */ +/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */ +/*****************************************************************************/ + +#include + +class nsSTSPreload +{ + public: + const char *mHost; + const bool mIncludeSubdomains; +}; + +static const nsSTSPreload kSTSPreloadList[] = { +""" +POSTFIX = """}; +""" + +def filterComments(stream): + lines = [] + for line in stream: + # each line still has '\n' at the end, so if find returns -1, + # the newline gets chopped off like we want + # (and otherwise, comments are filtered out like we want) + lines.append(line[0:line.find("//")]) + return "".join(lines) + +def readFile(source): + if source != "-": + f = open(source, 'r') + else: + f = sys.stdin + return filterComments(f) + +def download(source): + download = subprocess.Popen(["wget", "-O", "-", source], stdout = subprocess.PIPE, stderr = subprocess.PIPE) + contents = filterComments(download.stdout) + download.wait() + if download.returncode != 0: + raise Exception() + return contents + +def output(filename, jsonblob): + if filename != "-": + outstream = open(filename, 'w') + else: + outstream = sys.stdout + if not 'entries' in jsonblob: + raise Exception() + else: + outstream.write(PREFIX) + # use a dictionary to prevent duplicates + lines = {} + for entry in jsonblob['entries']: + if 'name' in entry and 'mode' in entry and entry['mode'] == "force-https": + line = " { \"" + entry['name'] + "\", " + if 'include_subdomains' in entry and entry['include_subdomains']: + line = line + "true },\n" + else: + line = line + "false },\n" + lines[line] = True + # The data must be sorted by domain name because we do a binary search to + # determine if a host is in the preload list. + keys = lines.keys() + keys.sort() + for line in keys: + outstream.write(line) + outstream.write(POSTFIX); + outstream.close() + +def main(): + parser = argparse.ArgumentParser(description="Download Chrome's STS preload list and format it for Firefox") + parser.add_argument("-s", "--source", default=SOURCE, help="Specify source for input list (can be a file, url, or '-' for stdin)") + parser.add_argument("-o", "--output", default=OUTPUT, help="Specify output file ('-' for stdout)") + args = parser.parse_args() + contents = None + try: + contents = readFile(args.source) + except: + pass + if not contents: + try: + contents = download(args.source) + except: + print >> sys.stderr, "Could not read source '%s'" % args.source + return 1 + try: + jsonblob = json.loads(contents) + except: + print >> sys.stderr, "Could not parse contents of file '%s'" % args.source + return 1 + try: + output(args.output, jsonblob) + except: + print >> sys.stderr, "Could not write to '%s'" % args.output + return 1 + return 0 + +if __name__ == "__main__": + sys.exit(main()) -- 2.11.4.GIT