Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

# 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/.
# Each test injects a synthetic token via nsISSLTokensCacheTest, triggers an
# operation that must not evict it, and asserts the count is unchanged.
# No TLS server required.
import time
from pathlib import Path
from marionette_harness import MarionetteTestCase
NORMAL_KEY = "test.example.com:443"
PBM_KEY = "test.example.com:443^privateBrowsingId=1"
HOST_A_KEY = "hostA.example.com:443"
HOST_B_KEY = "other.example.com:443"
HOST_A_CONTAINER_KEY = "hostA.example.com:443^userContextId=1"
# A non-cipher security.* pref: changing it must not clear the token cache.
INNOCENT_SECURITY_PREF = "security.certerrors.mitm.priming.enabled"
_PUT_SCRIPT = """
const [key, resolve] = arguments;
const c = Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(Ci.nsISSLTokensCacheTest);
c.putSSLTokenForTest(key);
resolve(c.countSSLTokens());
"""
_COUNT_SCRIPT = """
const [resolve] = arguments;
resolve(Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(Ci.nsISSLTokensCacheTest).countSSLTokens());
"""
class SSLTokenCacheClearingBugsTestCase(MarionetteTestCase):
"""Various operations must not incorrectly evict cached TLS session tokens."""
def setUp(self):
super().setUp()
self.marionette.set_context("chrome")
self.marionette.execute_script(
"Cc['@mozilla.org/network/ssl-tokens-cache;1']"
".getService(Ci.nsISSLTokensCache)"
".clearSSLExternalAndInternalSessionCache();"
)
def _put_token(self, key=NORMAL_KEY):
return self.marionette.execute_async_script(_PUT_SCRIPT, script_args=(key,))
def _count(self):
return self.marionette.execute_async_script(_COUNT_SCRIPT)
def _fire_last_pb_context_exited(self):
self.marionette.execute_script(
"Services.obs.notifyObservers(null, 'last-pb-context-exited', null);"
)
def test_direct_clear_actually_clears(self):
"""Sanity: clearSSLExternalAndInternalSessionCache() empties the cache."""
self.assertEqual(self._put_token(), 1)
self.marionette.execute_script(
"Cc['@mozilla.org/network/ssl-tokens-cache;1']"
".getService(Ci.nsISSLTokensCache)"
".clearSSLExternalAndInternalSessionCache();"
)
self.assertEqual(self._count(), 0)
def test_noop_certoverride_call_preserves_tokens(self):
"""setDisableAllSecurityChecks(false) when already false must not clear tokens."""
self.assertEqual(self._put_token(), 1)
self.marionette.execute_script(
"Cc['@mozilla.org/security/certoverride;1']"
" .getService(Ci.nsICertOverrideService)"
" .setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false);"
)
self.assertEqual(self._count(), 1, "no-op call must not clear the cache")
def test_last_pb_context_exited_preserves_normal_browsing_tokens(self):
"""last-pb-context-exited must not evict normal-browsing tokens."""
self.assertEqual(self._put_token(NORMAL_KEY), 1)
self._fire_last_pb_context_exited()
self.assertEqual(self._count(), 1)
def test_last_pb_context_exited_removes_pbm_tokens(self):
"""last-pb-context-exited must remove private-browsing tokens."""
self.assertEqual(self._put_token(PBM_KEY), 1)
self._fire_last_pb_context_exited()
self.assertEqual(self._count(), 0)
def test_last_pb_context_exited_is_scoped_to_pbm_only(self):
"""last-pb-context-exited with both token kinds: only PBM is removed."""
self._put_token(NORMAL_KEY)
self._put_token(PBM_KEY)
self.assertEqual(self._count(), 2)
self._fire_last_pb_context_exited()
self.assertEqual(self._count(), 1)
def test_application_background_writes_token_file(self):
"""application-background must write the token file to disk."""
# set_pref fires ReconcilePersistence(), which sets up mBackingFile and
# registers the application-background observer.
self.marionette.set_pref("network.ssl_tokens_cache_persistence", True)
cache_file = Path(self.marionette.profile_path) / "ssl_tokens_cache.bin"
self.assertEqual(self._put_token(), 1)
self.marionette.execute_script(
"Services.obs.notifyObservers(null, 'application-background', null);"
)
deadline = time.monotonic() + 5
while not cache_file.exists() and time.monotonic() < deadline:
time.sleep(0.25)
self.assertTrue(
cache_file.exists(), "file must appear after application-background"
)
self.assertGreater(cache_file.stat().st_size, 0, "file must be non-empty")
# Disabling persistence must delete the file immediately.
self.marionette.set_pref("network.ssl_tokens_cache_persistence", False)
deadline = time.monotonic() + 5
while cache_file.exists() and time.monotonic() < deadline:
time.sleep(0.25)
self.assertFalse(
cache_file.exists(),
"file must be deleted when persistence is disabled",
)
def test_non_cipher_security_pref_change_preserves_tokens(self):
"""Changing a non-cipher security.* pref must not evict tokens."""
self.assertEqual(self._put_token(), 1)
original = self.marionette.execute_script(
f"return Services.prefs.getBoolPref('{INNOCENT_SECURITY_PREF}', false);"
)
self.marionette.set_pref(INNOCENT_SECURITY_PREF, not original)
try:
self.assertEqual(
self._count(),
1,
f"{INNOCENT_SECURITY_PREF} change must not clear cache",
)
finally:
self.marionette.set_pref(INNOCENT_SECURITY_PREF, original)
def test_forget_client_auth_for_one_host_preserves_other_tokens(self):
"""ForgetRememberedDecision for host A must not evict tokens for host B."""
self.assertEqual(self._put_token(HOST_B_KEY), 1)
self.marionette.execute_script(
"""
const cars = Cc["@mozilla.org/security/clientAuthRememberService;1"]
.getService(Ci.nsIClientAuthRememberService);
try {
cars.forgetRememberedDecision("hostA.example.com,,");
} catch (_) {}
"""
)
self.assertEqual(self._count(), 1)
def test_delete_decisions_by_host_preserves_other_tokens(self):
"""DeleteDecisionsByHost for host A must not evict tokens for host B."""
self.assertEqual(self._put_token(HOST_B_KEY), 1)
self.marionette.execute_script(
"""
Cc["@mozilla.org/security/clientAuthRememberService;1"]
.getService(Ci.nsIClientAuthRememberService)
.deleteDecisionsByHost("hostA.example.com", {});
"""
)
self.assertEqual(self._count(), 1)
def test_delete_decisions_by_host_evicts_other_containers(self):
"""DeleteDecisionsByHost for host A must evict host A's tokens in
every container, not just the caller's exact origin attributes."""
self.assertEqual(self._put_token(HOST_A_CONTAINER_KEY), 1)
self.marionette.execute_script(
"""
Cc["@mozilla.org/security/clientAuthRememberService;1"]
.getService(Ci.nsIClientAuthRememberService)
.deleteDecisionsByHost("hostA.example.com", {});
"""
)
self.assertEqual(self._count(), 0)
def test_forget_client_auth_key_without_oa_suffix_evicts_all_containers(
self,
):
"""forgetRememberedDecision with a key that has no OA suffix (e.g. a
malformed/legacy key) must not narrow eviction to the default OA."""
self.assertEqual(self._put_token(HOST_A_CONTAINER_KEY), 1)
self.marionette.execute_script(
"""
Cc["@mozilla.org/security/clientAuthRememberService;1"]
.getService(Ci.nsIClientAuthRememberService)
.forgetRememberedDecision("hostA.example.com");
"""
)
self.assertEqual(self._count(), 0)