зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
fa054ecd7b
Коммит
8fc350a13d
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче