From 4a44bee968663d75f52aabff56d56cfed19da1c1 Mon Sep 17 00:00:00 2001 From: Shane B Date: Fri, 14 Aug 2026 20:31:14 +0200 Subject: [PATCH 1/4] Verify TLS certificates, instead of disabling verification library-wide `HttpSession` passed `ssl=False` on every request, so every connection this library makes -- authentication, CloudKit, the setup delegate -- was unauthenticated and interceptable by anything on the network path. The reason was one host. `gsa.apple.com` chains to the 2006 Apple Root CA, which certifi does not ship and Mozilla-derived stores do not carry, so it fails verification wherever those are in use while every other Apple host verifies fine: ssl.SSLCertVerificationError: certificate verify failed: self-signed certificate in certificate chain So bundle that one root and add it to the platform defaults, rather than switching verification off. Adding a trust anchor does not weaken the others, and the certificate is checked against its published SHA-256 before it is loaded, so a mangled copy fails immediately rather than at the first handshake. Measured live, comparing certifi's store against the context this builds: gsa.apple.com fails under certifi and passes here; gateway.icloud.com, setup.icloud.com and the escrow proxy pass under both, and were being sent unverified for no reason at all. Deliberately not pinning a leaf or an intermediate. A general TLS path has to survive certificate rotation, and a client that pins one breaks on a day Apple chooses; adding a root is the right strength. One opt-out, on `RemoteAnisetteProvider(allow_unverified_https=True)`, off by default and reaching no request but the one to that server. It exists for a self-hosted Anisette server with a self-signed certificate; a server reached over plain http:// needs nothing, since there is no TLS to verify. It is serialized only when true, so existing account files are unchanged and reloading one cannot silently turn a working self-signed setup into a failing one. --- findmy/reports/anisette.py | 59 ++++++++++++----- findmy/util/http.py | 17 ++++- findmy/util/tls.py | 130 +++++++++++++++++++++++++++++++++++++ tests/test_tls.py | 120 ++++++++++++++++++++++++++++++++++ 4 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 findmy/util/tls.py create mode 100644 tests/test_tls.py diff --git a/findmy/reports/anisette.py b/findmy/reports/anisette.py index 1c5e3e5..c6a78b1 100644 --- a/findmy/reports/anisette.py +++ b/findmy/reports/anisette.py @@ -15,18 +15,27 @@ from typing import BinaryIO, Literal, TypedDict from anisette import Anisette, AnisetteHeaders -from typing_extensions import override +from typing_extensions import Required, override from findmy import util logger = logging.getLogger(__name__) -class RemoteAnisetteMapping(TypedDict): +class RemoteAnisetteMapping(TypedDict, total=False): """JSON mapping representing state of a remote Anisette provider.""" - type: Literal["aniRemote"] - url: str + type: Required[Literal["aniRemote"]] + url: Required[str] + + allow_unverified_https: bool + """ + Only written when it is true, so existing files stay valid and unchanged. + + It has to be written at all because a provider is reconstructed from this: a user whose + own server has a self-signed certificate would otherwise find that saving and reloading + an account silently turned their working setup into a failing one. + """ class LocalAnisetteMapping(TypedDict): @@ -193,13 +202,29 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, util.abc.Serializable[RemoteA _ANISETTE_DATA_VALID_FOR = 30 - def __init__(self, server_url: str) -> None: - """Initialize the provider with URL to te remote server.""" + def __init__(self, server_url: str, *, allow_unverified_https: bool = False) -> None: + """ + Initialize the provider with URL to te remote server. + + :param server_url: Where to fetch Anisette headers from. + :param allow_unverified_https: Skip certificate verification for **this server + only**. Off by default, and the only switch of its kind in the library. + + It exists for one case: an Anisette server you run yourself, over HTTPS, with a + self-signed certificate. A server reached over plain `http://` needs nothing -- + there is no TLS to verify -- and a public server with a real certificate needs + nothing either. Everything Apple-facing is verified regardless of this flag; it + reaches no request but the one to this URL. + + Turning it on means anything on the network path to that server can read and + alter the Anisette data your logins are built from. + """ super().__init__() self._server_url = server_url + self._allow_unverified_https = allow_unverified_https - self._http = util.http.HttpSession() + self._http = util.http.HttpSession(verify_tls=not allow_unverified_https) self._anisette_data: dict[str, str] | None = None self._anisette_data_expires_at: float = 0 @@ -208,13 +233,14 @@ def __init__(self, server_url: str) -> None: @override def to_json(self, dst: str | Path | io.TextIOBase | None = None, /) -> RemoteAnisetteMapping: """See :meth:`BaseAnisetteProvider.serialize`.""" - return util.files.save_and_return_json( - { - "type": "aniRemote", - "url": self._server_url, - }, - dst, - ) + state: RemoteAnisetteMapping = { + "type": "aniRemote", + "url": self._server_url, + } + if self._allow_unverified_https: + state["allow_unverified_https"] = True + + return util.files.save_and_return_json(state, dst) @classmethod @override @@ -228,7 +254,10 @@ def from_json( server_url = val["url"] - return cls(server_url) + return cls( + server_url, + allow_unverified_https=val.get("allow_unverified_https", False), + ) @property @override diff --git a/findmy/util/http.py b/findmy/util/http.py index 780a23d..59c1be8 100644 --- a/findmy/util/http.py +++ b/findmy/util/http.py @@ -13,6 +13,7 @@ from .abc import Closable from .parsers import decode_plist +from .tls import tls_setting logger = logging.getLogger(__name__) @@ -71,11 +72,23 @@ def plist(self) -> dict[Any, Any]: class HttpSession(Closable): """Asynchronous HTTP session manager. For internal use only.""" - def __init__(self) -> None: # noqa: D107 + def __init__(self, *, verify_tls: bool = True) -> None: + """ + Initialize the session. + + :param verify_tls: Whether to verify server certificates. **Leave this alone.** + Every Apple host this library talks to verifies against + :func:`~findmy.util.tls.apple_trust_context`, and turning this off makes each + request readable and alterable by anything on the network path -- a login + included. The one case it exists for is a self-hosted Anisette server with a + self-signed certificate, which is why the switch a caller actually sees is on + that provider rather than here. + """ super().__init__() self._session: ClientSession | None = None self._closed: bool = False + self._ssl = tls_setting(verify=verify_tls) async def _get_session(self) -> ClientSession: if self._closed: @@ -133,7 +146,7 @@ async def request( async with await session.request( method, url, - ssl=False, + ssl=self._ssl, raise_for_status=auto_retry, **options, ) as r: diff --git a/findmy/util/tls.py b/findmy/util/tls.py new file mode 100644 index 0000000..3b94f61 --- /dev/null +++ b/findmy/util/tls.py @@ -0,0 +1,130 @@ +""" +The trust store this library verifies Apple's servers against. + +**`gsa.apple.com` presents a chain rooted at the 2006 "Apple Root CA", which most trust +stores do not carry.** It is absent from certifi's bundle entirely, so anywhere that bundle +is in use -- which is most Linux deployments and every container built from one -- the +authentication host fails verification while every other Apple host this library talks to +verifies fine: + + ssl.SSLCertVerificationError: certificate verify failed: self-signed certificate in + certificate chain + +That one host is the whole reason verification used to be disabled process-wide, on every +request the library makes. The rest -- CloudKit, the setup delegate, the iCloud gateway -- +verify fine today and were being sent unverified for no reason at all, carrying a login +among other things. + +So the fix is to **add** the missing root rather than to stop checking: a context built +from the platform defaults, with Apple's root loaded on top. Adding a trust anchor does not +weaken the ones already there, and this is strictly stricter than what a bare +`create_default_context()` gives on a machine whose store happens to include it. + +Note what this deliberately is *not*. It does not pin a leaf or an intermediate: a general +TLS path has to survive certificate rotation, and a client that pins one breaks on a day +Apple chooses. Adding a root is the right strength here. +""" + +from __future__ import annotations + +import hashlib +import logging +import ssl +from functools import lru_cache + +logger = logging.getLogger(__name__) + +APPLE_ROOT_CA_PEM = """-----BEGIN CERTIFICATE----- +MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET +MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 +MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw +bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ ++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 +XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w +tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW +q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM +aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 +R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE +ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 +d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl +IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 +YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj +b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp +Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc +NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP +y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 +R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg +xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP +IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX +UKqK1drk/NAJBzewdXUh +-----END CERTIFICATE----- +""" +""" +Apple Root CA, serial 2, valid from 2006 to 2035. + +Published by Apple at . Checked against its SHA-256 at +import, so a mangled copy fails immediately rather than at the first handshake. +""" + +APPLE_ROOT_CA_SHA256 = bytes.fromhex( + "b0b1730ecbc7ff4505142c49f1295e6eda6bcaed7e2c68c5be91b5a11001f024", +) +"""What the root above must hash to. A certificate is only a trust anchor once it does.""" + + +class TlsError(Exception): + """Raised when the trust store cannot be built.""" + + +def _verified_root() -> str: + """Return the bundled root, having checked it is the certificate it claims to be.""" + digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(APPLE_ROOT_CA_PEM)).digest() + if digest != APPLE_ROOT_CA_SHA256: + msg = ( + f"The bundled Apple root hashes to {digest.hex()}, not" + f" {APPLE_ROOT_CA_SHA256.hex()}. Refusing to trust it." + ) + raise TlsError(msg) + return APPLE_ROOT_CA_PEM + + +@lru_cache(maxsize=1) +def apple_trust_context() -> ssl.SSLContext: + """ + Build the context every request to Apple is verified against. + + The platform's own trust store plus Apple's 2006 root, which most stores lack and which + `gsa.apple.com` chains to. Cached because constructing one reads certificates from disk, + and that is blocking work best not done per request. + """ + context = ssl.create_default_context() + try: + context.load_verify_locations(cadata=_verified_root()) + except ssl.SSLError as e: + msg = f"Could not load the bundled Apple root: {e}" + raise TlsError(msg) from None + + return context + + +def tls_setting(*, verify: bool) -> ssl.SSLContext | bool: + """ + Resolve what to pass as aiohttp's `ssl` argument. + + :param verify: False disables certificate verification entirely, which makes the + connection interceptable by anything on the network path. There is exactly one + legitimate reason to do it -- an Anisette server of one's own with a self-signed + certificate -- and it is exposed there and nowhere else. + """ + if verify: + return apple_trust_context() + + logger.warning( + "TLS certificate verification is disabled for this session. Anything on the" + " network path can read and alter these requests.", + ) + return False diff --git a/tests/test_tls.py b/tests/test_tls.py new file mode 100644 index 0000000..75383d8 --- /dev/null +++ b/tests/test_tls.py @@ -0,0 +1,120 @@ +"""Tests for the trust store, and for verification being on unless it is turned off.""" + +from __future__ import annotations + +import hashlib +import ssl + +import pytest + +from findmy.reports.anisette import RemoteAnisetteProvider +from findmy.util.http import HttpSession +from findmy.util.tls import ( + APPLE_ROOT_CA_PEM, + APPLE_ROOT_CA_SHA256, + TlsError, + apple_trust_context, + tls_setting, +) + +def _common_names(context: ssl.SSLContext) -> set[str]: + """Every trust anchor the context holds, by common name.""" + return { + value + for certificate in context.get_ca_certs() + for group in certificate.get("subject", ()) + for key, value in group + if key == "commonName" + } + + +def test_the_bundled_root_is_the_certificate_it_claims_to_be() -> None: + digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(APPLE_ROOT_CA_PEM)).digest() + + assert digest == APPLE_ROOT_CA_SHA256 + assert digest.hex() == "b0b1730ecbc7ff4505142c49f1295e6eda6bcaed7e2c68c5be91b5a11001f024" + + +def test_a_root_that_does_not_match_its_fingerprint_is_refused( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # A trust anchor is only an anchor once it hashes to what it should. Catching a mangled + # copy here means failing at import rather than at the first handshake, where it would + # read as Apple's server being unreachable. + monkeypatch.setattr("findmy.util.tls.APPLE_ROOT_CA_SHA256", b"\x00" * 32) + apple_trust_context.cache_clear() + + with pytest.raises(TlsError, match="Refusing to trust it"): + apple_trust_context() + + apple_trust_context.cache_clear() + + +def test_the_context_adds_apples_root_to_the_platforms_own() -> None: + context = apple_trust_context() + + assert "Apple Root CA" in _common_names(context) + # The check above is only worth making if it can fail: an empty context has no anchors, + # and this is what distinguishes "the root was loaded" from "the helper always says + # yes". The platform store cannot serve as the negative case -- macOS ships this root + # and Mozilla-derived stores do not, which is the whole reason for bundling it. + assert "Apple Root CA" not in _common_names(ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)) + + # Added *on top of* the defaults, not instead of them: adding a trust anchor does not + # weaken the ones already there, and a context holding only Apple's root would refuse + # every other host this library talks to. + assert len(context.get_ca_certs()) > 1 + + assert context.verify_mode == ssl.CERT_REQUIRED + assert context.check_hostname + + +def test_verification_is_on_unless_it_is_turned_off() -> None: + assert tls_setting(verify=True) is apple_trust_context() + assert tls_setting(verify=False) is False + + +def test_disabling_verification_says_so(caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING"): + tls_setting(verify=False) + + assert "network path" in caplog.text + + +# These construct a session without ever making a request, so no aiohttp session is +# created and there is nothing to close -- which keeps them synchronous. + + +def test_an_http_session_verifies_by_default() -> None: + assert HttpSession()._ssl is apple_trust_context() # noqa: SLF001 + + +def test_an_http_session_can_be_told_not_to() -> None: + assert HttpSession(verify_tls=False)._ssl is False # noqa: SLF001 + + +def test_an_anisette_provider_verifies_by_default() -> None: + provider = RemoteAnisetteProvider("https://ani.example/") + + assert provider._http._ssl is apple_trust_context() # noqa: SLF001 + assert provider.to_json() == {"type": "aniRemote", "url": "https://ani.example/"} + + +def test_the_opt_in_reaches_the_session_and_survives_a_round_trip() -> None: + # It has to be serialized, or saving and reloading an account would silently turn a + # working self-signed setup into a failing one. + provider = RemoteAnisetteProvider("https://ani.example/", allow_unverified_https=True) + assert provider._http._ssl is False # noqa: SLF001 + + state = provider.to_json() + assert state["allow_unverified_https"] is True + + assert RemoteAnisetteProvider.from_json(state)._http._ssl is False # noqa: SLF001 + + +def test_an_older_saved_provider_still_loads_and_verifies() -> None: + # A file written before this option existed carries no flag, and must keep working -- + # verifying, which is the safe direction for a default to move in. + restored = RemoteAnisetteProvider.from_json({"type": "aniRemote", "url": "https://a/"}) + + assert restored._http._ssl is apple_trust_context() # noqa: SLF001 From f8e2f624f832dfb61940dfac182955a4bb0f1fff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:57:08 +0000 Subject: [PATCH 2/4] [pre-commit.ci lite] apply automatic fixes --- tests/test_tls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tls.py b/tests/test_tls.py index 75383d8..f192a0c 100644 --- a/tests/test_tls.py +++ b/tests/test_tls.py @@ -17,6 +17,7 @@ tls_setting, ) + def _common_names(context: ssl.SSLContext) -> set[str]: """Every trust anchor the context holds, by common name.""" return { From 03ca4c1ee7c0d2085d453fce9d69e99c34d7b00b Mon Sep 17 00:00:00 2001 From: Shane B Date: Sat, 15 Aug 2026 10:13:37 +0200 Subject: [PATCH 3/4] Fix the two CI failures in these tests, not in the code they test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **`test (3.14)`**: the test asserted `len(context.get_ca_certs()) > 1` to show Apple's root was added on top of the platform's store rather than instead of it. Whether a platform loads its store eagerly from a file or lazily from a hashed directory differs by OS and by Python build — the runner returns one entry where this machine returns hundreds — so that count measured the machine, not the code. It now asserts the default context's anchors are a subset of this one's, which is the property meant and holds under both loading strategies. **Pre-commit**: ten missing docstrings and an import order in the new test file. Worth recording why they were invisible locally: `pyproject.toml` excludes `tests/`, but pre-commit passes explicit filenames and ruff only honours `exclude` during traversal unless `force-exclude` is set — so CI lints tests and `ruff check findmy examples scripts` never did. Reproduced with `ruff check $(git ls-files '*.py')`, which is what the hook actually does. Verified against the version that failed: the suite passes on 3.14. --- tests/test_tls.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_tls.py b/tests/test_tls.py index f192a0c..30db3d1 100644 --- a/tests/test_tls.py +++ b/tests/test_tls.py @@ -30,6 +30,7 @@ def _common_names(context: ssl.SSLContext) -> set[str]: def test_the_bundled_root_is_the_certificate_it_claims_to_be() -> None: + """Test that the bundled root hashes to its published fingerprint.""" digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(APPLE_ROOT_CA_PEM)).digest() assert digest == APPLE_ROOT_CA_SHA256 @@ -39,6 +40,7 @@ def test_the_bundled_root_is_the_certificate_it_claims_to_be() -> None: def test_a_root_that_does_not_match_its_fingerprint_is_refused( monkeypatch: pytest.MonkeyPatch, ) -> None: + """Test that a root failing its own fingerprint is refused rather than trusted.""" # A trust anchor is only an anchor once it hashes to what it should. Catching a mangled # copy here means failing at import rather than at the first handshake, where it would # read as Apple's server being unreachable. @@ -52,6 +54,7 @@ def test_a_root_that_does_not_match_its_fingerprint_is_refused( def test_the_context_adds_apples_root_to_the_platforms_own() -> None: + """Test that Apple's root is added on top of the platform's trust store.""" context = apple_trust_context() assert "Apple Root CA" in _common_names(context) @@ -64,18 +67,25 @@ def test_the_context_adds_apples_root_to_the_platforms_own() -> None: # Added *on top of* the defaults, not instead of them: adding a trust anchor does not # weaken the ones already there, and a context holding only Apple's root would refuse # every other host this library talks to. - assert len(context.get_ca_certs()) > 1 + # + # Compared as a subset rather than by counting. Whether the platform's own store is + # loaded eagerly from a file or lazily from a hashed directory differs by platform and + # by Python build -- `get_ca_certs()` returns one entry on a runner that loads lazily + # and hundreds on one that does not -- so a count measures the machine, not this code. + assert _common_names(ssl.create_default_context()) <= _common_names(context) assert context.verify_mode == ssl.CERT_REQUIRED assert context.check_hostname def test_verification_is_on_unless_it_is_turned_off() -> None: + """Test that the default resolves to the verifying context.""" assert tls_setting(verify=True) is apple_trust_context() assert tls_setting(verify=False) is False def test_disabling_verification_says_so(caplog: pytest.LogCaptureFixture) -> None: + """Test that turning verification off is logged rather than silent.""" with caplog.at_level("WARNING"): tls_setting(verify=False) @@ -87,14 +97,17 @@ def test_disabling_verification_says_so(caplog: pytest.LogCaptureFixture) -> Non def test_an_http_session_verifies_by_default() -> None: + """Test that a session verifies unless told otherwise.""" assert HttpSession()._ssl is apple_trust_context() # noqa: SLF001 def test_an_http_session_can_be_told_not_to() -> None: + """Test that a session can be built without verification.""" assert HttpSession(verify_tls=False)._ssl is False # noqa: SLF001 def test_an_anisette_provider_verifies_by_default() -> None: + """Test that a provider verifies, and writes no flag when it does.""" provider = RemoteAnisetteProvider("https://ani.example/") assert provider._http._ssl is apple_trust_context() # noqa: SLF001 @@ -102,6 +115,7 @@ def test_an_anisette_provider_verifies_by_default() -> None: def test_the_opt_in_reaches_the_session_and_survives_a_round_trip() -> None: + """Test that the opt-in reaches the session and is restored from JSON.""" # It has to be serialized, or saving and reloading an account would silently turn a # working self-signed setup into a failing one. provider = RemoteAnisetteProvider("https://ani.example/", allow_unverified_https=True) @@ -114,6 +128,7 @@ def test_the_opt_in_reaches_the_session_and_survives_a_round_trip() -> None: def test_an_older_saved_provider_still_loads_and_verifies() -> None: + """Test that a provider saved before this option existed still verifies.""" # A file written before this option existed carries no flag, and must keep working -- # verifying, which is the safe direction for a default to move in. restored = RemoteAnisetteProvider.from_json({"type": "aniRemote", "url": "https://a/"}) From 308d11d6ac1eb85e4b985d05417b0f9daa6c8b72 Mon Sep 17 00:00:00 2001 From: Shane B Date: Sat, 15 Aug 2026 12:18:49 +0200 Subject: [PATCH 4/4] Subscript a NotRequired key with .get, which is what it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit basedpyright's `reportTypedDictNotRequiredAccess`: `allow_unverified_https` is written only when true, so it is `NotRequired` on the mapping and a subscript is an access that can raise. The assertion is unchanged — present, and true — and `.get` says what the type already said. Same cause as the previous commit and worth stating once: my verification was narrower than CI's on both linters. `ruff check findmy examples scripts` and `basedpyright findmy` never saw `tests/`, while pre-commit passes every tracked Python file explicitly. The reproduction is ` $(git ls-files '*.py')`. --- tests/test_tls.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_tls.py b/tests/test_tls.py index 30db3d1..2b8abd2 100644 --- a/tests/test_tls.py +++ b/tests/test_tls.py @@ -122,7 +122,10 @@ def test_the_opt_in_reaches_the_session_and_survives_a_round_trip() -> None: assert provider._http._ssl is False # noqa: SLF001 state = provider.to_json() - assert state["allow_unverified_https"] is True + # `.get`, because the key is `NotRequired` on the mapping -- it is written only when + # true, so a subscript is an access the type checker is right to object to. The + # assertion is unchanged: present, and true. + assert state.get("allow_unverified_https") is True assert RemoteAnisetteProvider.from_json(state)._http._ssl is False # noqa: SLF001