--- a/dom/locales/en-US/chrome/security/security.properties
+++ b/dom/locales/en-US/chrome/security/security.properties
@@ -34,16 +34,18 @@ PKPUntrustworthyConnection=Public-Key-Pi
PKPCouldNotParseHeader=Public-Key-Pins: The site specified a header that could not be parsed successfully.
PKPNoMaxAge=Public-Key-Pins: The site specified a header that did not include a ‘max-age’ directive.
PKPMultipleMaxAges=Public-Key-Pins: The site specified a header that included multiple ‘max-age’ directives.
PKPInvalidMaxAge=Public-Key-Pins: The site specified a header that included an invalid ‘max-age’ directive.
PKPMultipleIncludeSubdomains=Public-Key-Pins: The site specified a header that included multiple ‘includeSubDomains’ directives.
PKPInvalidIncludeSubdomains=Public-Key-Pins: The site specified a header that included an invalid ‘includeSubDomains’ directive.
PKPInvalidPin=Public-Key-Pins: The site specified a header that included an invalid pin.
PKPMultipleReportURIs=Public-Key-Pins: The site specified a header that included multiple ‘report-uri’ directives.
+PKPEmptyReportURI=Public-Key-Pins: The site specified a header that included an empty ‘report-uri’ directive.
+PKPInvalidReportURI=Public-Key-Pins: The site specified a header that included an invalid URI in the ‘report-uri’ directive.
PKPPinsetDoesNotMatch=Public-Key-Pins: The site specified a header that did not include a matching pin.
PKPNoBackupPin=Public-Key-Pins: The site specified a header that did not include a backup pin.
PKPCouldNotSaveState=Public-Key-Pins: An error occurred noting the site as a Public-Key-Pins host.
PKPRootNotBuiltIn=Public-Key-Pins: The certificate used by the site was not issued by a certificate in the default root certificate store. To prevent accidental breakage, the specified header was ignored.
# LOCALIZATION NOTE: Do not translate "SHA-1"
SHA1Sig=This site makes use of a SHA-1 Certificate; it’s recommended you use certificates with signature algorithms that use hash functions stronger than SHA-1.
InsecurePasswordsPresentOnPage=Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen.
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -1517,16 +1517,22 @@ GetPKPConsoleErrorTag(uint32_t failureRe
consoleErrorTag = NS_LITERAL_STRING("PKPInvalidIncludeSubdomains");
break;
case nsISiteSecurityService::ERROR_INVALID_PIN:
consoleErrorTag = NS_LITERAL_STRING("PKPInvalidPin");
break;
case nsISiteSecurityService::ERROR_MULTIPLE_REPORT_URIS:
consoleErrorTag = NS_LITERAL_STRING("PKPMultipleReportURIs");
break;
+ case nsISiteSecurityService::ERROR_EMPTY_REPORTURI:
+ consoleErrorTag = NS_LITERAL_STRING("PKPEmptyReportURI");
+ break;
+ case nsISiteSecurityService::ERROR_INVALID_REPORTURI:
+ consoleErrorTag = NS_LITERAL_STRING("PKPInvalidReportURI");
+ break;
case nsISiteSecurityService::ERROR_PINSET_DOES_NOT_MATCH_CHAIN:
consoleErrorTag = NS_LITERAL_STRING("PKPPinsetDoesNotMatch");
break;
case nsISiteSecurityService::ERROR_NO_BACKUP_PIN:
consoleErrorTag = NS_LITERAL_STRING("PKPNoBackupPin");
break;
case nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE:
consoleErrorTag = NS_LITERAL_STRING("PKPCouldNotSaveState");
@@ -1565,17 +1571,17 @@ nsHttpChannel::ProcessSingleSecurityHead
nsresult rv = mResponseHead->GetHeader(atom, securityHeader);
if (NS_SUCCEEDED(rv)) {
nsISiteSecurityService* sss = gHttpHandler->GetSSService();
NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY);
// Process header will now discard the headers itself if the channel
// wasn't secure (whereas before it had to be checked manually)
uint32_t failureResult;
rv = sss->ProcessHeader(aType, mURI, securityHeader.get(), aSSLStatus,
- aFlags, nullptr, nullptr, &failureResult);
+ aFlags, nullptr, nullptr, nullptr, &failureResult);
if (NS_FAILED(rv)) {
nsAutoString consoleErrorCategory;
nsAutoString consoleErrorTag;
switch (aType) {
case nsISiteSecurityService::HEADER_HSTS:
GetSTSConsoleErrorTag(failureResult, consoleErrorTag);
consoleErrorCategory = NS_LITERAL_STRING("Invalid HSTS Headers");
break;
--- a/security/manager/ssl/nsISiteSecurityService.idl
+++ b/security/manager/ssl/nsISiteSecurityService.idl
@@ -40,16 +40,18 @@ interface nsISiteSecurityService : nsISu
const uint32_t ERROR_MULTIPLE_INCLUDE_SUBDOMAINS = 7;
const uint32_t ERROR_INVALID_INCLUDE_SUBDOMAINS = 8;
const uint32_t ERROR_INVALID_PIN = 9;
const uint32_t ERROR_MULTIPLE_REPORT_URIS = 10;
const uint32_t ERROR_PINSET_DOES_NOT_MATCH_CHAIN = 11;
const uint32_t ERROR_NO_BACKUP_PIN = 12;
const uint32_t ERROR_COULD_NOT_SAVE_STATE = 13;
const uint32_t ERROR_ROOT_NOT_BUILT_IN = 14;
+ const uint32_t ERROR_EMPTY_REPORTURI = 15;
+ const uint32_t ERROR_INVALID_REPORTURI = 16;
/**
* Parses a given HTTP header and records the results internally.
* Currently two header types are supported: HSTS (aka STS) and HPKP
* The format of the HSTS header is defined by the HSTS specification:
* https://tools.ietf.org/html/rfc6797
* and allows a host to specify that future HTTP requests should be
* upgraded to HTTPS.
@@ -75,28 +77,30 @@ interface nsISiteSecurityService : nsISu
*/
void processHeader(in uint32_t aType,
in nsIURI aSourceURI,
in string aHeader,
in nsISSLStatus aSSLStatus,
in uint32_t aFlags,
[optional] out unsigned long long aMaxAge,
[optional] out boolean aIncludeSubdomains,
+ [optional] out nsIURI aReportURI,
[optional] out uint32_t aFailureResult);
/**
* Same as processHeader but without checking for the security properties
* of the connection. Use ONLY for testing.
*/
void unsafeProcessHeader(in uint32_t aType,
in nsIURI aSourceURI,
in string aHeader,
in uint32_t aFlags,
[optional] out unsigned long long aMaxAge,
[optional] out boolean aIncludeSubdomains,
+ [optional] out nsIURI aReportURI,
[optional] out uint32_t aFailureResult);
/**
* Given a header type, removes state relating to that header of a host,
* including the includeSubdomains state that would affect subdomains.
* This essentially removes the state for the domain tree rooted at this
* host.
* @param aType the type of security state in question
@@ -176,22 +180,24 @@ interface nsISiteSecurityService : nsISu
* and visible from private and non-private contexts. These pins replace
* any already set by this mechanism or those built-in to Gecko.
*
* @param aHost the hostname (punycode) that pins will apply to
* @param aIncludeSubdomains whether these pins also apply to subdomains
* @param aExpires the time this pin should expire (millis since epoch)
* @param aPinCount number of keys being pinnned
* @param aSha256Pins array of hashed key fingerprints (SHA-256, base64)
+ * @param aReportURI an optional (absolute) URI to submit pinning error reports to
* @param aIsPreload are these key pins for a preload entry? (false by
* default)
*/
boolean setKeyPins(in string aHost, in boolean aIncludeSubdomains,
in int64_t aExpires, in unsigned long aPinCount,
[array, size_is(aPinCount)] in string aSha256Pins,
+ [optional] in nsIURI aReportURI,
[optional] in boolean aIsPreload);
/**
* Mark a host as declining to provide a given security state so that features
* such as HSTS priming will not flood a server with requests.
*
* @param aURI the nsIURI that this applies to
* @param aMaxAge lifetime (in seconds) of this negative cache
--- a/security/manager/ssl/nsSiteSecurityService.cpp
+++ b/security/manager/ssl/nsSiteSecurityService.cpp
@@ -107,87 +107,111 @@ stringIsBase64EncodingOf256bitValue(nsCS
}
return true;
}
SiteHPKPState::SiteHPKPState()
: mExpireTime(0)
, mState(SecurityPropertyUnset)
, mIncludeSubdomains(false)
+ , mReportURI(nullptr)
{
}
SiteHPKPState::SiteHPKPState(nsCString& aStateString)
: mExpireTime(0)
, mState(SecurityPropertyUnset)
, mIncludeSubdomains(false)
+ , mReportURI(nullptr)
{
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);
+
+ const uint32_t MaxStateStringSize = 1024;
- if (aStateString.Length() >= MaxMergedHPKPPinSize) {
+ nsCOMPtr<nsIURI> reportURI;
+ char collectedReportUri[MaxStateStringSize];
+ memset(collectedReportUri, 0, MaxStateStringSize);
+
+ char mergedHPKPins[MaxStateStringSize];
+ memset(mergedHPKPins, 0, MaxStateStringSize);
+
+ if (aStateString.Length() >= MaxStateStringSize) {
SSSLOG(("SSS: Cannot parse PKPState string, too large\n"));
return;
}
- int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu,%s",
+ // Report URI was added later and may not be present
+ int32_t matches = PR_sscanf(aStateString.get(), "%lld,%lu,%lu,%[^,],%[^,]",
&mExpireTime, &hpkpState,
- &hpkpIncludeSubdomains, mergedHPKPins);
- bool valid = (matches == 4 &&
+ &hpkpIncludeSubdomains, mergedHPKPins,
+ collectedReportUri);
+ bool valid = ((matches == 4 || matches == 5) &&
(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;
+ mergedHPKPins[MaxStateStringSize - 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 && (strnlen(collectedReportUri, MaxStateStringSize) > 0)) {
+ nsresult rv = NS_NewURI(getter_AddRefs(reportURI), collectedReportUri);
+ if (NS_FAILED(rv)) {
+ // TODO: Perform any logging here? This function does not parse from
+ // user/webserver, only internal...
+ valid = false;
+ }
+ }
+
if (valid) {
mState = (SecurityPropertyState)hpkpState;
mIncludeSubdomains = (hpkpIncludeSubdomains == 1);
+ mReportURI = reportURI;
} else {
- SSSLOG(("%s is not a valid SiteHPKPState", aStateString.get()));
+ SSSLOG(("%s (len=%u) is not a valid SiteHPKPState", aStateString.get(),
+ aStateString.Length()));
mExpireTime = 0;
mState = SecurityPropertyUnset;
mIncludeSubdomains = false;
if (!mSHA256keys.IsEmpty()) {
mSHA256keys.Clear();
}
}
}
SiteHPKPState::SiteHPKPState(PRTime aExpireTime,
SecurityPropertyState aState,
bool aIncludeSubdomains,
+ nsIURI* aReportURI,
nsTArray<nsCString>& aSHA256keys)
: mExpireTime(aExpireTime)
, mState(aState)
, mIncludeSubdomains(aIncludeSubdomains)
+ , mReportURI(aReportURI)
, mSHA256keys(aSHA256keys)
{
}
void
SiteHPKPState::ToString(nsCString& aString)
{
aString.Truncate();
@@ -195,16 +219,27 @@ SiteHPKPState::ToString(nsCString& aStri
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]);
}
+
+ aString.Append(',');
+ if (mReportURI) {
+ nsresult rv;
+
+ nsAutoCString spec;
+ rv = mReportURI->GetAsciiSpec(spec);
+ if (rv == NS_OK) {
+ aString.Append(spec);
+ }
+ }
}
////////////////////////////////////////////////////////////////////////////////
const uint64_t kSixtyDaysInSeconds = 60 * 24 * 60 * 60;
nsSiteSecurityService::nsSiteSecurityService()
: mMaxMaxAge(kSixtyDaysInSeconds)
@@ -414,61 +449,70 @@ HostIsIPAddress(const char *hostname)
NS_IMETHODIMP
nsSiteSecurityService::ProcessHeader(uint32_t aType,
nsIURI* aSourceURI,
const char* aHeader,
nsISSLStatus* aSSLStatus,
uint32_t aFlags,
uint64_t* aMaxAge,
bool* aIncludeSubdomains,
+ nsIURI** aReportURI,
uint32_t* aFailureResult)
{
// Child processes are not allowed direct access to this.
if (!XRE_IsParentProcess()) {
MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::ProcessHeader");
}
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN;
}
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS ||
aType == nsISiteSecurityService::HEADER_HPKP,
NS_ERROR_NOT_IMPLEMENTED);
NS_ENSURE_ARG(aSSLStatus);
- return ProcessHeaderInternal(aType, aSourceURI, aHeader, aSSLStatus, aFlags,
- aMaxAge, aIncludeSubdomains, aFailureResult);
+ nsCOMPtr<nsIURI> reportURI;
+ nsresult rv = ProcessHeaderInternal(aType, aSourceURI, aHeader, aSSLStatus,
+ aFlags, aMaxAge, aIncludeSubdomains,
+ getter_AddRefs(reportURI), aFailureResult);
+ if (aReportURI) {
+ reportURI.forget(aReportURI);
+ }
+ return rv;
}
NS_IMETHODIMP
nsSiteSecurityService::UnsafeProcessHeader(uint32_t aType,
nsIURI* aSourceURI,
const char* aHeader,
uint32_t aFlags,
uint64_t* aMaxAge,
bool* aIncludeSubdomains,
+ nsIURI** aReportURI,
uint32_t* aFailureResult)
{
// Child processes are not allowed direct access to this.
if (!XRE_IsParentProcess()) {
MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::UnsafeProcessHeader");
}
return ProcessHeaderInternal(aType, aSourceURI, aHeader, nullptr, aFlags,
- aMaxAge, aIncludeSubdomains, aFailureResult);
+ aMaxAge, aIncludeSubdomains, aReportURI, aFailureResult);
}
nsresult
nsSiteSecurityService::ProcessHeaderInternal(uint32_t aType,
nsIURI* aSourceURI,
const char* aHeader,
nsISSLStatus* aSSLStatus,
uint32_t aFlags,
uint64_t* aMaxAge,
bool* aIncludeSubdomains,
+ nsIURI** aReportURI,
uint32_t* aFailureResult)
{
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN;
}
// Only HSTS and HPKP are supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS ||
aType == nsISiteSecurityService::HEADER_HPKP,
@@ -516,32 +560,35 @@ nsSiteSecurityService::ProcessHeaderInte
switch (aType) {
case nsISiteSecurityService::HEADER_HSTS:
rv = ProcessSTSHeader(aSourceURI, aHeader, aFlags, aMaxAge,
aIncludeSubdomains, aFailureResult);
break;
case nsISiteSecurityService::HEADER_HPKP:
rv = ProcessPKPHeader(aSourceURI, aHeader, aSSLStatus, aFlags, aMaxAge,
- aIncludeSubdomains, aFailureResult);
+ aIncludeSubdomains, aReportURI, aFailureResult);
break;
default:
MOZ_CRASH("unexpected header type");
}
return rv;
}
static uint32_t
-ParseSSSHeaders(uint32_t aType,
+ParseSSSHeaders(nsIURI* aSourceURI,
+ uint32_t aType,
const char* aHeader,
bool& foundIncludeSubdomains,
bool& foundMaxAge,
+ bool& foundReportUri,
bool& foundUnrecognizedDirective,
uint64_t& maxAge,
- nsTArray<nsCString>& sha256keys)
+ nsTArray<nsCString>& sha256keys,
+ nsIURI** reportURI)
{
// Strict transport security and Public Key Pinning have very similar
// Header formats.
// "Strict-Transport-Security" ":" OWS
// STS-d *( OWS ";" OWS STS-d OWS)
//
// ; STS directive
@@ -574,18 +621,16 @@ ParseSSSHeaders(uint32_t aType,
// The order of the directives is not significant.
// All directives must appear only once.
// Directive names are case-insensitive.
// The entire header is invalid if a directive not conforming to the
// syntax is encountered.
// Unrecognized directives (that are otherwise syntactically valid) are
// ignored, and the rest of the header is parsed as normal.
- bool foundReportURI = false;
-
NS_NAMED_LITERAL_CSTRING(max_age_var, "max-age");
NS_NAMED_LITERAL_CSTRING(include_subd_var, "includesubdomains");
NS_NAMED_LITERAL_CSTRING(pin_sha256_var, "pin-sha256");
NS_NAMED_LITERAL_CSTRING(report_uri_var, "report-uri");
nsSecurityHeaderParser parser(aHeader);
nsresult rv = parser.Parse();
if (NS_FAILED(rv)) {
@@ -648,57 +693,73 @@ ParseSSSHeaders(uint32_t aType,
if (!stringIsBase64EncodingOf256bitValue(directive->mValue)) {
return nsISiteSecurityService::ERROR_INVALID_PIN;
}
sha256keys.AppendElement(directive->mValue);
} else if (aType == nsISiteSecurityService::HEADER_HPKP &&
directive->mName.Length() == report_uri_var.Length() &&
directive->mName.EqualsIgnoreCase(report_uri_var.get(),
report_uri_var.Length())) {
- // We don't support the report-uri yet, but to avoid unrecognized
- // directive warnings, we still have to handle its presence
- if (foundReportURI) {
+ // We don't support the report-uri yet, but we have begun parsing it
+ if (!reportURI) {
+ MOZ_CRASH("reportURI was passed as a NULL value into ParseSSSHeaders");
+ }
+
+ if (foundReportUri) {
SSSLOG(("SSS: found two report-uri directives"));
return nsISiteSecurityService::ERROR_MULTIPLE_REPORT_URIS;
}
- SSSLOG(("SSS: found report-uri directive"));
- foundReportURI = true;
+ SSSLOG(("SSS: found report-uri directive '%s'", directive->mValue.get()));
+ foundReportUri = true;
+
+ if (directive->mValue.IsEmpty()) {
+ SSSLOG(("SSS: report-uri directive empty"));
+ return nsISiteSecurityService::ERROR_EMPTY_REPORTURI;
+ }
+
+ nsresult rv = NS_NewURI(reportURI, directive->mValue, nullptr, aSourceURI);
+ if (NS_FAILED(rv)) {
+ SSSLOG(("SSS: report-uri directive is an invalid URI"));
+ return nsISiteSecurityService::ERROR_INVALID_REPORTURI;
+ }
} else {
SSSLOG(("SSS: ignoring unrecognized directive '%s'",
directive->mName.get()));
foundUnrecognizedDirective = true;
}
}
return nsISiteSecurityService::Success;
}
nsresult
nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI,
const char* aHeader,
nsISSLStatus* aSSLStatus,
uint32_t aFlags,
uint64_t* aMaxAge,
bool* aIncludeSubdomains,
+ nsIURI** aReportURI,
uint32_t* aFailureResult)
{
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN;
}
SSSLOG(("SSS: processing HPKP header '%s'", aHeader));
- NS_ENSURE_ARG(aSSLStatus);
const uint32_t aType = nsISiteSecurityService::HEADER_HPKP;
bool foundMaxAge = false;
bool foundIncludeSubdomains = false;
+ bool foundReportUri = false;
bool foundUnrecognizedDirective = false;
uint64_t maxAge = 0;
nsTArray<nsCString> sha256keys;
- uint32_t sssrv = ParseSSSHeaders(aType, aHeader, foundIncludeSubdomains,
- foundMaxAge, foundUnrecognizedDirective,
- maxAge, sha256keys);
+ nsCOMPtr<nsIURI> reportURI = nullptr;
+ uint32_t sssrv = ParseSSSHeaders(aSourceURI, aType, aHeader, foundIncludeSubdomains,
+ foundMaxAge, foundReportUri, foundUnrecognizedDirective,
+ maxAge, sha256keys, getter_AddRefs(reportURI));
if (sssrv != nsISiteSecurityService::Success) {
if (aFailureResult) {
*aFailureResult = sssrv;
}
return NS_ERROR_FAILURE;
}
// after processing all the directives, make sure we came across max-age
@@ -706,124 +767,132 @@ nsSiteSecurityService::ProcessPKPHeader(
if (!foundMaxAge) {
SSSLOG(("SSS: did not encounter required max-age directive"));
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_NO_MAX_AGE;
}
return NS_ERROR_FAILURE;
}
- // before we add the pin we need to ensure it will not break the site as
- // currently visited so:
- // 1. recompute a valid chain (no external ocsp)
- // 2. use this chain to check if things would have broken!
+ nsresult rv;
nsAutoCString host;
- nsresult rv = GetHost(aSourceURI, host);
- NS_ENSURE_SUCCESS(rv, rv);
- nsCOMPtr<nsIX509Cert> cert;
- rv = aSSLStatus->GetServerCert(getter_AddRefs(cert));
- NS_ENSURE_SUCCESS(rv, rv);
- NS_ENSURE_TRUE(cert, NS_ERROR_FAILURE);
- UniqueCERTCertificate nssCert(cert->GetCert());
- NS_ENSURE_TRUE(nssCert, NS_ERROR_FAILURE);
-
- mozilla::pkix::Time now(mozilla::pkix::Now());
UniqueCERTCertList certList;
- RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
- NS_ENSURE_TRUE(certVerifier, NS_ERROR_UNEXPECTED);
- // We don't want this verification to cause any network traffic that would
- // block execution. Also, since we don't have access to the original stapled
- // OCSP response, we can't enforce this aspect of the TLS Feature extension.
- // This is ok, because it will have been enforced when we originally connected
- // to the site (or it's disabled, in which case we wouldn't want to enforce it
- // anyway).
- CertVerifier::Flags flags = CertVerifier::FLAG_LOCAL_ONLY |
- CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST;
- if (certVerifier->VerifySSLServerCert(nssCert,
- nullptr, // stapledOCSPResponse
- nullptr, // sctsFromTLSExtension
- now, nullptr, // pinarg
- host.get(), // hostname
- certList,
- false, // don't store intermediates
- flags)
- != mozilla::pkix::Success) {
- return NS_ERROR_FAILURE;
+ if (aSSLStatus) {
+ // before we add the pin we need to ensure it will not break the site as
+ // currently visited so:
+ // 1. recompute a valid chain (no external ocsp)
+ // 2. use this chain to check if things would have broken!
+ rv = GetHost(aSourceURI, host);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIX509Cert> cert;
+ rv = aSSLStatus->GetServerCert(getter_AddRefs(cert));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cert, NS_ERROR_FAILURE);
+ UniqueCERTCertificate nssCert(cert->GetCert());
+ NS_ENSURE_TRUE(nssCert, NS_ERROR_FAILURE);
+
+ mozilla::pkix::Time now(mozilla::pkix::Now());
+ RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
+ NS_ENSURE_TRUE(certVerifier, NS_ERROR_UNEXPECTED);
+ // We don't want this verification to cause any network traffic that would
+ // block execution. Also, since we don't have access to the original stapled
+ // OCSP response, we can't enforce this aspect of the TLS Feature extension.
+ // This is ok, because it will have been enforced when we originally connected
+ // to the site (or it's disabled, in which case we wouldn't want to enforce it
+ // anyway).
+ CertVerifier::Flags flags = CertVerifier::FLAG_LOCAL_ONLY |
+ CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST;
+ if (certVerifier->VerifySSLServerCert(nssCert,
+ nullptr, // stapledOCSPResponse
+ nullptr, // sctsFromTLSExtension
+ now, nullptr, // pinarg
+ host.get(), // hostname
+ certList,
+ false, // don't store intermediates
+ flags)
+ != mozilla::pkix::Success) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
+ if (CERT_LIST_END(rootNode, certList)) {
+ return NS_ERROR_FAILURE;
+ }
+ bool isBuiltIn = false;
+ mozilla::pkix::Result result = IsCertBuiltInRoot(rootNode->cert, isBuiltIn);
+ if (result != mozilla::pkix::Success) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!isBuiltIn && !mProcessPKPHeadersFromNonBuiltInRoots) {
+ if (aFailureResult) {
+ *aFailureResult = nsISiteSecurityService::ERROR_ROOT_NOT_BUILT_IN;
+ }
+ return NS_ERROR_FAILURE;
+ }
}
- CERTCertListNode* rootNode = CERT_LIST_TAIL(certList);
- if (CERT_LIST_END(rootNode, certList)) {
- return NS_ERROR_FAILURE;
- }
- bool isBuiltIn = false;
- mozilla::pkix::Result result = IsCertBuiltInRoot(rootNode->cert, isBuiltIn);
- if (result != mozilla::pkix::Success) {
- return NS_ERROR_FAILURE;
- }
-
- if (!isBuiltIn && !mProcessPKPHeadersFromNonBuiltInRoots) {
- if (aFailureResult) {
- *aFailureResult = nsISiteSecurityService::ERROR_ROOT_NOT_BUILT_IN;
- }
- return NS_ERROR_FAILURE;
- }
+ // TODO: Shouldn't we _not_ allow one to clear max-age if the pins provided do
+ // not match the certlist? (If an existing entry is present?)
// if maxAge == 0 we must delete all state, for now no hole-punching
if (maxAge == 0) {
return RemoveState(aType, aSourceURI, aFlags);
}
// clamp maxAge to the maximum set by pref
if (maxAge > mMaxMaxAge) {
maxAge = mMaxMaxAge;
}
- bool chainMatchesPinset;
- rv = PublicKeyPinningService::ChainMatchesPinset(certList, sha256keys,
- chainMatchesPinset);
- if (NS_FAILED(rv)) {
- return rv;
- }
- if (!chainMatchesPinset) {
- // is invalid
- SSSLOG(("SSS: Pins provided by %s are invalid no match with certList\n", host.get()));
- if (aFailureResult) {
- *aFailureResult = nsISiteSecurityService::ERROR_PINSET_DOES_NOT_MATCH_CHAIN;
- }
- return NS_ERROR_FAILURE;
- }
-
- // finally we need to ensure that there is a "backup pin" ie. There must be
- // at least one fingerprint hash that does NOT validate against the verified
- // chain (Section 2.5 of the spec)
- bool hasBackupPin = false;
- for (uint32_t i = 0; i < sha256keys.Length(); i++) {
- nsTArray<nsCString> singlePin;
- singlePin.AppendElement(sha256keys[i]);
- rv = PublicKeyPinningService::ChainMatchesPinset(certList, singlePin,
+ if (aSSLStatus) {
+ bool chainMatchesPinset;
+ rv = PublicKeyPinningService::ChainMatchesPinset(certList, sha256keys,
chainMatchesPinset);
if (NS_FAILED(rv)) {
return rv;
}
if (!chainMatchesPinset) {
- hasBackupPin = true;
+ // is invalid
+ SSSLOG(("SSS: Pins provided by %s are invalid no match with certList\n", host.get()));
+ if (aFailureResult) {
+ *aFailureResult = nsISiteSecurityService::ERROR_PINSET_DOES_NOT_MATCH_CHAIN;
+ }
+ return NS_ERROR_FAILURE;
}
- }
- if (!hasBackupPin) {
- // is invalid
- SSSLOG(("SSS: Pins provided by %s are invalid no backupPin\n", host.get()));
- if (aFailureResult) {
- *aFailureResult = nsISiteSecurityService::ERROR_NO_BACKUP_PIN;
+
+ // finally we need to ensure that there is a "backup pin" ie. There must be
+ // at least one fingerprint hash that does NOT validate against the verified
+ // chain (Section 2.5 of the spec)
+ bool hasBackupPin = false;
+ for (uint32_t i = 0; i < sha256keys.Length(); i++) {
+ nsTArray<nsCString> singlePin;
+ singlePin.AppendElement(sha256keys[i]);
+ rv = PublicKeyPinningService::ChainMatchesPinset(certList, singlePin,
+ chainMatchesPinset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!chainMatchesPinset) {
+ hasBackupPin = true;
+ }
}
- return NS_ERROR_FAILURE;
+ if (!hasBackupPin) {
+ // is invalid
+ SSSLOG(("SSS: Pins provided by %s are invalid no backupPin\n", host.get()));
+ if (aFailureResult) {
+ *aFailureResult = nsISiteSecurityService::ERROR_NO_BACKUP_PIN;
+ }
+ return NS_ERROR_FAILURE;
+ }
}
int64_t expireTime = ExpireTimeFromMaxAge(maxAge);
SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet,
- foundIncludeSubdomains, sha256keys);
+ foundIncludeSubdomains, reportURI, 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);
if (NS_FAILED(rv)) {
SSSLOG(("SSS: failed to set pins for %s\n", host.get()));
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE;
@@ -834,16 +903,20 @@ nsSiteSecurityService::ProcessPKPHeader(
if (aMaxAge != nullptr) {
*aMaxAge = maxAge;
}
if (aIncludeSubdomains != nullptr) {
*aIncludeSubdomains = foundIncludeSubdomains;
}
+ if (aReportURI != nullptr) {
+ reportURI.forget(aReportURI);
+ }
+
return foundUnrecognizedDirective
? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA
: NS_OK;
}
nsresult
nsSiteSecurityService::ProcessSTSHeader(nsIURI* aSourceURI,
const char* aHeader,
@@ -855,23 +928,25 @@ nsSiteSecurityService::ProcessSTSHeader(
if (aFailureResult) {
*aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN;
}
SSSLOG(("SSS: processing HSTS header '%s'", aHeader));
const uint32_t aType = nsISiteSecurityService::HEADER_HSTS;
bool foundMaxAge = false;
bool foundIncludeSubdomains = false;
+ bool foundReportUri = false;
bool foundUnrecognizedDirective = false;
uint64_t maxAge = 0;
nsTArray<nsCString> unusedSHA256keys; // Required for sane internal interface
- uint32_t sssrv = ParseSSSHeaders(aType, aHeader, foundIncludeSubdomains,
- foundMaxAge, foundUnrecognizedDirective,
- maxAge, unusedSHA256keys);
+ uint32_t sssrv = ParseSSSHeaders(aSourceURI, aType, aHeader, foundIncludeSubdomains,
+ foundMaxAge, foundReportUri,
+ foundUnrecognizedDirective,
+ maxAge, unusedSHA256keys, nullptr);
if (sssrv != nsISiteSecurityService::Success) {
if (aFailureResult) {
*aFailureResult = sssrv;
}
return NS_ERROR_FAILURE;
}
// after processing all the directives, make sure we came across max-age
@@ -1182,16 +1257,19 @@ nsSiteSecurityService::GetKeyPinsForHost
nsAutoCString host(PublicKeyPinningService::CanonicalizeHostname(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);
+ // TODO: shouldn't we check private first, so updates made in private browsing
+ // mode are saved?
+
// decode now
SiteHPKPState foundEntry(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)) {
// not in private storage, try dynamic preload
@@ -1211,16 +1289,17 @@ nsSiteSecurityService::GetKeyPinsForHost
*afound = true;
return NS_OK;
}
NS_IMETHODIMP
nsSiteSecurityService::SetKeyPins(const char* aHost, bool aIncludeSubdomains,
int64_t aExpires, uint32_t aPinCount,
const char** aSha256Pins,
+ nsIURI* aReportURI,
bool aIsPreload,
/*out*/ bool* aResult)
{
// Child processes are not allowed direct access to this.
if (!XRE_IsParentProcess()) {
MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::SetKeyPins");
}
@@ -1235,17 +1314,17 @@ nsSiteSecurityService::SetKeyPins(const
nsAutoCString pin(aSha256Pins[i]);
SSSLOG(("SetPins pin=%s\n", pin.get()));
if (!stringIsBase64EncodingOf256bitValue(pin)) {
return NS_ERROR_INVALID_ARG;
}
sha256keys.AppendElement(pin);
}
SiteHPKPState dynamicEntry(aExpires, SecurityPropertySet,
- aIncludeSubdomains, sha256keys);
+ aIncludeSubdomains, aReportURI, sha256keys);
// we always store data in permanent storage (ie no flags)
nsAutoCString host(PublicKeyPinningService::CanonicalizeHostname(aHost));
return SetHPKPState(host.get(), dynamicEntry, 0, aIsPreload);
}
nsresult
nsSiteSecurityService::SetHPKPState(const char* aHost, SiteHPKPState& entry,
uint32_t aFlags, bool aIsPreload)
--- a/security/manager/ssl/nsSiteSecurityService.h
+++ b/security/manager/ssl/nsSiteSecurityService.h
@@ -40,29 +40,31 @@ enum SecurityPropertyState {
/**
* 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)
+ * - A report URI (nsIURI*)
* - An array of sha-256 hashed base 64 encoded fingerprints of required keys
*/
class SiteHPKPState
{
public:
SiteHPKPState();
explicit SiteHPKPState(nsCString& aStateString);
SiteHPKPState(PRTime aExpireTime, SecurityPropertyState aState,
- bool aIncludeSubdomains, nsTArray<nsCString>& SHA256keys);
+ bool aIncludeSubdomains, nsIURI* aReportURI, nsTArray<nsCString>& SHA256keys);
PRTime mExpireTime;
SecurityPropertyState mState;
bool mIncludeSubdomains;
+ nsCOMPtr<nsIURI> mReportURI;
nsTArray<nsCString> mSHA256keys;
bool IsExpired(mozilla::pkix::Time aTime)
{
if (aTime > mozilla::pkix::TimeFromEpochInSeconds(mExpireTime /
PR_MSEC_PER_SEC)) {
return true;
}
@@ -129,25 +131,25 @@ protected:
private:
nsresult GetHost(nsIURI *aURI, nsACString &aResult);
nsresult SetHSTSState(uint32_t aType, nsIURI* aSourceURI, int64_t maxage,
bool includeSubdomains, uint32_t flags,
SecurityPropertyState aHSTSState);
nsresult ProcessHeaderInternal(uint32_t aType, nsIURI* aSourceURI,
const char* aHeader, nsISSLStatus* aSSLStatus,
uint32_t aFlags, uint64_t* aMaxAge,
- bool* aIncludeSubdomains,
+ bool* aIncludeSubdomains, nsIURI** aReportURI,
uint32_t* aFailureResult);
nsresult ProcessSTSHeader(nsIURI* aSourceURI, const char* aHeader,
uint32_t flags, uint64_t* aMaxAge,
bool* aIncludeSubdomains, uint32_t* aFailureResult);
nsresult ProcessPKPHeader(nsIURI* aSourceURI, const char* aHeader,
nsISSLStatus* aSSLStatus, uint32_t flags,
uint64_t* aMaxAge, bool* aIncludeSubdomains,
- uint32_t* aFailureResult);
+ nsIURI** aReportUri, uint32_t* aFailureResult);
nsresult SetHPKPState(const char* aHost, SiteHPKPState& entry, uint32_t flags,
bool aIsPreload);
const nsSTSPreload *GetPreloadListEntry(const char *aHost);
uint64_t mMaxMaxAge;
bool mUsePreloadList;
int64_t mPreloadListTimeOffset;
--- a/security/manager/ssl/tests/compiled/TestSTSParser.cpp
+++ b/security/manager/ssl/tests/compiled/TestSTSParser.cpp
@@ -30,50 +30,71 @@
fail(__VA_ARGS__); \
return false; \
} \
PR_END_MACRO
bool
TestSuccess(const char* hdr, bool extraTokens,
uint64_t expectedMaxAge, bool expectedIncludeSubdomains,
- nsISiteSecurityService* sss)
+ const char* expectedReportUri, nsISiteSecurityService* sss)
{
nsCOMPtr<nsIURI> dummyUri;
- nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/bar.html");
+ nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/path/bar.html");
EXPECT_SUCCESS(rv, "Failed to create URI");
uint64_t maxAge = 0;
bool includeSubdomains = false;
- rv = sss->UnsafeProcessHeader(nsISiteSecurityService::HEADER_HSTS, dummyUri,
- hdr, 0, &maxAge, &includeSubdomains, nullptr);
+ nsCOMPtr<nsIURI> reportUri;
+ rv = sss->UnsafeProcessHeader(nsISiteSecurityService::HEADER_HPKP, dummyUri,
+ hdr, 0, &maxAge, &includeSubdomains, getter_AddRefs(reportUri), nullptr);
EXPECT_SUCCESS(rv, "Failed to process valid header: %s", hdr);
REQUIRE_EQUAL(maxAge, expectedMaxAge, "Did not correctly parse maxAge");
REQUIRE_EQUAL(includeSubdomains, expectedIncludeSubdomains, "Did not correctly parse presence/absence of includeSubdomains");
+ if (expectedReportUri != nullptr && reportUri != nullptr) {
+ nsresult rv;
+ nsAutoCString spec;
+ rv = reportUri->GetAsciiSpec(spec);
+ REQUIRE_EQUAL(NS_OK, rv, "Could not retrieve ASCII Spec for report URI");
+ REQUIRE_EQUAL(spec, expectedReportUri, "Did not correctly parse reportUri, got '%s' expected '%s'", spec.BeginReading(), expectedReportUri);
+ } else if (expectedReportUri != nullptr && reportUri == nullptr) {
+ fail("Did not correctly parse reportUri");
+ return false;
+ }
+
if (extraTokens) {
REQUIRE_EQUAL(rv, NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA,
"Extra tokens were expected when parsing, but were not encountered.");
} else {
REQUIRE_EQUAL(rv, NS_OK, "Unexpected tokens found during parsing.");
}
passed(hdr);
return true;
}
+bool
+TestSuccess(const char* hdr, bool extraTokens,
+ uint64_t expectedMaxAge, bool expectedIncludeSubdomains,
+ nsISiteSecurityService* sss)
+{
+ return TestSuccess(hdr, extraTokens, expectedMaxAge,
+ expectedIncludeSubdomains, nullptr, sss);
+}
+
bool TestFailure(const char* hdr,
nsISiteSecurityService* sss)
{
nsCOMPtr<nsIURI> dummyUri;
- nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/bar.html");
+ nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/path/bar.html");
EXPECT_SUCCESS(rv, "Failed to create URI");
- rv = sss->UnsafeProcessHeader(nsISiteSecurityService::HEADER_HSTS, dummyUri,
+ rv = sss->UnsafeProcessHeader(nsISiteSecurityService::HEADER_HPKP, dummyUri,
hdr, 0, nullptr, nullptr, nullptr);
EXPECT_FAILURE(rv, "Parsed invalid header: %s", hdr);
passed(hdr);
return true;
}
int
@@ -114,37 +135,74 @@ main(int32_t argc, char *argv[])
rvs.AppendElement(TestSuccess("max-age = 100 ", false, 100, false, sss));
rvs.AppendElement(TestSuccess("maX-aGe=100", false, 100, false, sss));
rvs.AppendElement(TestSuccess("MAX-age =100", false, 100, false, sss));
rvs.AppendElement(TestSuccess("max-AGE=100", false, 100, false, sss));
rvs.AppendElement(TestSuccess("Max-Age = 100 ", false, 100, false, sss));
rvs.AppendElement(TestSuccess("MAX-AGE = 100 ", false, 100, false, sss));
+ rvs.AppendElement(TestSuccess("maX-aGe=\"100\"", false, 100, false, sss));
+ rvs.AppendElement(TestSuccess("MAX-age =\"100\"", false, 100, false, sss));
+ rvs.AppendElement(TestSuccess("max-AGE=\"100\"", false, 100, false, sss));
+ rvs.AppendElement(TestSuccess("Max-Age = \"100\" ", false, 100, false, sss));
+ rvs.AppendElement(TestSuccess("MAX-AGE = \"100\" ", false, 100, false, sss));
+
rvs.AppendElement(TestSuccess("max-age=100;includeSubdomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess("max-age=100\t; includeSubdomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess(" max-age=100; includeSubdomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, 100, true, sss));
+ rvs.AppendElement(TestSuccess("max-age=\"100\";includeSubdomains", false, 100, true, sss));
+ rvs.AppendElement(TestSuccess("max-age=\"100\"\t; includeSubdomains", false, 100, true, sss));
+ rvs.AppendElement(TestSuccess(" max-age=\"100\"; includeSubdomains", false, 100, true, sss));
+ rvs.AppendElement(TestSuccess("max-age = \"100\" ; includeSubdomains", false, 100, true, sss));
+ rvs.AppendElement(TestSuccess("max-age = \"100\" ; includeSubdomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess("maX-aGe=100; includeSUBDOMAINS", false, 100, true, sss));
rvs.AppendElement(TestSuccess("MAX-age =100; includeSubDomains", false, 100, true, sss));
rvs.AppendElement(TestSuccess("max-AGE=100; iNcLuDeSuBdoMaInS", false, 100, true, sss));
rvs.AppendElement(TestSuccess("Max-Age = 100; includesubdomains ", false, 100, true, sss));
rvs.AppendElement(TestSuccess("INCLUDESUBDOMAINS;MaX-AgE = 100 ", false, 100, true, sss));
// Turns out, the actual directive is entirely optional (hence the
// trailing semicolon)
rvs.AppendElement(TestSuccess("max-age=100;includeSubdomains;", true, 100, true, sss));
// these are weird tests, but are testing that some extended syntax is
// still allowed (but it is ignored)
rvs.AppendElement(TestSuccess("max-age=100 ; includesubdomainsSomeStuff", true, 100, false, sss));
- rvs.AppendElement(TestSuccess("\r\n\t\t \tcompletelyUnrelated = foobar; max-age= 34520103 \t \t; alsoUnrelated;asIsThis;\tincludeSubdomains\t\t \t", true, 34520103, true, sss));
+ rvs.AppendElement(TestSuccess("\r\n\t\t \tcompletelyUnrelated = foobar; max-age= 5184000 \t \t; alsoUnrelated;asIsThis;\tincludeSubdomains\t\t \t", true, 5184000, true, sss));
rvs.AppendElement(TestSuccess("max-age=100; unrelated=\"quoted \\\"thingy\\\"\"", true, 100, false, sss));
+ //Test capitalization
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"https://example.com/pkp\"", false, 100, false, "https://example.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"https://example.com\"", false, 100, false, "https://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"http://example.com\"", false, 100, false, "http://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"http://example.com/bob.JMP\"", false, 100, false, "http://example.com/bob.JMP", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;REPORT-uri=\"https://example.com/pkp\"", false, 100, false, "https://example.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;REPORT-uri=\"https://example.com\"", false, 100, false, "https://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;REPORT-uri=\"http://example.com\"", false, 100, false, "http://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;REPORT-uri=\"http://example.com/bob.JMP\"", false, 100, false, "http://example.com/bob.JMP", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-URI=\"https://example.com/pkp\"", false, 100, false, "https://example.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-URI=\"https://example.com\"", false, 100, false, "https://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-URI=\"http://example.com\"", false, 100, false, "http://example.com/", sss));
+
+ //Test spacing
+ rvs.AppendElement(TestSuccess("max-age=100; report-URI=\"http://example.com/bob.JMP\"", false, 100, false, "http://example.com/bob.JMP", sss));
+ rvs.AppendElement(TestSuccess("max-age=100; report-URI=\"https://example.com/pkp\"", false, 100, false, "https://example.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100; report-URI= \"https://example.com\"", false, 100, false, "https://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100; report-URI=\"http://example.com\"", false, 100, false, "http://example.com/", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-URI = \"http://example.com/bob.JMP\"", false, 100, false, "http://example.com/bob.JMP", sss));
+ rvs.AppendElement(TestSuccess("max-age=100; report-URI = \"https://example.com\"", false, 100, false, "https://example.com/", sss));
+
+ //Test relative URIs
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"/pkp\"", false, 100, false, "https://foo.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"../pkp\"", false, 100, false, "https://foo.com/pkp", sss));
+ rvs.AppendElement(TestSuccess("max-age=100;report-uri=\"pkp\"", false, 100, false, "https://foo.com/path/pkp", sss));
+
rv0 = rvs.Contains(false) ? 1 : 0;
if (rv0 == 0)
passed("Successfully Parsed STS headers with mixed case and LWS");
rvs.Clear();
// SHOULD FAIL:
printf("*** Attempting to parse invalid STS headers (should not parse)...\n");
@@ -175,15 +233,31 @@ main(int32_t argc, char *argv[])
// All directives MUST appear only once in an STS header field.
rvs.AppendElement(TestFailure("max-age=100; max-age=200", sss));
rvs.AppendElement(TestFailure("includeSubdomains; max-age=200; includeSubdomains", sss));
rvs.AppendElement(TestFailure("max-age=200; includeSubdomains; includeSubdomains", sss));
// The includeSubdomains directive is valueless.
rvs.AppendElement(TestFailure("max-age=100; includeSubdomains=unexpected", sss));
// LWS must have at least one space or horizontal tab
rvs.AppendElement(TestFailure("\r\nmax-age=200", sss));
+ //Invalid quotes
+
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=https://example.com/pkp\"", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=\"https://example.com", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri='http://example.com'", sss));
+ //Other errors
+ rvs.AppendElement(TestFailure("max-age=100;report-uri:\"https://example.com/pkp\"", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri \"https://example.com/pkp\"", sss));
+ rvs.AppendElement(TestFailure("report-uri=\"https://example.com/pkp\";max-age=100;report-uri=\"https://example.com/pkp\"", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=\"\"", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=\"\";includeSubdomains", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri=;includeSubdomains", sss));
+ rvs.AppendElement(TestFailure("max-age=100;report-uri;includeSubdomains", sss));
+
rv1 = rvs.Contains(false) ? 1 : 0;
if (rv1 == 0)
passed("Avoided parsing invalid STS headers");
return (rv0 + rv1);
}
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/gtest/HPKPTest.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "nsIURI.h"
+#include "gtest/gtest.h"
+
+class psm_HPKPTest : public ::testing::Test
+{
+protected:
+ psm_HPKPTest() { }
+};
+
+TEST_F(psm_HPKPTest, TestDecoding)
+{
+ //python -c 'import hashlib;import base64; import sys; sys.stdout.write(base64.b64encode(hashlib.sha256("whate3ver").digest()))'
+
+ //Test old-style with no comma at the end
+ nsCString val1("123456,1,1,hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=");
+ SiteHPKPState a(val1);
+ EXPECT_EQ(123456, a.mExpireTime);
+ EXPECT_EQ(SecurityPropertySet, a.mState);//State must be one for pins to be parsed
+ EXPECT_TRUE(a.mIncludeSubdomains);
+ EXPECT_TRUE(2 == a.mSHA256keys.Length());
+ EXPECT_TRUE(nsCString("hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=") == a.mSHA256keys[0]);
+ EXPECT_TRUE(nsCString("pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=") == a.mSHA256keys[1]);
+ EXPECT_EQ(nullptr, a.mReportURI);
+
+ //Test new-style with comma at the end but no report-uri
+ nsCString val2("123456,1,0,hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=,");
+ SiteHPKPState b(val2);
+ EXPECT_EQ(123456, b.mExpireTime);
+ EXPECT_EQ(SecurityPropertySet, b.mState);//State must be one for pins to be parsed
+ EXPECT_EQ(false, b.mIncludeSubdomains);
+ EXPECT_TRUE(2 == b.mSHA256keys.Length());
+ EXPECT_TRUE(nsCString("hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=") == b.mSHA256keys[0]);
+ EXPECT_TRUE(nsCString("pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=") == b.mSHA256keys[1]);
+ EXPECT_EQ(nullptr, b.mReportURI);
+
+ //Test new-style with report-uri
+ nsCString val3("123456,1,1,hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=,https://example.com/pkp");
+ SiteHPKPState c(val3);
+ EXPECT_EQ(123456, c.mExpireTime);
+ EXPECT_EQ(SecurityPropertySet, c.mState);//State must be one for pins to be parsed
+ EXPECT_TRUE(c.mIncludeSubdomains);
+ EXPECT_TRUE(2 == c.mSHA256keys.Length());
+ EXPECT_TRUE(nsCString("hXOPj5p/GwS1MpxZDry55CWSXG0JhAicQ6Ai3k8ZwoE=") == c.mSHA256keys[0]);
+ EXPECT_TRUE(nsCString("pIg4hW3UeZefhBM+NU6KF1iXA6zPBcO3C8pGXy+DuJQ=") == c.mSHA256keys[1]);
+
+ nsresult rv;
+ nsAutoCString spec;
+ rv = c.mReportURI->GetAsciiSpec(spec);
+ EXPECT_EQ(NS_OK, rv);
+ EXPECT_TRUE(nsCString("https://example.com/pkp") == spec);
+}
\ No newline at end of file
--- a/security/manager/ssl/tests/gtest/moz.build
+++ b/security/manager/ssl/tests/gtest/moz.build
@@ -2,16 +2,17 @@
# vim: set filetype=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/.
SOURCES += [
'DataStorageTest.cpp',
'DeserializeCertTest.cpp',
+ 'HPKPTest.cpp',
'MD4Test.cpp',
'OCSPCacheTest.cpp',
'TLSIntoleranceTest.cpp',
]
LOCAL_INCLUDES += [
'/security/certverifier',
'/security/manager/ssl',
--- a/security/manager/ssl/tests/unit/test_pinning_dynamic.js
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic.js
@@ -209,17 +209,17 @@ function checkStateRead(aSubject, aTopic
// Check a dynamic addition works as expected
// first, it should succeed with the badCA - because there's no pin
checkOK(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
// then we add a pin, and we should get a failure (ensuring the expiry is
// after the test timeout)
gSSService.setKeyPins("b.preload.example.com", false,
new Date().getTime() + 1000000, 2,
- [NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH], true);
+ [NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH], null, true);
checkFail(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
do_timeout(1250, checkExpiredState);
}
function checkExpiredState() {
checkOK(certFromFile('a.pinning2.example.com-badca'), "a.pinning2.example.com");
checkOK(certFromFile('a.pinning2.example.com-pinningroot'), "a.pinning2.example.com");
--- a/security/manager/ssl/tests/unit/test_sss_savestate.js
+++ b/security/manager/ssl/tests/unit/test_sss_savestate.js
@@ -1,22 +1,23 @@
/* 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";
// The purpose of this test is to see that the site security service properly
// writes its state file.
-const EXPECTED_ENTRIES = 6;
+const EXPECTED_ENTRIES = 7;
const EXPECTED_HSTS_COLUMNS = 3;
-const EXPECTED_HPKP_COLUMNS = 4;
+const EXPECTED_HPKP_COLUMNS = 5;
var gProfileDir = null;
const NON_ISSUED_KEY_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+const REPORT_URI = "https://report-uri.example.com/";
// For reference, the format of the state file is a list of:
// <domain name> <expiration time in milliseconds>,<sts status>,<includeSubdomains>
// separated by newlines ('\n')
function checkStateWritten(aSubject, aTopic, aData) {
if (aData == PRELOAD_STATE_FILE_NAME) {
return;
@@ -86,30 +87,43 @@ function checkStateWritten(aSubject, aTo
}
if (sites["dynamic-pin.example.com:HPKP"][1] != 1) {
return;
}
if (sites["dynamic-pin.example.com:HPKP"][2] != 1) {
return;
}
equal(sites["dynamic-pin.example.com:HPKP"][3], NON_ISSUED_KEY_HASH);
+ if (sites["dynamic-pin2.example.com:HPKP"][1] != 1) {
+ return;
+ }
+ if (sites["dynamic-pin2.example.com:HPKP"][2] != 1) {
+ return;
+ }
+ equal(sites["dynamic-pin2.example.com:HPKP"][3], NON_ISSUED_KEY_HASH);
+ equal(sites["dynamic-pin2.example.com:HPKP"][4], REPORT_URI);
do_test_finished();
}
function run_test() {
Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
gProfileDir = do_get_profile();
let SSService = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
// Put an HPKP entry
SSService.setKeyPins("dynamic-pin.example.com", true,
new Date().getTime() + 1000000, 1,
[NON_ISSUED_KEY_HASH]);
+ // Put an HPKP entry with report uri
+ SSService.setKeyPins("dynamic-pin2.example.com", true,
+ new Date().getTime() + 1000000, 1,
+ [NON_ISSUED_KEY_HASH], Services.io.newURI(REPORT_URI, null, null));
+
let uris = [ Services.io.newURI("http://bugzilla.mozilla.org", null, null),
Services.io.newURI("http://a.example.com", null, null),
Services.io.newURI("http://b.example.com", null, null),
Services.io.newURI("http://c.c.example.com", null, null),
Services.io.newURI("http://d.example.com", null, null) ];
for (let i = 0; i < 1000; i++) {
let uriIndex = i % uris.length;