author | Camilo Viecco <cviecco@mozilla.com> |
Fri, 12 Sep 2014 14:59:37 -0700 | |
changeset 205893 | 605c11a57482fa7c59dde1ca52e1f2338aa85f37 |
parent 205892 | 495dcc54c2b25829e268197a54611a19098f3293 |
child 205894 | 7b3ba487c01b6c55a4d290b1f1ab826f42c37eec |
push id | 27507 |
push user | ryanvm@gmail.com |
push date | Thu, 18 Sep 2014 02:16:54 +0000 |
treeherder | mozilla-central@488d490da742 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | keeler |
bugs | 787133 |
milestone | 35.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/security/manager/boot/src/PublicKeyPinningService.cpp +++ b/security/manager/boot/src/PublicKeyPinningService.cpp @@ -4,17 +4,21 @@ #include "PublicKeyPinningService.h" #include "pkix/nullptr.h" #include "StaticHPKPins.h" // autogenerated by genHPKPStaticpins.js #include "cert.h" #include "mozilla/Base64.h" #include "mozilla/Telemetry.h" +#include "nsISiteSecurityService.h" +#include "nsServiceManagerUtils.h" +#include "nsSiteSecurityService.h" #include "nsString.h" +#include "nsTArray.h" #include "nssb64.h" #include "pkix/pkixtypes.h" #include "prlog.h" #include "ScopedNSSTypes.h" #include "seccomon.h" #include "sechash.h" using namespace mozilla; @@ -48,96 +52,112 @@ GetBase64HashSPKI(const CERTCertificate* if (NS_WARN_IF(NS_FAILED(rv))) { return SECFailure; } return SECSuccess; } /* * Returns true if a given cert matches any hashType fingerprints from the - * given pinset, false otherwise. + * given pinset or the dynamicFingeprints array, false otherwise. */ static bool EvalCertWithHashType(const CERTCertificate* cert, SECOidTag hashType, - const StaticFingerprints* fingerprints) + const StaticFingerprints* fingerprints, + const nsTArray<nsCString>* dynamicFingerprints) { - if (!fingerprints) { + if (!fingerprints && !dynamicFingerprints) { PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: No hashes found for hash type: %d\n", hashType)); return false; } nsAutoCString base64Out; SECStatus srv = GetBase64HashSPKI(cert, hashType, base64Out); if (srv != SECSuccess) { PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: GetBase64HashSPKI failed!\n")); return false; } - for (size_t i = 0; i < fingerprints->size; i++) { - if (base64Out.Equals(fingerprints->data[i])) { - PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, - ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); - return true; + if (fingerprints) { + for (size_t i = 0; i < fingerprints->size; i++) { + if (base64Out.Equals(fingerprints->data[i])) { + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, + ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); + return true; + } + } + } + if (dynamicFingerprints) { + for (size_t i = 0; i < dynamicFingerprints->Length(); i++) { + if (base64Out.Equals((*dynamicFingerprints)[i])) { + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, + ("pkpin: found pin base_64 ='%s'\n", base64Out.get())); + return true; + } } } return false; } /* * Returns true if a given chain matches any hashType fingerprints from the - * given pinset, false otherwise. + * given pinset or the dynamicFingerprints array, false otherwise. */ static bool EvalChainWithHashType(const CERTCertList* certList, SECOidTag hashType, - const StaticPinset* pinset) + const StaticPinset* pinset, + const nsTArray<nsCString>* dynamicFingerprints) { CERTCertificate* currentCert; const StaticFingerprints* fingerprints = nullptr; - if (hashType == SEC_OID_SHA256) { - fingerprints = pinset->sha256; - } else if (hashType == SEC_OID_SHA1) { - fingerprints = pinset->sha1; + if (pinset) { + if (hashType == SEC_OID_SHA256) { + fingerprints = pinset->sha256; + } else if (hashType == SEC_OID_SHA1) { + fingerprints = pinset->sha1; + } } - if (!fingerprints) { + if (!fingerprints && !dynamicFingerprints) { return false; } CERTCertListNode* node; for (node = CERT_LIST_HEAD(certList); !CERT_LIST_END(node, certList); node = CERT_LIST_NEXT(node)) { currentCert = node->cert; PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: certArray subject: '%s'\n", currentCert->subjectName)); PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: certArray common_name: '%s'\n", CERT_GetCommonName(&(currentCert->issuer)))); - if (EvalCertWithHashType(currentCert, hashType, fingerprints)) { + if (EvalCertWithHashType(currentCert, hashType, fingerprints, + dynamicFingerprints)) { return true; } } PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: no matches found\n")); return false; } /** * Given a pinset and certlist, return true if one of the certificates on * the list matches a fingerprint in the pinset, false otherwise. */ static bool EvalChainWithPinset(const CERTCertList* certList, const StaticPinset* pinset) { // SHA256 is more trustworthy, try that first. - if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset)) { + if (EvalChainWithHashType(certList, SEC_OID_SHA256, pinset, nullptr)) { return true; } - return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset); + return EvalChainWithHashType(certList, SEC_OID_SHA1, pinset, nullptr); } /** Comparator for the is public key pinned host. */ static int TransportSecurityPreloadCompare(const void *key, const void *entry) { const char *keyStr = reinterpret_cast<const char *>(key); @@ -147,32 +167,55 @@ TransportSecurityPreloadCompare(const vo return strcmp(keyStr, preloadEntry->mHost); } /** * Check PKPins on the given certlist against the specified hostname */ static bool CheckPinsForHostname(const CERTCertList *certList, const char *hostname, - bool enforceTestMode) + bool enforceTestMode, mozilla::pkix::Time time) { if (!certList) { return false; } if (!hostname || hostname[0] == 0) { return false; } + nsCOMPtr<nsISiteSecurityService> sssService = + do_GetService(NS_SSSERVICE_CONTRACTID); + if (!sssService) { + return false; + } + SiteHPKPState dynamicEntry; TransportSecurityPreload *foundEntry = nullptr; char *evalHost = const_cast<char*>(hostname); char *evalPart; // Notice how the (xx = strchr) prevents pins for unqualified domain names. while (!foundEntry && (evalPart = strchr(evalHost, '.'))) { PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: Querying pinsets for host: '%s'\n", evalHost)); + // Attempt dynamic pins first + nsresult rv; + bool found; + bool includeSubdomains; + nsTArray<nsCString> pinArray; + rv = sssService->GetKeyPinsForHostname(evalHost, time, pinArray, + &includeSubdomains, &found); + if (NS_FAILED(rv)) { + return false; + } + if (found && (evalHost == hostname || includeSubdomains)) { + PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, + ("pkpin: Found dyn match for host: '%s'\n", evalHost)); + return EvalChainWithHashType(certList, SEC_OID_SHA256, nullptr, + &pinArray); + } + foundEntry = (TransportSecurityPreload *)bsearch(evalHost, kPublicKeyPinningPreloadList, sizeof(kPublicKeyPinningPreloadList) / sizeof(TransportSecurityPreload), sizeof(TransportSecurityPreload), TransportSecurityPreloadCompare); if (foundEntry) { PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: Found pinset for host: '%s'\n", evalHost)); @@ -186,16 +229,20 @@ CheckPinsForHostname(const CERTCertList PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: Didn't find pinset for host: '%s'\n", evalHost)); } // Add one for '.' evalHost = evalPart + 1; } if (foundEntry && foundEntry->pinset) { + if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime / + PR_USEC_PER_SEC)) { + return true; + } bool result = EvalChainWithPinset(certList, foundEntry->pinset); bool retval = result; Telemetry::ID histogram = foundEntry->mIsMoz ? Telemetry::CERT_PINNING_MOZ_RESULTS : Telemetry::CERT_PINNING_RESULTS; if (foundEntry->mTestMode) { histogram = foundEntry->mIsMoz ? Telemetry::CERT_PINNING_MOZ_TEST_RESULTS @@ -226,17 +273,18 @@ CheckPinsForHostname(const CERTCertList } /** * Extract all the DNS names for a host (including CN) and evaluate the * certifiate pins against all of them (Currently is an OR so we stop * evaluating at the first OK pin). */ static bool -CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode) +CheckChainAgainstAllNames(const CERTCertList* certList, bool enforceTestMode, + mozilla::pkix::Time time) { PR_LOG(gPublicKeyPinningLog, PR_LOG_DEBUG, ("pkpin: top of checkChainAgainstAllNames")); CERTCertListNode* node = CERT_LIST_HEAD(certList); if (!node) { return false; } CERTCertificate* cert = node->cert; @@ -271,17 +319,17 @@ CheckChainAgainstAllNames(const CERTCert // null terminated. hostName[currentName->name.other.len] = 0; memcpy(hostName, currentName->name.other.data, currentName->name.other.len); if (!hostName[0]) { // cannot call CheckPinsForHostname on empty or null hostname break; } - if (CheckPinsForHostname(certList, hostName, enforceTestMode)) { + if (CheckPinsForHostname(certList, hostName, enforceTestMode, time)) { hasValidPins = true; break; } } currentName = CERT_GetNextGeneralName(currentName); } while (currentName != nameList); return hasValidPins; @@ -291,17 +339,13 @@ bool PublicKeyPinningService::ChainHasValidPins(const CERTCertList* certList, const char* hostname, mozilla::pkix::Time time, bool enforceTestMode) { if (!certList) { return false; } - if (time > TimeFromEpochInSeconds(kPreloadPKPinsExpirationTime / - PR_USEC_PER_SEC)) { - return true; + if (!hostname || hostname[0] == 0) { + return CheckChainAgainstAllNames(certList, enforceTestMode, time); } - if (!hostname || hostname[0] == 0) { - return CheckChainAgainstAllNames(certList, enforceTestMode); - } - return CheckPinsForHostname(certList, hostname, enforceTestMode); + return CheckPinsForHostname(certList, hostname, enforceTestMode, time); }
--- a/security/manager/boot/src/nsSiteSecurityService.cpp +++ b/security/manager/boot/src/nsSiteSecurityService.cpp @@ -1,56 +1,64 @@ /* 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/. */ #include "nsSiteSecurityService.h" #include "mozilla/LinkedList.h" #include "mozilla/Preferences.h" +#include "mozilla/Base64.h" +#include "base64.h" #include "nsCRTGlue.h" #include "nsISSLStatus.h" #include "nsISSLStatusProvider.h" #include "nsISocketProvider.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsSecurityHeaderParser.h" #include "nsString.h" #include "nsThreadUtils.h" +#include "pkix/pkixtypes.h" #include "plstr.h" #include "prlog.h" #include "prnetdb.h" #include "prprf.h" +#include "PublicKeyPinningService.h" +#include "ScopedNSSTypes.h" #include "nsXULAppAPI.h" // A note about the preload list: // When a site specifically disables HSTS by sending a header with // 'max-age: 0', we keep a "knockout" value that means "we have no information // regarding the HSTS state of this host" (any ancestor of "this host" can still // influence its HSTS status via include subdomains, however). // This prevents the preload list from overriding the site's current // desired HSTS status. #include "nsSTSPreloadList.inc" +using namespace mozilla; +using namespace mozilla::psm; + #if defined(PR_LOGGING) static PRLogModuleInfo * GetSSSLog() { static PRLogModuleInfo *gSSSLog; if (!gSSSLog) gSSSLog = PR_NewLogModule("nsSSService"); return gSSSLog; } #endif #define SSSLOG(args) PR_LOG(GetSSSLog(), 4, args) //////////////////////////////////////////////////////////////////////////////// -SiteSecurityState::SiteSecurityState(nsCString& aStateString) +SiteHSTSState::SiteHSTSState(nsCString& aStateString) : mHSTSExpireTime(0) , mHSTSState(SecurityPropertyUnset) , mHSTSIncludeSubdomains(false) { uint32_t hstsState = 0; uint32_t hstsIncludeSubdomains = 0; // PR_sscanf doesn't handle bools. int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu", &mHSTSExpireTime, &hstsState, @@ -59,45 +67,153 @@ SiteSecurityState::SiteSecurityState(nsC (hstsIncludeSubdomains == 0 || hstsIncludeSubdomains == 1) && ((SecurityPropertyState)hstsState == SecurityPropertyUnset || (SecurityPropertyState)hstsState == SecurityPropertySet || (SecurityPropertyState)hstsState == SecurityPropertyKnockout)); if (valid) { mHSTSState = (SecurityPropertyState)hstsState; mHSTSIncludeSubdomains = (hstsIncludeSubdomains == 1); } else { - SSSLOG(("%s is not a valid SiteSecurityState", aStateString.get())); + SSSLOG(("%s is not a valid SiteHSTSState", aStateString.get())); mHSTSExpireTime = 0; mHSTSState = SecurityPropertyUnset; mHSTSIncludeSubdomains = false; } } -SiteSecurityState::SiteSecurityState(PRTime aHSTSExpireTime, - SecurityPropertyState aHSTSState, - bool aHSTSIncludeSubdomains) +SiteHSTSState::SiteHSTSState(PRTime aHSTSExpireTime, + SecurityPropertyState aHSTSState, + bool aHSTSIncludeSubdomains) : mHSTSExpireTime(aHSTSExpireTime) , mHSTSState(aHSTSState) , mHSTSIncludeSubdomains(aHSTSIncludeSubdomains) { } void -SiteSecurityState::ToString(nsCString& aString) +SiteHSTSState::ToString(nsCString& aString) { aString.Truncate(); aString.AppendInt(mHSTSExpireTime); aString.Append(','); aString.AppendInt(mHSTSState); aString.Append(','); aString.AppendInt(static_cast<uint32_t>(mHSTSIncludeSubdomains)); } //////////////////////////////////////////////////////////////////////////////// +static bool +stringIsBase64EncodingOf256bitValue(nsCString& encodedString) { + nsAutoCString binaryValue; + nsresult rv = mozilla::Base64Decode(encodedString, binaryValue); + if (NS_FAILED(rv)) { + return false; + } + if (binaryValue.Length() != SHA256_LENGTH) { + return false; + } + return true; +} + +SiteHPKPState::SiteHPKPState() + : mExpireTime(0) + , mState(SecurityPropertyUnset) + , mIncludeSubdomains(false) +{ +} + +SiteHPKPState::SiteHPKPState(nsCString& aStateString) + : mExpireTime(0) + , mState(SecurityPropertyUnset) + , mIncludeSubdomains(false) +{ + uint32_t hpkpState = 0; + uint32_t hpkpIncludeSubdomains = 0; // PR_sscanf doesn't handle bools. + const uint32_t MaxMergedHPKPPinSize = 1024; + char mergedHPKPins[MaxMergedHPKPPinSize]; + memset(mergedHPKPins, 0, MaxMergedHPKPPinSize); + + if (aStateString.Length() >= MaxMergedHPKPPinSize) { + SSSLOG(("SSS: Cannot parse PKPState string, too large\n")); + return; + } + + int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu,%s", + &mExpireTime, &hpkpState, + &hpkpIncludeSubdomains, mergedHPKPins); + bool valid = (matches == 4 && + (hpkpIncludeSubdomains == 0 || hpkpIncludeSubdomains == 1) && + ((SecurityPropertyState)hpkpState == SecurityPropertyUnset || + (SecurityPropertyState)hpkpState == SecurityPropertySet || + (SecurityPropertyState)hpkpState == SecurityPropertyKnockout)); + + SSSLOG(("SSS: loading SiteHPKPState matches=%d\n", matches)); + const uint32_t SHA256Base64Len = 44; + + if (valid && (SecurityPropertyState)hpkpState == SecurityPropertySet) { + // try to expand the merged PKPins + const char* cur = mergedHPKPins; + nsAutoCString pin; + uint32_t collectedLen = 0; + mergedHPKPins[MaxMergedHPKPPinSize - 1] = 0; + size_t totalLen = strlen(mergedHPKPins); + while (collectedLen + SHA256Base64Len <= totalLen) { + pin.Assign(cur, SHA256Base64Len); + if (stringIsBase64EncodingOf256bitValue(pin)) { + mSHA256keys.AppendElement(pin); + } + cur += SHA256Base64Len; + collectedLen += SHA256Base64Len; + } + if (mSHA256keys.IsEmpty()) { + valid = false; + } + } + if (valid) { + mState = (SecurityPropertyState)hpkpState; + mIncludeSubdomains = (hpkpIncludeSubdomains == 1); + } else { + SSSLOG(("%s is not a valid SiteHPKPState", aStateString.get())); + mExpireTime = 0; + mState = SecurityPropertyUnset; + mIncludeSubdomains = false; + if (!mSHA256keys.IsEmpty()) { + mSHA256keys.Clear(); + } + } +} + +SiteHPKPState::SiteHPKPState(PRTime aExpireTime, + SecurityPropertyState aState, + bool aIncludeSubdomains, + nsTArray<nsCString>& aSHA256keys) + : mExpireTime(aExpireTime) + , mState(aState) + , mIncludeSubdomains(aIncludeSubdomains) + , mSHA256keys(aSHA256keys) +{ +} + +void +SiteHPKPState::ToString(nsCString& aString) +{ + aString.Truncate(); + aString.AppendInt(mExpireTime); + aString.Append(','); + aString.AppendInt(mState); + aString.Append(','); + aString.AppendInt(static_cast<uint32_t>(mIncludeSubdomains)); + aString.Append(','); + for (unsigned int i = 0; i < mSHA256keys.Length(); i++) { + aString.Append(mSHA256keys[i]); + } +} + +//////////////////////////////////////////////////////////////////////////////// nsSiteSecurityService::nsSiteSecurityService() : mUsePreloadList(true) , mPreloadListTimeOffset(0) { } nsSiteSecurityService::~nsSiteSecurityService() @@ -156,47 +272,64 @@ nsSiteSecurityService::GetHost(nsIURI *a nsresult rv = innerURI->GetAsciiHost(aResult); if (NS_FAILED(rv) || aResult.IsEmpty()) return NS_ERROR_UNEXPECTED; return NS_OK; } +static void +SetStorageKey(nsAutoCString& storageKey, nsCString& hostname, uint32_t aType) +{ + storageKey = hostname; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + storageKey.AppendLiteral(":HSTS"); + break; + case nsISiteSecurityService::HEADER_HPKP: + storageKey.AppendLiteral(":HPKP"); + break; + default: + NS_ASSERTION(false, "SSS:SetStorageKey got invalid type"); + } +} + nsresult -nsSiteSecurityService::SetState(uint32_t aType, - nsIURI* aSourceURI, - int64_t maxage, - bool includeSubdomains, - uint32_t flags) +nsSiteSecurityService::SetHSTSState(uint32_t aType, + nsIURI* aSourceURI, + int64_t maxage, + bool includeSubdomains, + uint32_t flags) { // If max-age is zero, that's an indication to immediately remove the // security state, so here's a shortcut. if (!maxage) { return RemoveState(aType, aSourceURI, flags); } // 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() / PR_USEC_PER_MSEC) + (maxage * PR_MSEC_PER_SEC); - SiteSecurityState siteState(expiretime, SecurityPropertySet, - includeSubdomains); + SiteHSTSState siteState(expiretime, SecurityPropertySet, includeSubdomains); nsAutoCString stateString; siteState.ToString(stateString); nsAutoCString hostname; nsresult rv = GetHost(aSourceURI, hostname); NS_ENSURE_SUCCESS(rv, rv); SSSLOG(("SSS: setting state for %s", hostname.get())); bool isPrivate = flags & nsISocketProvider::NO_PERMANENT_STORAGE; mozilla::DataStorageType storageType = isPrivate ? mozilla::DataStorage_Private : mozilla::DataStorage_Persistent; - rv = mSiteStateStorage->Put(hostname, stateString, storageType); + nsAutoCString storageKey; + SetStorageKey(storageKey, hostname, aType); + rv = mSiteStateStorage->Put(storageKey, stateString, storageType); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } NS_IMETHODIMP nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI, uint32_t aFlags) @@ -211,24 +344,28 @@ nsSiteSecurityService::RemoveState(uint3 bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE; mozilla::DataStorageType storageType = isPrivate ? mozilla::DataStorage_Private : mozilla::DataStorage_Persistent; // If this host is in the preload list, we have to store a knockout entry. if (GetPreloadListEntry(hostname.get())) { SSSLOG(("SSS: storing knockout entry for %s", hostname.get())); - SiteSecurityState siteState(0, SecurityPropertyKnockout, false); + SiteHSTSState siteState(0, SecurityPropertyKnockout, false); nsAutoCString stateString; siteState.ToString(stateString); - rv = mSiteStateStorage->Put(hostname, stateString, storageType); + nsAutoCString storageKey; + SetStorageKey(storageKey, hostname, aType); + rv = mSiteStateStorage->Put(storageKey, stateString, storageType); NS_ENSURE_SUCCESS(rv, rv); } else { SSSLOG(("SSS: removing entry for %s", hostname.get())); - mSiteStateStorage->Remove(hostname, storageType); + nsAutoCString storageKey; + SetStorageKey(storageKey, hostname, aType); + mSiteStateStorage->Remove(storageKey, storageType); } return NS_OK; } static bool HostIsIPAddress(const char *hostname) { @@ -371,17 +508,17 @@ nsSiteSecurityService::ProcessHeaderMuta // after processing all the directives, make sure we came across max-age // somewhere. if (!foundMaxAge) { SSSLOG(("SSS: did not encounter required max-age directive")); return NS_ERROR_FAILURE; } // record the successfully parsed header data. - SetState(aType, aSourceURI, maxAge, foundIncludeSubdomains, aFlags); + SetHSTSState(aType, aSourceURI, maxAge, foundIncludeSubdomains, aFlags); if (aMaxAge != nullptr) { *aMaxAge = (uint64_t)maxAge; } if (aIncludeSubdomains != nullptr) { *aIncludeSubdomains = foundIncludeSubdomains; } @@ -434,28 +571,43 @@ nsSiteSecurityService::GetPreloadListEnt return nullptr; } NS_IMETHODIMP nsSiteSecurityService::IsSecureHost(uint32_t aType, const char* aHost, uint32_t aFlags, bool* aResult) { - // Only HSTS is supported at the moment. - NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS, + // Only HSTS and HPKP are supported at the moment. + NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS || + aType == nsISiteSecurityService::HEADER_HPKP, NS_ERROR_NOT_IMPLEMENTED); // set default in case if we can't find any STS information *aResult = false; /* An IP address never qualifies as a secure URI. */ if (HostIsIPAddress(aHost)) { return NS_OK; } + if (aType == nsISiteSecurityService::HEADER_HPKP) { + ScopedCERTCertList certList(CERT_NewCertList()); + if (!certList) { + return NS_ERROR_FAILURE; + } + // Todo: we need to update ChainHasValidPins to distinguish between there + // are no pins or there was an internal failure. + *aResult = !PublicKeyPinningService::ChainHasValidPins(certList, + aHost, + mozilla::pkix::Now(), + false); + return NS_OK; + } + // Holepunch chart.apis.google.com and subdomains. nsAutoCString host(aHost); ToLowerCase(host); if (host.EqualsLiteral("chart.apis.google.com") || StringEndsWith(host, NS_LITERAL_CSTRING(".chart.apis.google.com"))) { return NS_OK; } @@ -467,29 +619,31 @@ nsSiteSecurityService::IsSecureHost(uint // 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 security status of this host". bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE; mozilla::DataStorageType storageType = isPrivate ? mozilla::DataStorage_Private : mozilla::DataStorage_Persistent; - nsCString value = mSiteStateStorage->Get(host, storageType); - SiteSecurityState siteState(value); + nsAutoCString storageKey; + SetStorageKey(storageKey, host, aType); + nsCString value = mSiteStateStorage->Get(storageKey, storageType); + SiteHSTSState siteState(value); if (siteState.mHSTSState != SecurityPropertyUnset) { SSSLOG(("Found entry for %s", host.get())); bool expired = siteState.IsExpired(aType); if (!expired && siteState.mHSTSState == SecurityPropertySet) { *aResult = true; return NS_OK; } // If the entry is expired and not in the preload list, we can remove it. if (expired && !GetPreloadListEntry(host.get())) { - mSiteStateStorage->Remove(host, storageType); + mSiteStateStorage->Remove(storageKey, storageType); } } // Finally look in the preloaded list. This is the exact host, // so if an entry exists at all, this host is HSTS. else if (GetPreloadListEntry(host.get())) { SSSLOG(("%s is a preloaded STS host", host.get())); *aResult = true; return NS_OK; @@ -512,29 +666,31 @@ nsSiteSecurityService::IsSecureHost(uint // 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 flag (although we still have to check for a // SecurityPropertySet flag first to check that this is a secure 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). nsCString subdomainString(subdomain); - value = mSiteStateStorage->Get(subdomainString, storageType); - SiteSecurityState siteState(value); + nsAutoCString storageKey; + SetStorageKey(storageKey, subdomainString, aType); + value = mSiteStateStorage->Get(storageKey, storageType); + SiteHSTSState siteState(value); if (siteState.mHSTSState != SecurityPropertyUnset) { SSSLOG(("Found entry for %s", subdomain)); bool expired = siteState.IsExpired(aType); if (!expired && siteState.mHSTSState == SecurityPropertySet) { *aResult = siteState.mHSTSIncludeSubdomains; break; } // If the entry is expired and not in the preload list, we can remove it. if (expired && !GetPreloadListEntry(subdomain)) { - mSiteStateStorage->Remove(subdomainString, storageType); + mSiteStateStorage->Remove(storageKey, storageType); } } // 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) { SSSLOG(("%s is a preloaded STS host", subdomain)); *aResult = true; @@ -583,16 +739,90 @@ nsSiteSecurityService::ShouldIgnoreHeade } NS_IMETHODIMP nsSiteSecurityService::ClearAll() { return mSiteStateStorage->Clear(); } +NS_IMETHODIMP +nsSiteSecurityService::GetKeyPinsForHostname(const char* aHostname, + mozilla::pkix::Time& aEvalTime, + /*out*/ nsTArray<nsCString>& pinArray, + /*out*/ bool* aIncludeSubdomains, + /*out*/ bool* afound) { + NS_ENSURE_ARG(afound); + NS_ENSURE_ARG(aHostname); + + SSSLOG(("Top of GetKeyPinsForHostname")); + *afound = false; + *aIncludeSubdomains = false; + pinArray.Clear(); + + nsAutoCString host(aHostname); + nsAutoCString storageKey; + SetStorageKey(storageKey, host, nsISiteSecurityService::HEADER_HPKP); + + SSSLOG(("storagekey '%s'\n", storageKey.get())); + mozilla::DataStorageType storageType = mozilla::DataStorage_Persistent; + nsCString value = mSiteStateStorage->Get(storageKey, storageType); + //decode now + SiteHPKPState foundEntry(value); + if (foundEntry.mState != SecurityPropertySet || + foundEntry.IsExpired(aEvalTime) || + foundEntry.mSHA256keys.Length() < 1 ) { + return NS_OK; + } + pinArray = foundEntry.mSHA256keys; + *aIncludeSubdomains = foundEntry.mIncludeSubdomains; + *afound = true; + return NS_OK; +} + +NS_IMETHODIMP +nsSiteSecurityService::SetKeyPins(const char* aHost, bool aIncludeSubdomains, + uint32_t aMaxAge, uint32_t aPinCount, + const char** aSha256Pins, + /*out*/bool* aResult) +{ + NS_ENSURE_ARG_POINTER(aHost); + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aSha256Pins); + + SSSLOG(("Top of SetPins")); + + // Expire time is millis from now. Since HPKP max-age is in seconds, and + // PR_Now() is in micros, must normalize the units at milliseconds. + int64_t expireTime = (PR_Now() / PR_USEC_PER_MSEC) + + (aMaxAge * PR_MSEC_PER_SEC); + nsTArray<nsCString> sha256keys; + for (unsigned int i = 0; i < aPinCount; i++) { + nsAutoCString pin(aSha256Pins[i]); + SSSLOG(("SetPins pin=%s\n", pin.get())); + if (!stringIsBase64EncodingOf256bitValue(pin)) { + return NS_ERROR_INVALID_ARG; + } + sha256keys.AppendElement(pin); + } + SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet, + aIncludeSubdomains, sha256keys); + + nsAutoCString host(aHost); + nsAutoCString storageKey; + SetStorageKey(storageKey, host, nsISiteSecurityService::HEADER_HPKP); + // Note: setPins always stores data in the persistent storage + mozilla::DataStorageType storageType = mozilla::DataStorage_Persistent; + nsAutoCString stateString; + dynamicEntry.ToString(stateString); + nsresult rv = mSiteStateStorage->Put(storageKey, stateString, storageType); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + //------------------------------------------------------------ // nsSiteSecurityService::nsIObserver //------------------------------------------------------------ NS_IMETHODIMP nsSiteSecurityService::Observe(nsISupports *subject, const char *topic, const char16_t *data)
--- a/security/manager/boot/src/nsSiteSecurityService.h +++ b/security/manager/boot/src/nsSiteSecurityService.h @@ -5,16 +5,18 @@ #ifndef __nsSiteSecurityService_h__ #define __nsSiteSecurityService_h__ #include "mozilla/DataStorage.h" #include "nsCOMPtr.h" #include "nsIObserver.h" #include "nsISiteSecurityService.h" #include "nsString.h" +#include "nsTArray.h" +#include "pkix/pkixtypes.h" #include "prtime.h" class nsIURI; // {16955eee-6c48-4152-9309-c42a465138a1} #define NS_SITE_SECURITY_SERVICE_CID \ {0x16955eee, 0x6c48, 0x4152, \ {0x93, 0x09, 0xc4, 0x2a, 0x46, 0x51, 0x38, 0xa1} } @@ -30,29 +32,63 @@ class nsIURI; */ enum SecurityPropertyState { SecurityPropertyUnset = 0, SecurityPropertySet = 1, SecurityPropertyKnockout = 2 }; /** - * SiteSecurityState: A utility class that encodes/decodes a string describing + * SiteHPKPState: A utility class that encodes/decodes a string describing + * the public key pins of a site. + * HPKP state consists of: + * - 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 +{ +public: + SiteHPKPState(); + SiteHPKPState(nsCString& aStateString); + SiteHPKPState(PRTime aExpireTime, SecurityPropertyState aState, + bool aIncludeSubdomains, nsTArray<nsCString>& SHA256keys); + + PRTime mExpireTime; + SecurityPropertyState mState; + bool mIncludeSubdomains; + nsTArray<nsCString> mSHA256keys; + + bool IsExpired(mozilla::pkix::Time aTime) + { + if (aTime > mozilla::pkix::TimeFromEpochInSeconds(mExpireTime / + PR_MSEC_PER_SEC)) { + return true; + } + return false; + } + + void ToString(nsCString& aString); +}; + +/** + * SiteHSTSState: A utility class that encodes/decodes a string describing * the security state of a site. Currently only handles HSTS. * HSTS state consists of: * - Expiry time (PRTime (aka int64_t) in milliseconds) * - A state flag (SecurityPropertyState, default SecurityPropertyUnset) * - An include subdomains flag (bool, default false) */ -class SiteSecurityState +class SiteHSTSState { public: - explicit SiteSecurityState(nsCString& aStateString); - SiteSecurityState(PRTime aHSTSExpireTime, SecurityPropertyState aHSTSState, - bool aHSTSIncludeSubdomains); + SiteHSTSState(nsCString& aStateString); + SiteHSTSState(PRTime aHSTSExpireTime, SecurityPropertyState aHSTSState, + bool aHSTSIncludeSubdomains); PRTime mHSTSExpireTime; SecurityPropertyState mHSTSState; bool mHSTSIncludeSubdomains; bool IsExpired(uint32_t aType) { // If mHSTSExpireTime is 0, this entry never expires (this is the case for @@ -85,18 +121,18 @@ public: nsSiteSecurityService(); nsresult Init(); protected: virtual ~nsSiteSecurityService(); private: nsresult GetHost(nsIURI *aURI, nsACString &aResult); - nsresult SetState(uint32_t aType, nsIURI* aSourceURI, int64_t maxage, - bool includeSubdomains, uint32_t flags); + nsresult SetHSTSState(uint32_t aType, nsIURI* aSourceURI, int64_t maxage, + bool includeSubdomains, uint32_t flags); nsresult ProcessHeaderMutating(uint32_t aType, nsIURI* aSourceURI, char* aHeader, uint32_t flags, uint64_t *aMaxAge, bool *aIncludeSubdomains); const nsSTSPreload *GetPreloadListEntry(const char *aHost); bool mUsePreloadList; int64_t mPreloadListTimeOffset; nsRefPtr<mozilla::DataStorage> mSiteStateStorage;
--- a/security/manager/ssl/src/nsNSSComponent.cpp +++ b/security/manager/ssl/src/nsNSSComponent.cpp @@ -30,16 +30,17 @@ #include "nsCRT.h" #include "nsNTLMAuthModule.h" #include "nsIFile.h" #include "nsIProperties.h" #include "nsIWindowWatcher.h" #include "nsIPrompt.h" #include "nsIBufEntropyCollector.h" #include "nsITokenPasswordDialogs.h" +#include "nsISiteSecurityService.h" #include "nsServiceManagerUtils.h" #include "nsNSSShutDown.h" #include "SharedSSLState.h" #include "NSSErrorsService.h" #include "nss.h" #include "pkix/pkixnss.h" #include "ssl.h" @@ -1034,16 +1035,25 @@ nsNSSComponent::InitializeNSS() mHttpForNSS.initTable(); #ifndef MOZ_NO_SMART_CARDS LaunchSmartCardThreads(); #endif mozilla::pkix::RegisterErrorTable(); + // Initialize the site security service + nsCOMPtr<nsISiteSecurityService> sssService = + do_GetService(NS_SSSERVICE_CONTRACTID); + if (!sssService) { + PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("Cannot initialize site security service\n")); + return NS_ERROR_FAILURE; + } + + PR_LOG(gPIPNSSLog, PR_LOG_DEBUG, ("NSS Initialization done\n")); return NS_OK; } void nsNSSComponent::ShutdownNSS() { // Can be called both during init and profile change,
--- a/security/manager/ssl/tests/unit/test_sss_eviction.js +++ b/security/manager/ssl/tests/unit/test_sss_eviction.js @@ -24,17 +24,17 @@ function do_state_written(aSubject, aTop // so we return and wait for the next event if things aren't as we expect. // There should be 1024 entries. if (lines.length != 1024) { return; } let foundLegitSite = false; for (let line of lines) { - if (line.startsWith("frequentlyused.example.com")) { + if (line.startsWith("frequentlyused.example.com:HSTS")) { foundLegitSite = true; break; } } do_check_true(foundLegitSite); do_test_finished(); } @@ -59,17 +59,17 @@ function run_test() { gProfileDir = do_get_profile(); let stateFile = gProfileDir.clone(); stateFile.append(SSS_STATE_FILE_NAME); // Assuming we're working with a clean slate, the file shouldn't exist // until we create it. do_check_false(stateFile.exists()); let outputStream = FileUtils.openFileOutputStream(stateFile); let now = (new Date()).getTime(); - let line = "frequentlyused.example.com\t4\t0\t" + (now + 100000) + ",1,0\n"; + let line = "frequentlyused.example.com:HSTS\t4\t0\t" + (now + 100000) + ",1,0\n"; outputStream.write(line, line.length); outputStream.close(); Services.obs.addObserver(do_state_read, "data-storage-ready", false); do_test_pending(); gSSService = Cc["@mozilla.org/ssservice;1"] .getService(Ci.nsISiteSecurityService); do_check_true(gSSService != null); }
--- a/security/manager/ssl/tests/unit/test_sss_readstate.js +++ b/security/manager/ssl/tests/unit/test_sss_readstate.js @@ -56,22 +56,22 @@ function run_test() { let profileDir = do_get_profile(); let stateFile = profileDir.clone(); stateFile.append(SSS_STATE_FILE_NAME); // Assuming we're working with a clean slate, the file shouldn't exist // until we create it. do_check_false(stateFile.exists()); let outputStream = FileUtils.openFileOutputStream(stateFile); let now = (new Date()).getTime(); - writeLine("expired.example.com\t0\t0\t" + (now - 100000) + ",1,0\n", outputStream); - writeLine("notexpired.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); + writeLine("expired.example.com:HSTS\t0\t0\t" + (now - 100000) + ",1,0\n", outputStream); + writeLine("notexpired.example.com:HSTS\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); // This overrides an entry on the preload list. - writeLine("bugzilla.mozilla.org\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); - writeLine("incsubdomain.example.com\t0\t0\t" + (now + 100000) + ",1,1\n", outputStream); + writeLine("bugzilla.mozilla.org:HSTS\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); + writeLine("incsubdomain.example.com:HSTS\t0\t0\t" + (now + 100000) + ",1,1\n", outputStream); // This overrides an entry on the preload list. - writeLine("login.persona.org\t0\t0\t0,2,0\n", outputStream); + writeLine("login.persona.org:HSTS\t0\t0\t0,2,0\n", outputStream); outputStream.close(); Services.obs.addObserver(checkStateRead, "data-storage-ready", false); do_test_pending(); gSSService = Cc["@mozilla.org/ssservice;1"] .getService(Ci.nsISiteSecurityService); do_check_true(gSSService != null); }
--- a/security/manager/ssl/tests/unit/test_sss_readstate_garbage.js +++ b/security/manager/ssl/tests/unit/test_sss_readstate_garbage.js @@ -29,23 +29,23 @@ function run_test() { let profileDir = do_get_profile(); let stateFile = profileDir.clone(); stateFile.append(SSS_STATE_FILE_NAME); // Assuming we're working with a clean slate, the file shouldn't exist // until we create it. do_check_false(stateFile.exists()); let outputStream = FileUtils.openFileOutputStream(stateFile); let now = (new Date()).getTime(); - writeLine("example1.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); + writeLine("example1.example.com:HSTS\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); writeLine("I'm a lumberjack and I'm okay; I work all night and I sleep all day!\n", outputStream); writeLine("This is a totally bogus entry\t\n", outputStream); writeLine("0\t0\t0\t0\t\n", outputStream); writeLine("\t\t\t\t\t\t\t\n", outputStream); - writeLine("example.com\t\t\t\t\t\t\t\n", outputStream); - writeLine("example3.example.com\t0\t\t\t\t\t\t\n", outputStream); - writeLine("example2.example.com\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); + writeLine("example.com:HSTS\t\t\t\t\t\t\t\n", outputStream); + writeLine("example3.example.com:HSTS\t0\t\t\t\t\t\t\n", outputStream); + writeLine("example2.example.com:HSTS\t0\t0\t" + (now + 100000) + ",1,0\n", outputStream); outputStream.close(); Services.obs.addObserver(checkStateRead, "data-storage-ready", false); do_test_pending(); gSSService = Cc["@mozilla.org/ssservice;1"] .getService(Ci.nsISiteSecurityService); do_check_true(gSSService != null); }
--- a/security/manager/ssl/tests/unit/test_sss_readstate_huge.js +++ b/security/manager/ssl/tests/unit/test_sss_readstate_huge.js @@ -39,17 +39,17 @@ function run_test() { // Assuming we're working with a clean slate, the file shouldn't exist // until we create it. do_check_false(stateFile.exists()); let outputStream = FileUtils.openFileOutputStream(stateFile); let now = (new Date()).getTime(); for (let i = 0; i < 10000; i++) { // The 0s will all get squashed down into one 0 when they are read. // This is just to make the file size large (>2MB). - writeLine("example" + i + ".example.com\t0000000000000000000000000000000000000000000000000\t00000000000000000000000000000000000000\t" + (now + 100000) + ",1,0000000000000000000000000000000000000000000000000000000000000000000000000\n", outputStream); + writeLine("example" + i + ".example.com:HSTS\t0000000000000000000000000000000000000000000000000\t00000000000000000000000000000000000000\t" + (now + 100000) + ",1,0000000000000000000000000000000000000000000000000000000000000000000000000\n", outputStream); } outputStream.close(); Services.obs.addObserver(checkStateRead, "data-storage-ready", false); do_test_pending(); gSSService = Cc["@mozilla.org/ssservice;1"] .getService(Ci.nsISiteSecurityService); do_check_true(gSSService != null); }
--- a/security/manager/ssl/tests/unit/test_sss_savestate.js +++ b/security/manager/ssl/tests/unit/test_sss_savestate.js @@ -37,44 +37,44 @@ function checkStateWritten(aSubject, aTo // We can receive multiple data-storage-written events. In particular, we // may receive one where DataStorage wrote out data before we were done // processing all of our headers. In this case, the data may not be // as we expect. We only care about the final one being correct, however, // so we return and wait for the next event if things aren't as we expect. // sites[url][1] corresponds to SecurityPropertySet (if 1) and // SecurityPropertyUnset (if 0) // sites[url][2] corresponds to includeSubdomains - if (sites["bugzilla.mozilla.org"][1] != 1) { + if (sites["bugzilla.mozilla.org:HSTS"][1] != 1) { return; } - if (sites["bugzilla.mozilla.org"][2] != 0) { + if (sites["bugzilla.mozilla.org:HSTS"][2] != 0) { return; } - if (sites["a.example.com"][1] != 1) { + if (sites["a.example.com:HSTS"][1] != 1) { return; } - if (sites["a.example.com"][2] != 1) { + if (sites["a.example.com:HSTS"][2] != 1) { return; } - if (sites["b.example.com"][1] != 1) { + if (sites["b.example.com:HSTS"][1] != 1) { return; } - if (sites["b.example.com"][2] != 0) { + if (sites["b.example.com:HSTS"][2] != 0) { return; } - if (sites["c.c.example.com"][1] != 1) { + if (sites["c.c.example.com:HSTS"][1] != 1) { return; } - if (sites["c.c.example.com"][2] != 1) { + if (sites["c.c.example.com:HSTS"][2] != 1) { return; } - if (sites["d.example.com"][1] != 1) { + if (sites["d.example.com:HSTS"][1] != 1) { return; } - if (sites["d.example.com"][2] != 0) { + if (sites["d.example.com:HSTS"][2] != 0) { return; } do_test_finished(); } function run_test() { Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);