m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

474 lines
16 KiB

6 months ago
  1. """
  2. Low-level helpers for the SecureTransport bindings.
  3. These are Python functions that are not directly related to the high-level APIs
  4. but are necessary to get them to work. They include a whole bunch of low-level
  5. CoreFoundation messing about and memory management. The concerns in this module
  6. are almost entirely about trying to avoid memory leaks and providing
  7. appropriate and useful assistance to the higher-level code.
  8. """
  9. from __future__ import annotations
  10. import base64
  11. import ctypes
  12. import itertools
  13. import os
  14. import re
  15. import ssl
  16. import struct
  17. import tempfile
  18. import typing
  19. from .bindings import ( # type: ignore[attr-defined]
  20. CFArray,
  21. CFConst,
  22. CFData,
  23. CFDictionary,
  24. CFMutableArray,
  25. CFString,
  26. CFTypeRef,
  27. CoreFoundation,
  28. SecKeychainRef,
  29. Security,
  30. )
  31. # This regular expression is used to grab PEM data out of a PEM bundle.
  32. _PEM_CERTS_RE = re.compile(
  33. b"-----BEGIN CERTIFICATE-----\n(.*?)\n-----END CERTIFICATE-----", re.DOTALL
  34. )
  35. def _cf_data_from_bytes(bytestring: bytes) -> CFData:
  36. """
  37. Given a bytestring, create a CFData object from it. This CFData object must
  38. be CFReleased by the caller.
  39. """
  40. return CoreFoundation.CFDataCreate(
  41. CoreFoundation.kCFAllocatorDefault, bytestring, len(bytestring)
  42. )
  43. def _cf_dictionary_from_tuples(
  44. tuples: list[tuple[typing.Any, typing.Any]]
  45. ) -> CFDictionary:
  46. """
  47. Given a list of Python tuples, create an associated CFDictionary.
  48. """
  49. dictionary_size = len(tuples)
  50. # We need to get the dictionary keys and values out in the same order.
  51. keys = (t[0] for t in tuples)
  52. values = (t[1] for t in tuples)
  53. cf_keys = (CoreFoundation.CFTypeRef * dictionary_size)(*keys)
  54. cf_values = (CoreFoundation.CFTypeRef * dictionary_size)(*values)
  55. return CoreFoundation.CFDictionaryCreate(
  56. CoreFoundation.kCFAllocatorDefault,
  57. cf_keys,
  58. cf_values,
  59. dictionary_size,
  60. CoreFoundation.kCFTypeDictionaryKeyCallBacks,
  61. CoreFoundation.kCFTypeDictionaryValueCallBacks,
  62. )
  63. def _cfstr(py_bstr: bytes) -> CFString:
  64. """
  65. Given a Python binary data, create a CFString.
  66. The string must be CFReleased by the caller.
  67. """
  68. c_str = ctypes.c_char_p(py_bstr)
  69. cf_str = CoreFoundation.CFStringCreateWithCString(
  70. CoreFoundation.kCFAllocatorDefault,
  71. c_str,
  72. CFConst.kCFStringEncodingUTF8,
  73. )
  74. return cf_str
  75. def _create_cfstring_array(lst: list[bytes]) -> CFMutableArray:
  76. """
  77. Given a list of Python binary data, create an associated CFMutableArray.
  78. The array must be CFReleased by the caller.
  79. Raises an ssl.SSLError on failure.
  80. """
  81. cf_arr = None
  82. try:
  83. cf_arr = CoreFoundation.CFArrayCreateMutable(
  84. CoreFoundation.kCFAllocatorDefault,
  85. 0,
  86. ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),
  87. )
  88. if not cf_arr:
  89. raise MemoryError("Unable to allocate memory!")
  90. for item in lst:
  91. cf_str = _cfstr(item)
  92. if not cf_str:
  93. raise MemoryError("Unable to allocate memory!")
  94. try:
  95. CoreFoundation.CFArrayAppendValue(cf_arr, cf_str)
  96. finally:
  97. CoreFoundation.CFRelease(cf_str)
  98. except BaseException as e:
  99. if cf_arr:
  100. CoreFoundation.CFRelease(cf_arr)
  101. raise ssl.SSLError(f"Unable to allocate array: {e}") from None
  102. return cf_arr
  103. def _cf_string_to_unicode(value: CFString) -> str | None:
  104. """
  105. Creates a Unicode string from a CFString object. Used entirely for error
  106. reporting.
  107. Yes, it annoys me quite a lot that this function is this complex.
  108. """
  109. value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p))
  110. string = CoreFoundation.CFStringGetCStringPtr(
  111. value_as_void_p, CFConst.kCFStringEncodingUTF8
  112. )
  113. if string is None:
  114. buffer = ctypes.create_string_buffer(1024)
  115. result = CoreFoundation.CFStringGetCString(
  116. value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8
  117. )
  118. if not result:
  119. raise OSError("Error copying C string from CFStringRef")
  120. string = buffer.value
  121. if string is not None:
  122. string = string.decode("utf-8")
  123. return string # type: ignore[no-any-return]
  124. def _assert_no_error(
  125. error: int, exception_class: type[BaseException] | None = None
  126. ) -> None:
  127. """
  128. Checks the return code and throws an exception if there is an error to
  129. report
  130. """
  131. if error == 0:
  132. return
  133. cf_error_string = Security.SecCopyErrorMessageString(error, None)
  134. output = _cf_string_to_unicode(cf_error_string)
  135. CoreFoundation.CFRelease(cf_error_string)
  136. if output is None or output == "":
  137. output = f"OSStatus {error}"
  138. if exception_class is None:
  139. exception_class = ssl.SSLError
  140. raise exception_class(output)
  141. def _cert_array_from_pem(pem_bundle: bytes) -> CFArray:
  142. """
  143. Given a bundle of certs in PEM format, turns them into a CFArray of certs
  144. that can be used to validate a cert chain.
  145. """
  146. # Normalize the PEM bundle's line endings.
  147. pem_bundle = pem_bundle.replace(b"\r\n", b"\n")
  148. der_certs = [
  149. base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle)
  150. ]
  151. if not der_certs:
  152. raise ssl.SSLError("No root certificates specified")
  153. cert_array = CoreFoundation.CFArrayCreateMutable(
  154. CoreFoundation.kCFAllocatorDefault,
  155. 0,
  156. ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),
  157. )
  158. if not cert_array:
  159. raise ssl.SSLError("Unable to allocate memory!")
  160. try:
  161. for der_bytes in der_certs:
  162. certdata = _cf_data_from_bytes(der_bytes)
  163. if not certdata:
  164. raise ssl.SSLError("Unable to allocate memory!")
  165. cert = Security.SecCertificateCreateWithData(
  166. CoreFoundation.kCFAllocatorDefault, certdata
  167. )
  168. CoreFoundation.CFRelease(certdata)
  169. if not cert:
  170. raise ssl.SSLError("Unable to build cert object!")
  171. CoreFoundation.CFArrayAppendValue(cert_array, cert)
  172. CoreFoundation.CFRelease(cert)
  173. except Exception:
  174. # We need to free the array before the exception bubbles further.
  175. # We only want to do that if an error occurs: otherwise, the caller
  176. # should free.
  177. CoreFoundation.CFRelease(cert_array)
  178. raise
  179. return cert_array
  180. def _is_cert(item: CFTypeRef) -> bool:
  181. """
  182. Returns True if a given CFTypeRef is a certificate.
  183. """
  184. expected = Security.SecCertificateGetTypeID()
  185. return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return]
  186. def _is_identity(item: CFTypeRef) -> bool:
  187. """
  188. Returns True if a given CFTypeRef is an identity.
  189. """
  190. expected = Security.SecIdentityGetTypeID()
  191. return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return]
  192. def _temporary_keychain() -> tuple[SecKeychainRef, str]:
  193. """
  194. This function creates a temporary Mac keychain that we can use to work with
  195. credentials. This keychain uses a one-time password and a temporary file to
  196. store the data. We expect to have one keychain per socket. The returned
  197. SecKeychainRef must be freed by the caller, including calling
  198. SecKeychainDelete.
  199. Returns a tuple of the SecKeychainRef and the path to the temporary
  200. directory that contains it.
  201. """
  202. # Unfortunately, SecKeychainCreate requires a path to a keychain. This
  203. # means we cannot use mkstemp to use a generic temporary file. Instead,
  204. # we're going to create a temporary directory and a filename to use there.
  205. # This filename will be 8 random bytes expanded into base64. We also need
  206. # some random bytes to password-protect the keychain we're creating, so we
  207. # ask for 40 random bytes.
  208. random_bytes = os.urandom(40)
  209. filename = base64.b16encode(random_bytes[:8]).decode("utf-8")
  210. password = base64.b16encode(random_bytes[8:]) # Must be valid UTF-8
  211. tempdirectory = tempfile.mkdtemp()
  212. keychain_path = os.path.join(tempdirectory, filename).encode("utf-8")
  213. # We now want to create the keychain itself.
  214. keychain = Security.SecKeychainRef()
  215. status = Security.SecKeychainCreate(
  216. keychain_path, len(password), password, False, None, ctypes.byref(keychain)
  217. )
  218. _assert_no_error(status)
  219. # Having created the keychain, we want to pass it off to the caller.
  220. return keychain, tempdirectory
  221. def _load_items_from_file(
  222. keychain: SecKeychainRef, path: str
  223. ) -> tuple[list[CFTypeRef], list[CFTypeRef]]:
  224. """
  225. Given a single file, loads all the trust objects from it into arrays and
  226. the keychain.
  227. Returns a tuple of lists: the first list is a list of identities, the
  228. second a list of certs.
  229. """
  230. certificates = []
  231. identities = []
  232. result_array = None
  233. with open(path, "rb") as f:
  234. raw_filedata = f.read()
  235. try:
  236. filedata = CoreFoundation.CFDataCreate(
  237. CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata)
  238. )
  239. result_array = CoreFoundation.CFArrayRef()
  240. result = Security.SecItemImport(
  241. filedata, # cert data
  242. None, # Filename, leaving it out for now
  243. None, # What the type of the file is, we don't care
  244. None, # what's in the file, we don't care
  245. 0, # import flags
  246. None, # key params, can include passphrase in the future
  247. keychain, # The keychain to insert into
  248. ctypes.byref(result_array), # Results
  249. )
  250. _assert_no_error(result)
  251. # A CFArray is not very useful to us as an intermediary
  252. # representation, so we are going to extract the objects we want
  253. # and then free the array. We don't need to keep hold of keys: the
  254. # keychain already has them!
  255. result_count = CoreFoundation.CFArrayGetCount(result_array)
  256. for index in range(result_count):
  257. item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index)
  258. item = ctypes.cast(item, CoreFoundation.CFTypeRef)
  259. if _is_cert(item):
  260. CoreFoundation.CFRetain(item)
  261. certificates.append(item)
  262. elif _is_identity(item):
  263. CoreFoundation.CFRetain(item)
  264. identities.append(item)
  265. finally:
  266. if result_array:
  267. CoreFoundation.CFRelease(result_array)
  268. CoreFoundation.CFRelease(filedata)
  269. return (identities, certificates)
  270. def _load_client_cert_chain(keychain: SecKeychainRef, *paths: str | None) -> CFArray:
  271. """
  272. Load certificates and maybe keys from a number of files. Has the end goal
  273. of returning a CFArray containing one SecIdentityRef, and then zero or more
  274. SecCertificateRef objects, suitable for use as a client certificate trust
  275. chain.
  276. """
  277. # Ok, the strategy.
  278. #
  279. # This relies on knowing that macOS will not give you a SecIdentityRef
  280. # unless you have imported a key into a keychain. This is a somewhat
  281. # artificial limitation of macOS (for example, it doesn't necessarily
  282. # affect iOS), but there is nothing inside Security.framework that lets you
  283. # get a SecIdentityRef without having a key in a keychain.
  284. #
  285. # So the policy here is we take all the files and iterate them in order.
  286. # Each one will use SecItemImport to have one or more objects loaded from
  287. # it. We will also point at a keychain that macOS can use to work with the
  288. # private key.
  289. #
  290. # Once we have all the objects, we'll check what we actually have. If we
  291. # already have a SecIdentityRef in hand, fab: we'll use that. Otherwise,
  292. # we'll take the first certificate (which we assume to be our leaf) and
  293. # ask the keychain to give us a SecIdentityRef with that cert's associated
  294. # key.
  295. #
  296. # We'll then return a CFArray containing the trust chain: one
  297. # SecIdentityRef and then zero-or-more SecCertificateRef objects. The
  298. # responsibility for freeing this CFArray will be with the caller. This
  299. # CFArray must remain alive for the entire connection, so in practice it
  300. # will be stored with a single SSLSocket, along with the reference to the
  301. # keychain.
  302. certificates = []
  303. identities = []
  304. # Filter out bad paths.
  305. filtered_paths = (path for path in paths if path)
  306. try:
  307. for file_path in filtered_paths:
  308. new_identities, new_certs = _load_items_from_file(keychain, file_path)
  309. identities.extend(new_identities)
  310. certificates.extend(new_certs)
  311. # Ok, we have everything. The question is: do we have an identity? If
  312. # not, we want to grab one from the first cert we have.
  313. if not identities:
  314. new_identity = Security.SecIdentityRef()
  315. status = Security.SecIdentityCreateWithCertificate(
  316. keychain, certificates[0], ctypes.byref(new_identity)
  317. )
  318. _assert_no_error(status)
  319. identities.append(new_identity)
  320. # We now want to release the original certificate, as we no longer
  321. # need it.
  322. CoreFoundation.CFRelease(certificates.pop(0))
  323. # We now need to build a new CFArray that holds the trust chain.
  324. trust_chain = CoreFoundation.CFArrayCreateMutable(
  325. CoreFoundation.kCFAllocatorDefault,
  326. 0,
  327. ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks),
  328. )
  329. for item in itertools.chain(identities, certificates):
  330. # ArrayAppendValue does a CFRetain on the item. That's fine,
  331. # because the finally block will release our other refs to them.
  332. CoreFoundation.CFArrayAppendValue(trust_chain, item)
  333. return trust_chain
  334. finally:
  335. for obj in itertools.chain(identities, certificates):
  336. CoreFoundation.CFRelease(obj)
  337. TLS_PROTOCOL_VERSIONS = {
  338. "SSLv2": (0, 2),
  339. "SSLv3": (3, 0),
  340. "TLSv1": (3, 1),
  341. "TLSv1.1": (3, 2),
  342. "TLSv1.2": (3, 3),
  343. }
  344. def _build_tls_unknown_ca_alert(version: str) -> bytes:
  345. """
  346. Builds a TLS alert record for an unknown CA.
  347. """
  348. ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version]
  349. severity_fatal = 0x02
  350. description_unknown_ca = 0x30
  351. msg = struct.pack(">BB", severity_fatal, description_unknown_ca)
  352. msg_len = len(msg)
  353. record_type_alert = 0x15
  354. record = struct.pack(">BBBH", record_type_alert, ver_maj, ver_min, msg_len) + msg
  355. return record
  356. class SecurityConst:
  357. """
  358. A class object that acts as essentially a namespace for Security constants.
  359. """
  360. kSSLSessionOptionBreakOnServerAuth = 0
  361. kSSLProtocol2 = 1
  362. kSSLProtocol3 = 2
  363. kTLSProtocol1 = 4
  364. kTLSProtocol11 = 7
  365. kTLSProtocol12 = 8
  366. # SecureTransport does not support TLS 1.3 even if there's a constant for it
  367. kTLSProtocol13 = 10
  368. kTLSProtocolMaxSupported = 999
  369. kSSLClientSide = 1
  370. kSSLStreamType = 0
  371. kSecFormatPEMSequence = 10
  372. kSecTrustResultInvalid = 0
  373. kSecTrustResultProceed = 1
  374. # This gap is present on purpose: this was kSecTrustResultConfirm, which
  375. # is deprecated.
  376. kSecTrustResultDeny = 3
  377. kSecTrustResultUnspecified = 4
  378. kSecTrustResultRecoverableTrustFailure = 5
  379. kSecTrustResultFatalTrustFailure = 6
  380. kSecTrustResultOtherError = 7
  381. errSSLProtocol = -9800
  382. errSSLWouldBlock = -9803
  383. errSSLClosedGraceful = -9805
  384. errSSLClosedNoNotify = -9816
  385. errSSLClosedAbort = -9806
  386. errSSLXCertChainInvalid = -9807
  387. errSSLCrypto = -9809
  388. errSSLInternal = -9810
  389. errSSLCertExpired = -9814
  390. errSSLCertNotYetValid = -9815
  391. errSSLUnknownRootCert = -9812
  392. errSSLNoRootCert = -9813
  393. errSSLHostNameMismatch = -9843
  394. errSSLPeerHandshakeFail = -9824
  395. errSSLPeerUserCancelled = -9839
  396. errSSLWeakPeerEphemeralDHKey = -9850
  397. errSSLServerAuthCompleted = -9841
  398. errSSLRecordOverflow = -9847
  399. errSecVerifyFailed = -67808
  400. errSecNoTrustSettings = -25263
  401. errSecItemNotFound = -25300
  402. errSecInvalidTrustSettings = -25262