Bug 760307 - Preloaded strict-transport-security site list. r=mayhemer, bsmith

This commit is contained in:
David Keeler 2012-08-24 14:17:27 -07:00
parent b9ae21b684
commit d28bb14e18
6 changed files with 684 additions and 188 deletions

View File

@ -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 <prtypes.h>
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 },
};

View File

@ -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<nsIScriptSecurityManager> 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)
{
}
@ -123,6 +117,29 @@ nsStrictTransportSecurityService::GetHost(nsIURI *aURI, nsACString &aResult)
return NS_OK;
}
nsresult
nsStrictTransportSecurityService::GetPrincipalForURI(nsIURI* aURI,
nsIPrincipal** aPrincipal)
{
nsresult rv;
nsCOMPtr<nsIScriptSecurityManager> 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<nsIURI> 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,
@ -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);
// If any super-domain has the includeSubdomains permission, this is an
// STS host.
rv = TestPermission(aURI, STS_SUBDOMAIN_PERMISSION, &permGeneral, false);
const nsSTSPreload *preload = nullptr;
nsSTSHostEntry *pbEntry = nullptr;
if (mInPrivateMode) {
pbEntry = mPrivateModeHostTable.GetEntry(host.get());
}
nsCOMPtr<nsIPrincipal> principal;
rv = GetPrincipalForURI(aURI, getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
*aResult = ((permExact == nsIPermissionManager::ALLOW_ACTION) ||
(permGeneral == nsIPermissionManager::ALLOW_ACTION));
PRUint32 permMgrPermission;
rv = mPermMgr->TestExactPermissionFromPrincipal(principal, STS_PERMISSION,
&permMgrPermission);
NS_ENSURE_SUCCESS(rv, rv);
// 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<nsIURI> domainWalkURI;
nsCOMPtr<nsIPrincipal> 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<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri),
NS_LITERAL_CSTRING("http://") + aHost);
NS_LITERAL_CSTRING("https://") + aHost);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIPrincipal> 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;
STSLOG(("Created private mode deleted mask for %s", aHost.get()));
}
if (strcmp(aType, STS_PERMISSION) == 0) {
entry->mStsPermission = STS_KNOCKOUT;
}
else if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
entry->mIncludeSubdomains = false;
return NS_OK;
}
// 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<nsIPrincipal> 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);
}
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<nsIURI> 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<nsIPrincipal> 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;
}

View File

@ -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<nsIPermissionManager> mPermMgr;

View File

@ -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();
}

View File

@ -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]

View File

@ -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 <prtypes.h>
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())