Bug 1285052 - Enforce a maximum max-age for HPKP r=keeler

MozReview-Commit-ID: 1LD02GkqzTe

--HG--
extra : rebase_source : 127c9dd479b6a48e72da378a4df357a1bba1e6f3
This commit is contained in:
Richard Barnes 2016-07-06 19:16:29 -04:00
Родитель fa054ecd7b
Коммит 8fc350a13d
4 изменённых файлов: 51 добавлений и 16 удалений

Просмотреть файл

@ -92,3 +92,8 @@ pref("security.webauth.u2f_enable_usbtoken", false);
pref("security.ssl.errorReporting.enabled", true);
pref("security.ssl.errorReporting.url", "https://incoming.telemetry.mozilla.org/submit/sslreports/");
pref("security.ssl.errorReporting.automatic", false);
// Impose a maximum age on HPKP headers, to avoid sites getting permanently
// blacking themselves out by setting a bad pin. (60 days by default)
// https://tools.ietf.org/html/rfc7469#section-4.1
pref("security.cert_pinning.max_max_age_seconds", 5184000);

Просмотреть файл

@ -204,8 +204,11 @@ SiteHPKPState::ToString(nsCString& aString)
////////////////////////////////////////////////////////////////////////////////
const uint64_t kSixtyDaysInSeconds = 60 * 24 * 60 * 60;
nsSiteSecurityService::nsSiteSecurityService()
: mUsePreloadList(true)
: mMaxMaxAge(kSixtyDaysInSeconds)
, mUsePreloadList(true)
, mPreloadListTimeOffset(0)
{
}
@ -227,6 +230,10 @@ nsSiteSecurityService::Init()
return NS_ERROR_NOT_SAME_THREAD;
}
mMaxMaxAge = mozilla::Preferences::GetInt(
"security.cert_pinning.max_max_age_seconds", kSixtyDaysInSeconds);
mozilla::Preferences::AddStrongObserver(this,
"security.cert_pinning.max_max_age_seconds");
mUsePreloadList = mozilla::Preferences::GetBool(
"network.stricttransportsecurity.preloadlist", true);
mozilla::Preferences::AddStrongObserver(this,
@ -297,9 +304,9 @@ SetStorageKey(nsAutoCString& storageKey, nsCString& hostname, uint32_t aType)
// Expire times are in millis. Since Headers max-age is in seconds, and
// PR_Now() is in micros, normalize the units at milliseconds.
static int64_t
ExpireTimeFromMaxAge(int64_t maxAge)
ExpireTimeFromMaxAge(uint64_t maxAge)
{
return (PR_Now() / PR_USEC_PER_MSEC) + (maxAge * PR_MSEC_PER_SEC);
return (PR_Now() / PR_USEC_PER_MSEC) + ((int64_t)maxAge * PR_MSEC_PER_SEC);
}
nsresult
@ -508,7 +515,7 @@ ParseSSSHeaders(uint32_t aType,
bool& foundIncludeSubdomains,
bool& foundMaxAge,
bool& foundUnrecognizedDirective,
int64_t& maxAge,
uint64_t& maxAge,
nsTArray<nsCString>& sha256keys)
{
// Strict transport security and Public Key Pinning have very similar
@ -590,12 +597,12 @@ ParseSSSHeaders(uint32_t aType,
}
}
if (PR_sscanf(directive->mValue.get(), "%lld", &maxAge) != 1) {
if (PR_sscanf(directive->mValue.get(), "%llu", &maxAge) != 1) {
SSSLOG(("SSS: could not parse delta-seconds"));
return nsISiteSecurityService::ERROR_INVALID_MAX_AGE;
}
SSSLOG(("SSS: parsed delta-seconds: %lld", maxAge));
SSSLOG(("SSS: parsed delta-seconds: %llu", maxAge));
} else if (directive->mName.Length() == include_subd_var.Length() &&
directive->mName.EqualsIgnoreCase(include_subd_var.get(),
include_subd_var.Length())) {
@ -662,7 +669,7 @@ nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI,
bool foundMaxAge = false;
bool foundIncludeSubdomains = false;
bool foundUnrecognizedDirective = false;
int64_t maxAge = 0;
uint64_t maxAge = 0;
nsTArray<nsCString> sha256keys;
uint32_t sssrv = ParseSSSHeaders(aType, aHeader, foundIncludeSubdomains,
foundMaxAge, foundUnrecognizedDirective,
@ -742,6 +749,11 @@ nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI,
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);
@ -785,7 +797,7 @@ nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI,
int64_t expireTime = ExpireTimeFromMaxAge(maxAge);
SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet,
foundIncludeSubdomains, sha256keys);
SSSLOG(("SSS: about to set pins for %s, expires=%ld now=%ld maxAge=%ld\n",
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);
@ -798,7 +810,7 @@ nsSiteSecurityService::ProcessPKPHeader(nsIURI* aSourceURI,
}
if (aMaxAge != nullptr) {
*aMaxAge = (uint64_t)maxAge;
*aMaxAge = maxAge;
}
if (aIncludeSubdomains != nullptr) {
@ -827,7 +839,7 @@ nsSiteSecurityService::ProcessSTSHeader(nsIURI* aSourceURI,
bool foundMaxAge = false;
bool foundIncludeSubdomains = false;
bool foundUnrecognizedDirective = false;
int64_t maxAge = 0;
uint64_t maxAge = 0;
nsTArray<nsCString> unusedSHA256keys; // Required for sane internal interface
uint32_t sssrv = ParseSSSHeaders(aType, aHeader, foundIncludeSubdomains,
@ -862,7 +874,7 @@ nsSiteSecurityService::ProcessSTSHeader(nsIURI* aSourceURI,
}
if (aMaxAge != nullptr) {
*aMaxAge = (uint64_t)maxAge;
*aMaxAge = maxAge;
}
if (aIncludeSubdomains != nullptr) {
@ -1202,6 +1214,8 @@ nsSiteSecurityService::Observe(nsISupports *subject,
mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0);
mProcessPKPHeadersFromNonBuiltInRoots = mozilla::Preferences::GetBool(
"security.cert_pinning.process_headers_from_non_builtin_roots", false);
mMaxMaxAge = mozilla::Preferences::GetInt(
"security.cert_pinning.max_max_age_seconds", kSixtyDaysInSeconds);
}
return NS_OK;

Просмотреть файл

@ -145,6 +145,7 @@ private:
const nsSTSPreload *GetPreloadListEntry(const char *aHost);
uint64_t mMaxMaxAge;
bool mUsePreloadList;
int64_t mPreloadListTimeOffset;
bool mProcessPKPHeadersFromNonBuiltInRoots;

Просмотреть файл

@ -32,10 +32,11 @@ function checkFailParseInvalidPin(pinValue) {
}, /NS_ERROR_FAILURE/, `Invalid pin "${pinValue}" should be rejected`);
}
function checkPassValidPin(pinValue, settingPin) {
function checkPassValidPin(pinValue, settingPin, expectedMaxAge) {
let sslStatus = new FakeSSLStatus(
certFromFile('a.pinning2.example.com-pinningroot'));
let uri = Services.io.newURI("https://a.pinning2.example.com", null, null);
let maxAge = {};
// setup preconditions for the test, if setting ensure there is no previous
// state, if removing ensure there is a valid pin in place.
@ -49,11 +50,17 @@ function checkPassValidPin(pinValue, settingPin) {
}
try {
gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HPKP, uri,
pinValue, sslStatus, 0);
pinValue, sslStatus, 0, maxAge);
ok(true, "Valid pin should be accepted");
} catch (e) {
ok(false, "Valid pin should have been accepted");
}
// check that maxAge was processed correctly
if (settingPin && expectedMaxAge) {
ok(maxAge.value == expectedMaxAge, `max-age value should be ${expectedMaxAge}`)
}
// after processing ensure that the postconditions are true, if setting
// the host must be pinned, if removing the host must not be pinned
let hostIsPinned = gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
@ -65,14 +72,17 @@ function checkPassValidPin(pinValue, settingPin) {
}
}
function checkPassSettingPin(pinValue) {
return checkPassValidPin(pinValue, true);
function checkPassSettingPin(pinValue, expectedMaxAge) {
return checkPassValidPin(pinValue, true, expectedMaxAge);
}
function checkPassRemovingPin(pinValue) {
return checkPassValidPin(pinValue, false);
}
const MAX_MAX_AGE_SECONDS = 100000;
const GOOD_MAX_AGE_SECONDS = 69403;
const LONG_MAX_AGE_SECONDS = 2 * MAX_MAX_AGE_SECONDS;
const NON_ISSUED_KEY_HASH1 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
const NON_ISSUED_KEY_HASH2 = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ=";
const PINNING_ROOT_KEY_HASH = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=";
@ -81,13 +91,15 @@ const VALID_PIN1 = `pin-sha256="${PINNING_ROOT_KEY_HASH}";`;
const BACKUP_PIN1 = `pin-sha256="${NON_ISSUED_KEY_HASH1}";`;
const BACKUP_PIN2 = `pin-sha256="${NON_ISSUED_KEY_HASH2}";`;
const BROKEN_PIN1 = "pin-sha256=\"jdjsjsjs\";";
const GOOD_MAX_AGE = "max-age=69403;";
const GOOD_MAX_AGE = `max-age=${GOOD_MAX_AGE_SECONDS};`;
const LONG_MAX_AGE = `max-age=${LONG_MAX_AGE_SECONDS};`;
const INCLUDE_SUBDOMAINS = "includeSubdomains;";
const REPORT_URI = "report-uri=\"https://www.example.com/report/\";";
const UNRECOGNIZED_DIRECTIVE = "unreconized-dir=12343;";
function run_test() {
Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 2);
Services.prefs.setIntPref("security.cert_pinning.max_max_age_seconds", MAX_MAX_AGE_SECONDS);
Services.prefs.setBoolPref("security.cert_pinning.process_headers_from_non_builtin_roots", true);
loadCert("pinningroot", "CTu,CTu,CTu");
@ -115,6 +127,9 @@ function run_test() {
checkPassRemovingPin(MAX_AGE_ZERO);
checkPassRemovingPin(MAX_AGE_ZERO + VALID_PIN1);
checkPassSettingPin(GOOD_MAX_AGE + VALID_PIN1 + BACKUP_PIN1, GOOD_MAX_AGE_SECONDS);
checkPassSettingPin(LONG_MAX_AGE + VALID_PIN1 + BACKUP_PIN1, MAX_MAX_AGE_SECONDS);
checkPassRemovingPin(VALID_PIN1 + MAX_AGE_ZERO + VALID_PIN1);
checkPassSettingPin(GOOD_MAX_AGE + VALID_PIN1 + BACKUP_PIN1);
checkPassSettingPin(GOOD_MAX_AGE + VALID_PIN1 + BACKUP_PIN2);