diff --git a/docs/html/topics/https-certificates.md b/docs/html/topics/https-certificates.md
index b42c463e6cc..d1b908384e9 100644
--- a/docs/html/topics/https-certificates.md
+++ b/docs/html/topics/https-certificates.md
@@ -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
@@ -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
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 661c489c73e..005116d831d 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -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.",
@@ -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."),
)
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index 1044809f040..09dff3c0a11 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -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
@@ -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):
@@ -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
@@ -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)
diff --git a/src/pip/_vendor/truststore/__init__.py b/src/pip/_vendor/truststore/__init__.py
index 07290a05770..d0f2c853bc8 100644
--- a/src/pip/_vendor/truststore/__init__.py
+++ b/src/pip/_vendor/truststore/__init__.py
@@ -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"
diff --git a/src/pip/_vendor/truststore/_api.py b/src/pip/_vendor/truststore/_api.py
index d6cd3343a03..2831c3ce54e 100644
--- a/src/pip/_vendor/truststore/_api.py
+++ b/src/pip/_vendor/truststore/_api.py
@@ -1,10 +1,16 @@
+import array
+import ctypes
+import mmap
import os
+import pickle
import platform
import socket
import ssl
-from typing import Any
+import typing
-from _ssl import ENCODING_DER # type: ignore[import]
+import _ssl # type: ignore[import]
+
+from ._ssl_constants import _original_SSLContext, _original_super_SSLContext
if platform.system() == "Windows":
from ._windows import _configure_context, _verify_peercerts_impl
@@ -13,13 +19,53 @@
else:
from ._openssl import _configure_context, _verify_peercerts_impl
+# From typeshed/stdlib/ssl.pyi
+_StrOrBytesPath: typing.TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes]
+_PasswordType: typing.TypeAlias = str | bytes | typing.Callable[[], str | bytes]
+
+# From typeshed/stdlib/_typeshed/__init__.py
+_ReadableBuffer: typing.TypeAlias = typing.Union[
+ bytes,
+ memoryview,
+ bytearray,
+ "array.array[typing.Any]",
+ mmap.mmap,
+ "ctypes._CData",
+ pickle.PickleBuffer,
+]
+
+
+def inject_into_ssl() -> None:
+ """Injects the :class:`truststore.SSLContext` into the ``ssl``
+ module by replacing :class:`ssl.SSLContext`.
+ """
+ setattr(ssl, "SSLContext", SSLContext)
+ # urllib3 holds on to its own reference of ssl.SSLContext
+ # so we need to replace that reference too.
+ try:
+ import urllib3.util.ssl_ as urllib3_ssl
+
+ setattr(urllib3_ssl, "SSLContext", SSLContext)
+ except ImportError:
+ pass
+
+
+def extract_from_ssl() -> None:
+ """Restores the :class:`ssl.SSLContext` class to its original state"""
+ setattr(ssl, "SSLContext", _original_SSLContext)
+ try:
+ import urllib3.util.ssl_ as urllib3_ssl
+
+ urllib3_ssl.SSLContext = _original_SSLContext
+ except ImportError:
+ pass
+
class SSLContext(ssl.SSLContext):
"""SSLContext API that uses system certificates on all platforms"""
- def __init__(self, protocol: int = ssl.PROTOCOL_TLS) -> None:
- self._ctx = ssl.SSLContext(protocol)
- _configure_context(self._ctx)
+ def __init__(self, protocol: int = None) -> None: # type: ignore[assignment]
+ self._ctx = _original_SSLContext(protocol)
class TruststoreSSLObject(ssl.SSLObject):
# This object exists because wrap_bio() doesn't
@@ -42,17 +88,21 @@ def wrap_socket(
server_hostname: str | None = None,
session: ssl.SSLSession | None = None,
) -> ssl.SSLSocket:
- ssl_sock = self._ctx.wrap_socket(
- sock,
- server_side=server_side,
- server_hostname=server_hostname,
- do_handshake_on_connect=do_handshake_on_connect,
- suppress_ragged_eofs=suppress_ragged_eofs,
- session=session,
- )
+ # Use a context manager here because the
+ # inner SSLContext holds on to our state
+ # but also does the actual handshake.
+ with _configure_context(self._ctx):
+ ssl_sock = self._ctx.wrap_socket(
+ sock,
+ server_side=server_side,
+ server_hostname=server_hostname,
+ do_handshake_on_connect=do_handshake_on_connect,
+ suppress_ragged_eofs=suppress_ragged_eofs,
+ session=session,
+ )
try:
_verify_peercerts(ssl_sock, server_hostname=server_hostname)
- except ssl.SSLError:
+ except Exception:
ssl_sock.close()
raise
return ssl_sock
@@ -65,31 +115,161 @@ def wrap_bio(
server_hostname: str | None = None,
session: ssl.SSLSession | None = None,
) -> ssl.SSLObject:
- ssl_obj = self._ctx.wrap_bio(
- incoming,
- outgoing,
- server_hostname=server_hostname,
- server_side=server_side,
- session=session,
- )
+ with _configure_context(self._ctx):
+ ssl_obj = self._ctx.wrap_bio(
+ incoming,
+ outgoing,
+ server_hostname=server_hostname,
+ server_side=server_side,
+ session=session,
+ )
return ssl_obj
def load_verify_locations(
self,
cafile: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None,
capath: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None,
- cadata: str | bytes | None = None,
+ cadata: str | _ReadableBuffer | None = None,
+ ) -> None:
+ return self._ctx.load_verify_locations(
+ cafile=cafile, capath=capath, cadata=cadata
+ )
+
+ def load_cert_chain(
+ self,
+ certfile: _StrOrBytesPath,
+ keyfile: _StrOrBytesPath | None = None,
+ password: _PasswordType | None = None,
+ ) -> None:
+ return self._ctx.load_cert_chain(
+ certfile=certfile, keyfile=keyfile, password=password
+ )
+
+ def load_default_certs(
+ self, purpose: ssl.Purpose = ssl.Purpose.SERVER_AUTH
) -> None:
- return self._ctx.load_verify_locations(cafile, capath, cadata)
+ return self._ctx.load_default_certs(purpose)
+
+ def set_alpn_protocols(self, alpn_protocols: typing.Iterable[str]) -> None:
+ return self._ctx.set_alpn_protocols(alpn_protocols)
+
+ def set_npn_protocols(self, npn_protocols: typing.Iterable[str]) -> None:
+ return self._ctx.set_npn_protocols(npn_protocols)
+
+ def set_ciphers(self, __cipherlist: str) -> None:
+ return self._ctx.set_ciphers(__cipherlist)
+
+ def get_ciphers(self) -> typing.Any:
+ return self._ctx.get_ciphers()
+
+ def session_stats(self) -> dict[str, int]:
+ return self._ctx.session_stats()
- def __getattr__(self, name: str) -> Any:
- return getattr(self._ctx, name)
+ def cert_store_stats(self) -> dict[str, int]:
+ raise NotImplementedError()
- def __setattr__(self, name: str, value: Any) -> None:
- if name == "verify_flags":
- self._ctx.verify_flags = value
- else:
- return super().__setattr__(name, value)
+ @typing.overload
+ def get_ca_certs(
+ self, binary_form: typing.Literal[False] = ...
+ ) -> list[typing.Any]:
+ ...
+
+ @typing.overload
+ def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]:
+ ...
+
+ @typing.overload
+ def get_ca_certs(self, binary_form: bool = ...) -> typing.Any:
+ ...
+
+ def get_ca_certs(self, binary_form: bool = False) -> list[typing.Any] | list[bytes]:
+ raise NotImplementedError()
+
+ @property
+ def check_hostname(self) -> bool:
+ return self._ctx.check_hostname
+
+ @check_hostname.setter
+ def check_hostname(self, value: bool) -> None:
+ self._ctx.check_hostname = value
+
+ @property
+ def hostname_checks_common_name(self) -> bool:
+ return self._ctx.hostname_checks_common_name
+
+ @hostname_checks_common_name.setter
+ def hostname_checks_common_name(self, value: bool) -> None:
+ self._ctx.hostname_checks_common_name = value
+
+ @property
+ def keylog_filename(self) -> str:
+ return self._ctx.keylog_filename
+
+ @keylog_filename.setter
+ def keylog_filename(self, value: str) -> None:
+ self._ctx.keylog_filename = value
+
+ @property
+ def maximum_version(self) -> ssl.TLSVersion:
+ return self._ctx.maximum_version
+
+ @maximum_version.setter
+ def maximum_version(self, value: ssl.TLSVersion) -> None:
+ self._ctx.maximum_version = value
+
+ @property
+ def minimum_version(self) -> ssl.TLSVersion:
+ return self._ctx.minimum_version
+
+ @minimum_version.setter
+ def minimum_version(self, value: ssl.TLSVersion) -> None:
+ self._ctx.minimum_version = value
+
+ @property
+ def options(self) -> ssl.Options:
+ return self._ctx.options
+
+ @options.setter
+ def options(self, value: ssl.Options) -> None:
+ _original_super_SSLContext.options.__set__( # type: ignore[attr-defined]
+ self._ctx, value
+ )
+
+ @property
+ def post_handshake_auth(self) -> bool:
+ return self._ctx.post_handshake_auth
+
+ @post_handshake_auth.setter
+ def post_handshake_auth(self, value: bool) -> None:
+ self._ctx.post_handshake_auth = value
+
+ @property
+ def protocol(self) -> ssl._SSLMethod:
+ return self._ctx.protocol
+
+ @property
+ def security_level(self) -> int:
+ return self._ctx.security_level # type: ignore[attr-defined,no-any-return]
+
+ @property
+ def verify_flags(self) -> ssl.VerifyFlags:
+ return self._ctx.verify_flags
+
+ @verify_flags.setter
+ def verify_flags(self, value: ssl.VerifyFlags) -> None:
+ _original_super_SSLContext.verify_flags.__set__( # type: ignore[attr-defined]
+ self._ctx, value
+ )
+
+ @property
+ def verify_mode(self) -> ssl.VerifyMode:
+ return self._ctx.verify_mode
+
+ @verify_mode.setter
+ def verify_mode(self, value: ssl.VerifyMode) -> None:
+ _original_super_SSLContext.verify_mode.__set__( # type: ignore[attr-defined]
+ self._ctx, value
+ )
def _verify_peercerts(
@@ -106,9 +286,13 @@ def _verify_peercerts(
except AttributeError:
pass
- cert_bytes = [
- cert.public_bytes(ENCODING_DER) for cert in sslobj.get_unverified_chain() # type: ignore[attr-defined]
- ]
+ # SSLObject.get_unverified_chain() returns 'None'
+ # if the peer sends no certificates. This is common
+ # for the server-side scenario.
+ unverified_chain: typing.Sequence[_ssl.Certificate] = (
+ sslobj.get_unverified_chain() or () # type: ignore[attr-defined]
+ )
+ cert_bytes = [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain]
_verify_peercerts_impl(
sock_or_sslobj.context, cert_bytes, server_hostname=server_hostname
)
diff --git a/src/pip/_vendor/truststore/_macos.py b/src/pip/_vendor/truststore/_macos.py
index 5554dead4e0..e78c0afad6c 100644
--- a/src/pip/_vendor/truststore/_macos.py
+++ b/src/pip/_vendor/truststore/_macos.py
@@ -1,6 +1,8 @@
+import contextlib
import ctypes
import platform
import ssl
+import typing
from ctypes import (
CDLL,
POINTER,
@@ -13,7 +15,8 @@
c_void_p,
)
from ctypes.util import find_library
-from typing import Any
+
+from ._ssl_constants import _set_ssl_context_verify_mode
_mac_version = platform.mac_ver()[0]
_mac_version_info = tuple(map(int, _mac_version.split(".")))
@@ -201,7 +204,7 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
raise ImportError("Error initializing ctypes") from None
-def _handle_osstatus(result: OSStatus, _: Any, args: Any) -> Any:
+def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typing.Any:
"""
Raises an error if the OSStatus value is non-zero.
"""
@@ -254,9 +257,9 @@ def _handle_osstatus(result: OSStatus, _: Any, args: Any) -> Any:
raise ssl.SSLError(message)
-Security.SecTrustCreateWithCertificates.errcheck = _handle_osstatus # type: ignore[assignment,misc]
-Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment,misc]
-Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment,misc]
+Security.SecTrustCreateWithCertificates.errcheck = _handle_osstatus # type: ignore[assignment]
+Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment]
+Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment]
class CFConst:
@@ -264,6 +267,11 @@ class CFConst:
kCFStringEncodingUTF8 = CFStringEncoding(0x08000100)
+ errSecIncompleteCertRevocationCheck = -67635
+ errSecHostNameMismatch = -67602
+ errSecCertificateExpired = -67818
+ errSecNotTrusted = -67843
+
def _bytes_to_cf_data_ref(value: bytes) -> CFDataRef: # type: ignore[valid-type]
return CoreFoundation.CFDataCreate( # type: ignore[no-any-return]
@@ -338,9 +346,17 @@ def _der_certs_to_cf_cert_array(certs: list[bytes]) -> CFMutableArrayRef: # typ
return cf_array # type: ignore[no-any-return]
-def _configure_context(ctx: ssl.SSLContext) -> None:
+@contextlib.contextmanager
+def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]:
+ check_hostname = ctx.check_hostname
+ verify_mode = ctx.verify_mode
ctx.check_hostname = False
- ctx.verify_mode = ssl.CERT_NONE
+ _set_ssl_context_verify_mode(ctx, ssl.CERT_NONE)
+ try:
+ yield
+ finally:
+ ctx.check_hostname = check_hostname
+ _set_ssl_context_verify_mode(ctx, verify_mode)
def _verify_peercerts_impl(
@@ -432,8 +448,27 @@ def _verify_peercerts_impl(
f"Unknown result from Security.SecTrustEvaluateWithError: {sec_trust_eval_result!r}"
)
+ cf_error_code = 0
if not is_trusted:
cf_error_code = CoreFoundation.CFErrorGetCode(cf_error)
+
+ # If the error is a known failure that we're
+ # explicitly okay with from SSLContext configuration
+ # we can set is_trusted accordingly.
+ if ssl_context.verify_mode != ssl.CERT_REQUIRED and (
+ cf_error_code == CFConst.errSecNotTrusted
+ or cf_error_code == CFConst.errSecCertificateExpired
+ ):
+ is_trusted = True
+ elif (
+ not ssl_context.check_hostname
+ and cf_error_code == CFConst.errSecHostNameMismatch
+ ):
+ is_trusted = True
+
+ # If we're still not trusted then we start to
+ # construct and raise the SSLCertVerificationError.
+ if not is_trusted:
cf_error_string_ref = None
try:
cf_error_string_ref = CoreFoundation.CFErrorCopyDescription(cf_error)
diff --git a/src/pip/_vendor/truststore/_openssl.py b/src/pip/_vendor/truststore/_openssl.py
index 86f37eeb709..9951cf75c40 100644
--- a/src/pip/_vendor/truststore/_openssl.py
+++ b/src/pip/_vendor/truststore/_openssl.py
@@ -1,6 +1,8 @@
+import contextlib
import os
import re
import ssl
+import typing
# candidates based on https://github.com/tiran/certifi-system-store by Christian Heimes
_CA_FILE_CANDIDATES = [
@@ -17,7 +19,8 @@
_HASHED_CERT_FILENAME_RE = re.compile(r"^[0-9a-fA-F]{8}\.[0-9]$")
-def _configure_context(ctx: ssl.SSLContext) -> None:
+@contextlib.contextmanager
+def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]:
# First, check whether the default locations from OpenSSL
# seem like they will give us a usable set of CA certs.
# ssl.get_default_verify_paths already takes care of:
@@ -40,8 +43,7 @@ def _configure_context(ctx: ssl.SSLContext) -> None:
ctx.load_verify_locations(cafile=cafile)
break
- ctx.verify_mode = ssl.CERT_REQUIRED
- ctx.check_hostname = True
+ yield
def _capath_contains_certs(capath: str) -> bool:
diff --git a/src/pip/_vendor/truststore/_ssl_constants.py b/src/pip/_vendor/truststore/_ssl_constants.py
new file mode 100644
index 00000000000..be60f8301ec
--- /dev/null
+++ b/src/pip/_vendor/truststore/_ssl_constants.py
@@ -0,0 +1,12 @@
+import ssl
+
+# Hold on to the original class so we can create it consistently
+# even if we inject our own SSLContext into the ssl module.
+_original_SSLContext = ssl.SSLContext
+_original_super_SSLContext = super(_original_SSLContext, _original_SSLContext)
+
+
+def _set_ssl_context_verify_mode(
+ ssl_context: ssl.SSLContext, verify_mode: ssl.VerifyMode
+) -> None:
+ _original_super_SSLContext.verify_mode.__set__(ssl_context, verify_mode) # type: ignore[attr-defined]
diff --git a/src/pip/_vendor/truststore/_windows.py b/src/pip/_vendor/truststore/_windows.py
index 4dbf526536c..3de4960a1b0 100644
--- a/src/pip/_vendor/truststore/_windows.py
+++ b/src/pip/_vendor/truststore/_windows.py
@@ -1,4 +1,6 @@
+import contextlib
import ssl
+import typing
from ctypes import WinDLL # type: ignore
from ctypes import WinError # type: ignore
from ctypes import (
@@ -27,6 +29,8 @@
)
from typing import TYPE_CHECKING, Any
+from ._ssl_constants import _set_ssl_context_verify_mode
+
HCERTCHAINENGINE = HANDLE
HCERTSTORE = HANDLE
HCRYPTPROV_LEGACY = HANDLE
@@ -78,7 +82,7 @@ class CERT_CHAIN_PARA(Structure):
if TYPE_CHECKING:
- PCERT_CHAIN_PARA = pointer[CERT_CHAIN_PARA]
+ PCERT_CHAIN_PARA = pointer[CERT_CHAIN_PARA] # type: ignore[misc]
else:
PCERT_CHAIN_PARA = POINTER(CERT_CHAIN_PARA)
@@ -199,11 +203,33 @@ class CERT_CHAIN_ENGINE_CONFIG(Structure):
OID_PKIX_KP_SERVER_AUTH = c_char_p(b"1.3.6.1.5.5.7.3.1")
CERT_CHAIN_REVOCATION_CHECK_END_CERT = 0x10000000
CERT_CHAIN_REVOCATION_CHECK_CHAIN = 0x20000000
+CERT_CHAIN_POLICY_IGNORE_ALL_NOT_TIME_VALID_FLAGS = 0x00000007
+CERT_CHAIN_POLICY_IGNORE_INVALID_BASIC_CONSTRAINTS_FLAG = 0x00000008
+CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG = 0x00000010
+CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG = 0x00000040
+CERT_CHAIN_POLICY_IGNORE_WRONG_USAGE_FLAG = 0x00000020
+CERT_CHAIN_POLICY_IGNORE_INVALID_POLICY_FLAG = 0x00000080
+CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS = 0x00000F00
+CERT_CHAIN_POLICY_ALLOW_TESTROOT_FLAG = 0x00008000
+CERT_CHAIN_POLICY_TRUST_TESTROOT_FLAG = 0x00004000
AUTHTYPE_SERVER = 2
CERT_CHAIN_POLICY_SSL = 4
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
+# Flags to set for SSLContext.verify_mode=CERT_NONE
+CERT_CHAIN_POLICY_VERIFY_MODE_NONE_FLAGS = (
+ CERT_CHAIN_POLICY_IGNORE_ALL_NOT_TIME_VALID_FLAGS
+ | CERT_CHAIN_POLICY_IGNORE_INVALID_BASIC_CONSTRAINTS_FLAG
+ | CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG
+ | CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG
+ | CERT_CHAIN_POLICY_IGNORE_WRONG_USAGE_FLAG
+ | CERT_CHAIN_POLICY_IGNORE_INVALID_POLICY_FLAG
+ | CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS
+ | CERT_CHAIN_POLICY_ALLOW_TESTROOT_FLAG
+ | CERT_CHAIN_POLICY_TRUST_TESTROOT_FLAG
+)
+
wincrypt = WinDLL("crypt32.dll")
kernel32 = WinDLL("kernel32.dll")
@@ -341,6 +367,7 @@ def _verify_peercerts_impl(
# First attempt to verify using the default Windows system trust roots
# (default chain engine).
_get_and_verify_cert_chain(
+ ssl_context,
None,
hIntermediateCertStore,
pCertContext,
@@ -358,6 +385,7 @@ def _verify_peercerts_impl(
)
if custom_ca_certs:
_verify_using_custom_ca_certs(
+ ssl_context,
custom_ca_certs,
hIntermediateCertStore,
pCertContext,
@@ -374,17 +402,18 @@ def _verify_peercerts_impl(
def _get_and_verify_cert_chain(
+ ssl_context: ssl.SSLContext,
hChainEngine: HCERTCHAINENGINE | None,
hIntermediateCertStore: HCERTSTORE,
pPeerCertContext: c_void_p,
- pChainPara: PCERT_CHAIN_PARA,
+ pChainPara: PCERT_CHAIN_PARA, # type: ignore[valid-type]
server_hostname: str | None,
chain_flags: int,
) -> None:
ppChainContext = None
try:
# Get cert chain
- ppChainContext = pointer(PCERT_CHAIN_CONTEXT()) # type: ignore[call-arg]
+ ppChainContext = pointer(PCERT_CHAIN_CONTEXT())
CertGetCertificateChain(
hChainEngine, # chain engine
pPeerCertContext, # leaf cert context
@@ -406,11 +435,17 @@ def _get_and_verify_cert_chain(
ssl_extra_cert_chain_policy_para.fdwChecks = 0
if server_hostname:
ssl_extra_cert_chain_policy_para.pwszServerName = c_wchar_p(server_hostname)
+
chain_policy = CERT_CHAIN_POLICY_PARA()
chain_policy.pvExtraPolicyPara = cast(
pointer(ssl_extra_cert_chain_policy_para), c_void_p
)
+ if ssl_context.verify_mode == ssl.CERT_NONE:
+ chain_policy.dwFlags |= CERT_CHAIN_POLICY_VERIFY_MODE_NONE_FLAGS
+ if not ssl_context.check_hostname:
+ chain_policy.dwFlags |= CERT_CHAIN_POLICY_IGNORE_INVALID_NAME_FLAG
chain_policy.cbSize = sizeof(chain_policy)
+
pPolicyPara = pointer(chain_policy)
policy_status = CERT_CHAIN_POLICY_STATUS()
policy_status.cbSize = sizeof(policy_status)
@@ -425,7 +460,6 @@ def _get_and_verify_cert_chain(
# Check status
error_code = policy_status.dwError
if error_code:
-
# Try getting a human readable message for an error code.
error_message_buf = create_unicode_buffer(1024)
error_message_chars = FormatMessageW(
@@ -456,10 +490,11 @@ def _get_and_verify_cert_chain(
def _verify_using_custom_ca_certs(
+ ssl_context: ssl.SSLContext,
custom_ca_certs: list[bytes],
hIntermediateCertStore: HCERTSTORE,
pPeerCertContext: c_void_p,
- pChainPara: PCERT_CHAIN_PARA,
+ pChainPara: PCERT_CHAIN_PARA, # type: ignore[valid-type]
server_hostname: str | None,
chain_flags: int,
) -> None:
@@ -492,6 +527,7 @@ def _verify_using_custom_ca_certs(
# Get and verify a cert chain using the custom chain engine
_get_and_verify_cert_chain(
+ ssl_context,
hChainEngine,
hIntermediateCertStore,
pPeerCertContext,
@@ -505,6 +541,14 @@ def _verify_using_custom_ca_certs(
CertCloseStore(hRootCertStore, 0)
-def _configure_context(ctx: ssl.SSLContext) -> None:
+@contextlib.contextmanager
+def _configure_context(ctx: ssl.SSLContext) -> typing.Iterator[None]:
+ check_hostname = ctx.check_hostname
+ verify_mode = ctx.verify_mode
ctx.check_hostname = False
- ctx.verify_mode = ssl.CERT_NONE
+ _set_ssl_context_verify_mode(ctx, ssl.CERT_NONE)
+ try:
+ yield
+ finally:
+ ctx.check_hostname = check_hostname
+ _set_ssl_context_verify_mode(ctx, verify_mode)
diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt
index 3c59dca8a23..00394b31831 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -20,5 +20,5 @@ setuptools==44.0.0
six==1.16.0
tenacity==8.1.0
tomli==2.0.1
-truststore==0.5.0
+truststore==0.6.0
webencodings==0.5.1
diff --git a/tests/functional/test_truststore.py b/tests/functional/test_truststore.py
index 33153d0fbf9..5552f4fb535 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,39 +8,13 @@
@pytest.fixture()
-def pip(script: PipTestEnvironment) -> PipRunner:
+def pip_with_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")
-def test_truststore_error_without_preinstalled(pip: PipRunner) -> None:
- result = pip(
- "install",
- "--no-index",
- "does-not-matter",
- expect_error=True,
- )
- assert (
- "To use the truststore feature, 'truststore' must be installed into "
- "pip's current environment."
- ) in result.stderr
-
-
-@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for truststore")
@pytest.mark.network
@pytest.mark.parametrize(
"package",
@@ -51,11 +24,10 @@ def test_truststore_error_without_preinstalled(pip: PipRunner) -> None:
],
ids=["PyPI", "GitHub"],
)
-def test_trustore_can_install(
+def test_no_trustore_can_install(
script: PipTestEnvironment,
- pip: PipRunner,
+ pip_with_no_truststore: PipRunner,
package: str,
) -> None:
- script.pip("install", "truststore")
- result = pip("install", package)
+ result = pip_with_no_truststore("install", package)
assert "Successfully installed" in result.stdout