@@ -149,10 +149,18 @@ def make_tls_http_server(bind_addr, ssl_adapter, request):
149149 return httpserver
150150
151151
152+ def get_key_password ():
153+ """Return a predefined password string.
154+
155+ It is to be used for decrypting private keys.
156+ """
157+ return 'криївка'
158+
159+
152160@pytest .fixture (scope = 'session' )
153161def private_key_password ():
154162 """Provide hardcoded password for private key."""
155- return 'криївка'
163+ return get_key_password ()
156164
157165
158166@pytest .fixture
@@ -900,9 +908,17 @@ def test_http_over_https_ssl_handshake(
900908 ids = ('encrypted-key' , 'unencrypted-key' ),
901909)
902910@pytest .mark .parametrize (
903- 'password_as_bytes' ,
904- (True , False ),
905- ids = ('with-bytes-password' , 'with-str-password' ),
911+ 'transform_password_arg' ,
912+ (
913+ lambda pass_factory : pass_factory ().encode ('utf-8' ),
914+ lambda pass_factory : pass_factory (),
915+ lambda pass_factory : pass_factory ,
916+ ),
917+ ids = (
918+ 'with-bytes-password' ,
919+ 'with-str-password' ,
920+ 'with-callable-password-provider' ,
921+ ),
906922)
907923# pylint: disable-next=too-many-positional-arguments
908924def test_ssl_adapters_with_private_key_password (
@@ -915,25 +931,21 @@ def test_ssl_adapters_with_private_key_password(
915931 tls_certificate_private_key_pem_path ,
916932 adapter_type ,
917933 encrypted_key ,
918- password_as_bytes ,
934+ transform_password_arg ,
919935):
920936 """Check server decrypts private TLS keys with password as bytes or str."""
921937 key_file = (
922938 tls_certificate_passwd_private_key_pem_path
923939 if encrypted_key
924940 else tls_certificate_private_key_pem_path
925941 )
926- key_pass = (
927- private_key_password .encode ('utf-8' )
928- if password_as_bytes
929- else private_key_password
930- )
942+ private_key_password = transform_password_arg (get_key_password )
931943
932944 tls_adapter_cls = get_ssl_adapter_class (name = adapter_type )
933945 tls_adapter = tls_adapter_cls (
934946 certificate = tls_certificate_chain_pem_path ,
935947 private_key = key_file ,
936- private_key_password = key_pass ,
948+ private_key_password = private_key_password ,
937949 )
938950
939951 interface , _host , port = _get_conn_data (
@@ -1015,6 +1027,76 @@ def test_openssl_adapter_with_false_key_password(
10151027 )
10161028
10171029
1030+ @pytest .mark .parametrize (
1031+ 'adapter_type' ,
1032+ ('pyopenssl' , 'builtin' ),
1033+ )
1034+ def test_ssl_adapter_with_none_key_password (
1035+ tls_certificate_chain_pem_path ,
1036+ tls_certificate_passwd_private_key_pem_path ,
1037+ private_key_password ,
1038+ adapter_type ,
1039+ mocker ,
1040+ ):
1041+ """Check that TLS-adapters prompt for password when set as ``None``."""
1042+ tls_adapter_cls = get_ssl_adapter_class (name = adapter_type )
1043+ mocker .patch (
1044+ 'cheroot.ssl._ask_for_password_interactively' ,
1045+ return_value = private_key_password ,
1046+ )
1047+ tls_adapter = tls_adapter_cls (
1048+ certificate = tls_certificate_chain_pem_path ,
1049+ private_key = tls_certificate_passwd_private_key_pem_path ,
1050+ )
1051+
1052+ assert tls_adapter .context is not None
1053+
1054+
1055+ class PasswordCallbackHelper :
1056+ """Collects helper methods for mocking password callback."""
1057+
1058+ def __init__ (self , adapter : Adapter ):
1059+ """Initialize helper variables."""
1060+ self .counter = 0
1061+ self .callback = adapter ._password_callback
1062+
1063+ def get_password (self ):
1064+ """Provide correct password on first call, wrong on other calls."""
1065+ self .counter += 1
1066+ return get_key_password () * self .counter
1067+
1068+ def verify_twice_callback (self , max_length , _verify_twice , userdata ):
1069+ """Establish a mock callback for testing two-factor password prompt."""
1070+ return self .callback (self , max_length , True , userdata )
1071+
1072+
1073+ @pytest .mark .parametrize ('adapter_type' , ('pyopenssl' ,))
1074+ def test_openssl_adapter_verify_twice_callback (
1075+ tls_certificate_chain_pem_path ,
1076+ tls_certificate_passwd_private_key_pem_path ,
1077+ adapter_type ,
1078+ mocker ,
1079+ ):
1080+ """Check that two-time password verification fails with correct error."""
1081+ tls_adapter_cls = get_ssl_adapter_class (name = adapter_type )
1082+ helper = PasswordCallbackHelper (tls_adapter_cls )
1083+
1084+ mocker .patch (
1085+ 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter._password_callback' ,
1086+ side_effect = helper .verify_twice_callback ,
1087+ )
1088+
1089+ with pytest .raises (
1090+ ValueError ,
1091+ match = 'Verification failed: entered passwords do not match' ,
1092+ ):
1093+ tls_adapter_cls (
1094+ certificate = tls_certificate_chain_pem_path ,
1095+ private_key = tls_certificate_passwd_private_key_pem_path ,
1096+ private_key_password = helper .get_password ,
1097+ )
1098+
1099+
10181100@pytest .fixture
10191101def dummy_adapter (monkeypatch ):
10201102 """Provide a dummy SSL adapter instance."""
0 commit comments