Skip to content

Commit

Permalink
Use the 'SecTrustEvaluate' API for macOS 10.12 and earlier
Browse files Browse the repository at this point in the history
Co-authored-by: =?UTF-8?q?Ren=C3=A9=20Dudfield?= <renesd@gmail.com>
  • Loading branch information
sethmlarson and illume committed Oct 2, 2024
1 parent 419b692 commit ed7756c
Showing 1 changed file with 120 additions and 61 deletions.
181 changes: 120 additions & 61 deletions src/truststore/_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ 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.SecTrustEvaluate.argtypes = [
SecTrustRef,
POINTER(CFErrorRef),
]
Security.SecTrustEvaluate.restype = c_bool

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 @@ -258,6 +270,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,7 +378,6 @@ def _verify_peercerts_impl(
certs = None
policies = None
trust = None
cf_error = None
try:
if server_hostname is not None:
cf_str_hostname = None
Expand Down Expand Up @@ -431,69 +443,116 @@ 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.12 and earlier don't support SecTrustEvaluateWithError()
# so we use SecTrustEvaluate() which means we need to construct error
# messages ourselves.
if 1 or _mac_version_info < (10, 12):
_verify_peercerts_impl_macos_10_12(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_12(sec_trust_ref: typing.Any) -> None:
"""Verify using 'SecTrustEvaluate' API for macOS 10.12 and earlier.
macOS 10.13 added the 'SecTrustEvaluateError' 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

# See: https://developer.apple.com/documentation/security/sectrustevaluate(_:_:)?language=objc
if sec_trust_result_type_as_int not in (1, 4):
sec_trust_result_type_to_message = {
0: "Invalid trust result type",
# 1: "Trust evaluation succeeded",
2: "Trust was explicitly denied",
3: "Fatal trust failure occurred",
# 4: "Trust result is unspecified (but trusted)",
5: "Recoverable trust failure occurred",
6: "Unknown error occurred",
7: "User confirmation required",
}
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_13(
ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any
) -> None:
"""Verify using 'SecTrustEvaluateError' API for macOS 10.13+."""
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
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(
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)

0 comments on commit ed7756c

Please sign in to comment.