snowflake-connector-python 3.16.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. snowflake/connector/__init__.py +96 -0
  2. snowflake/connector/_query_context_cache.py +276 -0
  3. snowflake/connector/_sql_util.py +48 -0
  4. snowflake/connector/_utils.py +70 -0
  5. snowflake/connector/arrow_context.py +203 -0
  6. snowflake/connector/auth/__init__.py +53 -0
  7. snowflake/connector/auth/_auth.py +586 -0
  8. snowflake/connector/auth/_http_server.py +220 -0
  9. snowflake/connector/auth/_oauth_base.py +400 -0
  10. snowflake/connector/auth/by_plugin.py +219 -0
  11. snowflake/connector/auth/default.py +36 -0
  12. snowflake/connector/auth/idtoken.py +72 -0
  13. snowflake/connector/auth/keypair.py +222 -0
  14. snowflake/connector/auth/no_auth.py +39 -0
  15. snowflake/connector/auth/oauth.py +49 -0
  16. snowflake/connector/auth/oauth_code.py +479 -0
  17. snowflake/connector/auth/oauth_credentials.py +66 -0
  18. snowflake/connector/auth/okta.py +335 -0
  19. snowflake/connector/auth/pat.py +39 -0
  20. snowflake/connector/auth/usrpwdmfa.py +69 -0
  21. snowflake/connector/auth/webbrowser.py +502 -0
  22. snowflake/connector/auth/workload_identity.py +94 -0
  23. snowflake/connector/azure_storage_client.py +276 -0
  24. snowflake/connector/backoff_policies.py +141 -0
  25. snowflake/connector/bind_upload_agent.py +89 -0
  26. snowflake/connector/cache.py +696 -0
  27. snowflake/connector/compat.py +127 -0
  28. snowflake/connector/config_manager.py +496 -0
  29. snowflake/connector/connection.py +2273 -0
  30. snowflake/connector/connection_diagnostic.py +776 -0
  31. snowflake/connector/constants.py +442 -0
  32. snowflake/connector/converter.py +785 -0
  33. snowflake/connector/converter_issue23517.py +87 -0
  34. snowflake/connector/converter_null.py +14 -0
  35. snowflake/connector/converter_snowsql.py +205 -0
  36. snowflake/connector/cursor.py +1951 -0
  37. snowflake/connector/dbapi.py +53 -0
  38. snowflake/connector/description.py +19 -0
  39. snowflake/connector/direct_file_operation_utils.py +88 -0
  40. snowflake/connector/encryption_util.py +220 -0
  41. snowflake/connector/errorcode.py +91 -0
  42. snowflake/connector/errors.py +615 -0
  43. snowflake/connector/externals_utils/__init__.py +0 -0
  44. snowflake/connector/externals_utils/externals_setup.py +27 -0
  45. snowflake/connector/feature.py +4 -0
  46. snowflake/connector/file_compression_type.py +118 -0
  47. snowflake/connector/file_lock.py +72 -0
  48. snowflake/connector/file_transfer_agent.py +1215 -0
  49. snowflake/connector/file_util.py +153 -0
  50. snowflake/connector/gcs_storage_client.py +474 -0
  51. snowflake/connector/gzip_decoder.py +85 -0
  52. snowflake/connector/local_storage_client.py +90 -0
  53. snowflake/connector/log_configuration.py +60 -0
  54. snowflake/connector/logging_utils/__init__.py +0 -0
  55. snowflake/connector/logging_utils/filters.py +72 -0
  56. snowflake/connector/nanoarrow_arrow_iterator.cp313-win_amd64.pyd +0 -0
  57. snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +60 -0
  58. snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
  59. snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +19 -0
  60. snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
  61. snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
  62. snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
  63. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp +557 -0
  64. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.hpp +98 -0
  65. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +125 -0
  66. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +115 -0
  67. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp +1005 -0
  68. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.hpp +127 -0
  69. snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +47 -0
  70. snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
  71. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +83 -0
  72. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
  73. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +97 -0
  74. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +72 -0
  75. snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +73 -0
  76. snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
  77. snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +30 -0
  78. snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
  79. snowflake/connector/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
  80. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
  81. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +45 -0
  82. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +71 -0
  83. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +56 -0
  84. snowflake/connector/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
  85. snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +75 -0
  86. snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
  87. snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +46 -0
  88. snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
  89. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
  90. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +95 -0
  91. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +57 -0
  92. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +36 -0
  93. snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +34 -0
  94. snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
  95. snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +19 -0
  96. snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
  97. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +36 -0
  98. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
  99. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +346 -0
  100. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +145 -0
  101. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
  102. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
  103. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
  104. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +101 -0
  105. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +127 -0
  106. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
  107. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1908 -0
  108. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +215 -0
  109. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +125 -0
  110. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
  111. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +55 -0
  112. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +148 -0
  113. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
  114. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
  115. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
  116. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +162 -0
  117. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +97 -0
  118. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +239 -0
  119. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
  120. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +210 -0
  121. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +84 -0
  122. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +84 -0
  123. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
  124. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
  125. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +206 -0
  126. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +118 -0
  127. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
  128. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
  129. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
  130. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
  131. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
  132. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +162 -0
  133. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +898 -0
  134. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +190 -0
  135. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
  136. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
  137. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc.c +3204 -0
  138. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3217 -0
  139. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3618 -0
  140. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +379 -0
  141. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iterator.pyx +256 -0
  142. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.c +512 -0
  143. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.h +350 -0
  144. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +33273 -0
  145. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +397 -0
  146. snowflake/connector/nanoarrow_cpp/Logging/logging.cpp +100 -0
  147. snowflake/connector/nanoarrow_cpp/Logging/logging.hpp +49 -0
  148. snowflake/connector/network.py +1297 -0
  149. snowflake/connector/ocsp_asn1crypto.py +447 -0
  150. snowflake/connector/ocsp_snowflake.py +1883 -0
  151. snowflake/connector/options.py +131 -0
  152. snowflake/connector/pandas_tools.py +732 -0
  153. snowflake/connector/proxy.py +43 -0
  154. snowflake/connector/py.typed +0 -0
  155. snowflake/connector/result_batch.py +786 -0
  156. snowflake/connector/result_set.py +319 -0
  157. snowflake/connector/s3_storage_client.py +605 -0
  158. snowflake/connector/secret_detector.py +181 -0
  159. snowflake/connector/sf_dirs.py +64 -0
  160. snowflake/connector/sfbinaryformat.py +35 -0
  161. snowflake/connector/sfdatetime.py +345 -0
  162. snowflake/connector/snow_logging.py +144 -0
  163. snowflake/connector/sqlstate.py +9 -0
  164. snowflake/connector/ssd_internal_keys.py +30 -0
  165. snowflake/connector/ssl_wrap_socket.py +134 -0
  166. snowflake/connector/storage_client.py +478 -0
  167. snowflake/connector/telemetry.py +249 -0
  168. snowflake/connector/telemetry_oob.py +543 -0
  169. snowflake/connector/test_util.py +30 -0
  170. snowflake/connector/time_util.py +159 -0
  171. snowflake/connector/token_cache.py +402 -0
  172. snowflake/connector/tool/__init__.py +0 -0
  173. snowflake/connector/tool/dump_certs.py +57 -0
  174. snowflake/connector/tool/dump_ocsp_response.py +139 -0
  175. snowflake/connector/tool/dump_ocsp_response_cache.py +194 -0
  176. snowflake/connector/tool/probe_connection.py +69 -0
  177. snowflake/connector/url_util.py +49 -0
  178. snowflake/connector/util_text.py +301 -0
  179. snowflake/connector/vendored/__init__.py +3 -0
  180. snowflake/connector/vendored/requests/LICENSE +175 -0
  181. snowflake/connector/vendored/requests/__init__.py +169 -0
  182. snowflake/connector/vendored/requests/__version__.py +14 -0
  183. snowflake/connector/vendored/requests/_internal_utils.py +50 -0
  184. snowflake/connector/vendored/requests/adapters.py +537 -0
  185. snowflake/connector/vendored/requests/api.py +157 -0
  186. snowflake/connector/vendored/requests/auth.py +315 -0
  187. snowflake/connector/vendored/requests/certs.py +17 -0
  188. snowflake/connector/vendored/requests/compat.py +79 -0
  189. snowflake/connector/vendored/requests/cookies.py +561 -0
  190. snowflake/connector/vendored/requests/exceptions.py +140 -0
  191. snowflake/connector/vendored/requests/help.py +134 -0
  192. snowflake/connector/vendored/requests/hooks.py +33 -0
  193. snowflake/connector/vendored/requests/models.py +1033 -0
  194. snowflake/connector/vendored/requests/sessions.py +833 -0
  195. snowflake/connector/vendored/requests/status_codes.py +128 -0
  196. snowflake/connector/vendored/requests/structures.py +99 -0
  197. snowflake/connector/vendored/requests/utils.py +1093 -0
  198. snowflake/connector/vendored/urllib3/LICENSE.txt +21 -0
  199. snowflake/connector/vendored/urllib3/__init__.py +85 -0
  200. snowflake/connector/vendored/urllib3/_collections.py +355 -0
  201. snowflake/connector/vendored/urllib3/_version.py +2 -0
  202. snowflake/connector/vendored/urllib3/connection.py +572 -0
  203. snowflake/connector/vendored/urllib3/connectionpool.py +1137 -0
  204. snowflake/connector/vendored/urllib3/contrib/__init__.py +0 -0
  205. snowflake/connector/vendored/urllib3/contrib/_appengine_environ.py +36 -0
  206. snowflake/connector/vendored/urllib3/contrib/_securetransport/__init__.py +0 -0
  207. snowflake/connector/vendored/urllib3/contrib/_securetransport/bindings.py +519 -0
  208. snowflake/connector/vendored/urllib3/contrib/_securetransport/low_level.py +397 -0
  209. snowflake/connector/vendored/urllib3/contrib/appengine.py +314 -0
  210. snowflake/connector/vendored/urllib3/contrib/ntlmpool.py +130 -0
  211. snowflake/connector/vendored/urllib3/contrib/pyopenssl.py +509 -0
  212. snowflake/connector/vendored/urllib3/contrib/securetransport.py +920 -0
  213. snowflake/connector/vendored/urllib3/contrib/socks.py +216 -0
  214. snowflake/connector/vendored/urllib3/exceptions.py +323 -0
  215. snowflake/connector/vendored/urllib3/fields.py +274 -0
  216. snowflake/connector/vendored/urllib3/filepost.py +98 -0
  217. snowflake/connector/vendored/urllib3/packages/__init__.py +0 -0
  218. snowflake/connector/vendored/urllib3/packages/backports/__init__.py +0 -0
  219. snowflake/connector/vendored/urllib3/packages/backports/makefile.py +51 -0
  220. snowflake/connector/vendored/urllib3/packages/backports/weakref_finalize.py +155 -0
  221. snowflake/connector/vendored/urllib3/packages/six.py +1076 -0
  222. snowflake/connector/vendored/urllib3/poolmanager.py +540 -0
  223. snowflake/connector/vendored/urllib3/request.py +191 -0
  224. snowflake/connector/vendored/urllib3/response.py +885 -0
  225. snowflake/connector/vendored/urllib3/util/__init__.py +49 -0
  226. snowflake/connector/vendored/urllib3/util/connection.py +156 -0
  227. snowflake/connector/vendored/urllib3/util/proxy.py +57 -0
  228. snowflake/connector/vendored/urllib3/util/queue.py +22 -0
  229. snowflake/connector/vendored/urllib3/util/request.py +146 -0
  230. snowflake/connector/vendored/urllib3/util/response.py +107 -0
  231. snowflake/connector/vendored/urllib3/util/retry.py +620 -0
  232. snowflake/connector/vendored/urllib3/util/ssl_.py +495 -0
  233. snowflake/connector/vendored/urllib3/util/ssl_match_hostname.py +159 -0
  234. snowflake/connector/vendored/urllib3/util/ssltransport.py +221 -0
  235. snowflake/connector/vendored/urllib3/util/timeout.py +271 -0
  236. snowflake/connector/vendored/urllib3/util/url.py +435 -0
  237. snowflake/connector/vendored/urllib3/util/wait.py +152 -0
  238. snowflake/connector/version.py +3 -0
  239. snowflake/connector/wif_util.py +407 -0
  240. snowflake_connector_python-3.16.0.dist-info/METADATA +1475 -0
  241. snowflake_connector_python-3.16.0.dist-info/RECORD +246 -0
  242. snowflake_connector_python-3.16.0.dist-info/WHEEL +5 -0
  243. snowflake_connector_python-3.16.0.dist-info/entry_points.txt +4 -0
  244. snowflake_connector_python-3.16.0.dist-info/licenses/LICENSE.txt +202 -0
  245. snowflake_connector_python-3.16.0.dist-info/licenses/NOTICE +8 -0
  246. snowflake_connector_python-3.16.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1883 @@
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import codecs
5
+ import importlib
6
+ import json
7
+ import os
8
+ import platform
9
+ import re
10
+ import sys
11
+ import tempfile
12
+ import time
13
+ from base64 import b64decode, b64encode
14
+ from datetime import datetime, timezone
15
+ from logging import getLogger
16
+ from os import environ, path
17
+ from os.path import expanduser
18
+ from threading import Lock, RLock
19
+ from time import gmtime, strftime
20
+ from typing import Any, NamedTuple
21
+
22
+ # We use regular requests and urlib3 when we reach out to do OCSP checks, basically in this very narrow
23
+ # part of the code where we want to call out to check for revoked certificates,
24
+ # we don't want to use our hardened version of requests.
25
+ import requests as generic_requests
26
+ from asn1crypto.ocsp import CertId, OCSPRequest, SingleResponse
27
+ from asn1crypto.x509 import Certificate
28
+ from OpenSSL.SSL import Connection
29
+
30
+ from snowflake.connector import SNOWFLAKE_CONNECTOR_VERSION
31
+ from snowflake.connector.compat import OK, urlsplit, urlunparse
32
+ from snowflake.connector.constants import HTTP_HEADER_USER_AGENT
33
+ from snowflake.connector.errorcode import (
34
+ ER_INVALID_OCSP_RESPONSE_SSD,
35
+ ER_INVALID_SSD,
36
+ ER_OCSP_FAILED_TO_CONNECT_CACHE_SERVER,
37
+ ER_OCSP_RESPONSE_ATTACHED_CERT_EXPIRED,
38
+ ER_OCSP_RESPONSE_ATTACHED_CERT_INVALID,
39
+ ER_OCSP_RESPONSE_CACHE_DECODE_FAILED,
40
+ ER_OCSP_RESPONSE_CACHE_DOWNLOAD_FAILED,
41
+ ER_OCSP_RESPONSE_CERT_STATUS_INVALID,
42
+ ER_OCSP_RESPONSE_CERT_STATUS_REVOKED,
43
+ ER_OCSP_RESPONSE_CERT_STATUS_UNKNOWN,
44
+ ER_OCSP_RESPONSE_EXPIRED,
45
+ ER_OCSP_RESPONSE_FETCH_EXCEPTION,
46
+ ER_OCSP_RESPONSE_FETCH_FAILURE,
47
+ ER_OCSP_RESPONSE_INVALID_EXPIRY_INFO_MISSING,
48
+ ER_OCSP_RESPONSE_INVALID_SIGNATURE,
49
+ ER_OCSP_RESPONSE_LOAD_FAILURE,
50
+ ER_OCSP_RESPONSE_STATUS_UNSUCCESSFUL,
51
+ ER_OCSP_RESPONSE_UNAVAILABLE,
52
+ ER_OCSP_URL_INFO_MISSING,
53
+ )
54
+ from snowflake.connector.errors import RevocationCheckError
55
+ from snowflake.connector.network import PYTHON_CONNECTOR_USER_AGENT
56
+
57
+ from . import constants
58
+ from .backoff_policies import exponential_backoff
59
+ from .cache import CacheEntry, SFDictCache, SFDictFileCache
60
+ from .telemetry import TelemetryField, generate_telemetry_data_dict
61
+ from .url_util import extract_top_level_domain_from_hostname, url_encode_str
62
+ from .util_text import _base64_bytes_to_str
63
+
64
+
65
+ class OCSPResponseValidationResult(NamedTuple):
66
+ exception: Exception | None = None
67
+ issuer: Certificate | None = None
68
+ subject: Certificate | None = None
69
+ cert_id: CertId | None = None
70
+ ocsp_response: bytes | None = None
71
+ ts: int | None = None
72
+ validated: bool = False
73
+
74
+ def _serialize(self):
75
+ def serialize_exception(exc):
76
+ # serialization exception is not supported for all exceptions
77
+ # in the ocsp_snowflake.py, most exceptions are RevocationCheckError which is easy to serialize.
78
+ # however, it would require non-trivial effort to serialize other exceptions especially 3rd part errors
79
+ # as there can be un-serializable members and nondeterministic constructor arguments.
80
+ # here we do a general best efforts serialization for other exceptions recording only the error message.
81
+ if not exc:
82
+ return None
83
+
84
+ exc_type = type(exc)
85
+ ret = {"class": exc_type.__name__, "module": exc_type.__module__}
86
+ if isinstance(exc, RevocationCheckError):
87
+ ret.update({"errno": exc.errno, "msg": exc.raw_msg})
88
+ else:
89
+ ret.update({"msg": str(exc)})
90
+ return ret
91
+
92
+ return json.dumps(
93
+ {
94
+ "exception": serialize_exception(self.exception),
95
+ "issuer": (
96
+ _base64_bytes_to_str(self.issuer.dump()) if self.issuer else None
97
+ ),
98
+ "subject": (
99
+ _base64_bytes_to_str(self.subject.dump()) if self.subject else None
100
+ ),
101
+ "cert_id": (
102
+ _base64_bytes_to_str(self.cert_id.dump()) if self.cert_id else None
103
+ ),
104
+ "ocsp_response": _base64_bytes_to_str(self.ocsp_response),
105
+ "ts": self.ts,
106
+ "validated": self.validated,
107
+ }
108
+ )
109
+
110
+ @classmethod
111
+ def _deserialize(cls, json_str: str) -> OCSPResponseValidationResult:
112
+ json_obj = json.loads(json_str)
113
+
114
+ def deserialize_exception(exception_dict: dict | None) -> Exception | None:
115
+ # as pointed out in the serialization method, here we do the best effort deserialization
116
+ # for non-RevocationCheckError exceptions. If we can not deserialize the exception, we will
117
+ # return a RevocationCheckError with a message indicating the failure.
118
+ if not exception_dict:
119
+ return
120
+ exc_class = exception_dict.get("class")
121
+ exc_module = exception_dict.get("module")
122
+ try:
123
+ if (
124
+ exc_class == "RevocationCheckError"
125
+ and exc_module == "snowflake.connector.errors"
126
+ ):
127
+ return RevocationCheckError(
128
+ msg=exception_dict["msg"],
129
+ errno=exception_dict["errno"],
130
+ )
131
+ else:
132
+ module = importlib.import_module(exc_module)
133
+ exc_cls = getattr(module, exc_class)
134
+ return exc_cls(exception_dict["msg"])
135
+ except Exception as deserialize_exc:
136
+ logger.debug(
137
+ f"hitting error {str(deserialize_exc)} while deserializing exception,"
138
+ f" the original error error class and message are {exc_class} and {exception_dict['msg']}"
139
+ )
140
+ return RevocationCheckError(
141
+ f"Got error {str(deserialize_exc)} while deserializing ocsp cache, please try "
142
+ f"cleaning up the "
143
+ f"OCSP cache under directory {OCSP_RESPONSE_VALIDATION_CACHE.file_path}",
144
+ errno=ER_OCSP_RESPONSE_LOAD_FAILURE,
145
+ )
146
+
147
+ return OCSPResponseValidationResult(
148
+ exception=deserialize_exception(json_obj.get("exception")),
149
+ issuer=(
150
+ Certificate.load(b64decode(json_obj.get("issuer")))
151
+ if json_obj.get("issuer")
152
+ else None
153
+ ),
154
+ subject=(
155
+ Certificate.load(b64decode(json_obj.get("subject")))
156
+ if json_obj.get("subject")
157
+ else None
158
+ ),
159
+ cert_id=(
160
+ CertId.load(b64decode(json_obj.get("cert_id")))
161
+ if json_obj.get("cert_id")
162
+ else None
163
+ ),
164
+ ocsp_response=(
165
+ b64decode(json_obj.get("ocsp_response"))
166
+ if json_obj.get("ocsp_response")
167
+ else None
168
+ ),
169
+ ts=json_obj.get("ts"),
170
+ validated=json_obj.get("validated"),
171
+ )
172
+
173
+
174
+ class _OCSPResponseValidationResultCache(SFDictFileCache):
175
+ def _serialize(self) -> bytes:
176
+ entries = {
177
+ (
178
+ _base64_bytes_to_str(k[0]),
179
+ _base64_bytes_to_str(k[1]),
180
+ _base64_bytes_to_str(k[2]),
181
+ ): (v.expiry.isoformat(), v.entry._serialize())
182
+ for k, v in self._cache.items()
183
+ }
184
+
185
+ return json.dumps(
186
+ {
187
+ "cache_keys": list(entries.keys()),
188
+ "cache_items": list(entries.values()),
189
+ "entry_lifetime": self._entry_lifetime.total_seconds(),
190
+ "file_path": str(self.file_path),
191
+ "file_timeout": self.file_timeout,
192
+ "last_loaded": (
193
+ self.last_loaded.isoformat() if self.last_loaded else None
194
+ ),
195
+ "telemetry": self.telemetry,
196
+ "connector_version": SNOWFLAKE_CONNECTOR_VERSION, # reserved for schema version control
197
+ }
198
+ ).encode()
199
+
200
+ @classmethod
201
+ def _deserialize(cls, opened_fd) -> _OCSPResponseValidationResultCache:
202
+ data = json.loads(opened_fd.read().decode())
203
+ cache_instance = cls(
204
+ file_path=data["file_path"],
205
+ entry_lifetime=int(data["entry_lifetime"]),
206
+ file_timeout=data["file_timeout"],
207
+ load_if_file_exists=False,
208
+ )
209
+ cache_instance.file_path = os.path.expanduser(data["file_path"])
210
+ cache_instance.telemetry = data["telemetry"]
211
+ cache_instance.last_loaded = (
212
+ datetime.fromisoformat(data["last_loaded"]) if data["last_loaded"] else None
213
+ )
214
+ for k, v in zip(data["cache_keys"], data["cache_items"]):
215
+ cache_instance._cache[
216
+ (b64decode(k[0]), b64decode(k[1]), b64decode(k[2]))
217
+ ] = CacheEntry(
218
+ datetime.fromisoformat(v[0]),
219
+ OCSPResponseValidationResult._deserialize(v[1]),
220
+ )
221
+ return cache_instance
222
+
223
+
224
+ try:
225
+ OCSP_RESPONSE_VALIDATION_CACHE: SFDictFileCache[
226
+ tuple[bytes, bytes, bytes],
227
+ OCSPResponseValidationResult,
228
+ ] = _OCSPResponseValidationResultCache(
229
+ entry_lifetime=constants.DAY_IN_SECONDS,
230
+ file_path={
231
+ "linux": os.path.join(
232
+ "~", ".cache", "snowflake", "ocsp_response_validation_cache.json"
233
+ ),
234
+ "darwin": os.path.join(
235
+ "~",
236
+ "Library",
237
+ "Caches",
238
+ "Snowflake",
239
+ "ocsp_response_validation_cache.json",
240
+ ),
241
+ "windows": os.path.join(
242
+ "~",
243
+ "AppData",
244
+ "Local",
245
+ "Snowflake",
246
+ "Caches",
247
+ "ocsp_response_validation_cache.json",
248
+ ),
249
+ },
250
+ )
251
+ except OSError:
252
+ # In case we run into some read/write permission error fall back onto
253
+ # in memory caching
254
+ OCSP_RESPONSE_VALIDATION_CACHE: SFDictCache[
255
+ tuple[bytes, bytes, bytes],
256
+ OCSPResponseValidationResult,
257
+ ] = SFDictCache(
258
+ entry_lifetime=constants.DAY_IN_SECONDS,
259
+ )
260
+
261
+ logger = getLogger(__name__)
262
+
263
+
264
+ def generate_cache_key(
265
+ cert_id: CertId,
266
+ ) -> tuple[bytes, bytes, bytes]:
267
+ return (
268
+ cert_id["issuer_name_hash"].dump(),
269
+ cert_id["issuer_key_hash"].dump(),
270
+ cert_id["serial_number"].dump(),
271
+ )
272
+
273
+
274
+ class OCSPTelemetryData:
275
+ CERTIFICATE_EXTRACTION_FAILED = "CertificateExtractionFailed"
276
+ OCSP_URL_MISSING = "OCSPURLMissing"
277
+ OCSP_RESPONSE_UNAVAILABLE = "OCSPResponseUnavailable"
278
+ OCSP_RESPONSE_FETCH_EXCEPTION = "OCSPResponseFetchException"
279
+ OCSP_RESPONSE_FAILED_TO_CONNECT_CACHE_SERVER = (
280
+ "OCSPResponseFailedToConnectCacheServer"
281
+ )
282
+ OCSP_RESPONSE_CERT_STATUS_INVALID = "OCSPResponseCertStatusInvalid"
283
+ OCSP_RESPONSE_CERT_STATUS_REVOKED = "OCSPResponseCertStatusRevoked"
284
+ OCSP_RESPONSE_CERT_STATUS_UNKNOWN = "OCSPResponseCertStatusUnknown"
285
+ OCSP_RESPONSE_STATUS_UNSUCCESSFUL = "OCSPResponseStatusUnsuccessful"
286
+ OCSP_RESPONSE_ATTACHED_CERT_INVALID = "OCSPResponseAttachedCertInvalid"
287
+ OCSP_RESPONSE_ATTACHED_CERT_EXPIRED = "OCSPResponseAttachedCertExpired"
288
+ OCSP_RESPONSE_INVALID_SIGNATURE = "OCSPResponseSignatureInvalid"
289
+ OCSP_RESPONSE_EXPIRY_INFO_MISSING = "OCSPResponseExpiryInfoMissing"
290
+ OCSP_RESPONSE_EXPIRED = "OCSPResponseExpired"
291
+ OCSP_RESPONSE_FETCH_FAILURE = "OCSPResponseFetchFailure"
292
+ OCSP_RESPONSE_CACHE_DOWNLOAD_FAILED = "OCSPResponseCacheDownloadFailed"
293
+ OCSP_RESPONSE_CACHE_DECODE_FAILED = "OCSPResponseCacheDecodeFailed"
294
+ OCSP_RESPONSE_LOAD_FAILURE = "OCSPResponseLoadFailure"
295
+ OCSP_RESPONSE_INVALID_SSD = "OCSPResponseInvalidSSD"
296
+
297
+ ERROR_CODE_MAP = {
298
+ ER_OCSP_URL_INFO_MISSING: OCSP_URL_MISSING,
299
+ ER_OCSP_RESPONSE_UNAVAILABLE: OCSP_RESPONSE_UNAVAILABLE,
300
+ ER_OCSP_RESPONSE_FETCH_EXCEPTION: OCSP_RESPONSE_FETCH_EXCEPTION,
301
+ ER_OCSP_FAILED_TO_CONNECT_CACHE_SERVER: OCSP_RESPONSE_FAILED_TO_CONNECT_CACHE_SERVER,
302
+ ER_OCSP_RESPONSE_CERT_STATUS_INVALID: OCSP_RESPONSE_CERT_STATUS_INVALID,
303
+ ER_OCSP_RESPONSE_CERT_STATUS_REVOKED: OCSP_RESPONSE_CERT_STATUS_REVOKED,
304
+ ER_OCSP_RESPONSE_CERT_STATUS_UNKNOWN: OCSP_RESPONSE_CERT_STATUS_UNKNOWN,
305
+ ER_OCSP_RESPONSE_STATUS_UNSUCCESSFUL: OCSP_RESPONSE_STATUS_UNSUCCESSFUL,
306
+ ER_OCSP_RESPONSE_ATTACHED_CERT_INVALID: OCSP_RESPONSE_ATTACHED_CERT_INVALID,
307
+ ER_OCSP_RESPONSE_ATTACHED_CERT_EXPIRED: OCSP_RESPONSE_ATTACHED_CERT_EXPIRED,
308
+ ER_OCSP_RESPONSE_INVALID_SIGNATURE: OCSP_RESPONSE_INVALID_SIGNATURE,
309
+ ER_OCSP_RESPONSE_INVALID_EXPIRY_INFO_MISSING: OCSP_RESPONSE_EXPIRY_INFO_MISSING,
310
+ ER_OCSP_RESPONSE_EXPIRED: OCSP_RESPONSE_EXPIRED,
311
+ ER_OCSP_RESPONSE_FETCH_FAILURE: OCSP_RESPONSE_FETCH_FAILURE,
312
+ ER_OCSP_RESPONSE_LOAD_FAILURE: OCSP_RESPONSE_LOAD_FAILURE,
313
+ ER_OCSP_RESPONSE_CACHE_DOWNLOAD_FAILED: OCSP_RESPONSE_CACHE_DOWNLOAD_FAILED,
314
+ ER_OCSP_RESPONSE_CACHE_DECODE_FAILED: OCSP_RESPONSE_CACHE_DECODE_FAILED,
315
+ ER_INVALID_OCSP_RESPONSE_SSD: OCSP_RESPONSE_INVALID_SSD,
316
+ ER_INVALID_SSD: OCSP_RESPONSE_INVALID_SSD,
317
+ }
318
+
319
+ def __init__(self) -> None:
320
+ self.event_sub_type = None
321
+ self.ocsp_connection_method = None
322
+ self.cert_id = None
323
+ self.sfc_peer_host = None
324
+ self.ocsp_url = None
325
+ self.ocsp_req = None
326
+ self.error_msg = None
327
+ self.cache_enabled = False
328
+ self.cache_hit = False
329
+ self.fail_open = False
330
+ self.disable_ocsp_checks = False
331
+
332
+ def set_event_sub_type(self, event_sub_type: str) -> None:
333
+ """
334
+ Sets sub type for OCSP Telemetry Event.
335
+
336
+ There can be multiple event_sub_type that could have happened
337
+ during a single connection establishment. Ensure that all of them
338
+ are captured.
339
+ :param event_sub_type:
340
+ :return:
341
+ """
342
+ if self.event_sub_type is not None:
343
+ self.event_sub_type = f"{self.event_sub_type}|{event_sub_type}"
344
+ else:
345
+ self.event_sub_type = event_sub_type
346
+
347
+ def set_ocsp_connection_method(self, ocsp_conn_method: str) -> None:
348
+ self.ocsp_connection_method = ocsp_conn_method
349
+
350
+ def set_cert_id(self, cert_id) -> None:
351
+ self.cert_id = cert_id
352
+
353
+ def set_sfc_peer_host(self, sfc_peer_host) -> None:
354
+ self.sfc_peer_host = sfc_peer_host
355
+
356
+ def set_ocsp_url(self, ocsp_url) -> None:
357
+ self.ocsp_url = ocsp_url
358
+
359
+ def set_ocsp_req(self, ocsp_req) -> None:
360
+ self.ocsp_req = ocsp_req
361
+
362
+ def set_error_msg(self, error_msg) -> None:
363
+ self.error_msg = error_msg
364
+
365
+ def set_cache_enabled(self, cache_enabled) -> None:
366
+ self.cache_enabled = cache_enabled
367
+ if not cache_enabled:
368
+ self.cache_hit = False
369
+
370
+ def set_cache_hit(self, cache_hit) -> None:
371
+ if not self.cache_enabled:
372
+ self.cache_hit = False
373
+ else:
374
+ self.cache_hit = cache_hit
375
+
376
+ def set_fail_open(self, fail_open) -> None:
377
+ self.fail_open = fail_open
378
+
379
+ # Deprecated
380
+ def set_insecure_mode(self, insecure_mode) -> None:
381
+ self.disable_ocsp_checks = insecure_mode
382
+
383
+ def set_disable_ocsp_checks(self, disable_ocsp_checks) -> None:
384
+ self.disable_ocsp_checks = disable_ocsp_checks
385
+
386
+ def generate_telemetry_data(
387
+ self, event_type: str, urgent: bool = False
388
+ ) -> dict[str, Any]:
389
+ _, exception, _ = sys.exc_info()
390
+ telemetry_data = generate_telemetry_data_dict(
391
+ from_dict={
392
+ TelemetryField.KEY_OOB_EVENT_TYPE.value: event_type,
393
+ TelemetryField.KEY_OOB_EVENT_SUB_TYPE.value: self.event_sub_type,
394
+ TelemetryField.KEY_OOB_SFC_PEER_HOST.value: self.sfc_peer_host,
395
+ TelemetryField.KEY_OOB_CERT_ID.value: self.cert_id,
396
+ TelemetryField.KEY_OOB_OCSP_REQUEST_BASE64.value: self.ocsp_req,
397
+ TelemetryField.KEY_OOB_OCSP_RESPONDER_URL.value: self.ocsp_url,
398
+ TelemetryField.KEY_OOB_ERROR_MESSAGE.value: self.error_msg,
399
+ TelemetryField.KEY_OOB_INSECURE_MODE.value: self.disable_ocsp_checks,
400
+ TelemetryField.KEY_OOB_FAIL_OPEN.value: self.fail_open,
401
+ TelemetryField.KEY_OOB_CACHE_ENABLED.value: self.cache_enabled,
402
+ TelemetryField.KEY_OOB_CACHE_HIT.value: self.cache_hit,
403
+ },
404
+ is_oob_telemetry=True,
405
+ )
406
+
407
+ return telemetry_data
408
+ # To be updated once Python Driver has out of band telemetry.
409
+ # telemetry_client = TelemetryClient()
410
+ # telemetry_client.add_log_to_batch(TelemetryData(telemetry_data, datetime.now(timezone.utc).replace(tzinfo=None))
411
+
412
+
413
+ class OCSPServer:
414
+ MAX_RETRY = int(os.getenv("OCSP_MAX_RETRY", "3"))
415
+
416
+ def __init__(self, **kwargs) -> None:
417
+ top_level_domain = kwargs.pop(
418
+ "top_level_domain", constants._DEFAULT_HOSTNAME_TLD
419
+ )
420
+ self.DEFAULT_CACHE_SERVER_URL = (
421
+ f"http://ocsp.snowflakecomputing.{top_level_domain}"
422
+ )
423
+ """
424
+ The following will change to something like
425
+ http://ocspssd.snowflakecomputing.com/ocsp/
426
+ once the endpoint is up in the backend
427
+ """
428
+ self.NEW_DEFAULT_CACHE_SERVER_BASE_URL = (
429
+ f"https://ocspssd.snowflakecomputing.{top_level_domain}/ocsp/"
430
+ )
431
+ if not OCSPServer.is_enabled_new_ocsp_endpoint():
432
+ self.CACHE_SERVER_URL = os.getenv(
433
+ "SF_OCSP_RESPONSE_CACHE_SERVER_URL",
434
+ "{}/{}".format(
435
+ self.DEFAULT_CACHE_SERVER_URL,
436
+ OCSPCache.OCSP_RESPONSE_CACHE_FILE_NAME,
437
+ ),
438
+ )
439
+ else:
440
+ self.CACHE_SERVER_URL = os.getenv("SF_OCSP_RESPONSE_CACHE_SERVER_URL")
441
+
442
+ self.CACHE_SERVER_ENABLED = (
443
+ os.getenv("SF_OCSP_RESPONSE_CACHE_SERVER_ENABLED", "true") != "false"
444
+ )
445
+ # OCSP dynamic cache server URL pattern
446
+ self.OCSP_RETRY_URL = None
447
+
448
+ @staticmethod
449
+ def is_enabled_new_ocsp_endpoint() -> bool:
450
+ """Checks if new OCSP Endpoint has been enabled."""
451
+ return os.getenv("SF_OCSP_ACTIVATE_NEW_ENDPOINT", "false").lower() == "true"
452
+
453
+ def reset_ocsp_endpoint(self, hname) -> None:
454
+ """Resets current object members CACHE_SERVER_URL and RETRY_URL_PATTERN.
455
+
456
+ They will point at the new OCSP Fetch and Retry endpoints respectively. The new OCSP Endpoint address is based
457
+ on the hostname the customer is trying to connect to. The deployment or in case of client failover, the
458
+ replication ID is copied from the hostname.
459
+ """
460
+ top_level_domain = extract_top_level_domain_from_hostname(hname)
461
+ if "privatelink.snowflakecomputing." in hname:
462
+ temp_ocsp_endpoint = "".join(["https://ocspssd.", hname, "/ocsp/"])
463
+ elif "global.snowflakecomputing." in hname:
464
+ rep_id_begin = hname[hname.find("-") :]
465
+ temp_ocsp_endpoint = "".join(["https://ocspssd", rep_id_begin, "/ocsp/"])
466
+ elif not hname.endswith(f"snowflakecomputing.{top_level_domain}"):
467
+ temp_ocsp_endpoint = self.NEW_DEFAULT_CACHE_SERVER_BASE_URL
468
+ else:
469
+ hname_wo_acc = hname[hname.find(".") :]
470
+ temp_ocsp_endpoint = "".join(["https://ocspssd", hname_wo_acc, "/ocsp/"])
471
+
472
+ self.CACHE_SERVER_URL = "".join([temp_ocsp_endpoint, "fetch"])
473
+ self.OCSP_RETRY_URL = "".join([temp_ocsp_endpoint, "retry"])
474
+
475
+ def reset_ocsp_dynamic_cache_server_url(self, use_ocsp_cache_server) -> None:
476
+ """Resets OCSP dynamic cache server url pattern.
477
+
478
+ This is used only when OCSP cache server is updated.
479
+ """
480
+ if use_ocsp_cache_server is not None:
481
+ self.CACHE_SERVER_ENABLED = use_ocsp_cache_server
482
+
483
+ if self.CACHE_SERVER_ENABLED:
484
+ logger.debug(
485
+ "OCSP response cache server is enabled: %s", self.CACHE_SERVER_URL
486
+ )
487
+ else:
488
+ logger.debug("OCSP response cache server is disabled")
489
+
490
+ if self.OCSP_RETRY_URL is None:
491
+ if self.CACHE_SERVER_URL is not None and (
492
+ not self.CACHE_SERVER_URL.startswith(self.DEFAULT_CACHE_SERVER_URL)
493
+ ):
494
+ # only if custom OCSP cache server is used.
495
+ parsed_url = urlsplit(self.CACHE_SERVER_URL)
496
+ self.OCSP_RETRY_URL = f"{urlunparse((parsed_url.scheme, parsed_url.netloc, '', '', '', ''))}/retry/{{0}}/{{1}}"
497
+ logger.debug("OCSP dynamic cache server RETRY URL: %s", self.OCSP_RETRY_URL)
498
+
499
+ def download_cache_from_server(self, ocsp):
500
+ if self.CACHE_SERVER_ENABLED:
501
+ # if any of them is not cache, download the cache file from
502
+ # OCSP response cache server.
503
+ try:
504
+ retval = OCSPServer._download_ocsp_response_cache(
505
+ ocsp, self.CACHE_SERVER_URL
506
+ )
507
+ if not retval:
508
+ raise RevocationCheckError(
509
+ msg="OCSP Cache Server Unavailable.",
510
+ errno=ER_OCSP_RESPONSE_CACHE_DOWNLOAD_FAILED,
511
+ )
512
+ logger.debug(
513
+ "downloaded OCSP response cache file from %s", self.CACHE_SERVER_URL
514
+ )
515
+ # len(OCSP_RESPONSE_VALIDATION_CACHE) is thread-safe, however, we do not want to
516
+ # block for logging purpose, thus using len(OCSP_RESPONSE_VALIDATION_CACHE._cache) here.
517
+ logger.debug(
518
+ "# of certificates: %u",
519
+ len(OCSP_RESPONSE_VALIDATION_CACHE._cache),
520
+ )
521
+ except RevocationCheckError as rce:
522
+ logger.debug(
523
+ "OCSP Response cache download failed. The client"
524
+ "will reach out to the OCSP Responder directly for"
525
+ "any missing OCSP responses %s\n" % rce.msg
526
+ )
527
+ raise
528
+
529
+ @staticmethod
530
+ def _download_ocsp_response_cache(ocsp, url, do_retry: bool = True) -> bool:
531
+ """Downloads OCSP response cache from the cache server."""
532
+ headers = {HTTP_HEADER_USER_AGENT: PYTHON_CONNECTOR_USER_AGENT}
533
+ sf_timeout = SnowflakeOCSP.OCSP_CACHE_SERVER_CONNECTION_TIMEOUT
534
+
535
+ try:
536
+ start_time = time.time()
537
+ logger.debug("started downloading OCSP response cache file: %s", url)
538
+
539
+ if ocsp.test_mode is not None:
540
+ test_timeout = os.getenv(
541
+ "SF_TEST_OCSP_CACHE_SERVER_CONNECTION_TIMEOUT", None
542
+ )
543
+ sf_cache_server_url = os.getenv("SF_TEST_OCSP_CACHE_SERVER_URL", None)
544
+ if test_timeout is not None:
545
+ sf_timeout = int(test_timeout)
546
+ if sf_cache_server_url is not None:
547
+ url = sf_cache_server_url
548
+
549
+ with generic_requests.Session() as session:
550
+ max_retry = SnowflakeOCSP.OCSP_CACHE_SERVER_MAX_RETRY if do_retry else 1
551
+ sleep_time = 1
552
+ backoff = exponential_backoff()()
553
+ for _ in range(max_retry):
554
+ response = session.get(
555
+ url,
556
+ timeout=sf_timeout, # socket timeout
557
+ headers=headers,
558
+ )
559
+ if response.status_code == OK:
560
+ ocsp.decode_ocsp_response_cache(response.json())
561
+ elapsed_time = time.time() - start_time
562
+ logger.debug(
563
+ "ended downloading OCSP response cache file. "
564
+ "elapsed time: %ss",
565
+ elapsed_time,
566
+ )
567
+ break
568
+ elif max_retry > 1:
569
+ sleep_time = next(backoff)
570
+ logger.debug(
571
+ "OCSP server returned %s. Retrying in %s(s)",
572
+ response.status_code,
573
+ sleep_time,
574
+ )
575
+ time.sleep(sleep_time)
576
+ else:
577
+ logger.error(
578
+ "Failed to get OCSP response after %s attempt.", max_retry
579
+ )
580
+ return False
581
+ return True
582
+ except Exception as e:
583
+ logger.debug("Failed to get OCSP response cache from %s: %s", url, e)
584
+ raise RevocationCheckError(
585
+ msg=f"Failed to get OCSP Response Cache from {url}: {e}",
586
+ errno=ER_OCSP_FAILED_TO_CONNECT_CACHE_SERVER,
587
+ )
588
+
589
+ def generate_get_url(self, ocsp_url, b64data):
590
+ parsed_url = urlsplit(ocsp_url)
591
+ url_encoded_b64data = url_encode_str(b64data)
592
+ if self.OCSP_RETRY_URL is None:
593
+ target_url = f"{ocsp_url}/{url_encoded_b64data}"
594
+ else:
595
+ # values of parsed_url.netloc and parsed_url.path based on oscp_url are as follows:
596
+ # URL NETLOC PATH
597
+ # "http://oneocsp.microsoft.com" "oneocsp.microsoft.com" ""
598
+ # "http://oneocsp.microsoft.com:8080" "oneocsp.microsoft.com:8080" ""
599
+ # "http://oneocsp.microsoft.com/" "oneocsp.microsoft.com" "/"
600
+ # "http://oneocsp.microsoft.com/ocsp" "oneocsp.microsoft.com" "/ocsp"
601
+ # The check below is to treat first two urls same
602
+ path = parsed_url.path if parsed_url.path != "/" else ""
603
+ target_url = self.OCSP_RETRY_URL.format(
604
+ parsed_url.netloc + path, url_encoded_b64data
605
+ )
606
+
607
+ logger.debug("OCSP Retry URL is - %s", target_url)
608
+ return target_url
609
+
610
+
611
+ class OCSPCache:
612
+ # OCSP cache lock
613
+ CACHE_LOCK = Lock()
614
+
615
+ # OCSP cache update flag
616
+ CACHE_UPDATED = False
617
+
618
+ # Cache Expiration in seconds (120 hours). OCSP validation cache is
619
+ # invalidated every 120 hours (5 days)
620
+ CACHE_EXPIRATION = 432000
621
+
622
+ # OCSP Response Cache URI
623
+ OCSP_RESPONSE_CACHE_URI = None
624
+
625
+ # OCSP response cache file name
626
+ OCSP_RESPONSE_CACHE_FILE_NAME = "ocsp_response_cache.json"
627
+
628
+ # Cache directory
629
+ CACHE_DIR = None
630
+
631
+ @staticmethod
632
+ def reset_cache_dir() -> None:
633
+ # Cache directory
634
+ OCSPCache.CACHE_DIR = os.getenv("SF_OCSP_RESPONSE_CACHE_DIR")
635
+ if OCSPCache.CACHE_DIR is None:
636
+ cache_root_dir = expanduser("~") or tempfile.gettempdir()
637
+ if platform.system() == "Windows":
638
+ OCSPCache.CACHE_DIR = path.join(
639
+ cache_root_dir, "AppData", "Local", "Snowflake", "Caches"
640
+ )
641
+ elif platform.system() == "Darwin":
642
+ OCSPCache.CACHE_DIR = path.join(
643
+ cache_root_dir, "Library", "Caches", "Snowflake"
644
+ )
645
+ else:
646
+ OCSPCache.CACHE_DIR = path.join(cache_root_dir, ".cache", "snowflake")
647
+ logger.debug("cache directory: %s", OCSPCache.CACHE_DIR)
648
+
649
+ if not path.exists(OCSPCache.CACHE_DIR):
650
+ try:
651
+ os.makedirs(OCSPCache.CACHE_DIR, mode=0o700)
652
+ except Exception as ex:
653
+ logger.debug(
654
+ "cannot create a cache directory: [%s], err=[%s]",
655
+ OCSPCache.CACHE_DIR,
656
+ ex,
657
+ )
658
+ OCSPCache.CACHE_DIR = None
659
+
660
+ @staticmethod
661
+ def del_cache_file() -> None:
662
+ """Deletes the OCSP response cache file if exists."""
663
+ cache_file = path.join(
664
+ OCSPCache.CACHE_DIR, OCSPCache.OCSP_RESPONSE_CACHE_FILE_NAME
665
+ )
666
+ if path.exists(cache_file):
667
+ logger.debug(f"deleting cache file {cache_file}")
668
+ os.unlink(cache_file)
669
+
670
+ @staticmethod
671
+ def reset_ocsp_response_cache_uri(ocsp_response_cache_uri) -> None:
672
+ if ocsp_response_cache_uri is None and OCSPCache.CACHE_DIR is not None:
673
+ OCSPCache.OCSP_RESPONSE_CACHE_URI = "file://" + path.join(
674
+ OCSPCache.CACHE_DIR, OCSPCache.OCSP_RESPONSE_CACHE_FILE_NAME
675
+ )
676
+ else:
677
+ OCSPCache.OCSP_RESPONSE_CACHE_URI = ocsp_response_cache_uri
678
+
679
+ if OCSPCache.OCSP_RESPONSE_CACHE_URI is not None:
680
+ # normalize URI for Windows
681
+ OCSPCache.OCSP_RESPONSE_CACHE_URI = (
682
+ OCSPCache.OCSP_RESPONSE_CACHE_URI.replace("\\", "/")
683
+ )
684
+
685
+ logger.debug("ocsp_response_cache_uri: %s", OCSPCache.OCSP_RESPONSE_CACHE_URI)
686
+ # len(OCSP_RESPONSE_VALIDATION_CACHE) is thread-safe, however, we do not want to
687
+ # block for logging purpose, thus using len(OCSP_RESPONSE_VALIDATION_CACHE._cache) here.
688
+ logger.debug(
689
+ "OCSP_VALIDATION_CACHE size: %u",
690
+ len(OCSP_RESPONSE_VALIDATION_CACHE._cache),
691
+ )
692
+
693
+ @staticmethod
694
+ def read_file(ocsp):
695
+ """Reads OCSP Response cache data from the URI, which is very likely a file."""
696
+ try:
697
+ parsed_url = urlsplit(OCSPCache.OCSP_RESPONSE_CACHE_URI)
698
+ if parsed_url.scheme == "file":
699
+ OCSPCache.read_ocsp_response_cache_file(
700
+ ocsp, path.join(parsed_url.netloc, parsed_url.path)
701
+ )
702
+ else:
703
+ msg = "Unsupported OCSP URI: {}".format(
704
+ OCSPCache.OCSP_RESPONSE_CACHE_URI
705
+ )
706
+ raise Exception(msg)
707
+ except (RevocationCheckError, Exception) as rce:
708
+ logger.debug(
709
+ "Failed to read OCSP response cache file %s: %s, "
710
+ "No worry. It will validate with OCSP server. "
711
+ "Ignoring...",
712
+ OCSPCache.OCSP_RESPONSE_CACHE_URI,
713
+ rce,
714
+ exc_info=True,
715
+ )
716
+
717
+ @staticmethod
718
+ def read_ocsp_response_cache_file(ocsp, filename):
719
+ """Reads OCSP Response cache."""
720
+ try:
721
+ if OCSPCache.check_ocsp_response_cache_lock_dir(filename) and path.exists(
722
+ filename
723
+ ):
724
+ with codecs.open(filename, "r", encoding="utf-8", errors="ignore") as f:
725
+ ocsp.decode_ocsp_response_cache(json.load(f))
726
+ # len(OCSP_RESPONSE_VALIDATION_CACHE) is thread-safe, however, we do not want to
727
+ # block for logging purpose, thus using len(OCSP_RESPONSE_VALIDATION_CACHE._cache) here.
728
+ logger.debug(
729
+ "Read OCSP response cache file: %s, count=%s",
730
+ filename,
731
+ len(OCSP_RESPONSE_VALIDATION_CACHE._cache),
732
+ )
733
+ else:
734
+ logger.debug(
735
+ "Failed to locate OCSP response cache file. "
736
+ "No worry. It will validate with OCSP server: %s",
737
+ filename,
738
+ )
739
+ except Exception as ex:
740
+ logger.debug("Caught - %s", ex)
741
+ raise ex
742
+
743
+ @staticmethod
744
+ def update_file(ocsp) -> None:
745
+ """
746
+ Updates OCSP Response Cache file.
747
+ Two file shall be updated/saved:
748
+ 1. file for OCSP_RESPONSE_VALIDATION_CACHE which keeps ocsp response validation result
749
+ 2. ocsp_response_cache.json, the file in the same format as the one downloaded from snowflake cache service
750
+ """
751
+ if OCSPCache.CACHE_UPDATED:
752
+ if isinstance(OCSP_RESPONSE_VALIDATION_CACHE, SFDictFileCache):
753
+ OCSP_RESPONSE_VALIDATION_CACHE.save()
754
+ OCSPCache.update_ocsp_response_cache_file(
755
+ ocsp, OCSPCache.OCSP_RESPONSE_CACHE_URI
756
+ )
757
+ OCSPCache.CACHE_UPDATED = False
758
+
759
+ @staticmethod
760
+ def update_ocsp_response_cache_file(ocsp, ocsp_response_cache_uri) -> None:
761
+ """Updates OCSP Response Cache."""
762
+ if ocsp_response_cache_uri is not None:
763
+ try:
764
+ parsed_url = urlsplit(ocsp_response_cache_uri)
765
+ if parsed_url.scheme == "file":
766
+ filename = path.join(parsed_url.netloc, parsed_url.path)
767
+ lock_dir = filename + ".lck"
768
+ for _ in range(100):
769
+ # wait until the lck file has been removed
770
+ # or up to 1 second (0.01 x 100)
771
+ if OCSPCache.lock_cache_file(lock_dir):
772
+ break
773
+ time.sleep(0.01)
774
+ try:
775
+ OCSPCache.write_ocsp_response_cache_file(ocsp, filename)
776
+ finally:
777
+ OCSPCache.unlock_cache_file(lock_dir)
778
+ else:
779
+ logger.debug(
780
+ "No OCSP response cache file is written, because the "
781
+ "given URI is not a file: %s. Ignoring...",
782
+ ocsp_response_cache_uri,
783
+ )
784
+ except Exception as e:
785
+ logger.debug(
786
+ "Failed to write OCSP response cache "
787
+ "file. file: %s, error: %s, Ignoring...",
788
+ ocsp_response_cache_uri,
789
+ e,
790
+ exc_info=True,
791
+ )
792
+
793
+ @staticmethod
794
+ def write_ocsp_response_cache_file(ocsp, filename) -> None:
795
+ """Writes OCSP Response Cache."""
796
+ logger.debug(f"writing OCSP response cache file to {filename}")
797
+ file_cache_data = {}
798
+ ocsp.encode_ocsp_response_cache(file_cache_data)
799
+ with codecs.open(filename, "w", encoding="utf-8", errors="ignore") as f:
800
+ json.dump(file_cache_data, f)
801
+
802
+ @staticmethod
803
+ def check_ocsp_response_cache_lock_dir(filename) -> bool:
804
+ """Checks if the lock directory exists.
805
+
806
+ Returns:
807
+ True if it can update the cache file or False when some other process may be updating the cache file.
808
+ """
809
+ current_time = int(time.time())
810
+ lock_dir = filename + ".lck"
811
+
812
+ try:
813
+ ts_cache_file = OCSPCache._file_timestamp(filename)
814
+ if (
815
+ not path.exists(lock_dir)
816
+ and current_time - OCSPCache.CACHE_EXPIRATION <= ts_cache_file
817
+ ):
818
+ # use cache only if no lock directory exists and the cache file
819
+ # was created last 24 hours
820
+ return True
821
+
822
+ if path.exists(lock_dir):
823
+ # delete lock directory if older 60 seconds
824
+ ts_lock_dir = OCSPCache._file_timestamp(lock_dir)
825
+ if ts_lock_dir < current_time - 60:
826
+ OCSPCache.unlock_cache_file(lock_dir)
827
+ logger.debug(
828
+ "The lock directory is older than 60 seconds. "
829
+ "Deleted the lock directory and ignoring the cache: %s",
830
+ lock_dir,
831
+ )
832
+ else:
833
+ logger.debug(
834
+ "The lock directory exists. Other process may be "
835
+ "updating the cache file: %s, %s",
836
+ filename,
837
+ lock_dir,
838
+ )
839
+ else:
840
+ os.unlink(filename)
841
+ logger.debug(
842
+ "The cache is older than 1 day. " "Deleted the cache file: %s",
843
+ filename,
844
+ )
845
+ except Exception as e:
846
+ logger.debug(
847
+ "Failed to check OCSP response cache file. No worry. It will "
848
+ "validate with OCSP server: file: %s, lock directory: %s, "
849
+ "error: %s",
850
+ filename,
851
+ lock_dir,
852
+ e,
853
+ )
854
+ return False
855
+
856
+ @staticmethod
857
+ def is_cache_fresh(current_time: int, ts: int) -> bool:
858
+ return current_time - OCSPCache.CACHE_EXPIRATION <= ts
859
+
860
+ @staticmethod
861
+ def find_cache(
862
+ ocsp: SnowflakeOCSP, cert_id: CertId, subject: Certificate | None, **kwargs: Any
863
+ ) -> tuple[bool, bytes | None]:
864
+ subject_name = ocsp.subject_name(subject) if subject else None
865
+ current_time = int(time.time())
866
+ cache_key: tuple[bytes, bytes, bytes] = kwargs.get(
867
+ "cache_key", ocsp.decode_cert_id_key(cert_id)
868
+ )
869
+ lock_cache: bool = kwargs.get("lock_cache", True)
870
+ try:
871
+ ocsp_response_validation_result = (
872
+ OCSP_RESPONSE_VALIDATION_CACHE[cache_key]
873
+ if lock_cache
874
+ else OCSP_RESPONSE_VALIDATION_CACHE._getitem_non_locking(cache_key)
875
+ )
876
+ try:
877
+ # is_valid_time can raise exception if the cache
878
+ # entry is a SSD.
879
+ if OCSPCache.is_cache_fresh(
880
+ current_time, ocsp_response_validation_result.ts
881
+ ) and ocsp.is_valid_time(
882
+ cert_id, ocsp_response_validation_result.ocsp_response
883
+ ):
884
+ if subject_name:
885
+ logger.debug("hit cache for subject: %s", subject_name)
886
+ return True, ocsp_response_validation_result.ocsp_response
887
+ else:
888
+ OCSPCache.delete_cache(
889
+ ocsp, cert_id, cache_key=cache_key, lock_cache=lock_cache
890
+ )
891
+ except Exception as ex:
892
+ logger.debug(f"Could not validate cache entry {cert_id} {ex}")
893
+ OCSPCache.CACHE_UPDATED = True
894
+ except KeyError:
895
+ if subject_name:
896
+ logger.debug(f"cache miss for subject: '{subject_name}'")
897
+ return False, None
898
+
899
+ @staticmethod
900
+ def delete_cache(ocsp: SnowflakeOCSP, cert_id: CertId, **kwargs: Any) -> None:
901
+ cache_key: tuple[bytes, bytes, bytes] = kwargs.get(
902
+ "cache_key", ocsp.decode_cert_id_key(cert_id)
903
+ )
904
+ lock_cache: bool = kwargs.get("lock_cache", True)
905
+ try:
906
+ if lock_cache:
907
+ del OCSP_RESPONSE_VALIDATION_CACHE[cache_key]
908
+ else:
909
+ OCSP_RESPONSE_VALIDATION_CACHE._delitem(cache_key)
910
+ OCSPCache.CACHE_UPDATED = True
911
+ except KeyError:
912
+ pass
913
+
914
+ @staticmethod
915
+ def _file_timestamp(filename):
916
+ """Gets the last created timestamp of the file/dir."""
917
+ if platform.system() == "Windows":
918
+ ts = int(path.getctime(filename))
919
+ else:
920
+ stat = os.stat(filename)
921
+ if hasattr(stat, "st_birthtime"): # odx
922
+ ts = int(stat.st_birthtime)
923
+ else:
924
+ ts = int(stat.st_mtime) # linux
925
+ return ts
926
+
927
+ @staticmethod
928
+ def lock_cache_file(fname) -> bool:
929
+ """Locks a cache file by creating a directory."""
930
+ try:
931
+ os.mkdir(fname)
932
+ return True
933
+ except OSError:
934
+ return False
935
+
936
+ @staticmethod
937
+ def unlock_cache_file(fname) -> bool:
938
+ """Unlocks a cache file by deleting a directory."""
939
+ try:
940
+ os.rmdir(fname)
941
+ return True
942
+ except OSError:
943
+ return False
944
+
945
+ @staticmethod
946
+ def delete_cache_file() -> None:
947
+ """Deletes the cache file. Used by tests only."""
948
+ parsed_url = urlsplit(OCSPCache.OCSP_RESPONSE_CACHE_URI)
949
+ fname = path.join(parsed_url.netloc, parsed_url.path)
950
+ OCSPCache.lock_cache_file(fname)
951
+ try:
952
+ logger.debug(f"deleting cache file, used by tests only {fname}")
953
+ os.unlink(fname)
954
+ finally:
955
+ OCSPCache.unlock_cache_file(fname)
956
+
957
+ @staticmethod
958
+ def clear_cache() -> None:
959
+ """Clears cache."""
960
+ OCSP_RESPONSE_VALIDATION_CACHE.clear()
961
+
962
+ @staticmethod
963
+ def cache_size():
964
+ """Returns the cache's size."""
965
+ return len(OCSP_RESPONSE_VALIDATION_CACHE)
966
+
967
+
968
+ # Reset OCSP cache directory
969
+ OCSPCache.reset_cache_dir()
970
+
971
+
972
+ class SnowflakeOCSP:
973
+ """OCSP validator using PyOpenSSL and asn1crypto/pyasn1."""
974
+
975
+ # root certificate cache
976
+ ROOT_CERTIFICATES_DICT = {} # root certificates
977
+
978
+ # root certificate cache lock
979
+ ROOT_CERTIFICATES_DICT_LOCK = RLock()
980
+
981
+ # cache object
982
+ OCSP_CACHE = OCSPCache()
983
+
984
+ OCSP_WHITELIST = re.compile(
985
+ r"^"
986
+ r"(.*\.snowflakecomputing(\.[a-zA-Z]{1,63}){1,2}$"
987
+ r"|(?:|.*\.)s3.*\.amazonaws(\.[a-zA-Z]{1,63}){1,2}$" # start with s3 or .s3 in the middle
988
+ r"|.*\.okta\.com$"
989
+ r"|(?:|.*\.)storage\.googleapis\.com$"
990
+ r"|.*\.blob\.core\.windows\.net$"
991
+ r"|.*\.blob\.core\.usgovcloudapi\.net$)"
992
+ )
993
+
994
+ # Tolerable validity date range ratio. The OCSP response is valid up
995
+ # to (next update timestamp) + (next update timestamp -
996
+ # this update timestamp) * TOLERABLE_VALIDITY_RANGE_RATIO. This buffer
997
+ # yields some time for Root CA to update intermediate CA's certificate
998
+ # OCSP response. In fact, they don't update OCSP response in time. In Dec
999
+ # 2016, they left OCSP response expires for 5 hours at least, and it
1000
+ # caused the connectivity issues in customers.
1001
+ # With this buffer, about 2 days are given for 180 days validity date.
1002
+ TOLERABLE_VALIDITY_RANGE_RATIO = 0.01
1003
+
1004
+ # Maximum clock skew in seconds (15 minutes) allowed when checking
1005
+ # validity of OCSP responses
1006
+ MAX_CLOCK_SKEW = 900
1007
+
1008
+ # Epoch time
1009
+ ZERO_EPOCH = datetime.fromtimestamp(0, timezone.utc).replace(tzinfo=None)
1010
+
1011
+ # Timestamp format for logging
1012
+ OUTPUT_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%SZ"
1013
+
1014
+ # Connection timeout in seconds for CA OCSP Responder
1015
+ CA_OCSP_RESPONDER_CONNECTION_TIMEOUT = 10
1016
+
1017
+ # Connection timeout in seconds for Cache Server
1018
+ OCSP_CACHE_SERVER_CONNECTION_TIMEOUT = 5
1019
+
1020
+ # MAX number of connection retry attempts with Responder in Fail Open
1021
+ CA_OCSP_RESPONDER_MAX_RETRY_FO = 1
1022
+
1023
+ # MAX number of connection retry attempts with Responder in Fail Close
1024
+ CA_OCSP_RESPONDER_MAX_RETRY_FC = 3
1025
+
1026
+ # MAX number of connection retry attempts with Cache Server
1027
+ OCSP_CACHE_SERVER_MAX_RETRY = 1
1028
+
1029
+ def __init__(
1030
+ self,
1031
+ ocsp_response_cache_uri=None,
1032
+ use_ocsp_cache_server=None,
1033
+ use_post_method: bool = True,
1034
+ use_fail_open: bool = True,
1035
+ **kwargs,
1036
+ ) -> None:
1037
+ self.test_mode = os.getenv("SF_OCSP_TEST_MODE", None)
1038
+
1039
+ if self.test_mode == "true":
1040
+ logger.debug("WARNING - DRIVER CONFIGURED IN TEST MODE")
1041
+
1042
+ self._use_post_method = use_post_method
1043
+ self.OCSP_CACHE_SERVER = OCSPServer(
1044
+ top_level_domain=extract_top_level_domain_from_hostname(
1045
+ kwargs.pop("hostname", None)
1046
+ )
1047
+ )
1048
+
1049
+ self.debug_ocsp_failure_url = None
1050
+
1051
+ if os.getenv("SF_OCSP_FAIL_OPEN") is not None:
1052
+ # failOpen Env Variable is for internal usage/ testing only.
1053
+ # Using it in production is not advised and not supported.
1054
+ self.FAIL_OPEN = os.getenv("SF_OCSP_FAIL_OPEN").lower() == "true"
1055
+ else:
1056
+ self.FAIL_OPEN = use_fail_open
1057
+
1058
+ SnowflakeOCSP.OCSP_CACHE.reset_ocsp_response_cache_uri(ocsp_response_cache_uri)
1059
+
1060
+ if not OCSPServer.is_enabled_new_ocsp_endpoint():
1061
+ self.OCSP_CACHE_SERVER.reset_ocsp_dynamic_cache_server_url(
1062
+ use_ocsp_cache_server
1063
+ )
1064
+
1065
+ """
1066
+ Here we have a two-layer cache design:
1067
+
1068
+ The upper layer is the OCSP_RESPONSE_VALIDATION_CACHE which caches not only the ocsp responses but also
1069
+ the validation result of ocsp responses. This will be both in-memory and in-file (if program has the right
1070
+ to read and write files).
1071
+
1072
+ The bottom layer is the ocsp responses in the form of a json file which are either
1073
+ retrieved from Snowflake cache service or locally maintained by writing OCSP_RESPONSE_VALIDATION_CACHE back
1074
+ to the json file for any updates (certificate revoked, cache expired, etc.). This will be in-file.
1075
+
1076
+ The cache logic is as following:
1077
+ 1. The OCSP_RESPONSE_VALIDATION_CACHE will be loaded from disk first during module loading period.
1078
+ 2. If there's no content loaded either due to no cache file or all cache expired, then we try load ocsp
1079
+ response cache file. We will parse the content in the ocsp response cache json file, and
1080
+ then update OCSP_RESPONSE_VALIDATION_CACHE.
1081
+ 3. When validating certs, we will first check OCSP_RESPONSE_VALIDATION_CACHE, if cache is not found,
1082
+ when we will validate against the OCSP servers and cache the results.
1083
+ 4. After validating all the certs, we save OCSP_RESPONSE_VALIDATION_CACHE and ocsp response json
1084
+ onto disk.
1085
+ """
1086
+ if not OCSP_RESPONSE_VALIDATION_CACHE:
1087
+ SnowflakeOCSP.OCSP_CACHE.read_file(self)
1088
+
1089
+ def validate_certfile(self, cert_filename, no_exception: bool = False):
1090
+ """Validates that the certificate is NOT revoked."""
1091
+ cert_map = {}
1092
+ telemetry_data = OCSPTelemetryData()
1093
+ telemetry_data.set_cache_enabled(self.OCSP_CACHE_SERVER.CACHE_SERVER_ENABLED)
1094
+ telemetry_data.set_disable_ocsp_checks(False)
1095
+ telemetry_data.set_sfc_peer_host(cert_filename)
1096
+ telemetry_data.set_fail_open(self.is_enabled_fail_open())
1097
+ try:
1098
+ self.read_cert_bundle(cert_filename, cert_map)
1099
+ cert_data = self.create_pair_issuer_subject(cert_map)
1100
+ except Exception as ex:
1101
+ logger.debug("Caught exception while validating certfile %s", str(ex))
1102
+ raise ex
1103
+
1104
+ return self._validate(
1105
+ None, cert_data, telemetry_data, do_retry=False, no_exception=no_exception
1106
+ )
1107
+
1108
+ def validate(
1109
+ self,
1110
+ hostname: str | None,
1111
+ connection: Connection,
1112
+ no_exception: bool = False,
1113
+ ) -> (
1114
+ list[
1115
+ tuple[
1116
+ Exception | None,
1117
+ Certificate,
1118
+ Certificate,
1119
+ CertId,
1120
+ str | bytes,
1121
+ ]
1122
+ ]
1123
+ | None
1124
+ ):
1125
+ """Validates the certificate is not revoked using OCSP."""
1126
+ logger.debug("validating certificate: %s", hostname)
1127
+
1128
+ do_retry = SnowflakeOCSP.get_ocsp_retry_choice()
1129
+
1130
+ m = not SnowflakeOCSP.OCSP_WHITELIST.match(hostname)
1131
+ if m or hostname.startswith("ocspssd"):
1132
+ logger.debug("skipping OCSP check: %s", hostname)
1133
+ return [None, None, None, None, None]
1134
+
1135
+ if OCSPServer.is_enabled_new_ocsp_endpoint():
1136
+ self.OCSP_CACHE_SERVER.reset_ocsp_endpoint(hostname)
1137
+
1138
+ telemetry_data = OCSPTelemetryData()
1139
+ telemetry_data.set_cache_enabled(self.OCSP_CACHE_SERVER.CACHE_SERVER_ENABLED)
1140
+ telemetry_data.set_disable_ocsp_checks(False)
1141
+ telemetry_data.set_sfc_peer_host(hostname)
1142
+ telemetry_data.set_fail_open(self.is_enabled_fail_open())
1143
+
1144
+ try:
1145
+ cert_data = self.extract_certificate_chain(connection)
1146
+ except RevocationCheckError:
1147
+ telemetry_data.set_event_sub_type(
1148
+ OCSPTelemetryData.CERTIFICATE_EXTRACTION_FAILED
1149
+ )
1150
+ logger.debug(
1151
+ telemetry_data.generate_telemetry_data("RevocationCheckFailure")
1152
+ )
1153
+ return None
1154
+
1155
+ return self._validate(
1156
+ hostname, cert_data, telemetry_data, do_retry, no_exception
1157
+ )
1158
+
1159
+ def _validate(
1160
+ self,
1161
+ hostname: str | None,
1162
+ cert_data: list[tuple[Certificate, Certificate]],
1163
+ telemetry_data: OCSPTelemetryData,
1164
+ do_retry: bool = True,
1165
+ no_exception: bool = False,
1166
+ ) -> list[tuple[Exception | None, Certificate, Certificate, CertId, bytes]]:
1167
+ """Validate certs sequentially if OCSP response cache server is used."""
1168
+ results = self._validate_certificates_sequential(
1169
+ cert_data, telemetry_data, hostname, do_retry=do_retry
1170
+ )
1171
+
1172
+ SnowflakeOCSP.OCSP_CACHE.update_file(self)
1173
+
1174
+ any_err = False
1175
+ for err, _, _, _, _ in results:
1176
+ if isinstance(err, RevocationCheckError):
1177
+ err.msg += f" for {hostname}"
1178
+ if not no_exception and err is not None:
1179
+ raise err
1180
+ elif err is not None:
1181
+ any_err = True
1182
+
1183
+ logger.debug("ok" if not any_err else "failed")
1184
+ return results
1185
+
1186
+ @staticmethod
1187
+ def get_ocsp_retry_choice() -> bool:
1188
+ return os.getenv("SF_OCSP_DO_RETRY", "true") == "true"
1189
+
1190
+ def is_cert_id_in_cache(
1191
+ self, cert_id: CertId, subject: Certificate | None, **kwargs: Any
1192
+ ):
1193
+ """Decides whether OCSP CertID is in cache.
1194
+
1195
+ Args:
1196
+ cert_id: OCSP CertID.
1197
+ subject: Subject certificate.
1198
+
1199
+ Returns:
1200
+ True if in cache otherwise False, followed by the cached OCSP Response.
1201
+ """
1202
+ found, cache = SnowflakeOCSP.OCSP_CACHE.find_cache(
1203
+ self, cert_id, subject, **kwargs
1204
+ )
1205
+ return found, cache
1206
+
1207
+ def get_account_from_hostname(self, hostname: str) -> str:
1208
+ """Extracts the account name part from the hostname.
1209
+
1210
+ Args:
1211
+ hostname: Hostname that account name is in.
1212
+
1213
+ Returns:
1214
+ The extracted account name.
1215
+ """
1216
+ split_hname = hostname.split(".")
1217
+ if "global" in split_hname:
1218
+ acc_name = split_hname[0].split("-")[0]
1219
+ else:
1220
+ acc_name = split_hname[0]
1221
+ return acc_name
1222
+
1223
+ def is_enabled_fail_open(self) -> bool:
1224
+ return self.FAIL_OPEN
1225
+
1226
+ @staticmethod
1227
+ def print_fail_open_debug(ocsp_log) -> None:
1228
+ static_debug = "OCSP responder didn't respond correctly. Assuming certificate is not revoked. Details: "
1229
+ ocsp_debug = f"{static_debug} \n {ocsp_log}"
1230
+ logger.debug(ocsp_debug)
1231
+
1232
+ def validate_by_direct_connection(
1233
+ self,
1234
+ issuer: Certificate,
1235
+ subject: Certificate,
1236
+ telemetry_data: OCSPTelemetryData,
1237
+ hostname: str = None,
1238
+ do_retry: bool = True,
1239
+ **kwargs: Any,
1240
+ ) -> tuple[Exception | None, Certificate, Certificate, CertId, bytes]:
1241
+ cert_id, req = self.create_ocsp_request(issuer, subject)
1242
+ cache_status, ocsp_response = self.is_cert_id_in_cache(
1243
+ cert_id, subject, **kwargs
1244
+ )
1245
+
1246
+ try:
1247
+ if not cache_status:
1248
+ telemetry_data.set_cache_hit(False)
1249
+ logger.debug("getting OCSP response from CA's OCSP server")
1250
+ ocsp_response = self._fetch_ocsp_response(
1251
+ req, subject, cert_id, telemetry_data, hostname, do_retry
1252
+ )
1253
+ else:
1254
+ ocsp_url = self.extract_ocsp_url(subject)
1255
+ cert_id_enc = self.encode_cert_id_base64(
1256
+ self.decode_cert_id_key(cert_id)
1257
+ )
1258
+ telemetry_data.set_cache_hit(True)
1259
+ self.debug_ocsp_failure_url = SnowflakeOCSP.create_ocsp_debug_info(
1260
+ self, req, ocsp_url
1261
+ )
1262
+ telemetry_data.set_ocsp_url(ocsp_url)
1263
+ telemetry_data.set_ocsp_req(req)
1264
+ telemetry_data.set_cert_id(cert_id_enc)
1265
+ logger.debug("using OCSP response cache")
1266
+
1267
+ if not ocsp_response:
1268
+ telemetry_data.set_event_sub_type(
1269
+ OCSPTelemetryData.OCSP_RESPONSE_UNAVAILABLE
1270
+ )
1271
+ raise RevocationCheckError(
1272
+ msg="Could not retrieve OCSP Response. Cannot perform Revocation Check",
1273
+ errno=ER_OCSP_RESPONSE_UNAVAILABLE,
1274
+ )
1275
+ try:
1276
+ self.process_ocsp_response(issuer, cert_id, ocsp_response)
1277
+ err = None
1278
+ except RevocationCheckError as op_er:
1279
+ telemetry_data.set_event_sub_type(
1280
+ OCSPTelemetryData.ERROR_CODE_MAP[op_er.errno]
1281
+ )
1282
+ raise op_er
1283
+
1284
+ except RevocationCheckError as rce:
1285
+ telemetry_data.set_error_msg(rce.msg)
1286
+ err = self.verify_fail_open(rce, telemetry_data)
1287
+
1288
+ except Exception as ex:
1289
+ logger.debug("OCSP Validation failed %s", str(ex))
1290
+ telemetry_data.set_error_msg(str(ex))
1291
+ err = self.verify_fail_open(ex, telemetry_data)
1292
+ SnowflakeOCSP.OCSP_CACHE.delete_cache(self, cert_id)
1293
+
1294
+ return err, issuer, subject, cert_id, ocsp_response
1295
+
1296
+ def verify_fail_open(self, ex_obj, telemetry_data):
1297
+ if not self.is_enabled_fail_open():
1298
+ if ex_obj.errno is ER_OCSP_RESPONSE_CERT_STATUS_REVOKED:
1299
+ logger.debug(
1300
+ telemetry_data.generate_telemetry_data(
1301
+ "RevokedCertificateError", True
1302
+ )
1303
+ )
1304
+ else:
1305
+ logger.debug(
1306
+ telemetry_data.generate_telemetry_data("RevocationCheckFailure")
1307
+ )
1308
+ return ex_obj
1309
+ else:
1310
+ if ex_obj.errno is ER_OCSP_RESPONSE_CERT_STATUS_REVOKED:
1311
+ logger.debug(
1312
+ telemetry_data.generate_telemetry_data(
1313
+ "RevokedCertificateError", True
1314
+ )
1315
+ )
1316
+ return ex_obj
1317
+ else:
1318
+ SnowflakeOCSP.print_fail_open_debug(
1319
+ telemetry_data.generate_telemetry_data("RevocationCheckFailure")
1320
+ )
1321
+ return None
1322
+
1323
+ def _validate_certificates_sequential(
1324
+ self,
1325
+ cert_data: list[tuple[Certificate, Certificate]],
1326
+ telemetry_data: OCSPTelemetryData,
1327
+ hostname: str | None = None,
1328
+ do_retry: bool = True,
1329
+ ) -> list[tuple[Exception | None, Certificate, Certificate, CertId, bytes]]:
1330
+ results = []
1331
+ try:
1332
+ self._check_ocsp_response_cache_server(cert_data)
1333
+ except RevocationCheckError as rce:
1334
+ telemetry_data.set_event_sub_type(
1335
+ OCSPTelemetryData.ERROR_CODE_MAP[rce.errno]
1336
+ )
1337
+ except Exception as ex:
1338
+ logger.debug(
1339
+ "Caught unknown exception - %s. Continue to validate by direct connection",
1340
+ str(ex),
1341
+ )
1342
+
1343
+ to_update_cache_dict = {}
1344
+ for issuer, subject in cert_data:
1345
+ cert_id, _ = self.create_ocsp_request(issuer=issuer, subject=subject)
1346
+ cache_key = self.decode_cert_id_key(cert_id)
1347
+ ocsp_response_validation_result = OCSP_RESPONSE_VALIDATION_CACHE.get(
1348
+ cache_key
1349
+ )
1350
+
1351
+ if (
1352
+ ocsp_response_validation_result is None
1353
+ or not ocsp_response_validation_result.validated
1354
+ ):
1355
+ # r is a tuple of (err, issuer, subject, cert_id, ocsp_response)
1356
+ r = self.validate_by_direct_connection(
1357
+ issuer,
1358
+ subject,
1359
+ telemetry_data,
1360
+ hostname,
1361
+ do_retry=do_retry,
1362
+ cache_key=cache_key,
1363
+ )
1364
+
1365
+ # When OCSP server is down, the validation fails and the oscp_response will be None, and in fail open
1366
+ # case, we will also reset err to None.
1367
+ # In this case we don't need to write the response to cache because there is no information from a
1368
+ # connection error.
1369
+ if r[0] is not None or r[4] is not None:
1370
+ to_update_cache_dict[cache_key] = OCSPResponseValidationResult(
1371
+ *r,
1372
+ ts=int(time.time()),
1373
+ validated=True,
1374
+ )
1375
+ OCSPCache.CACHE_UPDATED = True
1376
+ results.append(r)
1377
+ else:
1378
+ results.append(
1379
+ (
1380
+ ocsp_response_validation_result.exception,
1381
+ ocsp_response_validation_result.issuer,
1382
+ ocsp_response_validation_result.subject,
1383
+ ocsp_response_validation_result.cert_id,
1384
+ ocsp_response_validation_result.ocsp_response,
1385
+ )
1386
+ )
1387
+ OCSP_RESPONSE_VALIDATION_CACHE.update(to_update_cache_dict)
1388
+ return results
1389
+
1390
+ def _check_ocsp_response_cache_server(
1391
+ self,
1392
+ cert_data: list[tuple[Certificate, Certificate]],
1393
+ ) -> None:
1394
+ """Checks if OCSP response is in cache, and if not it downloads the OCSP response cache from the server.
1395
+
1396
+ Args:
1397
+ cert_data: Tuple of issuer and subject certificates.
1398
+ """
1399
+ in_cache = False
1400
+ for issuer, subject in cert_data:
1401
+ # check if any OCSP response is NOT in cache
1402
+ cert_id, _ = self.create_ocsp_request(issuer, subject)
1403
+ in_cache, _ = SnowflakeOCSP.OCSP_CACHE.find_cache(self, cert_id, subject)
1404
+ if not in_cache:
1405
+ # not found any
1406
+ break
1407
+
1408
+ if not in_cache:
1409
+ self.OCSP_CACHE_SERVER.download_cache_from_server(self)
1410
+
1411
+ def _lazy_read_ca_bundle(self) -> None:
1412
+ """Reads the local cabundle file and cache it in memory."""
1413
+ with SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK:
1414
+ if SnowflakeOCSP.ROOT_CERTIFICATES_DICT:
1415
+ # return if already loaded
1416
+ return
1417
+
1418
+ try:
1419
+ ca_bundle = environ.get("REQUESTS_CA_BUNDLE") or environ.get(
1420
+ "CURL_CA_BUNDLE"
1421
+ )
1422
+ if ca_bundle and path.exists(ca_bundle):
1423
+ # if the user/application specifies cabundle.
1424
+ self.read_cert_bundle(ca_bundle)
1425
+ else:
1426
+ import sys
1427
+
1428
+ # This import that depends on these libraries is to import certificates from them,
1429
+ # we would like to have these as up to date as possible.
1430
+ from requests import certs
1431
+
1432
+ if (
1433
+ hasattr(certs, "__file__")
1434
+ and path.exists(certs.__file__)
1435
+ and path.exists(
1436
+ path.join(path.dirname(certs.__file__), "cacert.pem")
1437
+ )
1438
+ ):
1439
+ # if cacert.pem exists next to certs.py in request
1440
+ # package.
1441
+ ca_bundle = path.join(
1442
+ path.dirname(certs.__file__), "cacert.pem"
1443
+ )
1444
+ self.read_cert_bundle(ca_bundle)
1445
+ elif hasattr(sys, "_MEIPASS"):
1446
+ # if pyinstaller includes cacert.pem
1447
+ cabundle_candidates = [
1448
+ ["botocore", "vendored", "requests", "cacert.pem"],
1449
+ ["requests", "cacert.pem"],
1450
+ ["cacert.pem"],
1451
+ ]
1452
+ for filename in cabundle_candidates:
1453
+ ca_bundle = path.join(sys._MEIPASS, *filename)
1454
+ if path.exists(ca_bundle):
1455
+ self.read_cert_bundle(ca_bundle)
1456
+ break
1457
+ else:
1458
+ logger.error("No cabundle file is found in _MEIPASS")
1459
+ try:
1460
+ import certifi
1461
+
1462
+ self.read_cert_bundle(certifi.where())
1463
+ except Exception:
1464
+ logger.debug("no certifi is installed. ignored.")
1465
+
1466
+ except Exception as e:
1467
+ logger.error("Failed to read ca_bundle: %s", e)
1468
+
1469
+ if not SnowflakeOCSP.ROOT_CERTIFICATES_DICT:
1470
+ logger.error(
1471
+ "No CA bundle file is found in the system. "
1472
+ "Set REQUESTS_CA_BUNDLE to the file."
1473
+ )
1474
+
1475
+ @staticmethod
1476
+ def _calculate_tolerable_validity(this_update: float, next_update: float) -> int:
1477
+ return max(
1478
+ int(
1479
+ SnowflakeOCSP.TOLERABLE_VALIDITY_RANGE_RATIO
1480
+ * (next_update - this_update)
1481
+ ),
1482
+ SnowflakeOCSP.MAX_CLOCK_SKEW,
1483
+ )
1484
+
1485
+ @staticmethod
1486
+ def _is_validaity_range(
1487
+ current_time: int,
1488
+ this_update: float,
1489
+ next_update: float,
1490
+ test_mode: Any | None = None,
1491
+ ) -> bool:
1492
+ if test_mode is not None:
1493
+ force_validity_fail = os.getenv("SF_TEST_OCSP_FORCE_BAD_RESPONSE_VALIDITY")
1494
+ if force_validity_fail is not None:
1495
+ return False
1496
+
1497
+ tolerable_validity = SnowflakeOCSP._calculate_tolerable_validity(
1498
+ this_update, next_update
1499
+ )
1500
+ return (
1501
+ this_update - SnowflakeOCSP.MAX_CLOCK_SKEW
1502
+ <= current_time
1503
+ <= next_update + tolerable_validity
1504
+ )
1505
+
1506
+ @staticmethod
1507
+ def _validity_error_message(current_time, this_update, next_update) -> str:
1508
+ tolerable_validity = SnowflakeOCSP._calculate_tolerable_validity(
1509
+ this_update, next_update
1510
+ )
1511
+ return (
1512
+ "Response is unreliable. Its validity "
1513
+ "date is out of range: current_time={}, "
1514
+ "this_update={}, next_update={}, "
1515
+ "tolerable next_update={}. A potential cause is "
1516
+ "client clock is skewed, CA fails to update OCSP "
1517
+ "response in time.".format(
1518
+ strftime(SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(current_time)),
1519
+ strftime(SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(this_update)),
1520
+ strftime(SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(next_update)),
1521
+ strftime(
1522
+ SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT,
1523
+ gmtime(next_update + tolerable_validity),
1524
+ ),
1525
+ )
1526
+ )
1527
+
1528
+ @staticmethod
1529
+ def clear_cache() -> None:
1530
+ SnowflakeOCSP.OCSP_CACHE.clear_cache()
1531
+
1532
+ @staticmethod
1533
+ def cache_size():
1534
+ return SnowflakeOCSP.OCSP_CACHE.cache_size()
1535
+
1536
+ @staticmethod
1537
+ def delete_cache_file() -> None:
1538
+ SnowflakeOCSP.OCSP_CACHE.delete_cache_file()
1539
+
1540
+ @staticmethod
1541
+ def create_ocsp_debug_info(ocsp, ocsp_request, ocsp_url):
1542
+ b64data = ocsp.decode_ocsp_request_b64(ocsp_request)
1543
+ target_url = f"{ocsp_url}/{b64data}"
1544
+ return target_url
1545
+
1546
+ def _fetch_ocsp_response(
1547
+ self,
1548
+ ocsp_request,
1549
+ subject,
1550
+ cert_id,
1551
+ telemetry_data,
1552
+ hostname=None,
1553
+ do_retry: bool = True,
1554
+ ):
1555
+ """Fetches OCSP response using OCSPRequest."""
1556
+ sf_timeout = SnowflakeOCSP.CA_OCSP_RESPONDER_CONNECTION_TIMEOUT
1557
+ ocsp_url = self.extract_ocsp_url(subject)
1558
+ cert_id_enc = self.encode_cert_id_base64(self.decode_cert_id_key(cert_id))
1559
+ if not ocsp_url:
1560
+ telemetry_data.set_event_sub_type(OCSPTelemetryData.OCSP_URL_MISSING)
1561
+ raise RevocationCheckError(
1562
+ msg="No OCSP URL found in cert. Cannot perform Certificate Revocation check",
1563
+ errno=ER_OCSP_URL_INFO_MISSING,
1564
+ )
1565
+ headers = {HTTP_HEADER_USER_AGENT: PYTHON_CONNECTOR_USER_AGENT}
1566
+
1567
+ if not OCSPServer.is_enabled_new_ocsp_endpoint():
1568
+ actual_method = "post" if self._use_post_method else "get"
1569
+ if self.OCSP_CACHE_SERVER.OCSP_RETRY_URL:
1570
+ # no POST is supported for Retry URL at the moment.
1571
+ actual_method = "get"
1572
+
1573
+ if actual_method == "get":
1574
+ b64data = self.decode_ocsp_request_b64(ocsp_request)
1575
+ target_url = self.OCSP_CACHE_SERVER.generate_get_url(ocsp_url, b64data)
1576
+ payload = None
1577
+ else:
1578
+ target_url = ocsp_url
1579
+ payload = self.decode_ocsp_request(ocsp_request)
1580
+ headers["Content-Type"] = "application/ocsp-request"
1581
+ else:
1582
+ actual_method = "post"
1583
+ target_url = self.OCSP_CACHE_SERVER.OCSP_RETRY_URL
1584
+ ocsp_req_enc = self.decode_ocsp_request_b64(ocsp_request)
1585
+
1586
+ payload = json.dumps(
1587
+ {
1588
+ "hostname": hostname,
1589
+ "ocsp_request": ocsp_req_enc,
1590
+ "cert_id": cert_id_enc,
1591
+ "ocsp_responder_url": ocsp_url,
1592
+ }
1593
+ )
1594
+ headers["Content-Type"] = "application/json"
1595
+
1596
+ telemetry_data.set_ocsp_connection_method(actual_method)
1597
+ if self.test_mode is not None:
1598
+ logger.debug("WARNING - DRIVER IS CONFIGURED IN TESTMODE.")
1599
+ test_ocsp_url = os.getenv("SF_TEST_OCSP_URL", None)
1600
+ test_timeout = os.getenv(
1601
+ "SF_TEST_CA_OCSP_RESPONDER_CONNECTION_TIMEOUT", None
1602
+ )
1603
+ if test_timeout is not None:
1604
+ sf_timeout = int(test_timeout)
1605
+ if test_ocsp_url is not None:
1606
+ target_url = test_ocsp_url
1607
+
1608
+ self.debug_ocsp_failure_url = SnowflakeOCSP.create_ocsp_debug_info(
1609
+ self, ocsp_request, ocsp_url
1610
+ )
1611
+ telemetry_data.set_ocsp_req(self.decode_ocsp_request_b64(ocsp_request))
1612
+ telemetry_data.set_ocsp_url(ocsp_url)
1613
+ telemetry_data.set_cert_id(cert_id_enc)
1614
+
1615
+ ret = None
1616
+ logger.debug("url: %s", target_url)
1617
+ sf_max_retry = SnowflakeOCSP.CA_OCSP_RESPONDER_MAX_RETRY_FO
1618
+ if not self.is_enabled_fail_open():
1619
+ sf_max_retry = SnowflakeOCSP.CA_OCSP_RESPONDER_MAX_RETRY_FC
1620
+
1621
+ with generic_requests.Session() as session:
1622
+ max_retry = sf_max_retry if do_retry else 1
1623
+ sleep_time = 1
1624
+ backoff = exponential_backoff()()
1625
+ for _ in range(max_retry):
1626
+ try:
1627
+ response = session.request(
1628
+ headers=headers,
1629
+ method=actual_method,
1630
+ url=target_url,
1631
+ timeout=sf_timeout,
1632
+ data=payload,
1633
+ )
1634
+ if response.status_code == OK:
1635
+ logger.debug(
1636
+ "OCSP response was successfully returned from OCSP "
1637
+ "server."
1638
+ )
1639
+ ret = response.content
1640
+ break
1641
+ elif max_retry > 1:
1642
+ sleep_time = next(backoff)
1643
+ logger.debug(
1644
+ "OCSP server returned %s. Retrying in %s(s)",
1645
+ response.status_code,
1646
+ sleep_time,
1647
+ )
1648
+ time.sleep(sleep_time)
1649
+ except Exception as ex:
1650
+ if max_retry > 1:
1651
+ sleep_time = next(backoff)
1652
+ logger.debug(
1653
+ "Could not fetch OCSP Response from server"
1654
+ "Retrying in %s(s)",
1655
+ sleep_time,
1656
+ )
1657
+ time.sleep(sleep_time)
1658
+ else:
1659
+ telemetry_data.set_event_sub_type(
1660
+ OCSPTelemetryData.OCSP_RESPONSE_FETCH_EXCEPTION
1661
+ )
1662
+ raise RevocationCheckError(
1663
+ msg="Could not fetch OCSP Response from server. Consider"
1664
+ "checking your whitelists : Exception - {}".format(str(ex)),
1665
+ errno=ER_OCSP_RESPONSE_FETCH_EXCEPTION,
1666
+ )
1667
+ else:
1668
+ logger.error(
1669
+ "Failed to get OCSP response after {} attempt. Consider checking "
1670
+ "for OCSP URLs being blocked".format(max_retry)
1671
+ )
1672
+ telemetry_data.set_event_sub_type(
1673
+ OCSPTelemetryData.OCSP_RESPONSE_FETCH_FAILURE
1674
+ )
1675
+ raise RevocationCheckError(
1676
+ msg="Failed to get OCSP response after {} attempt.".format(
1677
+ max_retry
1678
+ ),
1679
+ errno=ER_OCSP_RESPONSE_FETCH_FAILURE,
1680
+ )
1681
+
1682
+ return ret
1683
+
1684
+ def _process_good_status(
1685
+ self, single_response: SingleResponse, cert_id: CertId, ocsp_response: bytes
1686
+ ) -> None:
1687
+ """Processes GOOD status."""
1688
+ current_time = int(time.time())
1689
+ this_update_native, next_update_native = self.extract_good_status(
1690
+ single_response
1691
+ )
1692
+
1693
+ if this_update_native is None or next_update_native is None:
1694
+ raise RevocationCheckError(
1695
+ msg="Either this update or next "
1696
+ "update is None. this_update: {}, next_update: {}".format(
1697
+ this_update_native, next_update_native
1698
+ ),
1699
+ errno=ER_OCSP_RESPONSE_INVALID_EXPIRY_INFO_MISSING,
1700
+ )
1701
+
1702
+ this_update = (
1703
+ this_update_native.replace(tzinfo=None) - SnowflakeOCSP.ZERO_EPOCH
1704
+ ).total_seconds()
1705
+ next_update = (
1706
+ next_update_native.replace(tzinfo=None) - SnowflakeOCSP.ZERO_EPOCH
1707
+ ).total_seconds()
1708
+ if not SnowflakeOCSP._is_validaity_range(
1709
+ current_time, this_update, next_update, self.test_mode
1710
+ ):
1711
+ raise RevocationCheckError(
1712
+ msg=SnowflakeOCSP._validity_error_message(
1713
+ current_time, this_update, next_update
1714
+ ),
1715
+ errno=ER_OCSP_RESPONSE_EXPIRED,
1716
+ )
1717
+
1718
+ def _process_revoked_status(self, single_response, cert_id):
1719
+ """Processes REVOKED status."""
1720
+ current_time = int(time.time())
1721
+ if self.test_mode is not None:
1722
+ test_cert_status = os.getenv("SF_TEST_OCSP_CERT_STATUS")
1723
+ if test_cert_status == "revoked":
1724
+ raise RevocationCheckError(
1725
+ msg="The certificate has been revoked: current_time={}, "
1726
+ "revocation_time={}, reason={}".format(
1727
+ strftime(
1728
+ SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(current_time)
1729
+ ),
1730
+ strftime(
1731
+ SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(current_time)
1732
+ ),
1733
+ "Force Revoke",
1734
+ ),
1735
+ errno=ER_OCSP_RESPONSE_CERT_STATUS_REVOKED,
1736
+ )
1737
+
1738
+ SnowflakeOCSP.OCSP_CACHE.delete_cache(self, cert_id)
1739
+ revocation_time, revocation_reason = self.extract_revoked_status(
1740
+ single_response
1741
+ )
1742
+ raise RevocationCheckError(
1743
+ msg="The certificate has been revoked: current_time={}, "
1744
+ "revocation_time={}, reason={}".format(
1745
+ strftime(SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT, gmtime(current_time)),
1746
+ revocation_time.strftime(SnowflakeOCSP.OUTPUT_TIMESTAMP_FORMAT),
1747
+ revocation_reason,
1748
+ ),
1749
+ errno=ER_OCSP_RESPONSE_CERT_STATUS_REVOKED,
1750
+ )
1751
+
1752
+ def _process_unknown_status(self, cert_id):
1753
+ """Processes UNKNOWN status."""
1754
+ SnowflakeOCSP.OCSP_CACHE.delete_cache(self, cert_id)
1755
+ raise RevocationCheckError(
1756
+ msg="The certificate is in UNKNOWN revocation status.",
1757
+ errno=ER_OCSP_RESPONSE_CERT_STATUS_UNKNOWN,
1758
+ )
1759
+
1760
+ def decode_ocsp_response_cache(self, ocsp_response_cache_json):
1761
+ """Decodes OCSP response cache from JSON."""
1762
+ try:
1763
+ with OCSP_RESPONSE_VALIDATION_CACHE._lock:
1764
+ new_cache_dict = {}
1765
+ for cert_id_base64, (
1766
+ ts,
1767
+ ocsp_response,
1768
+ ) in ocsp_response_cache_json.items():
1769
+ cert_id = self.decode_cert_id_base64(cert_id_base64)
1770
+ b64decoded_ocsp_response = b64decode(ocsp_response)
1771
+ if not self.is_valid_time(cert_id, b64decoded_ocsp_response):
1772
+ continue
1773
+ current_time = int(time.time())
1774
+ cache_key: tuple[bytes, bytes, bytes] = self.decode_cert_id_key(
1775
+ cert_id
1776
+ )
1777
+ found, _ = OCSPCache.find_cache(
1778
+ self, cert_id, None, cache_key=cache_key, lock_cache=False
1779
+ )
1780
+ if OCSPCache.is_cache_fresh(current_time, ts):
1781
+ new_cache_dict[cache_key] = OCSPResponseValidationResult(
1782
+ ocsp_response=b64decoded_ocsp_response,
1783
+ ts=current_time,
1784
+ validated=False,
1785
+ )
1786
+ elif found:
1787
+ OCSPCache.delete_cache(
1788
+ self, cert_id, cache_key=cache_key, lock_cache=False
1789
+ )
1790
+ if new_cache_dict:
1791
+ OCSP_RESPONSE_VALIDATION_CACHE.update(new_cache_dict)
1792
+ OCSPCache.CACHE_UPDATED = True
1793
+ except Exception as ex:
1794
+ logger.debug("Caught here - %s", ex)
1795
+ ermsg = "Exception raised while decoding OCSP Response Cache {}".format(
1796
+ str(ex)
1797
+ )
1798
+ raise RevocationCheckError(
1799
+ msg=ermsg, errno=ER_OCSP_RESPONSE_CACHE_DECODE_FAILED
1800
+ )
1801
+
1802
+ def encode_ocsp_response_cache(self, ocsp_response_cache_json) -> None:
1803
+ """Encodes OCSP response cache to JSON."""
1804
+ logger.debug("encoding OCSP response cache to JSON")
1805
+ for (
1806
+ cache_key,
1807
+ ocsp_response_validation_result,
1808
+ ) in OCSP_RESPONSE_VALIDATION_CACHE.items():
1809
+ k = self.encode_cert_id_base64(cache_key)
1810
+ v = b64encode(ocsp_response_validation_result.ocsp_response).decode("ascii")
1811
+ ocsp_response_cache_json[k] = (ocsp_response_validation_result.ts, v)
1812
+
1813
+ def read_cert_bundle(self, ca_bundle_file, storage=None):
1814
+ """Reads a certificate file including certificates in PEM format."""
1815
+ raise NotImplementedError
1816
+
1817
+ def encode_cert_id_key(self, _):
1818
+ """Encodes Cert ID key to native CertID."""
1819
+ raise NotImplementedError
1820
+
1821
+ def decode_cert_id_key(self, _):
1822
+ """Decodes name CertID to Cert ID key."""
1823
+ raise NotImplementedError
1824
+
1825
+ def encode_cert_id_base64(self, hkey):
1826
+ """Encodes native CertID to base64 Cert ID."""
1827
+ raise NotImplementedError
1828
+
1829
+ def decode_cert_id_base64(self, cert_id_base64):
1830
+ """Decodes base64 Cert ID to native CertID."""
1831
+ raise NotImplementedError
1832
+
1833
+ def create_ocsp_request(
1834
+ self,
1835
+ issuer: Certificate,
1836
+ subject: Certificate,
1837
+ ) -> tuple[CertId, OCSPRequest]:
1838
+ """Creates CertId and OCSPRequest."""
1839
+ raise NotImplementedError
1840
+
1841
+ def extract_ocsp_url(self, cert):
1842
+ """Extracts OCSP URL from Certificate."""
1843
+ raise NotImplementedError
1844
+
1845
+ def decode_ocsp_request(self, ocsp_request):
1846
+ """Decodes OCSP request to DER."""
1847
+ raise NotImplementedError
1848
+
1849
+ def decode_ocsp_request_b64(self, ocsp_request):
1850
+ """Decodes OCSP Request object to b64."""
1851
+ raise NotImplementedError
1852
+
1853
+ def extract_good_status(self, single_response):
1854
+ """Extracts Revocation Status GOOD."""
1855
+ raise NotImplementedError
1856
+
1857
+ def extract_revoked_status(self, single_response):
1858
+ """Extracts Revocation Status REVOKED."""
1859
+ raise NotImplementedError
1860
+
1861
+ def process_ocsp_response(self, issuer, cert_id, ocsp_response):
1862
+ """Processes OCSP response."""
1863
+ raise NotImplementedError
1864
+
1865
+ def verify_signature(self, signature_algorithm, signature, cert, data):
1866
+ """Verifies signature."""
1867
+ raise NotImplementedError
1868
+
1869
+ def extract_certificate_chain(self, connection):
1870
+ """Gets certificate chain and extract the key info from OpenSSL connection."""
1871
+ raise NotImplementedError
1872
+
1873
+ def create_pair_issuer_subject(self, cert_map):
1874
+ """Creates pairs of issuer and subject certificates."""
1875
+ raise NotImplementedError
1876
+
1877
+ def subject_name(self, subject):
1878
+ """Gets human readable Subject name."""
1879
+ raise NotImplementedError
1880
+
1881
+ def is_valid_time(self, cert_id, ocsp_response):
1882
+ """Checks whether ocsp_response is in valid time range."""
1883
+ raise NotImplementedError