Skip to content

Commit 8eadcab

Browse files
authored
Merge pull request #11647 from sethmlarson/truststore-by-default
Truststore by default
2 parents 5fb46a3 + 69874c7 commit 8eadcab

File tree

6 files changed

+58
-69
lines changed

6 files changed

+58
-69
lines changed

docs/html/topics/https-certificates.md

+21-31
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
By default, pip will perform SSL certificate verification for network
1010
connections it makes over HTTPS. These serve to prevent man-in-the-middle
11-
attacks against package downloads. This does not use the system certificate
12-
store but, instead, uses a bundled CA certificate store from {pypi}`certifi`.
11+
attacks against package downloads.
1312

1413
## Using a specific certificate store
1514

@@ -20,43 +19,34 @@ variables.
2019

2120
## Using system certificate stores
2221

23-
```{versionadded} 22.2
24-
Experimental support, behind `--use-feature=truststore`.
25-
As with any other CLI option, this can be enabled globally via config or environment variables.
26-
```
27-
28-
It is possible to use the system trust store, instead of the bundled certifi
29-
certificates for verifying HTTPS certificates. This approach will typically
30-
support corporate proxy certificates without additional configuration.
31-
32-
In order to use system trust stores, you need to use Python 3.10 or newer.
33-
34-
```{pip-cli}
35-
$ python -m pip install SomePackage --use-feature=truststore
36-
[...]
37-
Successfully installed SomePackage
38-
```
39-
40-
### When to use
22+
```{versionadded} 24.2
4123
42-
You should try using system trust stores when there is a custom certificate
43-
chain configured for your system that pip isn't aware of. Typically, this
44-
situation will manifest with an `SSLCertVerificationError` with the message
45-
"certificate verify failed: unable to get local issuer certificate":
24+
```
4625

47-
```{pip-cli}
48-
$ pip install -U SomePackage
49-
[...]
50-
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (\_ssl.c:997)'))) - skipping
26+
```{note}
27+
Versions of pip prior to v24.2 did not use system certificates by default.
28+
To use system certificates with pip v22.2 or later, you must opt-in using the `--use-feature=truststore` CLI flag.
5129
```
5230

53-
This error means that OpenSSL wasn't able to find a trust anchor to verify the
54-
chain against. Using system trust stores instead of certifi will likely solve
55-
this issue.
31+
On Python 3.10 or later, by default
32+
system certificates are used in addition to certifi to verify HTTPS connections.
33+
This functionality is provided through the {pypi}`truststore` package.
5634

5735
If you encounter a TLS/SSL error when using the `truststore` feature you should
5836
open an issue on the [truststore GitHub issue tracker] instead of pip's issue
5937
tracker. The maintainers of truststore will help diagnose and fix the issue.
6038

39+
To opt-out of using system certificates you can pass the `--use-deprecated=legacy-certs`
40+
flag to pip.
41+
42+
```{warning}
43+
On Python 3.9 or earlier, only certifi is used to verify HTTPS connections as
44+
`truststore` requires Python 3.10 or higher to function.
45+
46+
The system certificate store won't be used in this case, so some situations like proxies
47+
with their own certificates may not work. Upgrading to at least Python 3.10 or later is
48+
the recommended method to resolve this issue.
49+
```
50+
6151
[truststore github issue tracker]:
6252
https://github.com/sethmlarson/truststore/issues

news/11647.feature.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Changed pip to use system certificates and certifi to verify HTTPS connections.
2+
This change only affects Python 3.10 or later, Python 3.9 and earlier only use certifi.
3+
4+
To revert to previous behavior pass the flag ``--use-deprecated=legacy-certs``.

src/pip/_internal/cli/cmdoptions.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ def check_list_path_option(options: Values) -> None:
996996

997997
# Features that are now always on. A warning is printed if they are used.
998998
ALWAYS_ENABLED_FEATURES = [
999+
"truststore", # always on since 24.2
9991000
"no-binary-enable-wheel-cache", # always on since 23.1
10001001
]
10011002

@@ -1008,7 +1009,6 @@ def check_list_path_option(options: Values) -> None:
10081009
default=[],
10091010
choices=[
10101011
"fast-deps",
1011-
"truststore",
10121012
]
10131013
+ ALWAYS_ENABLED_FEATURES,
10141014
help="Enable new functionality, that may be backward incompatible.",
@@ -1023,6 +1023,7 @@ def check_list_path_option(options: Values) -> None:
10231023
default=[],
10241024
choices=[
10251025
"legacy-resolver",
1026+
"legacy-certs",
10261027
],
10271028
help=("Enable deprecated functionality, that will be removed in the future."),
10281029
)

src/pip/_internal/cli/index_command.py

+12-18
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
from optparse import Values
1313
from typing import TYPE_CHECKING, List, Optional
1414

15+
from pip._vendor import certifi
16+
1517
from pip._internal.cli.base_command import Command
1618
from pip._internal.cli.command_context import CommandContextMixIn
17-
from pip._internal.exceptions import CommandError
1819

1920
if TYPE_CHECKING:
2021
from ssl import SSLContext
@@ -26,7 +27,8 @@
2627

2728
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
2829
if sys.version_info < (3, 10):
29-
raise CommandError("The truststore feature is only available for Python 3.10+")
30+
logger.debug("Disabling truststore because Python version isn't 3.10+")
31+
return None
3032

3133
try:
3234
import ssl
@@ -36,10 +38,13 @@ def _create_truststore_ssl_context() -> Optional["SSLContext"]:
3638

3739
try:
3840
from pip._vendor import truststore
39-
except ImportError as e:
40-
raise CommandError(f"The truststore feature is unavailable: {e}")
41+
except ImportError:
42+
logger.warning("Disabling truststore because platform isn't supported")
43+
return None
4144

42-
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
45+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
46+
ctx.load_verify_locations(certifi.where())
47+
return ctx
4348

4449

4550
class SessionCommandMixin(CommandContextMixIn):
@@ -80,20 +85,14 @@ def _build_session(
8085
options: Values,
8186
retries: Optional[int] = None,
8287
timeout: Optional[int] = None,
83-
fallback_to_certifi: bool = False,
8488
) -> "PipSession":
8589
from pip._internal.network.session import PipSession
8690

8791
cache_dir = options.cache_dir
8892
assert not cache_dir or os.path.isabs(cache_dir)
8993

90-
if "truststore" in options.features_enabled:
91-
try:
92-
ssl_context = _create_truststore_ssl_context()
93-
except Exception:
94-
if not fallback_to_certifi:
95-
raise
96-
ssl_context = None
94+
if "legacy-certs" not in options.deprecated_features_enabled:
95+
ssl_context = _create_truststore_ssl_context()
9796
else:
9897
ssl_context = None
9998

@@ -162,11 +161,6 @@ def handle_pip_version_check(self, options: Values) -> None:
162161
options,
163162
retries=0,
164163
timeout=min(5, options.timeout),
165-
# This is set to ensure the function does not fail when truststore is
166-
# specified in use-feature but cannot be loaded. This usually raises a
167-
# CommandError and shows a nice user-facing error, but this function is not
168-
# called in that try-except block.
169-
fallback_to_certifi=True,
170164
)
171165
with session:
172166
_pip_self_version_check(session, options)

tests/functional/test_truststore.py

+5-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import sys
21
from typing import Any, Callable
32

43
import pytest
@@ -9,25 +8,13 @@
98

109

1110
@pytest.fixture()
12-
def pip(script: PipTestEnvironment) -> PipRunner:
11+
def pip_no_truststore(script: PipTestEnvironment) -> PipRunner:
1312
def pip(*args: str, **kwargs: Any) -> TestPipResult:
14-
return script.pip(*args, "--use-feature=truststore", **kwargs)
13+
return script.pip(*args, "--use-deprecated=legacy-certs", **kwargs)
1514

1615
return pip
1716

1817

19-
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="3.10 can run truststore")
20-
def test_truststore_error_on_old_python(pip: PipRunner) -> None:
21-
result = pip(
22-
"install",
23-
"--no-index",
24-
"does-not-matter",
25-
expect_error=True,
26-
)
27-
assert "The truststore feature is only available for Python 3.10+" in result.stderr
28-
29-
30-
@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for truststore")
3118
@pytest.mark.network
3219
@pytest.mark.parametrize(
3320
"package",
@@ -37,10 +24,10 @@ def test_truststore_error_on_old_python(pip: PipRunner) -> None:
3724
],
3825
ids=["PyPI", "GitHub"],
3926
)
40-
def test_trustore_can_install(
27+
def test_no_truststore_can_install(
4128
script: PipTestEnvironment,
42-
pip: PipRunner,
29+
pip_no_truststore: PipRunner,
4330
package: str,
4431
) -> None:
45-
result = pip("install", package)
32+
result = pip_no_truststore("install", package)
4633
assert "Successfully installed" in result.stdout

tests/lib/certs.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from cryptography.hazmat.backends import default_backend
66
from cryptography.hazmat.primitives import hashes, serialization
77
from cryptography.hazmat.primitives.asymmetric import rsa
8-
from cryptography.x509.oid import NameOID
8+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
99

1010

1111
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]:
2525
.serial_number(x509.random_serial_number())
2626
.not_valid_before(datetime.now(timezone.utc))
2727
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=10))
28+
.add_extension(
29+
x509.BasicConstraints(ca=True, path_length=9),
30+
critical=True,
31+
)
2832
.add_extension(
2933
x509.SubjectAlternativeName([x509.DNSName(hostname)]),
3034
critical=False,
3135
)
36+
.add_extension(
37+
x509.ExtendedKeyUsage(
38+
[
39+
ExtendedKeyUsageOID.CLIENT_AUTH,
40+
ExtendedKeyUsageOID.SERVER_AUTH,
41+
]
42+
),
43+
critical=True,
44+
)
3245
.sign(key, hashes.SHA256(), default_backend())
3346
)
3447
return cert, key

0 commit comments

Comments
 (0)