Bug 1091176 - Parsing and store report-uri for HPKP draft
authorTom Ritter <tom@mozilla.com>
Fri, 28 Oct 2016 11:06:03 -0500
changeset 431021 6ac49fccfcff315870df4b28092c40f8d2f90796
parent 425712 9079d167112122805f99f57bb8856e1b1675af0f
child 535344 7041df216e460e7c8dd252da400ca39530930f29
push id33981
push userbmo:tom@mozilla.com
push dateFri, 28 Oct 2016 16:18:04 +0000
bugs1091176, 88188
milestone52.0a1
Bug 1091176 - Parsing and store report-uri for HPKP - Tests (including new gtest of constructor) - Editing one HSTS test to account for the max-age that PKP enforces - Refactoring ProcessPKPHeader to allow a null SSL Status to support testing - Output Report URI when serializing SiteHPKPState, and support parsing serialized entries with and without it - Address some of the review issues identified by keeler (mostly style) in https://reviewboard.mozilla.org/r/88188/ - Address some of the review issues identified by Cykesiopka, mostly style and test in https://reviewboard.mozilla.org/r/88188/ MozReview-Commit-ID: 9TLdXH3ZrL2
dom/locales/en-US/chrome/security/security.properties
netwerk/protocol/http/nsHttpChannel.cpp
security/manager/ssl/nsISiteSecurityService.idl
security/manager/ssl/nsSiteSecurityService.cpp
security/manager/ssl/nsSiteSecurityService.h
security/manager/ssl/tests/compiled/TestSTSParser.cpp
security/manager/ssl/tests/gtest/HPKPTest.cpp
security/manager/ssl/tests/gtest/moz.build
security/manager/ssl/tests/unit/test_pinning_dynamic.js
security/manager/ssl/tests/unit/test_sss_savestate.js
--- 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;