Skip to content

Commit

Permalink
Revert "SNOW-843716: cryptography dep cleanup (#1773)" (#1778)
Browse files Browse the repository at this point in the history
This reverts commit 6398696.
  • Loading branch information
sfc-gh-aling committed Oct 17, 2023
1 parent d85e56a commit 5b61af7
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 50 deletions.
1 change: 0 additions & 1 deletion DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
- Added for non-Windows platforms command suggestions (chown/chmod) for insufficient file permissions of config files.
- Fixed issue with connection diagnostics failing to complete certificate checks.
- Fixed issue that arrow iterator causes `ImportError` when the c extensions are not compiled.
- 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.

- v3.3.0(October 10,2023)

Expand Down
1 change: 1 addition & 0 deletions ci/test_fips_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ user_id=$(id -u $USER)
docker run --network=host \
-e LANG=en_US.UTF-8 \
-e TERM=vt102 \
-e SF_USE_OPENSSL_ONLY=True \
-e PIP_DISABLE_PIP_VERSION_CHECK=1 \
-e LOCAL_USER_ID=$user_id \
-e CRYPTOGRAPHY_ALLOW_OPENSSL_102=1 \
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ install_requires =
asn1crypto>0.24.0,<2.0.0
cffi>=1.9,<2.0.0
cryptography>=3.1.0,<42.0.0
oscrypto<2.0.0
pyOpenSSL>=16.2.0,<24.0.0
pycryptodomex!=3.5.0,>=3.2,<4.0.0
pyjwt<3.0.0
pytz
requests<3.0.0
Expand Down
21 changes: 17 additions & 4 deletions src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ def DefaultConverterClass() -> type:
"client_store_temporary_credential": (False, bool),
"client_request_mfa_token": (False, bool),
"use_openssl_only": (
True,
False,
bool,
), # ignored - python only crypto modules are no longer used
), # only use openssl instead of python only crypto modules
# whether to convert Arrow number values to decimal instead of doubles
"arrow_number_to_decimal": (False, bool),
"enable_stage_s3_privatelink_for_us_east_1": (
Expand Down Expand Up @@ -287,6 +287,7 @@ class SnowflakeConnection:
validate_default_parameters: Validate database, schema, role and warehouse used on Snowflake.
is_pyformat: Whether the current argument binding is pyformat or format.
consent_cache_id_token: Consented cache ID token.
use_openssl_only: Use OpenSSL instead of pure Python libraries for signature verification and encryption.
enable_stage_s3_privatelink_for_us_east_1: when true, clients use regional s3 url to upload files.
enable_connection_diag: when true, clients will generate a connectivity diagnostic report.
connection_diag_log_path: path to location to create diag report with enable_connection_diag.
Expand Down Expand Up @@ -573,8 +574,7 @@ def disable_request_pooling(self, value) -> None:

@property
def use_openssl_only(self) -> bool:
# Deprecated, kept for backwards compatibility
return True
return self._use_openssl_only

@property
def arrow_number_to_decimal(self):
Expand Down Expand Up @@ -1117,6 +1117,19 @@ def __config(self, **kwargs):
"CHECKED."
)

if "SF_USE_OPENSSL_ONLY" not in os.environ:
logger.info("Setting use_openssl_only mode to %s", self.use_openssl_only)
os.environ["SF_USE_OPENSSL_ONLY"] = str(self.use_openssl_only)
elif (
os.environ.get("SF_USE_OPENSSL_ONLY", "False") == "True"
) != self.use_openssl_only:
logger.warning(
"Mode use_openssl_only is already set to: %s, ignoring set request to: %s",
os.environ["SF_USE_OPENSSL_ONLY"],
self.use_openssl_only,
)
self._use_openssl_only = os.environ["SF_USE_OPENSSL_ONLY"] == "True"

def cmd_query(
self,
sql: str,
Expand Down
70 changes: 51 additions & 19 deletions src/snowflake/connector/encryption_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from logging import getLogger
from typing import IO, TYPE_CHECKING

from Cryptodome.Cipher import AES
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

Expand Down Expand Up @@ -68,6 +69,7 @@ def encrypt_stream(
The encryption metadata.
"""
logger = getLogger(__name__)
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
decoded_key = base64.standard_b64decode(
encryption_material.query_stage_master_key
)
Expand All @@ -77,9 +79,14 @@ def encrypt_stream(
# Generate key for data encryption
iv_data = SnowflakeEncryptionUtil.get_secure_random(block_size)
file_key = SnowflakeEncryptionUtil.get_secure_random(key_size)
backend = default_backend()
cipher = Cipher(algorithms.AES(file_key), modes.CBC(iv_data), backend=backend)
encryptor = cipher.encryptor()
if not use_openssl_only:
data_cipher = AES.new(key=file_key, mode=AES.MODE_CBC, IV=iv_data)
else:
backend = default_backend()
cipher = Cipher(
algorithms.AES(file_key), modes.CBC(iv_data), backend=backend
)
encryptor = cipher.encryptor()

padded = False
while True:
Expand All @@ -89,17 +96,30 @@ def encrypt_stream(
elif len(chunk) % block_size != 0:
chunk = PKCS5_PAD(chunk, block_size)
padded = True
out.write(encryptor.update(chunk))
if not use_openssl_only:
out.write(data_cipher.encrypt(chunk))
else:
out.write(encryptor.update(chunk))
if not padded:
out.write(encryptor.update(block_size * chr(block_size).encode(UTF8)))
out.write(encryptor.finalize())
if not use_openssl_only:
out.write(
data_cipher.encrypt(block_size * chr(block_size).encode(UTF8))
)
else:
out.write(encryptor.update(block_size * chr(block_size).encode(UTF8)))
if use_openssl_only:
out.write(encryptor.finalize())

# encrypt key with QRMK
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
encryptor = cipher.encryptor()
enc_kek = (
encryptor.update(PKCS5_PAD(file_key, block_size)) + encryptor.finalize()
)
if not use_openssl_only:
key_cipher = AES.new(key=decoded_key, mode=AES.MODE_ECB)
enc_kek = key_cipher.encrypt(PKCS5_PAD(file_key, block_size))
else:
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
encryptor = cipher.encryptor()
enc_kek = (
encryptor.update(PKCS5_PAD(file_key, block_size)) + encryptor.finalize()
)

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

use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
key_base64 = metadata.key
iv_base64 = metadata.iv
decoded_key = base64.standard_b64decode(
Expand All @@ -166,26 +187,37 @@ def decrypt_stream(
key_bytes = base64.standard_b64decode(key_base64)
iv_bytes = base64.standard_b64decode(iv_base64)

backend = default_backend()
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
decryptor = cipher.decryptor()
file_key = PKCS5_UNPAD(decryptor.update(key_bytes) + decryptor.finalize())
cipher = Cipher(algorithms.AES(file_key), modes.CBC(iv_bytes), backend=backend)
decryptor = cipher.decryptor()
if not use_openssl_only:
key_cipher = AES.new(key=decoded_key, mode=AES.MODE_ECB)
file_key = PKCS5_UNPAD(key_cipher.decrypt(key_bytes))
data_cipher = AES.new(key=file_key, mode=AES.MODE_CBC, IV=iv_bytes)
else:
backend = default_backend()
cipher = Cipher(algorithms.AES(decoded_key), modes.ECB(), backend=backend)
decryptor = cipher.decryptor()
file_key = PKCS5_UNPAD(decryptor.update(key_bytes) + decryptor.finalize())
cipher = Cipher(
algorithms.AES(file_key), modes.CBC(iv_bytes), backend=backend
)
decryptor = cipher.decryptor()

last_decrypted_chunk = None
chunk = src.read(chunk_size)
while len(chunk) != 0:
if last_decrypted_chunk is not None:
out.write(last_decrypted_chunk)
d = decryptor.update(chunk)
if not use_openssl_only:
d = data_cipher.decrypt(chunk)
else:
d = decryptor.update(chunk)
last_decrypted_chunk = d
chunk = src.read(chunk_size)

if last_decrypted_chunk is not None:
offset = PKCS5_OFFSET(last_decrypted_chunk)
out.write(last_decrypted_chunk[:-offset])
out.write(decryptor.finalize())
if use_openssl_only:
out.write(decryptor.finalize())

@staticmethod
def decrypt_file(
Expand Down
23 changes: 17 additions & 6 deletions src/snowflake/connector/file_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from logging import getLogger
from typing import IO

from Cryptodome.Hash import SHA256
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

Expand All @@ -32,17 +33,27 @@ def get_digest_and_size(src: IO[bytes]) -> tuple[str, int]:
Returns:
Tuple of src's digest and src's size in bytes.
"""
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
CHUNK_SIZE = 64 * kilobyte
backend = default_backend()
chosen_hash = hashes.SHA256()
hasher = hashes.Hash(chosen_hash, backend)
if not use_openssl_only:
m = SHA256.new()
else:
backend = default_backend()
chosen_hash = hashes.SHA256()
hasher = hashes.Hash(chosen_hash, backend)
while True:
chunk = src.read(CHUNK_SIZE)
if chunk == b"":
break
hasher.update(chunk)

digest = base64.standard_b64encode(hasher.finalize()).decode(UTF8)
if not use_openssl_only:
m.update(chunk)
else:
hasher.update(chunk)

if not use_openssl_only:
digest = base64.standard_b64encode(m.digest()).decode(UTF8)
else:
digest = base64.standard_b64encode(hasher.finalize()).decode(UTF8)

size = src.tell()
src.seek(0)
Expand Down
89 changes: 69 additions & 20 deletions src/snowflake/connector/ocsp_asn1crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

from __future__ import annotations

import os
import platform
import sys
import warnings
from base64 import b64decode, b64encode
from collections import OrderedDict
from datetime import datetime, timezone
Expand All @@ -24,6 +28,9 @@
Version,
)
from asn1crypto.x509 import Certificate
from Cryptodome.Hash import SHA1, SHA256, SHA384, SHA512
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import PKCS1_v1_5
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
Expand All @@ -41,6 +48,20 @@
from snowflake.connector.errors import RevocationCheckError
from snowflake.connector.ocsp_snowflake import SnowflakeOCSP, generate_cache_key

with warnings.catch_warnings():
warnings.simplefilter("ignore")
# force versioned dylibs onto oscrypto ssl on catalina
if sys.platform == "darwin" and platform.mac_ver()[0].startswith("10.15"):
from oscrypto import _module_values, use_openssl

if _module_values["backend"] is None:
use_openssl(
libcrypto_path="/usr/lib/libcrypto.35.dylib",
libssl_path="/usr/lib/libssl.35.dylib",
)
from oscrypto import asymmetric


logger = getLogger(__name__)


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

# map signature algorithm name to digest class
SIGNATURE_ALGORITHM_TO_DIGEST_CLASS = {
"sha256": SHA256,
"sha384": SHA384,
"sha512": SHA512,
}

SIGNATURE_ALGORITHM_TO_DIGEST_CLASS_OPENSSL = {
"sha256": hashes.SHA256,
"sha384": hashes.SHA3_384,
"sha512": hashes.SHA3_512,
Expand Down Expand Up @@ -351,29 +378,51 @@ def process_ocsp_response(self, issuer, cert_id, ocsp_response):
raise RevocationCheckError(msg=debug_msg, errno=op_er.errno)

def verify_signature(self, signature_algorithm, signature, cert, data):
backend = default_backend()
public_key = serialization.load_der_public_key(
cert.public_key.dump(), backend=default_backend()
)
if (
signature_algorithm
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
):
chosen_hash = SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS[
use_openssl_only = os.getenv("SF_USE_OPENSSL_ONLY", "False") == "True"
if not use_openssl_only:
pubkey = asymmetric.load_public_key(cert.public_key).unwrap().dump()
rsakey = RSA.importKey(pubkey)
signer = PKCS1_v1_5.new(rsakey)
if (
signature_algorithm
]()
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
):
digest = SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS[
signature_algorithm
].new()
else:
# the last resort. should not happen.
digest = SHA1.new()
digest.update(data.dump())
if not signer.verify(digest, signature):
raise RevocationCheckError(msg="Failed to verify the signature")

else:
# the last resort. should not happen.
chosen_hash = hashes.SHA1()
hasher = hashes.Hash(chosen_hash, backend)
hasher.update(data.dump())
digest = hasher.finalize()
try:
public_key.verify(
signature, digest, padding.PKCS1v15(), utils.Prehashed(chosen_hash)
backend = default_backend()
public_key = serialization.load_der_public_key(
cert.public_key.dump(), backend=default_backend()
)
except InvalidSignature:
raise RevocationCheckError(msg="Failed to verify the signature")
if (
signature_algorithm
in SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS
):
chosen_hash = (
SnowflakeOCSPAsn1Crypto.SIGNATURE_ALGORITHM_TO_DIGEST_CLASS_OPENSSL[
signature_algorithm
]()
)
else:
# the last resort. should not happen.
chosen_hash = hashes.SHA1()
hasher = hashes.Hash(chosen_hash, backend)
hasher.update(data.dump())
digest = hasher.finalize()
try:
public_key.verify(
signature, digest, padding.PKCS1v15(), utils.Prehashed(chosen_hash)
)
except InvalidSignature:
raise RevocationCheckError(msg="Failed to verify the signature")

def extract_certificate_chain(
self, connection: Connection
Expand Down
Loading

0 comments on commit 5b61af7

Please sign in to comment.