Skip to content

Upgrade vendored truststore to 0.10.0 #13041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion news/truststore.vendor.rst
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Upgrade truststore to 0.9.2
Upgrade truststore to 0.10.0
2 changes: 1 addition & 1 deletion src/pip/_vendor/truststore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
del _api, _sys # type: ignore[name-defined] # noqa: F821

__all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"]
__version__ = "0.9.2"
__version__ = "0.10.0"
3 changes: 3 additions & 0 deletions src/pip/_vendor/truststore/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def session_stats(self) -> dict[str, int]:
def cert_store_stats(self) -> dict[str, int]:
raise NotImplementedError()

def set_default_verify_paths(self) -> None:
self._ctx.set_default_verify_paths()

@typing.overload
def get_ca_certs(
self, binary_form: typing.Literal[False] = ...
Expand Down
200 changes: 136 additions & 64 deletions src/pip/_vendor/truststore/_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
f"Only OS X 10.8 and newer are supported, not {_mac_version_info[0]}.{_mac_version_info[1]}"
)

_is_macos_version_10_14_or_later = _mac_version_info >= (10, 14)


def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
"""Loads a CDLL by name, falling back to known path on 10.16+"""
Expand Down Expand Up @@ -115,6 +117,12 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
]
Security.SecTrustGetTrustResult.restype = OSStatus

Security.SecTrustEvaluate.argtypes = [
SecTrustRef,
POINTER(SecTrustResultType),
]
Security.SecTrustEvaluate.restype = OSStatus

Security.SecTrustRef = SecTrustRef # type: ignore[attr-defined]
Security.SecTrustResultType = SecTrustResultType # type: ignore[attr-defined]
Security.OSStatus = OSStatus # type: ignore[attr-defined]
Expand Down Expand Up @@ -197,8 +205,19 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
CoreFoundation.CFStringRef = CFStringRef # type: ignore[attr-defined]
CoreFoundation.CFErrorRef = CFErrorRef # type: ignore[attr-defined]

except AttributeError:
raise ImportError("Error initializing ctypes") from None
except AttributeError as e:
raise ImportError(f"Error initializing ctypes: {e}") from None

# SecTrustEvaluateWithError is macOS 10.14+
if _is_macos_version_10_14_or_later:
try:
Security.SecTrustEvaluateWithError.argtypes = [
SecTrustRef,
POINTER(CFErrorRef),
]
Security.SecTrustEvaluateWithError.restype = c_bool
except AttributeError as e:
raise ImportError(f"Error initializing ctypes: {e}") from None


def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typing.Any:
Expand Down Expand Up @@ -258,6 +277,7 @@ def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typin
Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment]
Security.SecTrustSetAnchorCertificatesOnly.errcheck = _handle_osstatus # type: ignore[assignment]
Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment]
Security.SecTrustEvaluate.errcheck = _handle_osstatus # type: ignore[assignment]


class CFConst:
Expand Down Expand Up @@ -365,9 +385,10 @@ def _verify_peercerts_impl(
certs = None
policies = None
trust = None
cf_error = None
try:
if server_hostname is not None:
# Only set a hostname on the policy if we're verifying the hostname
# on the leaf certificate.
if server_hostname is not None and ssl_context.check_hostname:
cf_str_hostname = None
try:
cf_str_hostname = _bytes_to_cf_string(server_hostname.encode("ascii"))
Expand Down Expand Up @@ -431,69 +452,120 @@ def _verify_peercerts_impl(
# We always want system certificates.
Security.SecTrustSetAnchorCertificatesOnly(trust, False)

cf_error = CoreFoundation.CFErrorRef()
sec_trust_eval_result = Security.SecTrustEvaluateWithError(
trust, ctypes.byref(cf_error)
)
# sec_trust_eval_result is a bool (0 or 1)
# where 1 means that the certs are trusted.
if sec_trust_eval_result == 1:
is_trusted = True
elif sec_trust_eval_result == 0:
is_trusted = False
# macOS 10.13 and earlier don't support SecTrustEvaluateWithError()
# so we use SecTrustEvaluate() which means we need to construct error
# messages ourselves.
if _is_macos_version_10_14_or_later:
_verify_peercerts_impl_macos_10_14(ssl_context, trust)
else:
raise ssl.SSLError(
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)

# Can this ever return 'None' if there's a CFError?
cf_error_message = (
_cf_string_ref_to_str(cf_error_string_ref)
or "Certificate verification failed"
)

# TODO: Not sure if we need the SecTrustResultType for anything?
# We only care whether or not it's a success or failure for now.
sec_trust_result_type = Security.SecTrustResultType()
Security.SecTrustGetTrustResult(
trust, ctypes.byref(sec_trust_result_type)
)

err = ssl.SSLCertVerificationError(cf_error_message)
err.verify_message = cf_error_message
err.verify_code = cf_error_code
raise err
finally:
if cf_error_string_ref:
CoreFoundation.CFRelease(cf_error_string_ref)

_verify_peercerts_impl_macos_10_13(ssl_context, trust)
finally:
if policies:
CoreFoundation.CFRelease(policies)
if trust:
CoreFoundation.CFRelease(trust)


def _verify_peercerts_impl_macos_10_13(
ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any
) -> None:
"""Verify using 'SecTrustEvaluate' API for macOS 10.13 and earlier.
macOS 10.14 added the 'SecTrustEvaluateWithError' API.
"""
sec_trust_result_type = Security.SecTrustResultType()
Security.SecTrustEvaluate(sec_trust_ref, ctypes.byref(sec_trust_result_type))

try:
sec_trust_result_type_as_int = int(sec_trust_result_type.value)
except (ValueError, TypeError):
sec_trust_result_type_as_int = -1

# Apple doesn't document these values in their own API docs.
# See: https://github.com/xybp888/iOS-SDKs/blob/master/iPhoneOS13.0.sdk/System/Library/Frameworks/Security.framework/Headers/SecTrust.h#L84
if (
ssl_context.verify_mode == ssl.CERT_REQUIRED
and sec_trust_result_type_as_int not in (1, 4)
):
# Note that we're not able to ignore only hostname errors
# for macOS 10.13 and earlier, so check_hostname=False will
# still return an error.
sec_trust_result_type_to_message = {
0: "Invalid trust result type",
# 1: "Trust evaluation succeeded",
2: "User confirmation required",
3: "User specified that certificate is not trusted",
# 4: "Trust result is unspecified",
5: "Recoverable trust failure occurred",
6: "Fatal trust failure occurred",
7: "Other error occurred, certificate may be revoked",
}
error_message = sec_trust_result_type_to_message.get(
sec_trust_result_type_as_int,
f"Unknown trust result: {sec_trust_result_type_as_int}",
)

err = ssl.SSLCertVerificationError(error_message)
err.verify_message = error_message
err.verify_code = sec_trust_result_type_as_int
raise err


def _verify_peercerts_impl_macos_10_14(
ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any
) -> None:
"""Verify using 'SecTrustEvaluateWithError' API for macOS 10.14+."""
cf_error = CoreFoundation.CFErrorRef()
sec_trust_eval_result = Security.SecTrustEvaluateWithError(
sec_trust_ref, ctypes.byref(cf_error)
)
# sec_trust_eval_result is a bool (0 or 1)
# where 1 means that the certs are trusted.
if sec_trust_eval_result == 1:
is_trusted = True
elif sec_trust_eval_result == 0:
is_trusted = False
else:
raise ssl.SSLError(
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

# 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)

# Can this ever return 'None' if there's a CFError?
cf_error_message = (
_cf_string_ref_to_str(cf_error_string_ref)
or "Certificate verification failed"
)

# TODO: Not sure if we need the SecTrustResultType for anything?
# We only care whether or not it's a success or failure for now.
sec_trust_result_type = Security.SecTrustResultType()
Security.SecTrustGetTrustResult(
sec_trust_ref, ctypes.byref(sec_trust_result_type)
)

err = ssl.SSLCertVerificationError(cf_error_message)
err.verify_message = cf_error_message
err.verify_code = cf_error_code
raise err
finally:
if cf_error_string_ref:
CoreFoundation.CFRelease(cf_error_string_ref)
7 changes: 5 additions & 2 deletions src/pip/_vendor/truststore/_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ class CERT_CHAIN_ENGINE_CONFIG(Structure):
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS = 0x00000F00
CERT_CHAIN_POLICY_ALLOW_TESTROOT_FLAG = 0x00008000
CERT_CHAIN_POLICY_TRUST_TESTROOT_FLAG = 0x00004000
SECURITY_FLAG_IGNORE_CERT_CN_INVALID = 0x00001000
AUTHTYPE_SERVER = 2
CERT_CHAIN_POLICY_SSL = 4
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
Expand Down Expand Up @@ -443,6 +444,10 @@ def _get_and_verify_cert_chain(
)
ssl_extra_cert_chain_policy_para.dwAuthType = AUTHTYPE_SERVER
ssl_extra_cert_chain_policy_para.fdwChecks = 0
if ssl_context.check_hostname is False:
ssl_extra_cert_chain_policy_para.fdwChecks = (
SECURITY_FLAG_IGNORE_CERT_CN_INVALID
)
if server_hostname:
ssl_extra_cert_chain_policy_para.pwszServerName = c_wchar_p(server_hostname)

Expand All @@ -452,8 +457,6 @@ def _get_and_verify_cert_chain(
)
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)
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_vendor/vendor.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ rich==13.7.1
resolvelib==1.0.1
setuptools==70.3.0
tomli==2.0.1
truststore==0.9.2
truststore==0.10.0
Loading