Repository: urllib3/urllib3

Issue:
TLS 1.3 Post Handshake Auth no longer working with urllib 2.1.0 when ignoring cert validation
### Subject

The change https://github.com/urllib3/urllib3/commit/ae0668899a144edb06c4d2ca9cec96b760a93e6a#diff-f2922ee77cbe0497677cfadf8dff15456050c6670d37d012c9416f37970f0d13R324-R330 introduced when Python 3.7 support was dropped broke TLS 1.3 post_handshake_auth support when a context that was not verifying the server's certificate was being set. Previously the check was

```python
if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr(
    context, "post_handshake_auth", None
) is not None:
    context.post_handshake_auth = True
```

Which meant that `context.post_handshake_auth` was being set if Python 3.7.4 or newer was being used regardless of the `cert_reqs` value. With the change in the linked commit the check is now

```python
if (
    cert_reqs == ssl.CERT_REQUIRED
    and getattr(context, "post_handshake_auth", None) is not None
):
    context.post_handshake_auth = True
```

This means it's now only being set if `cert_reqs == ssl.CERT_REQUIRED` so using `ssl.CERT_NONE` to ignore the cert checks breaks this setting. ~~Now that Python 3.8 is the baseline you can just have the `context.post_handshake_auth is not None`~~ 

Edit: ignore that, looks like SSLContext from pyOpenSSL can be used in some scenario which does not have this attribute so the `getattr` is still needed.

See https://github.com/diyan/pywinrm/issues/376#issuecomment-1913759929 for more info.

### Environment

Describe your environment.
At least, paste here the output of:

```python
import platform
import ssl
import urllib3

print("OS", platform.platform())
print("Python", platform.python_version())
print(ssl.OPENSSL_VERSION)
print("urllib3", urllib3.__version__)
```

```
OS Linux-6.7.1-arch1-1-x86_64-with-glibc2.38
>>> print("Python", platform.python_version())
Python 3.12.0
>>> print(ssl.OPENSSL_VERSION)
OpenSSL 3.2.0 23 Nov 2023
>>> print("urllib3", urllib3.__version__)
urllib3 2.1.0
```

### Steps to Reproduce

I unfortunately don't know of a simple web server that provides PHA with TLS 1.3. My tests are against a Windows host with WinRM and Certificate auth which isn't the simplest to setup.

### Expected Behavior

Authentication works

### Actual Behavior

Authentication fails and the TLS socket is closed by the server because it never receives a certificate from the client.


Discussion:
Thank you for reporting this issue and opening a fix!



Patch:
diff --git a/changelog/3325.bugfix.rst b/changelog/3325.bugfix.rst
new file mode 100644
index 0000000000..22fd97c7c3
--- /dev/null
+++ b/changelog/3325.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed TLS 1.3 Post Handshake Auth when the server certificate validation was disabled.
diff --git a/src/urllib3/util/ssl_.py b/src/urllib3/util/ssl_.py
index e0a7c04a3c..b14cf27b61 100644
--- a/src/urllib3/util/ssl_.py
+++ b/src/urllib3/util/ssl_.py
@@ -319,14 +319,9 @@ def create_urllib3_context(
 
     # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is
     # necessary for conditional client cert authentication with TLS 1.3.
-    # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older
-    # versions of Python. We only enable if certificate verification is enabled to work
-    # around Python issue #37428
-    # See: https://bugs.python.org/issue37428
-    if (
-        cert_reqs == ssl.CERT_REQUIRED
-        and getattr(context, "post_handshake_auth", None) is not None
-    ):
+    # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using
+    # an SSLContext created by pyOpenSSL.
+    if getattr(context, "post_handshake_auth", None) is not None:
         context.post_handshake_auth = True
 
     # The order of the below lines setting verify_mode and check_hostname


Test patch:
diff --git a/test/test_ssl.py b/test/test_ssl.py
index c886d4e51c..43073cb263 100644
--- a/test/test_ssl.py
+++ b/test/test_ssl.py
@@ -108,13 +108,28 @@ def test_wrap_socket_no_ssltransport(self) -> None:
                 ssl_.ssl_wrap_socket(sock, tls_in_tls=True)
 
     @pytest.mark.parametrize(
-        ["pha", "expected_pha"], [(None, None), (False, True), (True, True)]
+        ["pha", "expected_pha", "cert_reqs"],
+        [
+            (None, None, None),
+            (None, None, ssl.CERT_NONE),
+            (None, None, ssl.CERT_OPTIONAL),
+            (None, None, ssl.CERT_REQUIRED),
+            (False, True, None),
+            (False, True, ssl.CERT_NONE),
+            (False, True, ssl.CERT_OPTIONAL),
+            (False, True, ssl.CERT_REQUIRED),
+            (True, True, None),
+            (True, True, ssl.CERT_NONE),
+            (True, True, ssl.CERT_OPTIONAL),
+            (True, True, ssl.CERT_REQUIRED),
+        ],
     )
     def test_create_urllib3_context_pha(
         self,
         monkeypatch: pytest.MonkeyPatch,
         pha: bool | None,
         expected_pha: bool | None,
+        cert_reqs: int | None,
     ) -> None:
         context = mock.create_autospec(ssl_.SSLContext)
         context.set_ciphers = mock.Mock()
@@ -122,7 +137,7 @@ def test_create_urllib3_context_pha(
         context.post_handshake_auth = pha
         monkeypatch.setattr(ssl_, "SSLContext", lambda *_, **__: context)
 
-        assert ssl_.create_urllib3_context() is context
+        assert ssl_.create_urllib3_context(cert_reqs=cert_reqs) is context
 
         assert context.post_handshake_auth == expected_pha