gecko-dev/security/manager/boot/src/nsStrictTransportSecurityService.cpp

626 lines
23 KiB
C++
Raw Normal View History

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Strict-Transport-Security.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Sid Stamm <sid@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include "plstr.h"
#include "prlog.h"
#include "prprf.h"
#include "nsCRTGlue.h"
#include "nsIPermissionManager.h"
#include "nsIPrivateBrowsingService.h"
#include "nsISSLStatus.h"
#include "nsISSLStatusProvider.h"
#include "nsStrictTransportSecurityService.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsThreadUtils.h"
#include "nsStringGlue.h"
#if defined(PR_LOGGING)
PRLogModuleInfo *gSTSLog = PR_NewLogModule("nsSTSService");
#endif
#define STSLOG(args) PR_LOG(gSTSLog, 4, args)
#define STS_PARSER_FAIL_IF(test,args) \
if (test) { \
STSLOG(args); \
return NS_ERROR_FAILURE; \
}
////////////////////////////////////////////////////////////////////////////////
nsSTSHostEntry::nsSTSHostEntry(const char* aHost)
: mHost(aHost)
, mExpireTime(0)
, mDeleted(PR_FALSE)
, mIncludeSubdomains(PR_FALSE)
{
}
nsSTSHostEntry::nsSTSHostEntry(const nsSTSHostEntry& toCopy)
: mHost(toCopy.mHost)
, mExpireTime(toCopy.mExpireTime)
, mDeleted(toCopy.mDeleted)
, mIncludeSubdomains(toCopy.mIncludeSubdomains)
{
}
////////////////////////////////////////////////////////////////////////////////
nsStrictTransportSecurityService::nsStrictTransportSecurityService()
: mInPrivateMode(PR_FALSE)
{
}
nsStrictTransportSecurityService::~nsStrictTransportSecurityService()
{
}
NS_IMPL_THREADSAFE_ISUPPORTS2(nsStrictTransportSecurityService,
nsIObserver,
nsIStrictTransportSecurityService)
nsresult
nsStrictTransportSecurityService::Init()
{
nsresult rv;
mPermMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
// figure out if we're starting in private browsing mode
nsCOMPtr<nsIPrivateBrowsingService> pbs =
do_GetService(NS_PRIVATE_BROWSING_SERVICE_CONTRACTID);
if (pbs)
pbs->GetPrivateBrowsingEnabled(&mInPrivateMode);
mObserverService = mozilla::services::GetObserverService();
if (mObserverService)
mObserverService->AddObserver(this, NS_PRIVATE_BROWSING_SWITCH_TOPIC, PR_FALSE);
if (mInPrivateMode && !mPrivateModeHostTable.Init())
return NS_ERROR_OUT_OF_MEMORY;
return NS_OK;
}
nsresult
nsStrictTransportSecurityService::GetHost(nsIURI *aURI, nsACString &aResult)
{
nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aURI);
if (!innerURI) return NS_ERROR_FAILURE;
nsresult rv = innerURI->GetAsciiHost(aResult);
if (NS_FAILED(rv) || aResult.IsEmpty())
return NS_ERROR_UNEXPECTED;
return NS_OK;
}
nsresult
nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI,
PRInt64 maxage,
bool includeSubdomains)
{
// If max-age is zero, that's an indication to immediately remove the
// permissions, so here's a shortcut.
if (!maxage)
return RemoveStsState(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.
PRInt64 expiretime = (PR_Now() / 1000) + (maxage * 1000);
// 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,
(PRUint32) nsIPermissionManager::ALLOW_ACTION,
(PRUint32) nsIPermissionManager::EXPIRE_TIME,
expiretime);
NS_ENSURE_SUCCESS(rv, rv);
if (includeSubdomains) {
// record entry for this host with include subdomains in the permissions manager
STSLOG(("STS: subdomains permission SET, adding permission\n"));
rv = AddPermission(aSourceURI,
STS_SUBDOMAIN_PERMISSION,
(PRUint32) nsIPermissionManager::ALLOW_ACTION,
(PRUint32) nsIPermissionManager::EXPIRE_TIME,
expiretime);
NS_ENSURE_SUCCESS(rv, rv);
} else { // !includeSubdomains
nsCAutoString hostname;
rv = GetHost(aSourceURI, hostname);
NS_ENSURE_SUCCESS(rv, rv);
STSLOG(("STS: subdomains permission UNSET, removing any existing ones\n"));
rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
NS_IMETHODIMP
nsStrictTransportSecurityService::RemoveStsState(nsIURI* aURI)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
nsCAutoString hostname;
nsresult rv = GetHost(aURI, hostname);
NS_ENSURE_SUCCESS(rv, rv);
rv = RemovePermission(hostname, STS_PERMISSION);
NS_ENSURE_SUCCESS(rv, rv);
STSLOG(("STS: deleted maxage permission\n"));
rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION);
NS_ENSURE_SUCCESS(rv, rv);
STSLOG(("STS: deleted subdomains permission\n"));
return NS_OK;
}
NS_IMETHODIMP
nsStrictTransportSecurityService::ProcessStsHeader(nsIURI* aSourceURI,
const char* aHeader)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
char * header = NS_strdup(aHeader);
if (!header) return NS_ERROR_OUT_OF_MEMORY;
nsresult rv = ProcessStsHeaderMutating(aSourceURI, header);
NS_Free(header);
return rv;
}
nsresult
nsStrictTransportSecurityService::ProcessStsHeaderMutating(nsIURI* aSourceURI,
char* aHeader)
{
STSLOG(("STS: ProcessStrictTransportHeader(%s)\n", aHeader));
// "Strict-Transport-Security" ":" OWS
// STS-d *( OWS ";" OWS STS-d OWS)
//
// ; STS directive
// STS-d = maxAge / includeSubDomains
//
// maxAge = "max-age" "=" delta-seconds v-ext
//
// includeSubDomains = [ "includeSubDomains" ]
const char* directive;
bool foundMaxAge = false;
bool foundUnrecognizedTokens = false;
bool includeSubdomains = false;
PRInt64 maxAge = 0;
NS_NAMED_LITERAL_CSTRING(max_age_var, "max-age");
NS_NAMED_LITERAL_CSTRING(include_subd_var, "includesubdomains");
while ((directive = NS_strtok(";", &aHeader))) {
//skip leading whitespace
directive = NS_strspnp(" \t", directive);
STS_PARSER_FAIL_IF(!(*directive), ("error removing initial whitespace\n."));
if (!PL_strncasecmp(directive, max_age_var.get(), max_age_var.Length())) {
// skip directive name
directive += max_age_var.Length();
// skip leading whitespace
directive = NS_strspnp(" \t", directive);
STS_PARSER_FAIL_IF(*directive != '=',
("No equal sign found in max-age directive\n"));
// skip over the equal sign
STS_PARSER_FAIL_IF(*(++directive) == '\0',
("No delta-seconds present\n"));
// obtain the delta-seconds value
STS_PARSER_FAIL_IF(PR_sscanf(directive, "%lld", &maxAge) != 1,
("Could not convert delta-seconds\n"));
STSLOG(("STS: ProcessStrictTransportHeader() STS found maxage %lld\n", maxAge));
foundMaxAge = PR_TRUE;
// skip max-age value and trailing whitespace
directive = NS_strspnp("0123456789 \t", directive);
// log unknown tokens, but don't fail (for forwards compatibility)
if (*directive != '\0') {
foundUnrecognizedTokens = PR_TRUE;
STSLOG(("Extra stuff in max-age after delta-seconds: %s \n", directive));
}
}
else if (!PL_strncasecmp(directive, include_subd_var.get(), include_subd_var.Length())) {
directive += include_subd_var.Length();
// only record "includesubdomains" if it is a token by itself... for
// example, don't set includeSubdomains = PR_TRUE if the directive is
// "includesubdomainsFooBar".
if (*directive == '\0' || *directive =='\t' || *directive == ' ') {
includeSubdomains = PR_TRUE;
STSLOG(("STS: ProcessStrictTransportHeader: obtained subdomains status\n"));
// skip trailing whitespace
directive = NS_strspnp(" \t", directive);
if (*directive != '\0') {
foundUnrecognizedTokens = PR_TRUE;
STSLOG(("Extra stuff after includesubdomains: %s\n", directive));
}
} else {
foundUnrecognizedTokens = PR_TRUE;
STSLOG(("Unrecognized directive in header: %s\n", directive));
}
}
else {
// log unknown directives, but don't fail (for backwards compatibility)
foundUnrecognizedTokens = PR_TRUE;
STSLOG(("Unrecognized directive in header: %s\n", directive));
}
}
// after processing all the directives, make sure we came across max-age
// somewhere.
STS_PARSER_FAIL_IF(!foundMaxAge,
("Parse ERROR: couldn't locate max-age token\n"));
// record the successfully parsed header data.
SetStsState(aSourceURI, maxAge, includeSubdomains);
return foundUnrecognizedTokens ?
NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA :
NS_OK;
}
NS_IMETHODIMP
nsStrictTransportSecurityService::IsStsHost(const char* aHost, bool* aResult)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
nsCOMPtr<nsIURI> uri;
nsDependentCString hostString(aHost);
nsresult rv = NS_NewURI(getter_AddRefs(uri),
NS_LITERAL_CSTRING("https://") + hostString);
NS_ENSURE_SUCCESS(rv, rv);
return IsStsURI(uri, aResult);
}
NS_IMETHODIMP
nsStrictTransportSecurityService::IsStsURI(nsIURI* aURI, bool* aResult)
{
// Should be called on the main thread (or via proxy) since the permission
// manager is used and it's not threadsafe.
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
nsresult rv;
PRUint32 permExact, permGeneral;
// If this domain has the forcehttps permission, this is an STS host.
rv = TestPermission(aURI, STS_PERMISSION, &permExact, PR_TRUE);
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, PR_FALSE);
NS_ENSURE_SUCCESS(rv, rv);
*aResult = ((permExact == nsIPermissionManager::ALLOW_ACTION) ||
(permGeneral == nsIPermissionManager::ALLOW_ACTION));
return NS_OK;
}
// Verify the trustworthiness of the security info (are there any cert errors?)
NS_IMETHODIMP
nsStrictTransportSecurityService::ShouldIgnoreStsHeader(nsISupports* aSecurityInfo,
bool* aResult)
{
nsresult rv;
bool tlsIsBroken = false;
nsCOMPtr<nsISSLStatusProvider> sslprov = do_QueryInterface(aSecurityInfo);
NS_ENSURE_TRUE(sslprov, NS_ERROR_FAILURE);
nsCOMPtr<nsISupports> isupstat;
rv = sslprov->GetSSLStatus(getter_AddRefs(isupstat));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsISSLStatus> sslstat = do_QueryInterface(isupstat);
NS_ENSURE_TRUE(sslstat, NS_ERROR_FAILURE);
bool trustcheck;
rv = sslstat->GetIsDomainMismatch(&trustcheck);
NS_ENSURE_SUCCESS(rv, rv);
tlsIsBroken = tlsIsBroken || trustcheck;
rv = sslstat->GetIsNotValidAtThisTime(&trustcheck);
NS_ENSURE_SUCCESS(rv, rv);
tlsIsBroken = tlsIsBroken || trustcheck;
rv = sslstat->GetIsUntrusted(&trustcheck);
NS_ENSURE_SUCCESS(rv, rv);
tlsIsBroken = tlsIsBroken || trustcheck;
*aResult = tlsIsBroken;
return NS_OK;
}
//------------------------------------------------------------
// nsStrictTransportSecurityService::nsIObserver
//------------------------------------------------------------
NS_IMETHODIMP
nsStrictTransportSecurityService::Observe(nsISupports *subject,
const char *topic,
const PRUnichar *data)
{
if (strcmp(topic, NS_PRIVATE_BROWSING_SWITCH_TOPIC) == 0) {
if(NS_LITERAL_STRING(NS_PRIVATE_BROWSING_ENTER).Equals(data)) {
// Indication to start recording stuff locally and not writing changes
// out to the permission manager.
if (!mPrivateModeHostTable.IsInitialized()
&& !mPrivateModeHostTable.Init()) {
return NS_ERROR_OUT_OF_MEMORY;
}
mInPrivateMode = PR_TRUE;
}
else if (NS_LITERAL_STRING(NS_PRIVATE_BROWSING_LEAVE).Equals(data)) {
mPrivateModeHostTable.Clear();
mInPrivateMode = PR_FALSE;
}
}
return NS_OK;
}
//------------------------------------------------------------
// Functions to overlay the permission manager calls in case
// we're in private browsing mode.
//------------------------------------------------------------
nsresult
nsStrictTransportSecurityService::AddPermission(nsIURI *aURI,
const char *aType,
PRUint32 aPermission,
PRUint32 aExpireType,
PRInt64 aExpireTime)
{
// Private mode doesn't address user-set (EXPIRE_NEVER) permissions: let
// those be stored persistently.
if (!mInPrivateMode || aExpireType == nsIPermissionManager::EXPIRE_NEVER) {
// Not in private mode, or manually-set permission
return mPermMgr->Add(aURI, aType, aPermission, aExpireType, aExpireTime);
}
nsCAutoString host;
nsresult rv = GetHost(aURI, host);
NS_ENSURE_SUCCESS(rv, rv);
STSLOG(("AddPermission for entry for for %s", host.get()));
// Update in mPrivateModeHostTable only, so any changes will be rolled
// back when exiting private mode.
// Note: EXPIRE_NEVER permissions should trump anything that shows up in
// the HTTP header, so if there's an EXPIRE_NEVER permission already
// don't store anything new.
// Currently there's no way to get the type of expiry out of the
// permission manager, but that's okay since there's nothing that stores
// EXPIRE_NEVER permissions.
// 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()));
// AddPermission() will be called twice if the STS header encountered has
// includeSubdomains (first for the main permission and second for the
// subdomains permission). If AddPermission() gets called a second time
// with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in
// the nsSTSHostEntry.
if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
entry->mIncludeSubdomains = PR_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 = PR_FALSE;
// Also refresh the expiration time.
entry->mExpireTime = aExpireTime;
return NS_OK;
}
nsresult
nsStrictTransportSecurityService::RemovePermission(const nsCString &aHost,
const char *aType)
{
if (!mInPrivateMode) {
// Not in private mode: remove permissions persistently.
return mPermMgr->Remove(aHost, aType);
}
// Make changes in mPrivateModeHostTable only, so any changes will be
// rolled back when exiting private mode.
nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get());
// Build up an nsIURI for use with the permission manager.
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri),
NS_LITERAL_CSTRING("http://") + aHost);
NS_ENSURE_SUCCESS(rv, rv);
// Check to see if there's STS data stored for this host in the
// permission manager (probably set outside private mode).
PRUint32 permmgrValue;
rv = mPermMgr->TestExactPermission(uri, 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());
STSLOG(("Created private mode deleted mask for for %s", aHost.get()));
}
entry->mDeleted = PR_TRUE;
entry->mIncludeSubdomains = PR_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,
PRUint32 *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.
if (testExact)
return mPermMgr->TestExactPermission(aURI, aType, aPermission);
else
return mPermMgr->TestPermission(aURI, aType, aPermission);
}
nsCAutoString host;
nsresult rv = GetHost(aURI, host);
if (NS_FAILED(rv)) return NS_OK;
nsSTSHostEntry *entry;
PRUint32 actualExactPermission;
PRUint32 offset = 0;
PRInt64 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 = PR_TRUE;
entry->mIncludeSubdomains = PR_FALSE;
}
rv = NS_NewURI(getter_AddRefs(domainWalkURI),
NS_LITERAL_CSTRING("http://") + Substring(host, offset));
NS_ENSURE_SUCCESS(rv, rv);
rv = mPermMgr->TestExactPermission(domainWalkURI,
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;
}