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.
- snowflake/connector/__init__.py +96 -0
- snowflake/connector/_query_context_cache.py +276 -0
- snowflake/connector/_sql_util.py +48 -0
- snowflake/connector/_utils.py +70 -0
- snowflake/connector/arrow_context.py +203 -0
- snowflake/connector/auth/__init__.py +53 -0
- snowflake/connector/auth/_auth.py +586 -0
- snowflake/connector/auth/_http_server.py +220 -0
- snowflake/connector/auth/_oauth_base.py +400 -0
- snowflake/connector/auth/by_plugin.py +219 -0
- snowflake/connector/auth/default.py +36 -0
- snowflake/connector/auth/idtoken.py +72 -0
- snowflake/connector/auth/keypair.py +222 -0
- snowflake/connector/auth/no_auth.py +39 -0
- snowflake/connector/auth/oauth.py +49 -0
- snowflake/connector/auth/oauth_code.py +479 -0
- snowflake/connector/auth/oauth_credentials.py +66 -0
- snowflake/connector/auth/okta.py +335 -0
- snowflake/connector/auth/pat.py +39 -0
- snowflake/connector/auth/usrpwdmfa.py +69 -0
- snowflake/connector/auth/webbrowser.py +502 -0
- snowflake/connector/auth/workload_identity.py +94 -0
- snowflake/connector/azure_storage_client.py +276 -0
- snowflake/connector/backoff_policies.py +141 -0
- snowflake/connector/bind_upload_agent.py +89 -0
- snowflake/connector/cache.py +696 -0
- snowflake/connector/compat.py +127 -0
- snowflake/connector/config_manager.py +496 -0
- snowflake/connector/connection.py +2273 -0
- snowflake/connector/connection_diagnostic.py +776 -0
- snowflake/connector/constants.py +442 -0
- snowflake/connector/converter.py +785 -0
- snowflake/connector/converter_issue23517.py +87 -0
- snowflake/connector/converter_null.py +14 -0
- snowflake/connector/converter_snowsql.py +205 -0
- snowflake/connector/cursor.py +1951 -0
- snowflake/connector/dbapi.py +53 -0
- snowflake/connector/description.py +19 -0
- snowflake/connector/direct_file_operation_utils.py +88 -0
- snowflake/connector/encryption_util.py +220 -0
- snowflake/connector/errorcode.py +91 -0
- snowflake/connector/errors.py +615 -0
- snowflake/connector/externals_utils/__init__.py +0 -0
- snowflake/connector/externals_utils/externals_setup.py +27 -0
- snowflake/connector/feature.py +4 -0
- snowflake/connector/file_compression_type.py +118 -0
- snowflake/connector/file_lock.py +72 -0
- snowflake/connector/file_transfer_agent.py +1215 -0
- snowflake/connector/file_util.py +153 -0
- snowflake/connector/gcs_storage_client.py +474 -0
- snowflake/connector/gzip_decoder.py +85 -0
- snowflake/connector/local_storage_client.py +90 -0
- snowflake/connector/log_configuration.py +60 -0
- snowflake/connector/logging_utils/__init__.py +0 -0
- snowflake/connector/logging_utils/filters.py +72 -0
- snowflake/connector/nanoarrow_arrow_iterator.cp313-win_amd64.pyd +0 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +60 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp +557 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.hpp +98 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +115 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp +1005 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.hpp +127 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +47 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +83 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +72 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +73 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +71 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +56 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +75 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +95 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +57 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +34 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +346 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +145 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +101 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +127 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1908 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +215 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +55 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +148 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +239 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +210 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +206 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +118 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +898 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +190 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc.c +3204 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3217 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3618 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +379 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iterator.pyx +256 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.c +512 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.h +350 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +33273 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +397 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.cpp +100 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.hpp +49 -0
- snowflake/connector/network.py +1297 -0
- snowflake/connector/ocsp_asn1crypto.py +447 -0
- snowflake/connector/ocsp_snowflake.py +1883 -0
- snowflake/connector/options.py +131 -0
- snowflake/connector/pandas_tools.py +732 -0
- snowflake/connector/proxy.py +43 -0
- snowflake/connector/py.typed +0 -0
- snowflake/connector/result_batch.py +786 -0
- snowflake/connector/result_set.py +319 -0
- snowflake/connector/s3_storage_client.py +605 -0
- snowflake/connector/secret_detector.py +181 -0
- snowflake/connector/sf_dirs.py +64 -0
- snowflake/connector/sfbinaryformat.py +35 -0
- snowflake/connector/sfdatetime.py +345 -0
- snowflake/connector/snow_logging.py +144 -0
- snowflake/connector/sqlstate.py +9 -0
- snowflake/connector/ssd_internal_keys.py +30 -0
- snowflake/connector/ssl_wrap_socket.py +134 -0
- snowflake/connector/storage_client.py +478 -0
- snowflake/connector/telemetry.py +249 -0
- snowflake/connector/telemetry_oob.py +543 -0
- snowflake/connector/test_util.py +30 -0
- snowflake/connector/time_util.py +159 -0
- snowflake/connector/token_cache.py +402 -0
- snowflake/connector/tool/__init__.py +0 -0
- snowflake/connector/tool/dump_certs.py +57 -0
- snowflake/connector/tool/dump_ocsp_response.py +139 -0
- snowflake/connector/tool/dump_ocsp_response_cache.py +194 -0
- snowflake/connector/tool/probe_connection.py +69 -0
- snowflake/connector/url_util.py +49 -0
- snowflake/connector/util_text.py +301 -0
- snowflake/connector/vendored/__init__.py +3 -0
- snowflake/connector/vendored/requests/LICENSE +175 -0
- snowflake/connector/vendored/requests/__init__.py +169 -0
- snowflake/connector/vendored/requests/__version__.py +14 -0
- snowflake/connector/vendored/requests/_internal_utils.py +50 -0
- snowflake/connector/vendored/requests/adapters.py +537 -0
- snowflake/connector/vendored/requests/api.py +157 -0
- snowflake/connector/vendored/requests/auth.py +315 -0
- snowflake/connector/vendored/requests/certs.py +17 -0
- snowflake/connector/vendored/requests/compat.py +79 -0
- snowflake/connector/vendored/requests/cookies.py +561 -0
- snowflake/connector/vendored/requests/exceptions.py +140 -0
- snowflake/connector/vendored/requests/help.py +134 -0
- snowflake/connector/vendored/requests/hooks.py +33 -0
- snowflake/connector/vendored/requests/models.py +1033 -0
- snowflake/connector/vendored/requests/sessions.py +833 -0
- snowflake/connector/vendored/requests/status_codes.py +128 -0
- snowflake/connector/vendored/requests/structures.py +99 -0
- snowflake/connector/vendored/requests/utils.py +1093 -0
- snowflake/connector/vendored/urllib3/LICENSE.txt +21 -0
- snowflake/connector/vendored/urllib3/__init__.py +85 -0
- snowflake/connector/vendored/urllib3/_collections.py +355 -0
- snowflake/connector/vendored/urllib3/_version.py +2 -0
- snowflake/connector/vendored/urllib3/connection.py +572 -0
- snowflake/connector/vendored/urllib3/connectionpool.py +1137 -0
- snowflake/connector/vendored/urllib3/contrib/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/contrib/_appengine_environ.py +36 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/bindings.py +519 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/low_level.py +397 -0
- snowflake/connector/vendored/urllib3/contrib/appengine.py +314 -0
- snowflake/connector/vendored/urllib3/contrib/ntlmpool.py +130 -0
- snowflake/connector/vendored/urllib3/contrib/pyopenssl.py +509 -0
- snowflake/connector/vendored/urllib3/contrib/securetransport.py +920 -0
- snowflake/connector/vendored/urllib3/contrib/socks.py +216 -0
- snowflake/connector/vendored/urllib3/exceptions.py +323 -0
- snowflake/connector/vendored/urllib3/fields.py +274 -0
- snowflake/connector/vendored/urllib3/filepost.py +98 -0
- snowflake/connector/vendored/urllib3/packages/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/packages/backports/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/packages/backports/makefile.py +51 -0
- snowflake/connector/vendored/urllib3/packages/backports/weakref_finalize.py +155 -0
- snowflake/connector/vendored/urllib3/packages/six.py +1076 -0
- snowflake/connector/vendored/urllib3/poolmanager.py +540 -0
- snowflake/connector/vendored/urllib3/request.py +191 -0
- snowflake/connector/vendored/urllib3/response.py +885 -0
- snowflake/connector/vendored/urllib3/util/__init__.py +49 -0
- snowflake/connector/vendored/urllib3/util/connection.py +156 -0
- snowflake/connector/vendored/urllib3/util/proxy.py +57 -0
- snowflake/connector/vendored/urllib3/util/queue.py +22 -0
- snowflake/connector/vendored/urllib3/util/request.py +146 -0
- snowflake/connector/vendored/urllib3/util/response.py +107 -0
- snowflake/connector/vendored/urllib3/util/retry.py +620 -0
- snowflake/connector/vendored/urllib3/util/ssl_.py +495 -0
- snowflake/connector/vendored/urllib3/util/ssl_match_hostname.py +159 -0
- snowflake/connector/vendored/urllib3/util/ssltransport.py +221 -0
- snowflake/connector/vendored/urllib3/util/timeout.py +271 -0
- snowflake/connector/vendored/urllib3/util/url.py +435 -0
- snowflake/connector/vendored/urllib3/util/wait.py +152 -0
- snowflake/connector/version.py +3 -0
- snowflake/connector/wif_util.py +407 -0
- snowflake_connector_python-3.16.0.dist-info/METADATA +1475 -0
- snowflake_connector_python-3.16.0.dist-info/RECORD +246 -0
- snowflake_connector_python-3.16.0.dist-info/WHEEL +5 -0
- snowflake_connector_python-3.16.0.dist-info/entry_points.txt +4 -0
- snowflake_connector_python-3.16.0.dist-info/licenses/LICENSE.txt +202 -0
- snowflake_connector_python-3.16.0.dist-info/licenses/NOTICE +8 -0
- 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
|