Skip to content

Commit fef4999

Browse files
Snow-2923722: add private key passphrase kwargs to connection init (#2736)
Co-authored-by: yassun7010 <yassun7010@outlook.com>
1 parent 7792c57 commit fef4999

6 files changed

Lines changed: 99 additions & 6 deletions

File tree

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1616
- Added `SnowflakeCursor.stats` property to expose granular DML statistics (rows inserted, deleted, updated, and duplicates) for operations like CTAS where `rowcount` is insufficient.
1717
- Added support for injecting SPCS service identifier token (`SPCS_TOKEN`) into login requests when present in SPCS containers.
1818
- Introduced shared library([source code](https://github.com/snowflakedb/universal-driver/tree/main/sf_mini_core)) for extended telemetry to identify and prepare testing platform for native rust extensions.
19+
- Added `private_key_passphrase` connection parameter for key pair authentication with encrypted private keys.
1920

2021
- v4.1.1(December 12,2025)
2122
- Relaxed pandas dependency requirements for Python below 3.12.

src/snowflake/connector/aio/_connection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ async def __open_connection(self):
343343

344344
elif self._authenticator == KEY_PAIR_AUTHENTICATOR:
345345
private_key = self._private_key
346+
private_key_passphrase = self._private_key_passphrase
346347

347348
if self._private_key_file:
348349
private_key = _get_private_bytes_from_file(
@@ -352,6 +353,7 @@ async def __open_connection(self):
352353

353354
self.auth_class = AuthByKeyPair(
354355
private_key=private_key,
356+
private_key_passphrase=private_key_passphrase,
355357
timeout=self.login_timeout,
356358
backoff_generator=self._backoff_generator,
357359
)

src/snowflake/connector/auth/_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,8 @@ def get_token_from_private_key(
633633
from . import AuthByKeyPair
634634

635635
auth_instance = AuthByKeyPair(
636-
private_key,
637-
DAY_IN_SECONDS,
636+
private_key=private_key,
637+
lifetime_in_seconds=DAY_IN_SECONDS,
638638
) # token valid for 24 hours
639639
return auth_instance.prepare(account=account, user=user)
640640

src/snowflake/connector/auth/keypair.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class AuthByKeyPair(AuthByPlugin):
4040
def __init__(
4141
self,
4242
private_key: bytes | str | RSAPrivateKey,
43+
private_key_passphrase: bytes | None = None,
4344
lifetime_in_seconds: int = LIFETIME,
4445
**kwargs,
4546
) -> None:
@@ -72,6 +73,7 @@ def __init__(
7273
)
7374

7475
self._private_key: bytes | str | RSAPrivateKey | None = private_key
76+
self._private_key_passphrase: bytes | None = private_key_passphrase
7577
self._jwt_token = ""
7678
self._jwt_token_exp = 0
7779
self._lifetime = timedelta(
@@ -116,13 +118,14 @@ def prepare(
116118
try:
117119
private_key = load_der_private_key(
118120
data=self._private_key,
119-
password=None,
121+
password=self._private_key_passphrase,
120122
backend=default_backend(),
121123
)
122124
except Exception as e:
123125
raise ProgrammingError(
124126
msg=f"Failed to load private key: {e}\nPlease provide a valid "
125-
"unencrypted rsa private key in DER format as bytes object",
127+
"rsa private key in DER format as bytes object. If the key is "
128+
"encrypted, provide the passphrase via private_key_passphrase",
126129
errno=ER_INVALID_PRIVATE_KEY,
127130
)
128131

src/snowflake/connector/connection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def _get_private_bytes_from_file(
232232
"passcode_in_password": (False, bool), # Snowflake MFA
233233
"passcode": (None, (type(None), str)), # Snowflake MFA
234234
"private_key": (None, (type(None), bytes, str, RSAPrivateKey)),
235+
"private_key_passphrase": (None, (type(None), bytes)),
235236
"private_key_file": (None, (type(None), str)),
236237
"private_key_file_pwd": (None, (type(None), str, bytes)),
237238
"token": (None, (type(None), str)), # OAuth/JWT/PAT/OIDC Token
@@ -1472,6 +1473,7 @@ def __open_connection(self):
14721473

14731474
elif self._authenticator == KEY_PAIR_AUTHENTICATOR:
14741475
private_key = self._private_key
1476+
private_key_passphrase = self._private_key_passphrase
14751477

14761478
if self._private_key_file:
14771479
private_key = _get_private_bytes_from_file(
@@ -1481,6 +1483,7 @@ def __open_connection(self):
14811483

14821484
self.auth_class = AuthByKeyPair(
14831485
private_key=private_key,
1486+
private_key_passphrase=private_key_passphrase,
14841487
timeout=self.login_timeout,
14851488
backoff_generator=self._backoff_generator,
14861489
)

test/unit/test_auth_keypair.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,86 @@ def test_auth_keypair(authenticator):
6464
assert rest.master_token == "MASTER_TOKEN"
6565

6666

67+
def test_auth_keypair_with_passphrase():
68+
"""Simple Key Pair test with passphrase."""
69+
70+
passphrase = b"test"
71+
private_key_der, public_key_der_encoded = generate_key_pair(
72+
2048,
73+
passphrase=passphrase,
74+
)
75+
application = "testapplication"
76+
account = "testaccount"
77+
user = "testuser"
78+
auth_instance = AuthByKeyPair(
79+
private_key=private_key_der,
80+
private_key_passphrase=passphrase,
81+
)
82+
auth_instance._retry_ctx.set_start_time()
83+
auth_instance.handle_timeout(
84+
authenticator="SNOWFLAKE_JWT",
85+
service_name=None,
86+
account=account,
87+
user=user,
88+
password=None,
89+
)
90+
91+
# success test case
92+
rest = _init_rest(application, _create_mock_auth_keypair_rest_response())
93+
auth = Auth(rest)
94+
auth.authenticate(auth_instance, account, user)
95+
assert not rest._connection.errorhandler.called # not error
96+
assert rest.token == "TOKEN"
97+
assert rest.master_token == "MASTER_TOKEN"
98+
99+
100+
def test_auth_keypair_encrypted_without_passphrase():
101+
"""Test that encrypted key without passphrase raises error with helpful message."""
102+
from snowflake.connector.errors import ProgrammingError
103+
104+
passphrase = b"test"
105+
private_key_der, _ = generate_key_pair(
106+
2048,
107+
passphrase=passphrase,
108+
)
109+
account = "testaccount"
110+
user = "testuser"
111+
112+
# Create auth instance without providing passphrase for encrypted key
113+
auth_instance = AuthByKeyPair(private_key=private_key_der)
114+
115+
with raises(ProgrammingError) as ex:
116+
auth_instance.prepare(account=account, user=user)
117+
118+
# Verify the error message mentions the passphrase option
119+
assert "private_key_passphrase" in str(ex.value)
120+
121+
122+
def test_auth_keypair_wrong_passphrase():
123+
"""Test that wrong passphrase raises error."""
124+
from snowflake.connector.errors import ProgrammingError
125+
126+
passphrase = b"correct_passphrase"
127+
private_key_der, _ = generate_key_pair(
128+
2048,
129+
passphrase=passphrase,
130+
)
131+
account = "testaccount"
132+
user = "testuser"
133+
134+
# Create auth instance with wrong passphrase
135+
auth_instance = AuthByKeyPair(
136+
private_key=private_key_der,
137+
private_key_passphrase=b"wrong_passphrase",
138+
)
139+
140+
with raises(ProgrammingError) as ex:
141+
auth_instance.prepare(account=account, user=user)
142+
143+
# Verify the error mentions the private key loading failure
144+
assert "Failed to load private key" in str(ex.value)
145+
146+
67147
def test_auth_prepare_body_does_not_overwrite_client_environment_fields():
68148
private_key_der, _ = generate_key_pair(2048)
69149
auth_class = AuthByKeyPair(private_key=private_key_der)
@@ -169,15 +249,19 @@ def _init_rest(application, post_requset):
169249
return rest
170250

171251

172-
def generate_key_pair(key_length):
252+
def generate_key_pair(key_length: int, *, passphrase: bytes | None = None):
173253
private_key = rsa.generate_private_key(
174254
backend=default_backend(), public_exponent=65537, key_size=key_length
175255
)
176256

177257
private_key_der = private_key.private_bytes(
178258
encoding=serialization.Encoding.DER,
179259
format=serialization.PrivateFormat.PKCS8,
180-
encryption_algorithm=serialization.NoEncryption(),
260+
encryption_algorithm=(
261+
serialization.BestAvailableEncryption(passphrase)
262+
if passphrase
263+
else serialization.NoEncryption()
264+
),
181265
)
182266

183267
public_key_pem = (

0 commit comments

Comments
 (0)