import ssl import pytest import pytest_httpbin.certs import requests.exceptions import urllib3 from unittest import mock from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS from httpie.status import ExitStatus from .utils import HTTP_OK, TESTS_ROOT, IS_PYOPENSSL, http try: # Handle OpenSSL errors, if installed. # See # noinspection PyUnresolvedReferences import OpenSSL.SSL ssl_errors = ( requests.exceptions.SSLError, OpenSSL.SSL.Error, ValueError, # TODO: Remove with OSS-65 ) except ImportError: ssl_errors = ( requests.exceptions.SSLError, ) CERTS_ROOT = TESTS_ROOT / 'client_certs' CLIENT_CERT = str(CERTS_ROOT / 'client.crt') CLIENT_KEY = str(CERTS_ROOT / 'client.key') CLIENT_PEM = str(CERTS_ROOT / 'client.pem') # In case of a regeneration, use the following commands # in the PWD_TESTS_ROOT: # $ openssl genrsa -aes128 -passout pass:password 3072 > client.pem # $ openssl req -new -x509 -nodes -days 10000 -key client.pem > client.pem PWD_TESTS_ROOT = CERTS_ROOT / 'password_protected' PWD_CLIENT_PEM = str(PWD_TESTS_ROOT / 'client.pem') PWD_CLIENT_KEY = str(PWD_TESTS_ROOT / 'client.key') PWD_CLIENT_PASS = 'password' PWD_CLIENT_INVALID_PASS = PWD_CLIENT_PASS + 'invalid' # We test against a local httpbin instance which uses a self-signed cert. # Requests without --verify= will fail with a verification error. # See: https://github.com/kevin1024/pytest-httpbin#https-support CA_BUNDLE = pytest_httpbin.certs.where() @pytest.mark.parametrize('ssl_version', AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()) def test_ssl_version(httpbin_secure, ssl_version): try: r = http( '--ssl', ssl_version, httpbin_secure + '/get' ) assert HTTP_OK in r except ssl_errors as e: if ssl_version == 'ssl3': # pytest-httpbin doesn't support ssl3 pass elif e.__context__ is not None: # Check if root cause was an unsupported TLS version root = e.__context__ while root.__context__ is not None: root = root.__context__ if isinstance(root, ssl.SSLError) and root.reason == "TLSV1_ALERT_PROTOCOL_VERSION": pytest.skip(f'Unsupported TLS version: {ssl_version}') elif 'No such protocol' in str(e): # TODO: Remove with OSS-65 pytest.skip(f'Unsupported TLS version: {ssl_version}') else: raise class TestClientCert: def test_cert_and_key(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', CLIENT_CERT, '--cert-key', CLIENT_KEY) assert HTTP_OK in r def test_cert_pem(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', CLIENT_PEM) assert HTTP_OK in r def test_cert_file_not_found(self, httpbin_secure): r = http(httpbin_secure + '/get', '--cert', '/__not_found__', tolerate_error_exit_status=True) assert r.exit_status == ExitStatus.ERROR assert '/__not_found__: No such file or directory' in r.stderr def test_cert_file_invalid(self, httpbin_secure): with pytest.raises(ssl_errors): http(httpbin_secure + '/get', '--cert', __file__) def test_cert_ok_but_missing_key(self, httpbin_secure): with pytest.raises(ssl_errors): http(httpbin_secure + '/get', '--cert', CLIENT_CERT) class TestServerCert: def test_verify_no_OK(self, httpbin_secure): # Avoid warnings when explicitly testing insecure requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) r = http(httpbin_secure.url + '/get', '--verify=no') assert HTTP_OK in r @pytest.mark.parametrize('verify_value', ['false', 'fALse']) def test_verify_false_OK(self, httpbin_secure, verify_value): urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) r = http(httpbin_secure.url + '/get', '--verify', verify_value) assert HTTP_OK in r def test_verify_custom_ca_bundle_path( self, httpbin_secure_untrusted ): r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE) assert HTTP_OK in r def test_self_signed_server_cert_by_default_raises_ssl_error( self, httpbin_secure_untrusted ): with pytest.raises(ssl_errors): http(httpbin_secure_untrusted.url + '/get') def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure): # since 2.14.0 requests raises IOError (an OSError subclass) with pytest.raises(ssl_errors + (OSError,)): http(httpbin_secure.url + '/get', '--verify', '/__not_found__') def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure): with pytest.raises(ssl_errors): http(httpbin_secure.url + '/get', '--verify', __file__) def test_ciphers(httpbin_secure): r = http( httpbin_secure.url + '/get', '--ciphers', DEFAULT_SSL_CIPHERS, ) assert HTTP_OK in r @pytest.mark.skipif(IS_PYOPENSSL, reason='pyOpenSSL uses a different message format.') def test_ciphers_none_can_be_selected(httpbin_secure): r = http( httpbin_secure.url + '/get', '--ciphers', '__FOO__', tolerate_error_exit_status=True, ) assert r.exit_status == ExitStatus.ERROR # Linux/macOS: # http: error: SSLError: ('No cipher can be selected.',) # OpenBSD: # # http: error: Error: [('SSL routines', '(UNKNOWN)SSL_internal', 'no cipher match')] assert 'cipher' in r.stderr def test_pyopenssl_presence(): if not IS_PYOPENSSL: assert not urllib3.util.ssl_.IS_PYOPENSSL assert not urllib3.util.IS_PYOPENSSL else: assert urllib3.util.ssl_.IS_PYOPENSSL assert urllib3.util.IS_PYOPENSSL @mock.patch('httpie.cli.argtypes.SSLCredentials._prompt_password', new=lambda self, prompt: PWD_CLIENT_PASS) def test_password_protected_cert_prompt(httpbin_secure): r = http(httpbin_secure + '/get', '--cert', PWD_CLIENT_PEM, '--cert-key', PWD_CLIENT_KEY) assert HTTP_OK in r @mock.patch('httpie.cli.argtypes.SSLCredentials._prompt_password', new=lambda self, prompt: PWD_CLIENT_INVALID_PASS) def test_password_protected_cert_prompt_invalid(httpbin_secure): with pytest.raises(ssl_errors): http(httpbin_secure + '/get', '--cert', PWD_CLIENT_PEM, '--cert-key', PWD_CLIENT_KEY) def test_password_protected_cert_cli_arg(httpbin_secure): r = http(httpbin_secure + '/get', '--cert', PWD_CLIENT_PEM, '--cert-key', PWD_CLIENT_KEY, '--cert-key-pass', PWD_CLIENT_PASS) assert HTTP_OK in r def test_password_protected_cert_cli_arg_invalid(httpbin_secure): with pytest.raises(ssl_errors): http(httpbin_secure + '/get', '--cert', PWD_CLIENT_PEM, '--cert-key', PWD_CLIENT_KEY, '--cert-key-pass', PWD_CLIENT_INVALID_PASS)