Skip to content

Commit 5b61af7

Browse files
authored
Revert "SNOW-843716: cryptography dep cleanup (#1773)" (#1778)
This reverts commit 6398696.
1 parent d85e56a commit 5b61af7

File tree

8 files changed

+186
-50
lines changed

8 files changed

+186
-50
lines changed

DESCRIPTION.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1313
- Added for non-Windows platforms command suggestions (chown/chmod) for insufficient file permissions of config files.
1414
- Fixed issue with connection diagnostics failing to complete certificate checks.
1515
- Fixed issue that arrow iterator causes `ImportError` when the c extensions are not compiled.
16-
- Removed dependencies on Cryptodome and oscrypto and removed the `use_openssl_only` parameter. All connections now go through OpenSSL via the cryptography library, which was already a dependency.
1716

1817
- v3.3.0(October 10,2023)
1918

ci/test_fips_docker.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ user_id=$(id -u $USER)
2121
docker run --network=host \
2222
-e LANG=en_US.UTF-8 \
2323
-e TERM=vt102 \
24+
-e SF_USE_OPENSSL_ONLY=True \
2425
-e PIP_DISABLE_PIP_VERSION_CHECK=1 \
2526
-e LOCAL_USER_ID=$user_id \
2627
-e CRYPTOGRAPHY_ALLOW_OPENSSL_102=1 \

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ install_requires =
4545
asn1crypto>0.24.0,<2.0.0
4646
cffi>=1.9,<2.0.0
4747
cryptography>=3.1.0,<42.0.0
48+
oscrypto<2.0.0
4849
pyOpenSSL>=16.2.0,<24.0.0
50+
pycryptodomex!=3.5.0,>=3.2,<4.0.0
4951
pyjwt<3.0.0
5052
pytz
5153
requests<3.0.0

src/snowflake/connector/connection.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ def DefaultConverterClass() -> type:
189189
"client_store_temporary_credential": (False, bool),
190190
"client_request_mfa_token": (False, bool),
191191
"use_openssl_only": (
192-
True,
192+
False,
193193
bool,
194-
), # ignored - python only crypto modules are no longer used
194+
), # only use openssl instead of python only crypto modules
195195
# whether to convert Arrow number values to decimal instead of doubles
196196
"arrow_number_to_decimal": (False, bool),
197197
"enable_stage_s3_privatelink_for_us_east_1": (
@@ -287,6 +287,7 @@ class SnowflakeConnection:
287287
validate_default_parameters: Validate database, schema, role and warehouse used on Snowflake.
288288
is_pyformat: Whether the current argument binding is pyformat or format.
289289
consent_cache_id_token: Consented cache ID token.
290+
use_openssl_only: Use OpenSSL instead of pure Python libraries for signature verification and encryption.
290291
enable_stage_s3_privatelink_for_us_east_1: when true, clients use regional s3 url to upload files.
291292
enable_connection_diag: when true, clients will generate a connectivity diagnostic report.
292293
connection_diag_log_path: path to location to create diag report with enable_connection_diag.
@@ -573,8 +574,7 @@ def disable_request_pooling(self, value) -> None:
573574

574575
@property
575576
def use_openssl_only(self) -> bool:
576-
# Deprecated, kept for backwards compatibility
577-
return True
577+
return self._use_openssl_only
578578

579579
@property
580580
def arrow_number_to_decimal(self):
@@ -1117,6 +1117,19 @@ def __config(self, **kwargs):
11171117
"CHECKED."
11181118
)
11191119

1120+
if "SF_USE_OPENSSL_ONLY" not in os.environ:
1121+
logger.info("Setting use_openssl_only mode to %s", self.use_openssl_only)
1122+
os.environ["SF_USE_OPENSSL_ONLY"] = str(self.use_openssl_only)
1123+
elif (
1124+
os.environ.get("SF_USE_OPENSSL_ONLY", "False") == "True"
1125+
) != self.use_openssl_only:
1126+
logger.warning(
1127+
"Mode use_openssl_only is already set to: %s, ignoring set request to: %s",
1128+
os.environ["SF_USE_OPENSSL_ONLY"],
1129+
self.use_openssl_only,
1130+
)
1131+
self._use_openssl_only = os.environ["SF_USE_OPENSSL_ONLY"] == "True"
1132+
11201133
def cmd_query(
11211134
self,
11221135
sql: str,

src/snowflake/connector/encryption_util.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from logging import getLogger
1313
from typing import IO, TYPE_CHECKING
1414

15+
from Cryptodome.Cipher import AES
1516
from cryptography.hazmat.backends import default_backend
1617
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
1718

@@ -68,6 +69,7 @@ def encrypt_stream(
6869
The encryption metadata.
6970
"""
7071
logger = getLogger(__name__)
72+
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
7173
decoded_key = base64.standard_b64decode(
7274
encryption_material.query_stage_master_key
7375
)
@@ -77,9 +79,14 @@ def encrypt_stream(
7779
# Generate key for data encryption
7880
iv_data = SnowflakeEncryptionUtil.get_secure_random(block_size)
7981
file_key = SnowflakeEncryptionUtil.get_secure_random(key_size)
80-
backend = default_backend()
81-
cipher = Cipher(algorithms.AES(file_key), modes.CBC(iv_data), backend=backend)
82-
encryptor = cipher.encryptor()
82+
if not use_openssl_only:
83+
data_cipher = AES.new(key=file_key, mode=AES.MODE_CBC, IV=iv_data)
84+
else:
85+
backend = default_backend()
86+
cipher = Cipher(
87+
algorithms.AES(file_key), modes.CBC(iv_data), backend=backend
88+
)
89+
encryptor = cipher.encryptor()
8390

8491
padded = False
8592
while True:
@@ -89,17 +96,30 @@ def encrypt_stream(
8996
elif len(chunk) % block_size != 0:
9097
chunk = PKCS5_PAD(chunk, block_size)
9198
padded = True
92-
out.write(encryptor.update(chunk))
99+
if not use_openssl_only:
100+
out.write(data_cipher.encrypt(chunk))
101+
else:
102+
out.write(encryptor.update(chunk))
93103
if not padded:
94-
out.write(encryptor.update(block_size * chr(block_size).encode(UTF8)))
95-
out.write(encryptor.finalize())
104+
if not use_openssl_only:
105+
out.write(
106+
data_cipher.encrypt(block_size * chr(block_size).encode(UTF8))
107+
)
108+
else:
109+
out.write(encryptor.update(block_size * chr(block_size).encode(UTF8)))
110+
if use_openssl_only:
111+
out.write(encryptor.finalize())
96112

97113
# encrypt key with QRMK
98-
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
99-
encryptor = cipher.encryptor()
100-
enc_kek = (
101-
encryptor.update(PKCS5_PAD(file_key, block_size)) + encryptor.finalize()
102-
)
114+
if not use_openssl_only:
115+
key_cipher = AES.new(key=decoded_key, mode=AES.MODE_ECB)
116+
enc_kek = key_cipher.encrypt(PKCS5_PAD(file_key, block_size))
117+
else:
118+
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
119+
encryptor = cipher.encryptor()
120+
enc_kek = (
121+
encryptor.update(PKCS5_PAD(file_key, block_size)) + encryptor.finalize()
122+
)
103123

104124
mat_desc = MaterialDescriptor(
105125
smk_id=encryption_material.smk_id,
@@ -158,6 +178,7 @@ def decrypt_stream(
158178
) -> None:
159179
"""To read from `src` stream then decrypt to `out` stream."""
160180

181+
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
161182
key_base64 = metadata.key
162183
iv_base64 = metadata.iv
163184
decoded_key = base64.standard_b64decode(
@@ -166,26 +187,37 @@ def decrypt_stream(
166187
key_bytes = base64.standard_b64decode(key_base64)
167188
iv_bytes = base64.standard_b64decode(iv_base64)
168189

169-
backend = default_backend()
170-
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
171-
decryptor = cipher.decryptor()
172-
file_key = PKCS5_UNPAD(decryptor.update(key_bytes) + decryptor.finalize())
173-
cipher = Cipher(algorithms.AES(file_key), modes.CBC(iv_bytes), backend=backend)
174-
decryptor = cipher.decryptor()
190+
if not use_openssl_only:
191+
key_cipher = AES.new(key=decoded_key, mode=AES.MODE_ECB)
192+
file_key = PKCS5_UNPAD(key_cipher.decrypt(key_bytes))
193+
data_cipher = AES.new(key=file_key, mode=AES.MODE_CBC, IV=iv_bytes)
194+
else:
195+
backend = default_backend()
196+
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
197+
decryptor = cipher.decryptor()
198+
file_key = PKCS5_UNPAD(decryptor.update(key_bytes) + decryptor.finalize())
199+
cipher = Cipher(
200+
algorithms.AES(file_key), modes.CBC(iv_bytes), backend=backend
201+
)
202+
decryptor = cipher.decryptor()
175203

176204
last_decrypted_chunk = None
177205
chunk = src.read(chunk_size)
178206
while len(chunk) != 0:
179207
if last_decrypted_chunk is not None:
180208
out.write(last_decrypted_chunk)
181-
d = decryptor.update(chunk)
209+
if not use_openssl_only:
210+
d = data_cipher.decrypt(chunk)
211+
else:
212+
d = decryptor.update(chunk)
182213
last_decrypted_chunk = d
183214
chunk = src.read(chunk_size)
184215

185216
if last_decrypted_chunk is not None:
186217
offset = PKCS5_OFFSET(last_decrypted_chunk)
187218
out.write(last_decrypted_chunk[:-offset])
188-
out.write(decryptor.finalize())
219+
if use_openssl_only:
220+
out.write(decryptor.finalize())
189221

190222
@staticmethod
191223
def decrypt_file(

src/snowflake/connector/file_util.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from logging import getLogger
1414
from typing import IO
1515

16+
from Cryptodome.Hash import SHA256
1617
from cryptography.hazmat.backends import default_backend
1718
from cryptography.hazmat.primitives import hashes
1819

@@ -32,17 +33,27 @@ def get_digest_and_size(src: IO[bytes]) -> tuple[str, int]:
3233
Returns:
3334
Tuple of src's digest and src's size in bytes.
3435
"""
36+
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
3537
CHUNK_SIZE = 64 * kilobyte
36-
backend = default_backend()
37-
chosen_hash = hashes.SHA256()
38-
hasher = hashes.Hash(chosen_hash, backend)
38+
if not use_openssl_only:
39+
m = SHA256.new()
40+
else:
41+
backend = default_backend()
42+
chosen_hash = hashes.SHA256()
43+
hasher = hashes.Hash(chosen_hash, backend)
3944
while True:
4045
chunk = src.read(CHUNK_SIZE)
4146
if chunk == b"":
4247
break
43-
hasher.update(chunk)
44-
45-
digest = base64.standard_b64encode(hasher.finalize()).decode(UTF8)
48+
if not use_openssl_only:
49+
m.update(chunk)
50+
else:
51+
hasher.update(chunk)
52+
53+
if not use_openssl_only:
54+
digest = base64.standard_b64encode(m.digest()).decode(UTF8)
55+
else:
56+
digest = base64.standard_b64encode(hasher.finalize()).decode(UTF8)
4657

4758
size = src.tell()
4859
src.seek(0)

src/snowflake/connector/ocsp_asn1crypto.py

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
from __future__ import annotations
77

8+
import os
9+
import platform
10+
import sys
11+
import warnings
812
from base64 import b64decode, b64encode
913
from collections import OrderedDict
1014
from datetime import datetime, timezone
@@ -24,6 +28,9 @@
2428
Version,
2529
)
2630
from asn1crypto.x509 import Certificate
31+
from Cryptodome.Hash import SHA1, SHA256, SHA384, SHA512
32+
from Cryptodome.PublicKey import RSA
33+
from Cryptodome.Signature import PKCS1_v1_5
2734
from cryptography.exceptions import InvalidSignature
2835
from cryptography.hazmat.backends import default_backend
2936
from cryptography.hazmat.primitives import hashes, serialization
@@ -41,6 +48,20 @@
4148
from snowflake.connector.errors import RevocationCheckError
4249
from snowflake.connector.ocsp_snowflake import SnowflakeOCSP, generate_cache_key
4350

51+
with warnings.catch_warnings():
52+
warnings.simplefilter("ignore")
53+
# force versioned dylibs onto oscrypto ssl on catalina
54+
if sys.platform == "darwin" and platform.mac_ver()[0].startswith("10.15"):
55+
from oscrypto import _module_values, use_openssl
56+
57+
if _module_values["backend"] is None:
58+
use_openssl(
59+
libcrypto_path="/usr/lib/libcrypto.35.dylib",
60+
libssl_path="/usr/lib/libssl.35.dylib",
61+
)
62+
from oscrypto import asymmetric
63+
64+
4465
logger = getLogger(__name__)
4566

4667

@@ -49,6 +70,12 @@ class SnowflakeOCSPAsn1Crypto(SnowflakeOCSP):
4970

5071
# map signature algorithm name to digest class
5172
SIGNATURE_ALGORITHM_TO_DIGEST_CLASS = {
73+
"sha256": SHA256,
74+
"sha384": SHA384,
75+
"sha512": SHA512,
76+
}
77+
78+
SIGNATURE_ALGORITHM_TO_DIGEST_CLASS_OPENSSL = {
5279
"sha256": hashes.SHA256,
5380
"sha384": hashes.SHA3_384,
5481
"sha512": hashes.SHA3_512,
@@ -351,29 +378,51 @@ def process_ocsp_response(self, issuer, cert_id, ocsp_response):
351378
raise RevocationCheckError(msg=debug_msg, errno=op_er.errno)
352379

353380
def verify_signature(self, signature_algorithm, signature, cert, data):
354-
backend = default_backend()
355-
public_key = serialization.load_der_public_key(
356-
cert.public_key.dump(), backend=default_backend()
357-
)
358-
if (
359-
signature_algorithm
360-
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
361-
):
362-
chosen_hash = SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS[
381+
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
382+
if not use_openssl_only:
383+
pubkey = asymmetric.load_public_key(cert.public_key).unwrap().dump()
384+
rsakey = RSA.importKey(pubkey)
385+
signer = PKCS1_v1_5.new(rsakey)
386+
if (
363387
signature_algorithm
364-
]()
388+
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
389+
):
390+
digest = SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS[
391+
signature_algorithm
392+
].new()
393+
else:
394+
# the last resort. should not happen.
395+
digest = SHA1.new()
396+
digest.update(data.dump())
397+
if not signer.verify(digest, signature):
398+
raise RevocationCheckError(msg="Failed to verify the signature")
399+
365400
else:
366-
# the last resort. should not happen.
367-
chosen_hash = hashes.SHA1()
368-
hasher = hashes.Hash(chosen_hash, backend)
369-
hasher.update(data.dump())
370-
digest = hasher.finalize()
371-
try:
372-
public_key.verify(
373-
signature, digest, padding.PKCS1v15(), utils.Prehashed(chosen_hash)
401+
backend = default_backend()
402+
public_key = serialization.load_der_public_key(
403+
cert.public_key.dump(), backend=default_backend()
374404
)
375-
except InvalidSignature:
376-
raise RevocationCheckError(msg="Failed to verify the signature")
405+
if (
406+
signature_algorithm
407+
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
408+
):
409+
chosen_hash = (
410+
SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS_OPENSSL[
411+
signature_algorithm
412+
]()
413+
)
414+
else:
415+
# the last resort. should not happen.
416+
chosen_hash = hashes.SHA1()
417+
hasher = hashes.Hash(chosen_hash, backend)
418+
hasher.update(data.dump())
419+
digest = hasher.finalize()
420+
try:
421+
public_key.verify(
422+
signature, digest, padding.PKCS1v15(), utils.Prehashed(chosen_hash)
423+
)
424+
except InvalidSignature:
425+
raise RevocationCheckError(msg="Failed to verify the signature")
377426

378427
def extract_certificate_chain(
379428
self, connection: Connection

0 commit comments

Comments
 (0)