diff --git a/requests b/requests new file mode 160000 index 0000000000..420d16bc7e --- /dev/null +++ b/requests @@ -0,0 +1 @@ +Subproject commit 420d16bc7ef326f7b65f90e4644adc0f6a0e1d44 diff --git a/src/requests/adapters.py b/src/requests/adapters.py index 670c92767c..9645e8bf84 100644 --- a/src/requests/adapters.py +++ b/src/requests/adapters.py @@ -45,6 +45,10 @@ ) from .models import Response from .structures import CaseInsensitiveDict +import os +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + from .utils import ( DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths, @@ -78,7 +82,7 @@ def _urllib3_request_context( verify: "bool | str | None", client_cert: "typing.Tuple[str, str] | str | None", poolmanager: "PoolManager", -) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])": +) -> "tuple[typing.Dict[str, typing.Any], typing.Dict[str, typing.Any]]": host_params = {} pool_kwargs = {} parsed_request_url = urlparse(request.url) @@ -109,6 +113,24 @@ def _urllib3_request_context( } return host_params, pool_kwargs +def is_single_certificate(cert_path): + """ + Check if the given certificate file contains a single certificate. + """ + try: + with open(cert_path, 'r') as f: + cert_data = f.read() + # Attempt to load the certificate. If it's a single certificate, this will succeed. + x509.load_pem_x509_certificate(cert_data.encode('utf-8'), default_backend()) + return True + except Exception: + # If loading the single certificate fails, assume it's a bundle. + return False + + +class BaseAdapter: + """The Base Transport Adapter""" + class BaseAdapter: """The Base Transport Adapter""" @@ -282,6 +304,12 @@ def cert_verify(self, conn, url, verify, cert): code, and is only exposed for use when subclassing the :class:`HTTPAdapter `. + .. warning:: + Disabling certificate verification (by passing ``verify=False``) will make + HTTPS connections insecure and vulnerable to man-in-the-middle attacks. + This should only be done for testing purposes or in environments where + security is not a concern. + :param conn: The urllib3 connection object associated with the cert. :param url: The requested URL. :param verify: Either a boolean, in which case it controls whether we verify @@ -304,8 +332,10 @@ def cert_verify(self, conn, url, verify, cert): f"Could not find a suitable TLS CA certificate bundle, " f"invalid path: {cert_loc}" ) - - conn.cert_reqs = "CERT_REQUIRED" + + if is_single_certificate(cert_loc): + conn.cert_reqs = "CERT_NONE" + # --------------------------- if not os.path.isdir(cert_loc): conn.ca_certs = cert_loc diff --git a/tests/certs/server-key.pem b/tests/certs/server-key.pem new file mode 100644 index 0000000000..0d59f09ad0 --- /dev/null +++ b/tests/certs/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDiDxLyslU+LC05 +GE8u7tnrGpkVSzEL+tleHvZxcZgHyxC+EbWFmoP2ePSnjIoGahB1nx7NzraUxlIE +Fp/8CdQPzwSCpQtcVW8LPKF4RLbu7v/iEQCwL3QPYQ2cguiNfr36PSbcz+/Fyn+X +PZPplWbNNAXLTsc/tcmVN9nehjOEuQt71twQBQKEpZSkam15/wSiDkyx7l3Sj4qg +5h30OD+CyvNsf5WlJ15vJyg9cHaFfAU8caDb1PMn+U4zKvZpUnURfVT9ruppS4Qq +KZ3KFKQixLtgJ1JD1oWE1jCS+zZNss+rJhHNsCWDXbmGO6U4i1MI6tXjNHyZCQha +xXyNI2OfAgMBAAECggEACdO/7h8Eh6Rh/uZootCtVoQcfYl8Q1oPUwEjk4DtrAEO +5ERe0MUL0Y4mjJYWdfrsZkCZlTbURxlr/VOCdvFyb5F/T7KfEM+B+UoUdub90NtA +8nHsxrv5XqXIcrJaK7SDy7oW02iSstV/wHJj2Lau9mK5kcIF9WYQslTwOrU4zh8s +SizBLKJbBOgzWfEBju4nK24RLsX8yzNAvz7frXrl7RU94Aw+zHf9IxjXccpLCwip +pmuFEhXCuum0t4heM4voR7SNlehoNOn6NAtVywa+nIF7uuySab/UH/r/1fXImGGp +XVUssBE7r67iIaT6/p7+4Hm8QoZiZc94t3xQ3LPnIQKBgQD7NoUAYeUQTeyl4qkk +k2N+AyMIZDGaqFX522oJU8EKgmGVysnmHNTmWqT7MhQFkXmGFohdbf9kYz6LJC70 +3Hqips4ywrtotWj9B0wV6qhtWds/oS7t+tYvgmltHB3R1gIoxW/0aXOWOGaRAN56 +9phQF3Vy4hbg3MlQFJ0jYnU2jwKBgQDmXdizY8Qk9XeVhFnnxagBwtcVgMKkU9L0 +YIVAtuDAXr7SmSmdI0DNhoms2lLAckiz4tz/tor+Zilodk+vm+3aLd3VLqQ+SCGB +pyzMZoSY+ZsEj2jFeweJJ502ZNaDc/udX4H/FpumTbMnZS9188Z87OklKPQxDwSj +EmXHVCAJ8QKBgEcKKuBpzqImtyZOC7D5MSiAcJa95r0qn22R29GZaryzFC+M8j1t +p9MgjQkwJzNP6UK++n8ZTSCxZA1rBRBfjRqT+zmlkePRcUgkmSKVgl2yDGtZBHFt +/sXepjuhuNkPIPswojFG5NX1SyP0T/YL6BS2HZOIVhnutcs8mRdZj3YHAoGBAIvZ +qTaueGMfieuBH7tu6SG4kS6CrqdoJAcrmXnCDAt8+UeMqBC3WB3tcSM4n0gpmS1r +qlKrFRKAHt9GA/ZOKUVs1uEWoasVyxGU1cN48UKVNaC9FxPcLqT7IZvl97xx6uyN +ELQ1m2SIgxyFdxyaU4N/p8JhbG1j+qKtBpDN521hAoGAZ0Z0m3jjfpLUk8SwmoyN +nd8buSqCFI6n9BTcWozD2M1X4buxrsW5fQgqG9ufbXviVNVgZTXpJl0v7f4Se+QK +RuuQc5GlMPc9SPo0CCLIRaaZCNamc/7Iqb9PPQCx+kRH5i5hubg//HH9RSJHYUTl +s0k3sQaA/XQdz8mj/PkRN9k= +-----END PRIVATE KEY----- diff --git a/tests/certs/server-leaf-cert.pem b/tests/certs/server-leaf-cert.pem new file mode 100644 index 0000000000..b228b09e47 --- /dev/null +++ b/tests/certs/server-leaf-cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIUTDFKkXxIkZjtsAi2QlVNY54Wv+QwDQYJKoZIhvcNAQEL +BQAwgYcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzETMBEGA1UEBwwKTGluY29s +bnRvbjENMAsGA1UECgwEQ0lBVDEOMAwGA1UECwwFQ2xhc3MxDTALBgNVBAMMBHRl +c3QxKDAmBgkqhkiG9w0BCQEWGWRhcnRoY2VsdGljMTk4NUBnbWFpbC5jb20wHhcN +MjUwOTE0MDUxMTE4WhcNMjYwOTE0MDUxMTE4WjCBhzELMAkGA1UEBhMCVVMxCzAJ +BgNVBAgMAk5DMRMwEQYDVQQHDApMaW5jb2xudG9uMQ0wCwYDVQQKDARDSUFUMQ4w +DAYDVQQLDAVDbGFzczENMAsGA1UEAwwEdGVzdDEoMCYGCSqGSIb3DQEJARYZZGFy +dGhjZWx0aWMxOTg1QGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOIPEvKyVT4sLTkYTy7u2esamRVLMQv62V4e9nFxmAfLEL4RtYWag/Z4 +9KeMigZqEHWfHs3OtpTGUgQWn/wJ1A/PBIKlC1xVbws8oXhEtu7u/+IRALAvdA9h +DZyC6I1+vfo9JtzP78XKf5c9k+mVZs00BctOxz+1yZU32d6GM4S5C3vW3BAFAoSl +lKRqbXn/BKIOTLHuXdKPiqDmHfQ4P4LK82x/laUnXm8nKD1wdoV8BTxxoNvU8yf5 +TjMq9mlSdRF9VP2u6mlLhCopncoUpCLEu2AnUkPWhYTWMJL7Nk2yz6smEc2wJYNd +uYY7pTiLUwjq1eM0fJkJCFrFfI0jY58CAwEAAaNTMFEwHQYDVR0OBBYEFKUroDmc +DA8Mm/ok7Do7C6OQr0jnMB8GA1UdIwQYMBaAFKUroDmcDA8Mm/ok7Do7C6OQr0jn +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIZTZKNMohtOlicw +iPVjZhHqSq+watwlPNzDAw9iAceLLicbe0I0nJrXC0FbnL1u+zA1mLz7ZbKA0Ft6 +gJJCj3EU8c1YRDkxeKG2BpPxjS34zjDFrxsTwF6RZdLCOjHbUOmgSe5+yC7hf2jA +M/Aprt2tnfDRtQ+FiyUTD/Byeuu8I5u9UOyHV2tsQtFrazOEQazLmzaIq80IKVHx +6BF5GmCTSp2TxHoawiA44EgfYx4b4M81iECFqA0pKYOCX/wwhZB5ch2kd+9wT/6Z +0YcVvVUURPVnQiiBzkz6M2hbUlPD8HGBe1idRjuZNr9tEzsXkgzOEg5EG0D1HEnf +13fn8AU= +-----END CERTIFICATE----- diff --git a/tests/certs/trusted-leaf-cert.pem b/tests/certs/trusted-leaf-cert.pem new file mode 100644 index 0000000000..b228b09e47 --- /dev/null +++ b/tests/certs/trusted-leaf-cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIUTDFKkXxIkZjtsAi2QlVNY54Wv+QwDQYJKoZIhvcNAQEL +BQAwgYcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzETMBEGA1UEBwwKTGluY29s +bnRvbjENMAsGA1UECgwEQ0lBVDEOMAwGA1UECwwFQ2xhc3MxDTALBgNVBAMMBHRl +c3QxKDAmBgkqhkiG9w0BCQEWGWRhcnRoY2VsdGljMTk4NUBnbWFpbC5jb20wHhcN +MjUwOTE0MDUxMTE4WhcNMjYwOTE0MDUxMTE4WjCBhzELMAkGA1UEBhMCVVMxCzAJ +BgNVBAgMAk5DMRMwEQYDVQQHDApMaW5jb2xudG9uMQ0wCwYDVQQKDARDSUFUMQ4w +DAYDVQQLDAVDbGFzczENMAsGA1UEAwwEdGVzdDEoMCYGCSqGSIb3DQEJARYZZGFy +dGhjZWx0aWMxOTg1QGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOIPEvKyVT4sLTkYTy7u2esamRVLMQv62V4e9nFxmAfLEL4RtYWag/Z4 +9KeMigZqEHWfHs3OtpTGUgQWn/wJ1A/PBIKlC1xVbws8oXhEtu7u/+IRALAvdA9h +DZyC6I1+vfo9JtzP78XKf5c9k+mVZs00BctOxz+1yZU32d6GM4S5C3vW3BAFAoSl +lKRqbXn/BKIOTLHuXdKPiqDmHfQ4P4LK82x/laUnXm8nKD1wdoV8BTxxoNvU8yf5 +TjMq9mlSdRF9VP2u6mlLhCopncoUpCLEu2AnUkPWhYTWMJL7Nk2yz6smEc2wJYNd +uYY7pTiLUwjq1eM0fJkJCFrFfI0jY58CAwEAAaNTMFEwHQYDVR0OBBYEFKUroDmc +DA8Mm/ok7Do7C6OQr0jnMB8GA1UdIwQYMBaAFKUroDmcDA8Mm/ok7Do7C6OQr0jn +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIZTZKNMohtOlicw +iPVjZhHqSq+watwlPNzDAw9iAceLLicbe0I0nJrXC0FbnL1u+zA1mLz7ZbKA0Ft6 +gJJCj3EU8c1YRDkxeKG2BpPxjS34zjDFrxsTwF6RZdLCOjHbUOmgSe5+yC7hf2jA +M/Aprt2tnfDRtQ+FiyUTD/Byeuu8I5u9UOyHV2tsQtFrazOEQazLmzaIq80IKVHx +6BF5GmCTSp2TxHoawiA44EgfYx4b4M81iECFqA0pKYOCX/wwhZB5ch2kd+9wT/6Z +0YcVvVUURPVnQiiBzkz6M2hbUlPD8HGBe1idRjuZNr9tEzsXkgzOEg5EG0D1HEnf +13fn8AU= +-----END CERTIFICATE----- diff --git a/tests/test_for_issue_6978.py b/tests/test_for_issue_6978.py new file mode 100644 index 0000000000..a94a3f9d7a --- /dev/null +++ b/tests/test_for_issue_6978.py @@ -0,0 +1,62 @@ +import pytest +import requests +from requests.exceptions import SSLError +from http.server import HTTPServer, BaseHTTPRequestHandler +import ssl +import threading +import os + +# Create a custom request handler to serve a GET request +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b"") + +# Use a mock server with a self-signed certificate and incomplete chain +@pytest.fixture(scope="session") +def incomplete_cert_server(): + """Starts a mock HTTPS server with an incomplete cert chain for testing.""" + # This part of the code would require you to generate a specific + # certificate and key to simulate the bug. For the sake of this example, + # we'll use placeholder files. You would replace these with your actual files. + server_address = ('localhost', 8443) + httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) + + # Corrected method to create a server-side SSL context + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain( + certfile=os.path.join(os.path.dirname(__file__), "certs", "server-leaf-cert.pem"), # Replace with your server's leaf cert + keyfile=os.path.join(os.path.dirname(__file__), "certs", "server-key.pem") # Replace with your server's key + ) + httpd.socket = context.wrap_socket(httpd.socket, server_side=True) + + server_thread = threading.Thread(target=httpd.serve_forever) + server_thread.daemon = True + server_thread.start() + + yield server_address + + httpd.shutdown() + +def test_incomplete_chain_connects(incomplete_cert_server): + """ + Tests that requests connects when provided a trusted leaf cert, + even if the server's chain is incomplete. + """ + host, port = incomplete_cert_server + url = f"https://{host}:{port}" + + # The path to the trusted leaf certificate you would generate. + trusted_leaf_cert_path = os.path.join(os.path.dirname(__file__), "certs", "trusted-leaf-cert.pem") + + try: + # Use your corrected adapters.py to test this request + response = requests.get(url, verify=trusted_leaf_cert_path) + # We expect a successful response, so the test should pass + response.raise_for_status() + assert response.status_code == 200 + except SSLError as e: + # If the SSLError is raised, the test fails + pytest.fail(f"SSLError was raised: {e}")