diff --git a/fido2/rpid.py b/fido2/rpid.py index 9124953..ec276df 100644 --- a/fido2/rpid.py +++ b/fido2/rpid.py @@ -42,11 +42,39 @@ tld_fname = os.path.join(os.path.dirname(__file__), "public_suffix_list.dat") with open(tld_fname, "rb") as f: - suffixes = [ - entry - for entry in (line.decode("utf8").strip() for line in f.readlines()) - if entry and not entry.startswith("//") - ] + _suffix_set: set[str] = set() + _wildcard_set: set[str] = set() + _exception_set: set[str] = set() + for _line in f: + _entry = _line.decode("utf8").strip() + if not _entry or _entry.startswith("//"): + continue + if _entry.startswith("!"): + _exception_set.add(_entry[1:]) + elif _entry.startswith("*."): + _wildcard_set.add(_entry[2:]) + else: + _suffix_set.add(_entry) + +# Keep `suffixes` as a list for backward compatibility +suffixes = sorted( + _suffix_set | {f"*.{w}" for w in _wildcard_set} | {f"!{e}" for e in _exception_set} +) + + +def _is_public_suffix(domain: str) -> bool: + """Check if a domain is a public suffix per the PSL algorithm. + + https://github.com/publicsuffix/list/wiki/Format + """ + if domain in _exception_set: + return False + if domain in _suffix_set: + return True + parts = domain.split(".", 1) + if len(parts) == 2 and parts[1] in _wildcard_set: + return True + return False def verify_rp_id(rp_id: str, origin: str) -> bool: @@ -72,6 +100,6 @@ def verify_rp_id(rp_id: str, origin: str) -> bool: return False if host == rp_id: return True - if host and host.endswith("." + rp_id) and rp_id not in suffixes: + if host and host.endswith("." + rp_id) and not _is_public_suffix(rp_id): return True return False diff --git a/tests/test_rpid.py b/tests/test_rpid.py index ea37303..ab270b7 100644 --- a/tests/test_rpid.py +++ b/tests/test_rpid.py @@ -60,6 +60,18 @@ def test_suffix_list(self): verify_rp_id("example.appspot.com", "https://example.appspot.com") ) + def test_suffix_list_wildcard(self): + # *.bd means any X.bd is a public suffix + self.assertFalse(verify_rp_id("example.bd", "https://evil.example.bd")) + self.assertFalse(verify_rp_id("example.ck", "https://evil.example.ck")) + self.assertFalse(verify_rp_id("example.np", "https://evil.example.np")) + # Exact match still works + self.assertTrue(verify_rp_id("example.bd", "https://example.bd")) + + def test_suffix_list_wildcard_exception(self): + # !www.ck means www.ck is NOT a public suffix despite *.ck + self.assertTrue(verify_rp_id("www.ck", "https://sub.www.ck")) + def test_localhost_http_secure_context(self): # Localhost and subdomains are secure contexts in most browsers self.assertTrue(verify_rp_id("localhost", "http://localhost"))