|
25 | 25 | f"Only OS X 10.8 and newer are supported, not {_mac_version_info[0]}.{_mac_version_info[1]}"
|
26 | 26 | )
|
27 | 27 |
|
| 28 | +_is_macos_version_10_14_or_later = _mac_version_info >= (10, 14) |
| 29 | + |
28 | 30 |
|
29 | 31 | def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
|
30 | 32 | """Loads a CDLL by name, falling back to known path on 10.16+"""
|
@@ -115,6 +117,12 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
|
115 | 117 | ]
|
116 | 118 | Security.SecTrustGetTrustResult.restype = OSStatus
|
117 | 119 |
|
| 120 | + Security.SecTrustEvaluate.argtypes = [ |
| 121 | + SecTrustRef, |
| 122 | + POINTER(SecTrustResultType), |
| 123 | + ] |
| 124 | + Security.SecTrustEvaluate.restype = OSStatus |
| 125 | + |
118 | 126 | Security.SecTrustRef = SecTrustRef # type: ignore[attr-defined]
|
119 | 127 | Security.SecTrustResultType = SecTrustResultType # type: ignore[attr-defined]
|
120 | 128 | Security.OSStatus = OSStatus # type: ignore[attr-defined]
|
@@ -197,8 +205,19 @@ def _load_cdll(name: str, macos10_16_path: str) -> CDLL:
|
197 | 205 | CoreFoundation.CFStringRef = CFStringRef # type: ignore[attr-defined]
|
198 | 206 | CoreFoundation.CFErrorRef = CFErrorRef # type: ignore[attr-defined]
|
199 | 207 |
|
200 |
| -except AttributeError: |
201 |
| - raise ImportError("Error initializing ctypes") from None |
| 208 | +except AttributeError as e: |
| 209 | + raise ImportError(f"Error initializing ctypes: {e}") from None |
| 210 | + |
| 211 | +# SecTrustEvaluateWithError is macOS 10.14+ |
| 212 | +if _is_macos_version_10_14_or_later: |
| 213 | + try: |
| 214 | + Security.SecTrustEvaluateWithError.argtypes = [ |
| 215 | + SecTrustRef, |
| 216 | + POINTER(CFErrorRef), |
| 217 | + ] |
| 218 | + Security.SecTrustEvaluateWithError.restype = c_bool |
| 219 | + except AttributeError as e: |
| 220 | + raise ImportError(f"Error initializing ctypes: {e}") from None |
202 | 221 |
|
203 | 222 |
|
204 | 223 | def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typing.Any:
|
@@ -258,6 +277,7 @@ def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typin
|
258 | 277 | Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus # type: ignore[assignment]
|
259 | 278 | Security.SecTrustSetAnchorCertificatesOnly.errcheck = _handle_osstatus # type: ignore[assignment]
|
260 | 279 | Security.SecTrustGetTrustResult.errcheck = _handle_osstatus # type: ignore[assignment]
|
| 280 | +Security.SecTrustEvaluate.errcheck = _handle_osstatus # type: ignore[assignment] |
261 | 281 |
|
262 | 282 |
|
263 | 283 | class CFConst:
|
@@ -365,9 +385,10 @@ def _verify_peercerts_impl(
|
365 | 385 | certs = None
|
366 | 386 | policies = None
|
367 | 387 | trust = None
|
368 |
| - cf_error = None |
369 | 388 | try:
|
370 |
| - if server_hostname is not None: |
| 389 | + # Only set a hostname on the policy if we're verifying the hostname |
| 390 | + # on the leaf certificate. |
| 391 | + if server_hostname is not None and ssl_context.check_hostname: |
371 | 392 | cf_str_hostname = None
|
372 | 393 | try:
|
373 | 394 | cf_str_hostname = _bytes_to_cf_string(server_hostname.encode("ascii"))
|
@@ -431,69 +452,120 @@ def _verify_peercerts_impl(
|
431 | 452 | # We always want system certificates.
|
432 | 453 | Security.SecTrustSetAnchorCertificatesOnly(trust, False)
|
433 | 454 |
|
434 |
| - cf_error = CoreFoundation.CFErrorRef() |
435 |
| - sec_trust_eval_result = Security.SecTrustEvaluateWithError( |
436 |
| - trust, ctypes.byref(cf_error) |
437 |
| - ) |
438 |
| - # sec_trust_eval_result is a bool (0 or 1) |
439 |
| - # where 1 means that the certs are trusted. |
440 |
| - if sec_trust_eval_result == 1: |
441 |
| - is_trusted = True |
442 |
| - elif sec_trust_eval_result == 0: |
443 |
| - is_trusted = False |
| 455 | + # macOS 10.13 and earlier don't support SecTrustEvaluateWithError() |
| 456 | + # so we use SecTrustEvaluate() which means we need to construct error |
| 457 | + # messages ourselves. |
| 458 | + if _is_macos_version_10_14_or_later: |
| 459 | + _verify_peercerts_impl_macos_10_14(ssl_context, trust) |
444 | 460 | else:
|
445 |
| - raise ssl.SSLError( |
446 |
| - f"Unknown result from Security.SecTrustEvaluateWithError: {sec_trust_eval_result!r}" |
447 |
| - ) |
448 |
| - |
449 |
| - cf_error_code = 0 |
450 |
| - if not is_trusted: |
451 |
| - cf_error_code = CoreFoundation.CFErrorGetCode(cf_error) |
452 |
| - |
453 |
| - # If the error is a known failure that we're |
454 |
| - # explicitly okay with from SSLContext configuration |
455 |
| - # we can set is_trusted accordingly. |
456 |
| - if ssl_context.verify_mode != ssl.CERT_REQUIRED and ( |
457 |
| - cf_error_code == CFConst.errSecNotTrusted |
458 |
| - or cf_error_code == CFConst.errSecCertificateExpired |
459 |
| - ): |
460 |
| - is_trusted = True |
461 |
| - elif ( |
462 |
| - not ssl_context.check_hostname |
463 |
| - and cf_error_code == CFConst.errSecHostNameMismatch |
464 |
| - ): |
465 |
| - is_trusted = True |
466 |
| - |
467 |
| - # If we're still not trusted then we start to |
468 |
| - # construct and raise the SSLCertVerificationError. |
469 |
| - if not is_trusted: |
470 |
| - cf_error_string_ref = None |
471 |
| - try: |
472 |
| - cf_error_string_ref = CoreFoundation.CFErrorCopyDescription(cf_error) |
473 |
| - |
474 |
| - # Can this ever return 'None' if there's a CFError? |
475 |
| - cf_error_message = ( |
476 |
| - _cf_string_ref_to_str(cf_error_string_ref) |
477 |
| - or "Certificate verification failed" |
478 |
| - ) |
479 |
| - |
480 |
| - # TODO: Not sure if we need the SecTrustResultType for anything? |
481 |
| - # We only care whether or not it's a success or failure for now. |
482 |
| - sec_trust_result_type = Security.SecTrustResultType() |
483 |
| - Security.SecTrustGetTrustResult( |
484 |
| - trust, ctypes.byref(sec_trust_result_type) |
485 |
| - ) |
486 |
| - |
487 |
| - err = ssl.SSLCertVerificationError(cf_error_message) |
488 |
| - err.verify_message = cf_error_message |
489 |
| - err.verify_code = cf_error_code |
490 |
| - raise err |
491 |
| - finally: |
492 |
| - if cf_error_string_ref: |
493 |
| - CoreFoundation.CFRelease(cf_error_string_ref) |
494 |
| - |
| 461 | + _verify_peercerts_impl_macos_10_13(ssl_context, trust) |
495 | 462 | finally:
|
496 | 463 | if policies:
|
497 | 464 | CoreFoundation.CFRelease(policies)
|
498 | 465 | if trust:
|
499 | 466 | CoreFoundation.CFRelease(trust)
|
| 467 | + |
| 468 | + |
| 469 | +def _verify_peercerts_impl_macos_10_13( |
| 470 | + ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any |
| 471 | +) -> None: |
| 472 | + """Verify using 'SecTrustEvaluate' API for macOS 10.13 and earlier. |
| 473 | + macOS 10.14 added the 'SecTrustEvaluateWithError' API. |
| 474 | + """ |
| 475 | + sec_trust_result_type = Security.SecTrustResultType() |
| 476 | + Security.SecTrustEvaluate(sec_trust_ref, ctypes.byref(sec_trust_result_type)) |
| 477 | + |
| 478 | + try: |
| 479 | + sec_trust_result_type_as_int = int(sec_trust_result_type.value) |
| 480 | + except (ValueError, TypeError): |
| 481 | + sec_trust_result_type_as_int = -1 |
| 482 | + |
| 483 | + # Apple doesn't document these values in their own API docs. |
| 484 | + # See: https://github.com/xybp888/iOS-SDKs/blob/master/iPhoneOS13.0.sdk/System/Library/Frameworks/Security.framework/Headers/SecTrust.h#L84 |
| 485 | + if ( |
| 486 | + ssl_context.verify_mode == ssl.CERT_REQUIRED |
| 487 | + and sec_trust_result_type_as_int not in (1, 4) |
| 488 | + ): |
| 489 | + # Note that we're not able to ignore only hostname errors |
| 490 | + # for macOS 10.13 and earlier, so check_hostname=False will |
| 491 | + # still return an error. |
| 492 | + sec_trust_result_type_to_message = { |
| 493 | + 0: "Invalid trust result type", |
| 494 | + # 1: "Trust evaluation succeeded", |
| 495 | + 2: "User confirmation required", |
| 496 | + 3: "User specified that certificate is not trusted", |
| 497 | + # 4: "Trust result is unspecified", |
| 498 | + 5: "Recoverable trust failure occurred", |
| 499 | + 6: "Fatal trust failure occurred", |
| 500 | + 7: "Other error occurred, certificate may be revoked", |
| 501 | + } |
| 502 | + error_message = sec_trust_result_type_to_message.get( |
| 503 | + sec_trust_result_type_as_int, |
| 504 | + f"Unknown trust result: {sec_trust_result_type_as_int}", |
| 505 | + ) |
| 506 | + |
| 507 | + err = ssl.SSLCertVerificationError(error_message) |
| 508 | + err.verify_message = error_message |
| 509 | + err.verify_code = sec_trust_result_type_as_int |
| 510 | + raise err |
| 511 | + |
| 512 | + |
| 513 | +def _verify_peercerts_impl_macos_10_14( |
| 514 | + ssl_context: ssl.SSLContext, sec_trust_ref: typing.Any |
| 515 | +) -> None: |
| 516 | + """Verify using 'SecTrustEvaluateWithError' API for macOS 10.14+.""" |
| 517 | + cf_error = CoreFoundation.CFErrorRef() |
| 518 | + sec_trust_eval_result = Security.SecTrustEvaluateWithError( |
| 519 | + sec_trust_ref, ctypes.byref(cf_error) |
| 520 | + ) |
| 521 | + # sec_trust_eval_result is a bool (0 or 1) |
| 522 | + # where 1 means that the certs are trusted. |
| 523 | + if sec_trust_eval_result == 1: |
| 524 | + is_trusted = True |
| 525 | + elif sec_trust_eval_result == 0: |
| 526 | + is_trusted = False |
| 527 | + else: |
| 528 | + raise ssl.SSLError( |
| 529 | + f"Unknown result from Security.SecTrustEvaluateWithError: {sec_trust_eval_result!r}" |
| 530 | + ) |
| 531 | + |
| 532 | + cf_error_code = 0 |
| 533 | + if not is_trusted: |
| 534 | + cf_error_code = CoreFoundation.CFErrorGetCode(cf_error) |
| 535 | + |
| 536 | + # If the error is a known failure that we're |
| 537 | + # explicitly okay with from SSLContext configuration |
| 538 | + # we can set is_trusted accordingly. |
| 539 | + if ssl_context.verify_mode != ssl.CERT_REQUIRED and ( |
| 540 | + cf_error_code == CFConst.errSecNotTrusted |
| 541 | + or cf_error_code == CFConst.errSecCertificateExpired |
| 542 | + ): |
| 543 | + is_trusted = True |
| 544 | + |
| 545 | + # If we're still not trusted then we start to |
| 546 | + # construct and raise the SSLCertVerificationError. |
| 547 | + if not is_trusted: |
| 548 | + cf_error_string_ref = None |
| 549 | + try: |
| 550 | + cf_error_string_ref = CoreFoundation.CFErrorCopyDescription(cf_error) |
| 551 | + |
| 552 | + # Can this ever return 'None' if there's a CFError? |
| 553 | + cf_error_message = ( |
| 554 | + _cf_string_ref_to_str(cf_error_string_ref) |
| 555 | + or "Certificate verification failed" |
| 556 | + ) |
| 557 | + |
| 558 | + # TODO: Not sure if we need the SecTrustResultType for anything? |
| 559 | + # We only care whether or not it's a success or failure for now. |
| 560 | + sec_trust_result_type = Security.SecTrustResultType() |
| 561 | + Security.SecTrustGetTrustResult( |
| 562 | + sec_trust_ref, ctypes.byref(sec_trust_result_type) |
| 563 | + ) |
| 564 | + |
| 565 | + err = ssl.SSLCertVerificationError(cf_error_message) |
| 566 | + err.verify_message = cf_error_message |
| 567 | + err.verify_code = cf_error_code |
| 568 | + raise err |
| 569 | + finally: |
| 570 | + if cf_error_string_ref: |
| 571 | + CoreFoundation.CFRelease(cf_error_string_ref) |
0 commit comments