diff --git a/security/manager/ssl/nsISiteSecurityService.idl b/security/manager/ssl/nsISiteSecurityService.idl index 5fb4e825ff5f..22921970b85c 100644 --- a/security/manager/ssl/nsISiteSecurityService.idl +++ b/security/manager/ssl/nsISiteSecurityService.idl @@ -8,6 +8,7 @@ interface nsIURI; interface nsIObserver; interface nsIHttpChannel; interface nsISSLStatus; +interface nsISimpleEnumerator; %{C++ #include "nsTArrayForwardDeclare.h" @@ -23,6 +24,44 @@ namespace mozilla [ref] native nsCStringTArrayRef(nsTArray); [ref] native mozillaPkixTime(mozilla::pkix::Time); +// [infallible] attributes are only allowed on [builtinclass] +[scriptable, uuid(31313372-842c-4110-bdf1-6aea17c845ad), builtinclass] +interface nsISiteSecurityState : nsISupports +{ + readonly attribute ACString hostname; + [infallible] readonly attribute long long expireTime; + [infallible] readonly attribute short securityPropertyState; + [infallible] readonly attribute boolean includeSubdomains; + + /* + * SECURITY_PROPERTY_SET and SECURITY_PROPERTY_UNSET correspond to indicating + * a site has or does not have the security property in question, + * respectively. + * SECURITY_PROPERTY_KNOCKOUT indicates a value on a preloaded + * list is being overridden, and the associated site does not have the + * security property in question. + * SECURITY_PROPERTY_NEGATIVE is used when we've gotten a negative result from + * HSTS priming. + */ + const short SECURITY_PROPERTY_UNSET = 0; + const short SECURITY_PROPERTY_SET = 1; + const short SECURITY_PROPERTY_KNOCKOUT = 2; + const short SECURITY_PROPERTY_NEGATIVE = 3; +}; + +// This has to be a builtinclass because it derives from a builtinclass. +[scriptable, uuid(9ff16e40-1029-496c-95c2-bc819872b216), builtinclass] +interface nsISiteHSTSState : nsISiteSecurityState +{ +}; + +// This has to be a builtinclass because it derives from a builtinclass. +[scriptable, uuid(ae395078-c7d0-474d-b147-f4aa203a9b2c), builtinclass] +interface nsISiteHPKPState : nsISiteSecurityState +{ + readonly attribute nsISimpleEnumerator sha256Keys; +}; + [scriptable, uuid(275127f8-dbd7-4681-afbf-6df0c6587a01)] interface nsISiteSecurityService : nsISupports { @@ -210,6 +249,17 @@ interface nsISiteSecurityService : nsISupports * @param aMaxAge lifetime (in seconds) of this negative cache */ [noscript] void cacheNegativeHSTSResult(in nsIURI aURI, in unsigned long long aMaxAge); + + /** + * Returns an enumerator of the nsISiteSecurityService storage. Each item in + * the enumeration is a nsISiteSecurityState that can be QueryInterfaced to + * the appropriate nsISiteHSTSState or nsISiteHPKPState, depending on the + * provided type. Doesn't include preloaded entries (either the hard-coded + * ones or the preloaded-delivered-by-kinto ones). + * + * @param aType the type of security state in question. + */ + nsISimpleEnumerator enumerate(in uint32_t aType); }; %{C++ diff --git a/security/manager/ssl/nsSiteSecurityService.cpp b/security/manager/ssl/nsSiteSecurityService.cpp index 5abf99b79a15..8859151decd7 100644 --- a/security/manager/ssl/nsSiteSecurityService.cpp +++ b/security/manager/ssl/nsSiteSecurityService.cpp @@ -11,9 +11,12 @@ #include "base64.h" #include "mozilla/Assertions.h" #include "mozilla/Base64.h" +#include "mozilla/dom/PContent.h" #include "mozilla/LinkedList.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" #include "nsCRTGlue.h" #include "nsISSLStatus.h" #include "nsISocketProvider.h" @@ -22,9 +25,11 @@ #include "nsNSSComponent.h" #include "nsNetUtil.h" #include "nsPromiseFlatString.h" +#include "nsReadableUtils.h" #include "nsSecurityHeaderParser.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" +#include "nsVariant.h" #include "plstr.h" #include "prnetdb.h" #include "prprf.h" @@ -45,10 +50,17 @@ static LazyLogModule gSSSLog("nsSSService"); #define SSSLOG(args) MOZ_LOG(gSSSLog, mozilla::LogLevel::Debug, args) +const char kHSTSKeySuffix[] = ":HSTS"; +const char kHPKPKeySuffix[] = ":HPKP"; + //////////////////////////////////////////////////////////////////////////////// -SiteHSTSState::SiteHSTSState(nsCString& aStateString) - : mHSTSExpireTime(0) +NS_IMPL_ISUPPORTS(SiteHSTSState, nsISiteSecurityState, nsISiteHSTSState) + +SiteHSTSState::SiteHSTSState(const nsCString& aHost, + const nsCString& aStateString) + : mHostname(aHost) + , mHSTSExpireTime(0) , mHSTSState(SecurityPropertyUnset) , mHSTSIncludeSubdomains(false) { @@ -74,11 +86,13 @@ SiteHSTSState::SiteHSTSState(nsCString& aStateString) } } -SiteHSTSState::SiteHSTSState(PRTime aHSTSExpireTime, +SiteHSTSState::SiteHSTSState(const nsCString& aHost, + PRTime aHSTSExpireTime, SecurityPropertyState aHSTSState, bool aHSTSIncludeSubdomains) - : mHSTSExpireTime(aHSTSExpireTime) + : mHostname(aHost) + , mHSTSExpireTime(aHSTSExpireTime) , mHSTSState(aHSTSState) , mHSTSIncludeSubdomains(aHSTSIncludeSubdomains) { @@ -95,7 +109,41 @@ SiteHSTSState::ToString(nsCString& aString) aString.AppendInt(static_cast(mHSTSIncludeSubdomains)); } +NS_IMETHODIMP +SiteHSTSState::GetHostname(nsACString& aHostname) +{ + aHostname = mHostname; + return NS_OK; +} + +NS_IMETHODIMP +SiteHSTSState::GetExpireTime(int64_t* aExpireTime) +{ + NS_ENSURE_ARG(aExpireTime); + *aExpireTime = mHSTSExpireTime; + return NS_OK; +} + +NS_IMETHODIMP +SiteHSTSState::GetSecurityPropertyState(int16_t* aSecurityPropertyState) +{ + NS_ENSURE_ARG(aSecurityPropertyState); + *aSecurityPropertyState = mHSTSState; + return NS_OK; +} + +NS_IMETHODIMP +SiteHSTSState::GetIncludeSubdomains(bool* aIncludeSubdomains) +{ + NS_ENSURE_ARG(aIncludeSubdomains); + *aIncludeSubdomains = mHSTSIncludeSubdomains; + return NS_OK; +} + //////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(SiteHPKPState, nsISiteSecurityState, nsISiteHPKPState) + static bool stringIsBase64EncodingOf256bitValue(nsCString& encodedString) { nsAutoCString binaryValue; @@ -116,8 +164,10 @@ SiteHPKPState::SiteHPKPState() { } -SiteHPKPState::SiteHPKPState(nsCString& aStateString) - : mExpireTime(0) +SiteHPKPState::SiteHPKPState(const nsCString& aHost, + const nsCString& aStateString) + : mHostname(aHost) + , mExpireTime(0) , mState(SecurityPropertyUnset) , mIncludeSubdomains(false) { @@ -177,17 +227,50 @@ SiteHPKPState::SiteHPKPState(nsCString& aStateString) } } -SiteHPKPState::SiteHPKPState(PRTime aExpireTime, +SiteHPKPState::SiteHPKPState(const nsCString& aHost, + PRTime aExpireTime, SecurityPropertyState aState, bool aIncludeSubdomains, nsTArray& aSHA256keys) - : mExpireTime(aExpireTime) + : mHostname(aHost) + , mExpireTime(aExpireTime) , mState(aState) , mIncludeSubdomains(aIncludeSubdomains) , mSHA256keys(aSHA256keys) { } +NS_IMETHODIMP +SiteHPKPState::GetHostname(nsACString& aHostname) +{ + aHostname = mHostname; + return NS_OK; +} + +NS_IMETHODIMP +SiteHPKPState::GetExpireTime(int64_t* aExpireTime) +{ + NS_ENSURE_ARG(aExpireTime); + *aExpireTime = mExpireTime; + return NS_OK; +} + +NS_IMETHODIMP +SiteHPKPState::GetSecurityPropertyState(int16_t* aSecurityPropertyState) +{ + NS_ENSURE_ARG(aSecurityPropertyState); + *aSecurityPropertyState = mState; + return NS_OK; +} + +NS_IMETHODIMP +SiteHPKPState::GetIncludeSubdomains(bool* aIncludeSubdomains) +{ + NS_ENSURE_ARG(aIncludeSubdomains); + *aIncludeSubdomains = mIncludeSubdomains; + return NS_OK; +} + void SiteHPKPState::ToString(nsCString& aString) { @@ -203,6 +286,25 @@ SiteHPKPState::ToString(nsCString& aString) } } +NS_IMETHODIMP +SiteHPKPState::GetSha256Keys(nsISimpleEnumerator** aSha256Keys) +{ + NS_ENSURE_ARG(aSha256Keys); + + nsCOMArray keys; + for (const nsCString& key : mSHA256keys) { + nsCOMPtr variant = new nsVariant(); + nsresult rv = variant->SetAsAUTF8String(key); + if (NS_FAILED(rv)) { + return rv; + } + if (!keys.AppendObject(variant)) { + return NS_ERROR_FAILURE; + } + } + return NS_NewArrayEnumerator(aSha256Keys, keys); +} + //////////////////////////////////////////////////////////////////////////////// const uint64_t kSixtyDaysInSeconds = 60 * 24 * 60 * 60; @@ -299,10 +401,10 @@ SetStorageKey(nsAutoCString& storageKey, const nsACString& hostname, uint32_t aT storageKey = hostname; switch (aType) { case nsISiteSecurityService::HEADER_HSTS: - storageKey.AppendLiteral(":HSTS"); + storageKey.AppendASCII(kHSTSKeySuffix); break; case nsISiteSecurityService::HEADER_HPKP: - storageKey.AppendLiteral(":HPKP"); + storageKey.AppendASCII(kHPKPKeySuffix); break; default: MOZ_ASSERT_UNREACHABLE("SSS:SetStorageKey got invalid type"); @@ -338,9 +440,10 @@ nsSiteSecurityService::SetHSTSState(uint32_t aType, "HSTS State must be SecurityPropertySet or SecurityPropertyNegative"); int64_t expiretime = ExpireTimeFromMaxAge(maxage); - SiteHSTSState siteState(expiretime, aHSTSState, includeSubdomains); + RefPtr siteState = + new SiteHSTSState(hostname, expiretime, aHSTSState, includeSubdomains); nsAutoCString stateString; - siteState.ToString(stateString); + siteState->ToString(stateString); SSSLOG(("SSS: setting state for %s", hostname.get())); bool isPrivate = flags & nsISocketProvider::NO_PERMANENT_STORAGE; mozilla::DataStorageType storageType = isPrivate @@ -398,13 +501,14 @@ nsSiteSecurityService::RemoveStateInternal(uint32_t aType, nsCString value = mPreloadStateStorage->Get(storageKey, mozilla::DataStorage_Persistent); - SiteHSTSState dynamicState(value); + RefPtr dynamicState = new SiteHSTSState(aHost, value); if (GetPreloadListEntry(aHost.get()) || - dynamicState.mHSTSState != SecurityPropertyUnset) { + dynamicState->mHSTSState != SecurityPropertyUnset) { SSSLOG(("SSS: storing knockout entry for %s", aHost.get())); - SiteHSTSState siteState(0, SecurityPropertyKnockout, false); + RefPtr siteState = + new SiteHSTSState(aHost, 0, SecurityPropertyKnockout, false); nsAutoCString stateString; - siteState.ToString(stateString); + siteState->ToString(stateString); nsresult rv; if (aIsPreload) { rv = mPreloadStateStorage->Put(storageKey, stateString, @@ -852,12 +956,13 @@ nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI, } int64_t expireTime = ExpireTimeFromMaxAge(maxAge); - SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet, - foundIncludeSubdomains, sha256keys); + RefPtr dynamicEntry = + new SiteHPKPState(host, expireTime, SecurityPropertySet, + foundIncludeSubdomains, sha256keys); SSSLOG(("SSS: about to set pins for %s, expires=%ld now=%ld maxAge=%lu\n", host.get(), expireTime, PR_Now() / PR_USEC_PER_MSEC, maxAge)); - rv = SetHPKPState(host.get(), dynamicEntry, aFlags, false); + rv = SetHPKPState(host.get(), *dynamicEntry, aFlags, false); if (NS_FAILED(rv)) { SSSLOG(("SSS: failed to set pins for %s\n", host.get())); if (aFailureResult) { @@ -1028,20 +1133,20 @@ nsSiteSecurityService::HostHasHSTSEntry(const nsAutoCString& aHost, SSSLOG(("Seeking HSTS entry for %s", aHost.get())); SetStorageKey(storageKey, aHost, nsISiteSecurityService::HEADER_HSTS); nsCString value = mSiteStateStorage->Get(storageKey, storageType); - SiteHSTSState siteState(value); - if (siteState.mHSTSState != SecurityPropertyUnset) { + RefPtr siteState = new SiteHSTSState(aHost, value); + if (siteState->mHSTSState != SecurityPropertyUnset) { SSSLOG(("Found HSTS entry for %s", aHost.get())); - bool expired = siteState.IsExpired(nsISiteSecurityService::HEADER_HSTS); + bool expired = siteState->IsExpired(nsISiteSecurityService::HEADER_HSTS); if (!expired) { SSSLOG(("Entry for %s is not expired", aHost.get())); if (aCached) { *aCached = true; } - if (siteState.mHSTSState == SecurityPropertySet) { - *aResult = aRequireIncludeSubdomains ? siteState.mHSTSIncludeSubdomains + if (siteState->mHSTSState == SecurityPropertySet) { + *aResult = aRequireIncludeSubdomains ? siteState->mHSTSIncludeSubdomains : true; return true; - } else if (siteState.mHSTSState == SecurityPropertyNegative) { + } else if (siteState->mHSTSState == SecurityPropertyNegative) { *aResult = false; return true; } @@ -1054,8 +1159,8 @@ nsSiteSecurityService::HostHasHSTSEntry(const nsAutoCString& aHost, // First, check the dynamic preload list. value = mPreloadStateStorage->Get(storageKey, mozilla::DataStorage_Persistent); - SiteHSTSState dynamicState(value); - if (dynamicState.mHSTSState == SecurityPropertyUnset) { + RefPtr dynamicState = new SiteHSTSState(aHost, value); + if (dynamicState->mHSTSState == SecurityPropertyUnset) { SSSLOG(("No dynamic preload - checking for static preload")); // Now check the static preload list. if (!GetPreloadListEntry(aHost.get())) { @@ -1070,16 +1175,16 @@ nsSiteSecurityService::HostHasHSTSEntry(const nsAutoCString& aHost, // Next, look in the dynamic preload list. value = mPreloadStateStorage->Get(storageKey, mozilla::DataStorage_Persistent); - SiteHSTSState dynamicState(value); - if (dynamicState.mHSTSState != SecurityPropertyUnset) { + RefPtr dynamicState = new SiteHSTSState(aHost, value); + if (dynamicState->mHSTSState != SecurityPropertyUnset) { SSSLOG(("Found dynamic preload entry for %s", aHost.get())); - bool expired = dynamicState.IsExpired(nsISiteSecurityService::HEADER_HSTS); + bool expired = dynamicState->IsExpired(nsISiteSecurityService::HEADER_HSTS); if (!expired) { - if (dynamicState.mHSTSState == SecurityPropertySet) { - *aResult = aRequireIncludeSubdomains ? dynamicState.mHSTSIncludeSubdomains + if (dynamicState->mHSTSState == SecurityPropertySet) { + *aResult = aRequireIncludeSubdomains ? dynamicState->mHSTSIncludeSubdomains : true; return true; - } else if (dynamicState.mHSTSState == SecurityPropertyNegative) { + } else if (dynamicState->mHSTSState == SecurityPropertyNegative) { *aResult = false; return true; } @@ -1097,8 +1202,8 @@ nsSiteSecurityService::HostHasHSTSEntry(const nsAutoCString& aHost, const nsSTSPreload* preload = nullptr; // Finally look in the static preload list. - if (siteState.mHSTSState == SecurityPropertyUnset && - dynamicState.mHSTSState == SecurityPropertyUnset && + if (siteState->mHSTSState == SecurityPropertyUnset && + dynamicState->mHSTSState == SecurityPropertyUnset && (preload = GetPreloadListEntry(aHost.get())) != nullptr) { SSSLOG(("%s is a preloaded HSTS host", aHost.get())); *aResult = aRequireIncludeSubdomains ? preload->mIncludeSubdomains @@ -1264,17 +1369,17 @@ nsSiteSecurityService::GetKeyPinsForHostname(const nsACString& aHostname, nsCString value = mSiteStateStorage->Get(storageKey, storageType); // decode now - SiteHPKPState foundEntry(value); - if (entryStateNotOK(foundEntry, aEvalTime)) { + RefPtr foundEntry = new SiteHPKPState(host, value); + if (entryStateNotOK(*foundEntry, aEvalTime)) { // not in permanent storage, try now private value = mSiteStateStorage->Get(storageKey, mozilla::DataStorage_Private); - SiteHPKPState privateEntry(value); - if (entryStateNotOK(privateEntry, aEvalTime)) { + RefPtr privateEntry = new SiteHPKPState(host, value); + if (entryStateNotOK(*privateEntry, aEvalTime)) { // not in private storage, try dynamic preload value = mPreloadStateStorage->Get(storageKey, mozilla::DataStorage_Persistent); - SiteHPKPState preloadEntry(value); - if (entryStateNotOK(preloadEntry, aEvalTime)) { + RefPtr preloadEntry = new SiteHPKPState(host, value); + if (entryStateNotOK(*preloadEntry, aEvalTime)) { return NS_OK; } foundEntry = preloadEntry; @@ -1282,8 +1387,8 @@ nsSiteSecurityService::GetKeyPinsForHostname(const nsACString& aHostname, foundEntry = privateEntry; } } - pinArray = foundEntry.mSHA256keys; - *aIncludeSubdomains = foundEntry.mIncludeSubdomains; + pinArray = foundEntry->mSHA256keys; + *aIncludeSubdomains = foundEntry->mIncludeSubdomains; *afound = true; return NS_OK; } @@ -1316,13 +1421,14 @@ nsSiteSecurityService::SetKeyPins(const nsACString& aHost, } sha256keys.AppendElement(pin); } - SiteHPKPState dynamicEntry(aExpires, SecurityPropertySet, - aIncludeSubdomains, sha256keys); // we always store data in permanent storage (ie no flags) const nsCString& flatHost = PromiseFlatCString(aHost); nsAutoCString host( PublicKeyPinningService::CanonicalizeHostname(flatHost.get())); - return SetHPKPState(host.get(), dynamicEntry, 0, aIsPreload); + RefPtr dynamicEntry = + new SiteHPKPState(host, aExpires, SecurityPropertySet, aIncludeSubdomains, + sha256keys); + return SetHPKPState(host.get(), *dynamicEntry, 0, aIsPreload); } NS_IMETHODIMP @@ -1374,6 +1480,55 @@ nsSiteSecurityService::SetHPKPState(const char* aHost, SiteHPKPState& entry, return NS_OK; } +NS_IMETHODIMP +nsSiteSecurityService::Enumerate(uint32_t aType, + nsISimpleEnumerator** aEnumerator) +{ + NS_ENSURE_ARG(aEnumerator); + + nsAutoCString keySuffix; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + keySuffix.AssignASCII(kHSTSKeySuffix); + break; + case nsISiteSecurityService::HEADER_HPKP: + keySuffix.AssignASCII(kHPKPKeySuffix); + break; + default: + return NS_ERROR_INVALID_ARG; + } + + InfallibleTArray items; + mSiteStateStorage->GetAll(&items); + + nsCOMArray states; + for (const mozilla::dom::DataStorageItem& item : items) { + if (!StringEndsWith(item.key(), keySuffix)) { + // The key does not end with correct suffix, so is not the type we want. + continue; + } + + nsCString hostname( + StringHead(item.key(), item.key().Length() - keySuffix.Length())); + nsCOMPtr state; + switch(aType) { + case nsISiteSecurityService::HEADER_HSTS: + state = new SiteHSTSState(hostname, item.value()); + break; + case nsISiteSecurityService::HEADER_HPKP: + state = new SiteHPKPState(hostname, item.value()); + break; + default: + MOZ_ASSERT_UNREACHABLE("SSS:Enumerate got invalid type"); + } + + states.AppendObject(state); + } + + NS_NewArrayEnumerator(aEnumerator, states); + return NS_OK; +} + //------------------------------------------------------------ // nsSiteSecurityService::nsIObserver //------------------------------------------------------------ diff --git a/security/manager/ssl/nsSiteSecurityService.h b/security/manager/ssl/nsSiteSecurityService.h index 6b20a64a9ce6..dc2167091024 100644 --- a/security/manager/ssl/nsSiteSecurityService.h +++ b/security/manager/ssl/nsSiteSecurityService.h @@ -6,6 +6,7 @@ #define __nsSiteSecurityService_h__ #include "mozilla/DataStorage.h" +#include "mozilla/RefPtr.h" #include "nsCOMPtr.h" #include "nsIObserver.h" #include "nsISiteSecurityService.h" @@ -32,29 +33,36 @@ class nsISSLStatus; * in question. */ enum SecurityPropertyState { - SecurityPropertyUnset = 0, - SecurityPropertySet = 1, - SecurityPropertyKnockout = 2, - SecurityPropertyNegative = 3, + SecurityPropertyUnset = nsISiteSecurityState::SECURITY_PROPERTY_UNSET, + SecurityPropertySet = nsISiteSecurityState::SECURITY_PROPERTY_SET, + SecurityPropertyKnockout = nsISiteSecurityState::SECURITY_PROPERTY_KNOCKOUT, + SecurityPropertyNegative = nsISiteSecurityState::SECURITY_PROPERTY_NEGATIVE, }; /** * SiteHPKPState: A utility class that encodes/decodes a string describing * the public key pins of a site. * HPKP state consists of: + * - Hostname (nsCString) * - Expiry time (PRTime (aka int64_t) in milliseconds) * - A state flag (SecurityPropertyState, default SecurityPropertyUnset) * - An include subdomains flag (bool, default false) * - An array of sha-256 hashed base 64 encoded fingerprints of required keys */ -class SiteHPKPState +class SiteHPKPState : public nsISiteHPKPState { public: - SiteHPKPState(); - explicit SiteHPKPState(nsCString& aStateString); - SiteHPKPState(PRTime aExpireTime, SecurityPropertyState aState, - bool aIncludeSubdomains, nsTArray& SHA256keys); + NS_DECL_ISUPPORTS + NS_DECL_NSISITEHPKPSTATE + NS_DECL_NSISITESECURITYSTATE + SiteHPKPState(); + SiteHPKPState(const nsCString& aHost, const nsCString& aStateString); + SiteHPKPState(const nsCString& aHost, PRTime aExpireTime, + SecurityPropertyState aState, bool aIncludeSubdomains, + nsTArray& SHA256keys); + + nsCString mHostname; PRTime mExpireTime; SecurityPropertyState mState; bool mIncludeSubdomains; @@ -70,23 +78,32 @@ public: } void ToString(nsCString& aString); + +protected: + virtual ~SiteHPKPState() {}; }; /** * SiteHSTSState: A utility class that encodes/decodes a string describing * the security state of a site. Currently only handles HSTS. * HSTS state consists of: + * - Hostname (nsCString) * - Expiry time (PRTime (aka int64_t) in milliseconds) * - A state flag (SecurityPropertyState, default SecurityPropertyUnset) * - An include subdomains flag (bool, default false) */ -class SiteHSTSState +class SiteHSTSState : public nsISiteHSTSState { public: - explicit SiteHSTSState(nsCString& aStateString); - SiteHSTSState(PRTime aHSTSExpireTime, SecurityPropertyState aHSTSState, - bool aHSTSIncludeSubdomains); + NS_DECL_ISUPPORTS + NS_DECL_NSISITEHSTSSTATE + NS_DECL_NSISITESECURITYSTATE + SiteHSTSState(const nsCString& aHost, const nsCString& aStateString); + SiteHSTSState(const nsCString& aHost, PRTime aHSTSExpireTime, + SecurityPropertyState aHSTSState, bool aHSTSIncludeSubdomains); + + nsCString mHostname; PRTime mHSTSExpireTime; SecurityPropertyState mHSTSState; bool mHSTSIncludeSubdomains; @@ -108,6 +125,9 @@ public: } void ToString(nsCString &aString); + +protected: + virtual ~SiteHSTSState() {} }; struct nsSTSPreload; diff --git a/security/manager/ssl/tests/unit/test_sss_enumerate.js b/security/manager/ssl/tests/unit/test_sss_enumerate.js new file mode 100644 index 000000000000..659c98cac2ab --- /dev/null +++ b/security/manager/ssl/tests/unit/test_sss_enumerate.js @@ -0,0 +1,125 @@ +/* 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/. */ +"use strict"; + +// This had better not be larger than the maximum maxAge for HPKP. +const NON_ISSUED_KEY_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; +const PINNING_ROOT_KEY_HASH = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8="; +const KEY_HASHES = [ NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH ]; +const SECS_IN_A_WEEK = 7 * 24 * 60 * 60 * 1000; +const TESTCASES = [ + { + hostname: "a.pinning2.example.com", + includeSubdomains: true, + expireTime: Date.now() + 12 * SECS_IN_A_WEEK * 1000, + }, + { + hostname: "b.pinning2.example.com", + includeSubdomains: false, + expireTime: Date.now() + 13 * SECS_IN_A_WEEK * 1000, + }, +].sort((a, b) => a.expireTime - b.expireTime); + +do_register_cleanup(() => { + Services.prefs.clearUserPref( + "security.cert_pinning.process_headers_from_non_builtin_roots"); + Services.prefs.clearUserPref("security.cert_pinning.max_max_age_seconds"); +}); + +do_get_profile(); + +Services.prefs.setBoolPref( + "security.cert_pinning.process_headers_from_non_builtin_roots", true); +Services.prefs.setIntPref("security.cert_pinning.max_max_age_seconds", + 20 * SECS_IN_A_WEEK); + +let certdb = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); +addCertFromFile(certdb, "test_pinning_dynamic/pinningroot.pem", "CTu,CTu,CTu"); + +let sss = Cc["@mozilla.org/ssservice;1"].getService(Ci.nsISiteSecurityService); + +function insertEntries() { + for (let testcase of TESTCASES) { + let uri = Services.io.newURI("https://" + testcase.hostname); + let sslStatus = new FakeSSLStatus(constructCertFromFile( + `test_pinning_dynamic/${testcase.hostname}-pinningroot.pem`)); + // MaxAge is in seconds. + let maxAge = Math.round((testcase.expireTime - Date.now()) / 1000); + let header = `max-age=${maxAge}`; + if (testcase.includeSubdomains) { + header += "; includeSubdomains"; + } + sss.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS, uri, header, + sslStatus, 0); + for (let key of KEY_HASHES) { + header += `; pin-sha256="${key}"`; + } + sss.processHeader(Ci.nsISiteSecurityService.HEADER_HPKP, uri, header, + sslStatus, 0); + } +} + +function getEntries(type) { + let entryEnumerator = sss.enumerate(type); + let entries = []; + while (entryEnumerator.hasMoreElements()) { + let entry = entryEnumerator.getNext(); + entries.push(entry.QueryInterface(Ci.nsISiteSecurityState)); + } + return entries; +} + +function checkSiteSecurityStateAttrs(entries) { + entries.sort((a, b) => a.expireTime - b.expireTime); + equal(entries.length, TESTCASES.length, + "Should get correct number of entries"); + for (let i = 0; i < TESTCASES.length; i++) { + equal(entries[i].hostname, TESTCASES[i].hostname, "Hostnames should match"); + equal(entries[i].securityPropertyState, + Ci.nsISiteSecurityState.SECURITY_PROPERTY_SET, + "Entries should have security property set"); + equal(entries[i].includeSubdomains, TESTCASES[i].includeSubdomains, + "IncludeSubdomains should match"); + // There's a delay from our "now" and the "now" that the implementation uses. + less(Math.abs(entries[i].expireTime - TESTCASES[i].expireTime), 60000, + "ExpireTime should be within 60-second error"); + } +} + +function checkSha256Keys(hpkpEntries) { + for (let hpkpEntry of hpkpEntries) { + let enumerator = hpkpEntry.QueryInterface(Ci.nsISiteHPKPState).sha256Keys; + let keys = []; + while (enumerator.hasMoreElements()) { + keys.push(enumerator.getNext().QueryInterface(Ci.nsIVariant)); + } + equal(keys.length, KEY_HASHES.length, "Should get correct number of keys"); + keys.sort(); + for (let i = 0; i < KEY_HASHES.length; i++) { + equal(keys[i], KEY_HASHES[i], "Should get correct keys"); + } + } +} + +function run_test() { + sss.clearAll(); + + insertEntries(); + + let hstsEntries = getEntries(Ci.nsISiteSecurityService.HEADER_HSTS); + let hpkpEntries = getEntries(Ci.nsISiteSecurityService.HEADER_HPKP); + + checkSiteSecurityStateAttrs(hstsEntries); + checkSiteSecurityStateAttrs(hpkpEntries); + + checkSha256Keys(hpkpEntries); + + sss.clearAll(); + hstsEntries = getEntries(Ci.nsISiteSecurityService.HEADER_HSTS); + hpkpEntries = getEntries(Ci.nsISiteSecurityService.HEADER_HPKP); + + equal(hstsEntries.length, 0, "Should clear all HSTS entries"); + equal(hpkpEntries.length, 0, "Should clear all HPKP entries"); +} diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index 90798f1b8b7e..6d4924c7245a 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -124,6 +124,7 @@ run-sequentially = hardcoded ports [test_signed_apps-marketplace.js] [test_signed_dir.js] tags = addons psm +[test_sss_enumerate.js] [test_sss_eviction.js] [test_sss_readstate.js] [test_sss_readstate_child.js]