Skip to content

Commit

Permalink
Use truststore by default, add '--use-deprecated=legacy-certs' to dis…
Browse files Browse the repository at this point in the history
…able
  • Loading branch information
sethmlarson committed Mar 4, 2023
1 parent 18c0a7f commit 61d3293
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 154 deletions.
54 changes: 13 additions & 41 deletions docs/html/topics/https-certificates.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

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. Pip by default uses a bundled CA certificate
store from {pypi}`certifi`.

## Using a specific certificate store

Expand All @@ -20,52 +20,24 @@ variables.

## Using system certificate stores

```{versionadded} 22.2
Experimental support, behind `--use-feature=truststore`.
```

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.
- Install the {pypi}`truststore` package, in the Python environment you're
running pip in.

This is typically done by installing this package using a system package
manager or by using pip in {ref}`Hash-checking mode` for this package and
trusting the network using the `--trusted-host` flag.

```{pip-cli}
$ python -m pip install truststore
[...]
$ python -m pip install SomePackage --use-feature=truststore
[...]
Successfully installed SomePackage
```
```{versionadded} 23.1
### When to use

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
```

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.
If Python 3.10 or later is being used then 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}
If Python 3.9 or earlier is in use then only certifi is used to verify HTTPS connections.
```

[truststore github issue tracker]:
https://github.com/sethmlarson/truststore/issues
2 changes: 1 addition & 1 deletion src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ def check_list_path_option(options: Values) -> None:
default=[],
choices=[
"fast-deps",
"truststore",
"no-binary-enable-wheel-cache",
],
help="Enable new functionality, that may be backward incompatible.",
Expand All @@ -997,6 +996,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."),
)
Expand Down
41 changes: 18 additions & 23 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from optparse import Values
from typing import TYPE_CHECKING, Any, List, Optional, Tuple

from pip._vendor import certifi

from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
Expand Down Expand Up @@ -48,24 +50,24 @@


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+")

try:
import ssl
except ImportError:
logger.warning("Disabling truststore since ssl support is missing")
return None

# Since truststore is developed with only Python 3.10+ in mind
# we delay the import until we know we're running pip with Python 3.10+.
try:
import truststore
except ImportError:
raise CommandError(
"To use the truststore feature, 'truststore' must be installed into "
"pip's current environment."
)
from pip._vendor import truststore
# Truststore doesn't work on macOS versions earlier than 10.8
except OSError:
logger.warning("Disabling truststore since OS version isn't supported")
return None

return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_verify_locations(cafile=certifi.where())
return ctx


class SessionCommandMixin(CommandContextMixIn):
Expand Down Expand Up @@ -107,18 +109,16 @@ def _build_session(
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
fallback_to_certifi: bool = False,
) -> 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
# Truststore only works with Python 3.10+
if (
sys.version_info >= (3, 10)
and "legacy-certs" not in options.deprecated_features_enabled
):
ssl_context = _create_truststore_ssl_context()
else:
ssl_context = None

Expand Down Expand Up @@ -180,11 +180,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)
Expand Down
14 changes: 9 additions & 5 deletions src/pip/_vendor/truststore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Verify certificates using OS trust stores"""
"""Verify certificates using OS trust stores. This is useful when your system contains
custom certificate authorities such as when using a corporate proxy or using test certificates.
Supports macOS, Windows, and Linux (with OpenSSL).
"""

import sys as _sys

if _sys.version_info < (3, 10):
raise ImportError("truststore requires Python 3.10 or later")
del _sys

from ._api import SSLContext # noqa: E402
from ._api import SSLContext, extract_from_ssl, inject_into_ssl # noqa: E402

__all__ = ["SSLContext"]
__version__ = "0.5.0"
del _api, _sys # type: ignore[name-defined] # noqa: F821

__all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"]
__version__ = "0.6.0"
Loading

0 comments on commit 61d3293

Please sign in to comment.