Repository: pallets/werkzeug

Issue:
TLS validation in local dev does not work
TLS validation on localhost does not work with the auto generated certificate.

To replicate:
- Follow install & setup, pass --cert-file
```shell
python3 manage.py runserver_plus --cert-file dev-cert.crt
Django version 5.0.4, using settings 'ourapp.devsettings'
Development server is running at https://ourapp.localhost:8000/
Using the Werkzeug debugger (https://werkzeug.palletsprojects.com/)
Quit the server with CONTROL-C.
```

dev-cert.crt is generated, but does not work, a sample ERROR using wget:

```shell
wget -vvv --ca-certificate=$PWD/dev-cert.crt https://ourapp.localhost:8000
--2024-05-02 07:49:10--  https://ourapp.localhost:8000/
Resolving ourapp.localhost (ourapp.localhost)... 127.0.0.1
Connecting to ourapp.localhost (ourapp.localhost)|127.0.0.1|:8000... connected.
ERROR: no certificate subject alternative name matches
	requested host name ‘ourapp.localhost’.
To connect to ourapp.localhost insecurely, use `--no-check-certificate'.
```

Certificate looks valid, however I think wget, curl, python requests does not like the cn name?

```shell
openssl x509 -in dev-cert.crt --text --noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            25:44:40:09:99:71:4e:8c:b3:78:9e:ba:1b:de:d2:d0:f3:4f:d4:3e
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O=Dummy Certificate, CN=*.localhost/CN=localhost
        Validity
            Not Before: May  2 07:02:26 2024 GMT
            Not After : May  2 07:02:26 2025 GMT
        Subject: O=Dummy Certificate, CN=*.localhost/CN=localhost
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ad:fb:88:dd:bd:9f:d7:af:33:6f:92:5e:25:1e:
                    2d:de:22:b2:ec:e6:67:91:24:23:6e:f2:7f:08:dc:
                    6c:4d:be:b8:ca:a9:34:4c:08:71:c3:9f:dd:ac:67:
                    35:a5:72:8f:9f:dc:b1:47:9c:e7:9f:6b:b3:9c:a4:
                    f4:28:a8:5f:fd:9a:f1:a4:c0:59:88:bb:25:31:e5:
                    7c:75:33:67:ee:01:cc:f6:e0:59:b7:f4:ff:99:44:
                    a9:31:13:6a:eb:13:4e:e1:fa:ec:54:5c:0d:a6:a5:
                    38:59:5c:ae:b7:0c:d9:ee:23:40:db:1e:5e:42:47:
                    99:96:26:31:1a:62:23:44:41:31:1d:3a:9d:35:b0:
                    8b:49:3d:76:cb:6d:41:da:e8:10:a8:6d:82:7c:fb:
                    22:fd:8d:c8:9f:ed:90:1c:cd:3c:31:34:dc:d4:a1:
                    56:c4:c2:1f:f0:ca:b5:5a:9d:dd:06:43:7a:49:ed:
                    9a:74:e2:ea:31:e7:04:35:9b:f6:4a:75:8f:df:2c:
                    55:14:3b:56:85:cf:0b:b1:ea:8f:52:99:8e:33:b1:
                    cd:fc:9d:e0:38:24:e7:23:b5:da:cb:a8:14:ad:2d:
                    d6:f7:2c:fa:bc:e0:c3:15:e2:8f:72:22:7f:db:ad:
                    a7:14:1e:4b:09:fc:2d:71:6b:fa:15:0d:ec:da:5b:
                    c0:6f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name:
                DNS:*.localhost/CN=localhost
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        99:51:30:ea:30:3f:57:01:28:80:78:44:fa:f6:8a:47:af:7b:
        01:1f:ff:d2:eb:8f:d2:21:46:f2:b7:6c:29:a1:b0:7a:1f:80:
        a7:1e:93:aa:af:6f:c2:fe:19:28:6a:93:6b:19:94:8c:2c:c3:
        82:9a:d6:39:35:21:ea:02:1f:62:00:14:51:38:51:42:56:11:
        af:1d:53:60:90:d7:e2:6c:28:8e:af:b8:b6:ef:73:95:5b:4d:
        dd:3e:14:a0:95:20:52:3b:93:88:9f:c2:e3:c4:29:d6:7e:a8:
        de:c0:db:e9:2f:cb:d7:e2:f7:06:e4:f2:ee:5f:4b:e3:43:2c:
        fc:bd:be:df:f7:06:37:89:10:35:19:99:c0:b5:ff:0a:20:20:
        e4:64:af:40:7d:f0:e6:e7:a1:e1:fc:ee:70:c0:71:be:ee:c9:
        9a:a2:4c:7d:33:36:d1:a7:de:cc:d1:47:09:2e:d5:ae:1a:0f:
        4a:68:9a:08:be:d1:6d:03:1b:b3:0c:b0:e8:5a:ac:fa:73:4a:
        c5:4d:39:e0:b7:47:26:c6:66:d2:26:d0:88:6e:f4:9e:15:13:
        d9:e0:a8:48:cc:f9:40:93:82:49:0f:4b:cd:16:95:ca:cd:78:
        1a:7c:67:71:c5:8a:a9:03:10:ed:47:ac:89:63:41:af:5c:d7:
        f0:ce:1f:3d
```

I've been using this library and running it insecure as a result for a while but today it really bugged me so thought i would dig deeper.

If i generate the certificate manually, it works and validates fine.  With minor modifications to serving.py to fix the SANS/CN fields, i can make it work ok:

```shell
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            38:a7:d1:e8:46:1c:f2:94:04:eb:ac:86:19:6d:7f:87:22:4f:a1
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O=Dummy Certificate, CN=localhost
        Validity
            Not Before: May  2 07:13:25 2024 GMT
            Not After : May  2 07:13:25 2025 GMT
        Subject: O=Dummy Certificate, CN=localhost
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:c2:b7:20:f0:b2:5d:d5:e5:e6:b4:9b:a6:b7:e4:
                    65:4d:d3:52:f5:9c:a5:3c:b5:12:7c:2b:cd:c3:f5:
                    d2:43:72:b1:dd:39:74:8c:23:ff:b5:c9:4a:2a:18:
                    96:33:b8:d4:2a:8f:bd:6c:b5:10:24:1a:3e:19:5a:
                    7d:aa:b6:76:e2:37:dc:57:98:6e:dc:80:38:d7:c0:
                    b4:3c:b6:98:68:31:54:e3:a4:d0:fe:d5:14:97:56:
                    7f:5d:f0:5a:8e:ee:ae:cf:15:ef:4b:98:52:40:c4:
                    45:a5:af:cb:39:6f:67:95:19:62:24:52:64:8a:d1:
                    1d:77:86:40:fe:db:92:68:c7:7c:bc:56:1d:fe:e7:
                    61:ba:11:d5:a7:e4:3b:d9:b7:d9:fb:42:22:ba:27:
                    81:2a:7f:72:b7:81:f2:73:eb:1c:8f:90:ac:ba:80:
                    ac:c2:4f:4a:aa:bc:2b:d8:05:cd:98:b3:0d:11:18:
                    45:09:b1:bc:43:6e:53:c3:19:ff:6d:55:64:1d:ea:
                    73:27:3f:c1:f6:87:b7:6b:13:12:77:6e:de:05:bf:
                    bb:8c:42:6d:49:32:0f:a0:d5:06:20:14:ff:39:58:
                    28:67:39:34:15:40:72:d3:f8:4b:a8:07:0c:82:14:
                    21:f5:2a:c1:05:6e:4e:7f:3a:86:0c:c1:0b:97:6f:
                    61:4b
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name:
                DNS:localhost, DNS:*.localhost
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        72:bc:10:27:91:1b:11:6e:29:11:1d:91:66:93:7d:f4:d8:8d:
        0e:57:14:c8:16:44:c9:bd:e6:1e:cb:12:53:ee:59:02:29:ea:
        53:f8:09:cf:a0:fe:f3:1e:0c:0c:78:34:83:dd:4c:03:6f:e2:
        f7:7d:56:44:82:1a:bb:50:c1:bb:3d:1c:58:ea:0d:a6:12:24:
        34:4d:b5:3c:a5:10:d3:a6:a5:2d:f9:86:20:bb:e3:fd:62:14:
        ec:b7:aa:45:36:3e:f1:f6:02:1f:4a:8b:97:69:98:5e:22:54:
        14:5c:87:ee:f0:f7:e2:fc:72:9b:c0:bd:67:a6:fa:4a:69:3e:
        6e:48:5e:fe:6d:7a:f6:9e:e6:0f:33:4c:44:39:24:9c:98:06:
        da:a9:a1:12:4c:bc:f8:b1:cb:bf:0f:c2:c9:83:aa:e2:4d:c5:
        1a:0d:7e:60:da:a6:49:83:b6:f6:a7:da:34:db:aa:0b:8d:19:
        b6:c7:f9:67:73:cf:2f:af:e7:09:6c:63:5c:86:c7:4f:cb:a3:
        f2:98:77:e0:3a:de:ef:b8:38:df:67:97:8e:fd:79:9b:8b:7b:
        a7:05:2c:3f:f7:cf:9a:ff:31:a9:f0:6c:f7:7f:1e:a2:1f:58:
        dd:e2:c8:99:b6:3e:c4:41:9d:0f:29:e6:8a:88:f1:6d:a6:b5:
        36:e8:40:a2
```

Environment:

- Python version:3.12.1
- Poetry 1.8.2
- Werkzeug version:3.0.1
- Mac Sonoma 14.4.1

Is this a bug?  I could submit a pull request with my local change to serving.py, which appears to resolve?


Discussion:
If you already have a fix, then submitting a PR (or at least showing the patch in here) is always a good idea - also because that way we immediately see what (you think) is wrong with our code, which is often much faster than reading a long natural language description ;)

Resolving patch:
diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
--- a/src/werkzeug/serving.py
+++ b/src/werkzeug/serving.py
@@ -532,7 +532,10 @@ def generate_adhoc_ssl_pair(
         .not_valid_before(dt.now(timezone.utc))
         .not_valid_after(dt.now(timezone.utc) + timedelta(days=365))
         .add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False)
-        .add_extension(x509.SubjectAlternativeName([x509.DNSName(cn)]), critical=False)
+        .add_extension(
+            x509.SubjectAlternativeName([x509.DNSName(cn), x509.DNSName(f"*.{cn}")]),
+            critical=False,
+        )
         .sign(pkey, hashes.SHA256(), backend)
     )
     return cert, pkey
@@ -560,7 +563,7 @@ def make_ssl_devcert(
     """
 
     if host is not None:
-        cn = f"*.{host}/CN={host}"
+        cn = host
     cert, pkey = generate_adhoc_ssl_pair(cn=cn)
 
     from cryptography.hazmat.primitives import serialization


Test patch: