From 61d3293f9e8b7724dad786381a769177115cd03b Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Wed, 7 Dec 2022 22:25:47 -0600 Subject: [PATCH] Use truststore by default, add '--use-deprecated=legacy-certs' to disable --- docs/html/topics/https-certificates.md | 54 +--- src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/cli/req_command.py | 41 ++- src/pip/_vendor/truststore/__init__.py | 14 +- src/pip/_vendor/truststore/_api.py | 250 ++++++++++++++++--- src/pip/_vendor/truststore/_macos.py | 49 +++- src/pip/_vendor/truststore/_openssl.py | 8 +- src/pip/_vendor/truststore/_ssl_constants.py | 12 + src/pip/_vendor/truststore/_windows.py | 58 ++++- src/pip/_vendor/vendor.txt | 2 +- tests/functional/test_truststore.py | 38 +-- 11 files changed, 374 insertions(+), 154 deletions(-) create mode 100644 src/pip/_vendor/truststore/_ssl_constants.py 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