Skip to content

Commit

Permalink
feat: Support OS truststore for the TLS certificate verification
Browse files Browse the repository at this point in the history
This commit add support for loading the OS truststore root certificates in addition to the "legacy" certifi bundle. It will come in handy for every user behind a company proxy or just using a private PyPI warehouse.

Close #9249
  • Loading branch information
Ousret committed Sep 13, 2024
1 parent 3183126 commit 1a73cd6
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 6 deletions.
4 changes: 4 additions & 0 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,10 @@ poetry config -- http-basic.pypi myUsername -myPasswordStartingWithDash

## Certificates

### OS Truststore

Poetry access the system truststore by default and retrieve the root certificates appropriately on Linux, Windows, and MacOS.

### Custom certificate authority and mutual TLS authentication

Poetry supports repositories that are secured by a custom certificate authority as well as those that require
Expand Down
95 changes: 91 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ tomlkit = ">=0.11.4,<1.0.0"
trove-classifiers = ">=2022.5.19"
virtualenv = "^20.23.0"
xattr = { version = "^1.0.0", markers = "sys_platform == 'darwin'" }
wassima = "^1.1.2"

[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
Expand Down
4 changes: 4 additions & 0 deletions src/poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from poetry.publishing.hash_manager import HashManager
from poetry.utils.constants import REQUESTS_TIMEOUT
from poetry.utils.patterns import wheel_file_re
from poetry.utils.truststore import WithTrustStoreAdapter


if TYPE_CHECKING:
Expand Down Expand Up @@ -88,6 +89,9 @@ def auth(self, username: str | None, password: str | None) -> None:

def make_session(self) -> requests.Session:
session = requests.Session()
adapter = WithTrustStoreAdapter()
session.mount("http://", adapter)
session.mount("https://", adapter)
auth = self.get_auth()
if auth is not None:
session.auth = auth
Expand Down
4 changes: 2 additions & 2 deletions src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import requests.auth
import requests.exceptions

from cachecontrol import CacheControlAdapter
from cachecontrol.caches import FileCache
from requests_toolbelt import user_agent

Expand All @@ -28,6 +27,7 @@
from poetry.utils.constants import STATUS_FORCELIST
from poetry.utils.password_manager import HTTPAuthCredential
from poetry.utils.password_manager import PasswordManager
from poetry.utils.truststore import CacheControlWithTrustStoreAdapter


if TYPE_CHECKING:
Expand Down Expand Up @@ -137,7 +137,7 @@ def create_session(self) -> requests.Session:
if self._cache_control is None:
return session

adapter = CacheControlAdapter(
adapter = CacheControlWithTrustStoreAdapter(
cache=self._cache_control,
pool_maxsize=self._pool_size,
)
Expand Down
41 changes: 41 additions & 0 deletions src/poetry/utils/truststore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from cachecontrol import CacheControlAdapter
from requests.adapters import HTTPAdapter
from wassima import RUSTLS_LOADED
from wassima import generate_ca_bundle


DEFAULT_CA_BUNDLE: str = generate_ca_bundle()


class WithTrustStoreAdapter(HTTPAdapter):
"""
Inject the OS truststore in Requests.
Certifi is still loaded in addition to the OS truststore for backward compatibility purposes.
See https://github.com/jawah/wassima for more details.
"""

# we ignore untyped def error because Requests is untyped.
def cert_verify(self, conn, url, verify, cert): # type: ignore[no-untyped-def]
#: only apply truststore cert if "verify" is set with default value "True".
#: RUSTLS_LOADED means that "wassima" is not the py3 none wheel and does not fallback on "certifi"
#: if "RUSTLS_LOADED" is False then "wassima" just return "certifi" bundle instead.
if RUSTLS_LOADED and url.lower().startswith("https") and verify is True:
conn.ca_cert_data = DEFAULT_CA_BUNDLE

# still apply upstream logic as before
super().cert_verify(conn, url, verify, cert) # type: ignore[no-untyped-def]


class CacheControlWithTrustStoreAdapter(CacheControlAdapter):
"""
Same as WithTrustStoreAdapter but with CacheControlAdapter as its parent
class.
"""

def cert_verify(self, conn, url, verify, cert): # type: ignore[no-untyped-def]
if RUSTLS_LOADED and url.lower().startswith("https") and verify is True:
conn.ca_cert_data = DEFAULT_CA_BUNDLE

super().cert_verify(conn, url, verify, cert) # type: ignore[no-untyped-def]

0 comments on commit 1a73cd6

Please sign in to comment.