diff --git a/docs/html/topics/https-certificates.md b/docs/html/topics/https-certificates.md
index 0cf88b4b644..ff640575e6c 100644
--- a/docs/html/topics/https-certificates.md
+++ b/docs/html/topics/https-certificates.md
@@ -8,8 +8,7 @@
By default, pip will perform SSL certificate verification for network
connections it makes over HTTPS. These serve to prevent man-in-the-middle
-attacks against package downloads. This does not use the system certificate
-store but, instead, uses a bundled CA certificate store from {pypi}`certifi`.
+attacks against package downloads.
## Using a specific certificate store
@@ -20,43 +19,34 @@ variables.
## Using system certificate stores
-```{versionadded} 22.2
-Experimental support, behind `--use-feature=truststore`.
-As with any other CLI option, this can be enabled globally via config or environment variables.
-```
-
-It is possible to use the system trust store, instead of the bundled certifi
-certificates for verifying HTTPS certificates. This approach will typically
-support corporate proxy certificates without additional configuration.
-
-In order to use system trust stores, you need to use Python 3.10 or newer.
-
- ```{pip-cli}
- $ python -m pip install SomePackage --use-feature=truststore
- [...]
- Successfully installed SomePackage
- ```
-
-### When to use
+```{versionadded} 24.2
-You should try using system trust stores when there is a custom certificate
-chain configured for your system that pip isn't aware of. Typically, this
-situation will manifest with an `SSLCertVerificationError` with the message
-"certificate verify failed: unable to get local issuer certificate":
+```
-```{pip-cli}
-$ pip install -U SomePackage
-[...]
- SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (\_ssl.c:997)'))) - skipping
+```{note}
+Versions of pip prior to v24.2 did not use system certificates by default.
+To use system certificates with pip v22.2 or later, you must opt-in using the `--use-feature=truststore` CLI flag.
```
-This error means that OpenSSL wasn't able to find a trust anchor to verify the
-chain against. Using system trust stores instead of certifi will likely solve
-this issue.
+On Python 3.10 or later, by default
+system certificates are used in addition to certifi to verify HTTPS connections.
+This functionality is provided through the {pypi}`truststore` package.
If you encounter a TLS/SSL error when using the `truststore` feature you should
open an issue on the [truststore GitHub issue tracker] instead of pip's issue
tracker. The maintainers of truststore will help diagnose and fix the issue.
+To opt-out of using system certificates you can pass the `--use-deprecated=legacy-certs`
+flag to pip.
+
+```{warning}
+On Python 3.9 or earlier, only certifi is used to verify HTTPS connections as
+`truststore` requires Python 3.10 or higher to function.
+
+The system certificate store won't be used in this case, so some situations like proxies
+with their own certificates may not work. Upgrading to at least Python 3.10 or later is
+the recommended method to resolve this issue.
+```
+
[truststore github issue tracker]:
https://github.com/sethmlarson/truststore/issues
diff --git a/news/11647.feature.rst b/news/11647.feature.rst
new file mode 100644
index 00000000000..26d04d49165
--- /dev/null
+++ b/news/11647.feature.rst
@@ -0,0 +1,4 @@
+Changed pip to use system certificates and certifi to verify HTTPS connections.
+This change only affects Python 3.10 or later, Python 3.9 and earlier only use certifi.
+
+To revert to previous behavior pass the flag ``--use-deprecated=legacy-certs``.
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index a47f8a3f46a..0b7cff77bdd 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -996,6 +996,7 @@ def check_list_path_option(options: Values) -> None:
# Features that are now always on. A warning is printed if they are used.
ALWAYS_ENABLED_FEATURES = [
+ "truststore", # always on since 24.2
"no-binary-enable-wheel-cache", # always on since 23.1
]
@@ -1008,7 +1009,6 @@ def check_list_path_option(options: Values) -> None:
default=[],
choices=[
"fast-deps",
- "truststore",
]
+ ALWAYS_ENABLED_FEATURES,
help="Enable new functionality, that may be backward incompatible.",
@@ -1023,6 +1023,7 @@ def check_list_path_option(options: Values) -> None:
default=[],
choices=[
"legacy-resolver",
+ "legacy-certs",
],
help=("Enable deprecated functionality, that will be removed in the future."),
)
diff --git a/src/pip/_internal/cli/index_command.py b/src/pip/_internal/cli/index_command.py
index 4ff7b2c3a5d..9991326f36b 100644
--- a/src/pip/_internal/cli/index_command.py
+++ b/src/pip/_internal/cli/index_command.py
@@ -12,9 +12,10 @@
from optparse import Values
from typing import TYPE_CHECKING, List, Optional
+from pip._vendor import certifi
+
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
-from pip._internal.exceptions import CommandError
if TYPE_CHECKING:
from ssl import SSLContext
@@ -26,7 +27,8 @@
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
if sys.version_info < (3, 10):
- raise CommandError("The truststore feature is only available for Python 3.10+")
+ logger.debug("Disabling truststore because Python version isn't 3.10+")
+ return None
try:
import ssl
@@ -36,10 +38,13 @@ def _create_truststore_ssl_context() -> Optional["SSLContext"]:
try:
from pip._vendor import truststore
- except ImportError as e:
- raise CommandError(f"The truststore feature is unavailable: {e}")
+ except ImportError:
+ logger.warning("Disabling truststore because platform isn't supported")
+ return None
- return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.load_verify_locations(certifi.where())
+ return ctx
class SessionCommandMixin(CommandContextMixIn):
@@ -80,20 +85,14 @@ def _build_session(
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
- fallback_to_certifi: bool = False,
) -> "PipSession":
from pip._internal.network.session import PipSession
cache_dir = options.cache_dir
assert not cache_dir or os.path.isabs(cache_dir)
- if "truststore" in options.features_enabled:
- try:
- ssl_context = _create_truststore_ssl_context()
- except Exception:
- if not fallback_to_certifi:
- raise
- ssl_context = None
+ if "legacy-certs" not in options.deprecated_features_enabled:
+ ssl_context = _create_truststore_ssl_context()
else:
ssl_context = None
@@ -162,11 +161,6 @@ def handle_pip_version_check(self, options: Values) -> None:
options,
retries=0,
timeout=min(5, options.timeout),
- # This is set to ensure the function does not fail when truststore is
- # specified in use-feature but cannot be loaded. This usually raises a
- # CommandError and shows a nice user-facing error, but this function is not
- # called in that try-except block.
- fallback_to_certifi=True,
)
with session:
_pip_self_version_check(session, options)
diff --git a/tests/functional/test_truststore.py b/tests/functional/test_truststore.py
index cc90343b52d..c534ddb954d 100644
--- a/tests/functional/test_truststore.py
+++ b/tests/functional/test_truststore.py
@@ -1,4 +1,3 @@
-import sys
from typing import Any, Callable
import pytest
@@ -9,25 +8,13 @@
@pytest.fixture()
-def pip(script: PipTestEnvironment) -> PipRunner:
+def pip_no_truststore(script: PipTestEnvironment) -> PipRunner:
def pip(*args: str, **kwargs: Any) -> TestPipResult:
- return script.pip(*args, "--use-feature=truststore", **kwargs)
+ return script.pip(*args, "--use-deprecated=legacy-certs", **kwargs)
return pip
-@pytest.mark.skipif(sys.version_info >= (3, 10), reason="3.10 can run truststore")
-def test_truststore_error_on_old_python(pip: PipRunner) -> None:
- result = pip(
- "install",
- "--no-index",
- "does-not-matter",
- expect_error=True,
- )
- assert "The truststore feature is only available for Python 3.10+" in result.stderr
-
-
-@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for truststore")
@pytest.mark.network
@pytest.mark.parametrize(
"package",
@@ -37,10 +24,10 @@ def test_truststore_error_on_old_python(pip: PipRunner) -> None:
],
ids=["PyPI", "GitHub"],
)
-def test_trustore_can_install(
+def test_no_truststore_can_install(
script: PipTestEnvironment,
- pip: PipRunner,
+ pip_no_truststore: PipRunner,
package: str,
) -> None:
- result = pip("install", package)
+ result = pip_no_truststore("install", package)
assert "Successfully installed" in result.stdout
diff --git a/tests/lib/certs.py b/tests/lib/certs.py
index 9e6542d2d57..6f899acfe48 100644
--- a/tests/lib/certs.py
+++ b/tests/lib/certs.py
@@ -5,7 +5,7 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.x509.oid import NameOID
+from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
def make_tls_cert(hostname: str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey]:
@@ -25,10 +25,23 @@ def make_tls_cert(hostname: str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey]:
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=10))
+ .add_extension(
+ x509.BasicConstraints(ca=True, path_length=9),
+ critical=True,
+ )
.add_extension(
x509.SubjectAlternativeName([x509.DNSName(hostname)]),
critical=False,
)
+ .add_extension(
+ x509.ExtendedKeyUsage(
+ [
+ ExtendedKeyUsageOID.CLIENT_AUTH,
+ ExtendedKeyUsageOID.SERVER_AUTH,
+ ]
+ ),
+ critical=True,
+ )
.sign(key, hashes.SHA256(), default_backend())
)
return cert, key