diff --git a/poetry.lock b/poetry.lock index 1cb6c86aeb9..51d204b2cf2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1570,6 +1570,19 @@ files = [ {file = "trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3"}, ] +[[package]] +name = "truststore" +version = "0.10.0" +description = "Verify certificates using native system trust stores" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "truststore-0.10.0-py3-none-any.whl", hash = "sha256:b3798548e421ffe2ca2a6217cca49e7a17baf40b72d86a5505dc7d701e77d15b"}, + {file = "truststore-0.10.0.tar.gz", hash = "sha256:5da347c665714fdfbd46f738c823fe9f0d8775e41ac5fb94f325749091187896"}, +] + [[package]] name = "types-requests" version = "2.32.0.20241016" @@ -1735,4 +1748,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "308952b9d3e6645aa1a55c5c021949e2300dd78a8f0049cdf2814785e1196dc5" +content-hash = "8b5159eb8fa3222d9b096fdd0efd2fd042fd016a8359f47981e8444680e83dbf" diff --git a/pyproject.toml b/pyproject.toml index e6037c56bb2..bf01c0e7b66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "tomlkit (>=0.11.4,<1.0.0)", # trove-classifiers uses calver, so version is unclamped "trove-classifiers (>=2022.5.19)", + "truststore (>=0.10.0,<1.0.0) ; python_version >= '3.10'", "virtualenv (>=20.26.6,<21.0.0)", "xattr (>=1.0.0,<2.0.0) ; sys_platform == 'darwin'", ] @@ -187,6 +188,7 @@ module = [ 'shellingham.*', 'virtualenv.*', 'xattr.*', + 'truststore.*' # not available on Python < 3.10 ] ignore_missing_imports = true diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 22906db6a19..f4634a8f838 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -136,6 +136,8 @@ class Config: "keyring": { "enabled": True, }, + # TODO: Flip to default True on the next release after dropping Python 3.9 + "system-truststore": False, } def __init__(self, use_environment: bool = True) -> None: @@ -303,6 +305,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: "solver.lazy-wheel", "system-git-client", "keyring.enabled", + "system-truststore", }: return boolean_normalizer diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 89f78bf0819..1630020d9b6 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -226,6 +226,7 @@ def _run(self, io: IO) -> int: self._configure_custom_application_options(io) self._load_plugins(io) + self._load_system_truststore() with directory(self._working_directory): exit_code: int = super()._run(io) @@ -441,6 +442,15 @@ def _load_plugins(self, io: IO) -> None: self._plugins_loaded = True + @staticmethod + def _load_system_truststore() -> None: + from poetry.utils.ssl_truststore import is_truststore_enabled + + if is_truststore_enabled(): + import truststore + + truststore.inject_into_ssl() + def main() -> int: exit_code: int = Application().run() diff --git a/src/poetry/utils/ssl_truststore.py b/src/poetry/utils/ssl_truststore.py new file mode 100644 index 00000000000..534fdae8e35 --- /dev/null +++ b/src/poetry/utils/ssl_truststore.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import logging +import sys + +from poetry.config.config import Config + + +logger = logging.getLogger(__name__) + + +def _is_truststore_available() -> bool: + if sys.version_info < (3, 10): + logger.debug("Disabling truststore because Python version isn't 3.10+") + return False + + try: + import ssl # noqa: F401 + except ImportError: + logger.warning("Disabling truststore since ssl support is missing") + return False + + try: + import truststore # noqa: F401 + except ImportError: + logger.warning("Disabling truststore because `truststore` package is missing`") + return False + return True + + +def is_truststore_enabled() -> bool: + return Config.create().get("system-truststore") and _is_truststore_available() diff --git a/tests/conftest.py b/tests/conftest.py index 39d71e172fc..931c83769b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -337,7 +337,8 @@ def git_mock(mocker: MockerFixture, request: FixtureRequest) -> None: @pytest.fixture -def http() -> Iterator[type[httpretty.httpretty]]: +def http(mocker: MockerFixture) -> Iterator[type[httpretty.httpretty]]: + mocker.patch("truststore.inject_into_ssl") httpretty.reset() with httpretty.enabled(allow_net_connect=False, verbose=True): yield httpretty diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index a6585c29145..9a709c47b91 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -64,6 +64,7 @@ def test_list_displays_default_value_if_not_set( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false @@ -96,6 +97,7 @@ def test_list_displays_set_get_setting( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = false virtualenvs.in-project = null virtualenvs.options.always-copy = false @@ -149,6 +151,7 @@ def test_unset_setting( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false @@ -180,6 +183,7 @@ def test_unset_repo_setting( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false @@ -309,6 +313,7 @@ def test_list_displays_set_get_local_setting( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = false virtualenvs.in-project = null virtualenvs.options.always-copy = false @@ -349,6 +354,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( requests.max-retries = 0 solver.lazy-wheel = true system-git-client = false +system-truststore = false virtualenvs.create = true virtualenvs.in-project = null virtualenvs.options.always-copy = false