Skip to content

Commit ae214a5

Browse files
Jw/snow 3349955 handle empty passphrases (#2893)
* feat: [SNOW-3063581] handle empty passphrases * feat: [SNOW-3063581] handle empty passphrases * feat: [SNOW-3349955] add tests and release notes entry # Conflicts: # RELEASE-NOTES.md * fix: [SNOW-3349955] add tests and release notes entry
1 parent bb1f71f commit ae214a5

3 files changed

Lines changed: 98 additions & 18 deletions

File tree

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
## Fixes and improvements
3030
* Fixed `snow streamlit deploy` failing with a collision error when `pages/*.py` glob in `additional_source_files` overlaps with the automatically-included `pages/` directory. Overlapping glob patterns are now deduplicated during v1-to-v2 definition conversion.
3131
* Updated `snowflake-connector-python` to version 4.4.0. Connector python 4.x series introduced stricter permission checks. In future versions of Snowflake CLI strict configuration file permissions will become mandatory. To test if your files have correct permissions set SNOWFLAKE_CLI_FEATURES_ENFORCE_STRICT_CONFIG_PERMISSIONS=1 before running CLI commands.
32+
* Fixed error message when `PRIVATE_KEY_PASSPHRASE` environment variable is set to an empty string.
3233

3334
# v3.16.0
3435

src/snowflake/cli/_app/snow_connector.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -351,31 +351,35 @@ def _load_pem_from_parameters(private_key_raw: str) -> SecretType:
351351
return SecretType(private_key_raw.encode("utf-8"))
352352

353353

354+
def _validate_passphrase(passphrase: SecretType) -> None:
355+
if passphrase.value is None:
356+
raise CliError(
357+
"Encrypted private key, you must provide the "
358+
"passphrase in the environment variable PRIVATE_KEY_PASSPHRASE."
359+
)
360+
if passphrase.value == "":
361+
raise CliError(
362+
"PRIVATE_KEY_PASSPHRASE environment variable is set but empty. "
363+
"Provide a non-empty passphrase or use an unencrypted private key."
364+
)
365+
366+
354367
def _load_pem_to_der(private_key_pem: SecretType) -> SecretType:
355368
"""
356369
Given a private key file path (in PEM format), decode key data into DER
357370
format
358371
"""
359372
private_key_passphrase = SecretType(os.getenv("PRIVATE_KEY_PASSPHRASE", None))
360-
if (
361-
private_key_pem.value.startswith(ENCRYPTED_PKCS8_PK_HEADER)
362-
and private_key_passphrase.value is None
363-
):
364-
raise ClickException(
365-
"Encrypted private key, you must provide the "
366-
"passphrase in the environment variable PRIVATE_KEY_PASSPHRASE"
367-
)
368373

369-
if not private_key_pem.value.startswith(
370-
ENCRYPTED_PKCS8_PK_HEADER
371-
) and not private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
372-
raise ClickException(
374+
if private_key_pem.value.startswith(ENCRYPTED_PKCS8_PK_HEADER):
375+
_validate_passphrase(private_key_passphrase)
376+
elif private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
377+
private_key_passphrase = SecretType(None)
378+
else:
379+
raise CliError(
373380
"Private key provided is not in PKCS#8 format. Please use correct format."
374381
)
375382

376-
if private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
377-
private_key_passphrase = SecretType(None)
378-
379383
return prepare_private_key(private_key_pem, private_key_passphrase)
380384

381385

tests/test_connection.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
import pytest
2323
import tomlkit
24+
from cryptography.hazmat.backends import default_backend
25+
from cryptography.hazmat.primitives import serialization
26+
from cryptography.hazmat.primitives.asymmetric import rsa
2427
from snowflake.cli._plugins.connection import commands as connection_commands
2528
from snowflake.cli.api.config import ConnectionConfig
2629
from snowflake.cli.api.config_ng.masking import MASKED_VALUE
@@ -681,9 +684,6 @@ def test_temporary_connection(mock_connector, mock_ctx, option, runner):
681684
def test_key_pair_authentication(
682685
mock_connector, mock_ctx, runner, private_key_flag_name
683686
):
684-
from cryptography.hazmat.backends import default_backend
685-
from cryptography.hazmat.primitives import serialization
686-
from cryptography.hazmat.primitives.asymmetric import rsa
687687

688688
ctx = mock_ctx()
689689
mock_connector.return_value = ctx
@@ -877,6 +877,81 @@ def test_key_pair_authentication_from_config(
877877
)
878878

879879

880+
@mock.patch.dict(os.environ, {}, clear=True)
881+
def test_key_pair_authentication_no_passphrase_error(
882+
mock_ctx, temporary_directory, runner
883+
):
884+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
885+
encrypted_pem = private_key.private_bytes(
886+
encoding=serialization.Encoding.PEM,
887+
format=serialization.PrivateFormat.PKCS8,
888+
encryption_algorithm=serialization.BestAvailableEncryption(b"notempty"),
889+
)
890+
891+
with NamedTemporaryFile("wb", suffix=".p8") as tmp_file:
892+
tmp_file.write(encrypted_pem)
893+
tmp_file.flush()
894+
895+
result = runner.invoke(
896+
[
897+
"object",
898+
"list",
899+
"warehouse",
900+
"--temporary-connection",
901+
"--account",
902+
"test_account",
903+
"--user",
904+
"snowcli_test",
905+
"--authenticator",
906+
"SNOWFLAKE_JWT",
907+
"--private-key-file",
908+
tmp_file.name,
909+
]
910+
)
911+
912+
assert result.exit_code == 1
913+
assert "Encrypted private key, you must provide the passphrase" in result.output
914+
assert "PRIVATE_KEY_PASSPHRASE" in result.output
915+
916+
917+
@mock.patch.dict(os.environ, {"PRIVATE_KEY_PASSPHRASE": ""}, clear=True)
918+
def test_key_pair_authentication_empty_passphrase_error(
919+
mock_ctx, temporary_directory, runner
920+
):
921+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
922+
encrypted_pem = private_key.private_bytes(
923+
encoding=serialization.Encoding.PEM,
924+
format=serialization.PrivateFormat.PKCS8,
925+
encryption_algorithm=serialization.BestAvailableEncryption(b"notempty"),
926+
)
927+
928+
with NamedTemporaryFile("wb", suffix=".p8") as tmp_file:
929+
tmp_file.write(encrypted_pem)
930+
tmp_file.flush()
931+
932+
result = runner.invoke(
933+
[
934+
"object",
935+
"list",
936+
"warehouse",
937+
"--temporary-connection",
938+
"--account",
939+
"test_account",
940+
"--user",
941+
"snowcli_test",
942+
"--authenticator",
943+
"SNOWFLAKE_JWT",
944+
"--private-key-file",
945+
tmp_file.name,
946+
]
947+
)
948+
949+
assert result.exit_code == 1
950+
assert (
951+
"PRIVATE_KEY_PASSPHRASE environment variable is set but empty" in result.output
952+
)
953+
954+
880955
@pytest.mark.parametrize(
881956
"command",
882957
[

0 commit comments

Comments
 (0)