Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "SNOW-843716: cryptography dep cleanup (#1773)" #1778

Merged
merged 1 commit into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading