Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions fido2/rpid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
12 changes: 12 additions & 0 deletions tests/test_rpid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading