diff --git a/news/truststore.vendor.rst b/news/truststore.vendor.rst new file mode 100644 index 00000000000..08841296ff6 --- /dev/null +++ b/news/truststore.vendor.rst @@ -0,0 +1 @@ +Upgraded truststore to 0.9.1. diff --git a/src/pip/_vendor/truststore/__init__.py b/src/pip/_vendor/truststore/__init__.py index 59930f455b0..86368145a7e 100644 --- a/src/pip/_vendor/truststore/__init__.py +++ b/src/pip/_vendor/truststore/__init__.py @@ -10,4 +10,4 @@ del _api, _sys # type: ignore[name-defined] # noqa: F821 __all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"] -__version__ = "0.8.0" +__version__ = "0.9.1" diff --git a/src/pip/_vendor/truststore/_api.py b/src/pip/_vendor/truststore/_api.py index 829aff72672..b1ea3b05c6d 100644 --- a/src/pip/_vendor/truststore/_api.py +++ b/src/pip/_vendor/truststore/_api.py @@ -2,9 +2,10 @@ import platform import socket import ssl +import sys import typing -import _ssl # type: ignore[import] +import _ssl # type: ignore[import-not-found] from ._ssl_constants import ( _original_SSLContext, @@ -49,7 +50,7 @@ def extract_from_ssl() -> None: try: import pip._vendor.urllib3.util.ssl_ as urllib3_ssl - urllib3_ssl.SSLContext = _original_SSLContext + urllib3_ssl.SSLContext = _original_SSLContext # type: ignore[assignment] except ImportError: pass @@ -171,16 +172,13 @@ def cert_store_stats(self) -> dict[str, int]: @typing.overload def get_ca_certs( self, binary_form: typing.Literal[False] = ... - ) -> list[typing.Any]: - ... + ) -> list[typing.Any]: ... @typing.overload - def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]: - ... + 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 = ...) -> typing.Any: ... def get_ca_certs(self, binary_form: bool = False) -> list[typing.Any] | list[bytes]: raise NotImplementedError() @@ -276,6 +274,25 @@ def verify_mode(self, value: ssl.VerifyMode) -> None: ) +# Python 3.13+ makes get_unverified_chain() a public API that only returns DER +# encoded certificates. We detect whether we need to call public_bytes() for 3.10->3.12 +# Pre-3.13 returned None instead of an empty list from get_unverified_chain() +if sys.version_info >= (3, 13): + + def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]: + unverified_chain = sslobj.get_unverified_chain() or () # type: ignore[attr-defined] + return [ + cert if isinstance(cert, bytes) else cert.public_bytes(_ssl.ENCODING_DER) + for cert in unverified_chain + ] + +else: + + def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]: + unverified_chain = sslobj.get_unverified_chain() or () # type: ignore[attr-defined] + return [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain] + + def _verify_peercerts( sock_or_sslobj: ssl.SSLSocket | ssl.SSLObject, server_hostname: str | None ) -> None: @@ -290,13 +307,7 @@ def _verify_peercerts( except AttributeError: pass - # 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] + cert_bytes = _get_unverified_chain_bytes(sslobj) _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 7dc440bf362..b234ffec723 100644 --- a/src/pip/_vendor/truststore/_macos.py +++ b/src/pip/_vendor/truststore/_macos.py @@ -96,9 +96,6 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL: Security.SecTrustSetAnchorCertificatesOnly.argtypes = [SecTrustRef, Boolean] Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus - Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)] - Security.SecTrustEvaluate.restype = OSStatus - Security.SecPolicyCreateRevocation.argtypes = [CFOptionFlags] Security.SecPolicyCreateRevocation.restype = SecPolicyRef @@ -259,6 +256,7 @@ def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typin Security.SecTrustCreateWithCertificates.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment] +Security.SecTrustSetAnchorCertificatesOnly.errcheck = _handle_osstatus # type: ignore[assignment] Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment] @@ -417,21 +415,21 @@ def _verify_peercerts_impl( CoreFoundation.CFRelease(certs) # If there are additional trust anchors to load we need to transform - # the list of DER-encoded certificates into a CFArray. Otherwise - # pass 'None' to signal that we only want system / fetched certificates. + # the list of DER-encoded certificates into a CFArray. ctx_ca_certs_der: list[bytes] | None = ssl_context.get_ca_certs( binary_form=True ) if ctx_ca_certs_der: ctx_ca_certs = None try: - ctx_ca_certs = _der_certs_to_cf_cert_array(cert_chain) + ctx_ca_certs = _der_certs_to_cf_cert_array(ctx_ca_certs_der) Security.SecTrustSetAnchorCertificates(trust, ctx_ca_certs) finally: if ctx_ca_certs: CoreFoundation.CFRelease(ctx_ca_certs) - else: - Security.SecTrustSetAnchorCertificates(trust, None) + + # We always want system certificates. + Security.SecTrustSetAnchorCertificatesOnly(trust, False) cf_error = CoreFoundation.CFErrorRef() sec_trust_eval_result = Security.SecTrustEvaluateWithError( diff --git a/src/pip/_vendor/truststore/_windows.py b/src/pip/_vendor/truststore/_windows.py index 3de4960a1b0..3d00d467f99 100644 --- a/src/pip/_vendor/truststore/_windows.py +++ b/src/pip/_vendor/truststore/_windows.py @@ -325,6 +325,12 @@ def _verify_peercerts_impl( server_hostname: str | None = None, ) -> None: """Verify the cert_chain from the server using Windows APIs.""" + + # If the peer didn't send any certificates then + # we can't do verification. Raise an error. + if not cert_chain: + raise ssl.SSLCertVerificationError("Peer sent no certificates to verify") + pCertContext = None hIntermediateCertStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, None, 0, None) try: @@ -375,7 +381,7 @@ def _verify_peercerts_impl( server_hostname, chain_flags=chain_flags, ) - except ssl.SSLCertVerificationError: + except ssl.SSLCertVerificationError as e: # If that fails but custom CA certs have been added # to the SSLContext using load_verify_locations, # try verifying using a custom chain engine @@ -384,15 +390,19 @@ def _verify_peercerts_impl( binary_form=True ) if custom_ca_certs: - _verify_using_custom_ca_certs( - ssl_context, - custom_ca_certs, - hIntermediateCertStore, - pCertContext, - pChainPara, - server_hostname, - chain_flags=chain_flags, - ) + try: + _verify_using_custom_ca_certs( + ssl_context, + custom_ca_certs, + hIntermediateCertStore, + pCertContext, + pChainPara, + server_hostname, + chain_flags=chain_flags, + ) + # Raise the original error, not the new error. + except ssl.SSLCertVerificationError: + raise e from None else: raise finally: diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 3eb4b893022..74eb58ae709 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -16,4 +16,4 @@ resolvelib==1.0.1 setuptools==69.5.1 tenacity==8.2.3 tomli==2.0.1 -truststore==0.8.0 +truststore==0.9.1