]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
TLS: IP address verification, extend test
authorStefan Eissing <stefan@eissing.org>
Mon, 27 Oct 2025 11:16:59 +0000 (12:16 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Mon, 27 Oct 2025 16:22:17 +0000 (17:22 +0100)
Change the test certificate to carry a altname 'dns:127.0.0.1' which
should *not* match in test_17_05_bad_ip_addr.

wolfSSL: since `wolfSSL_check_domain_name()` does not differentiate
between DNS and IP names, use if only for DNS names. For IP addresses,
get the peer certificate after the handshake and check that using
wolfSSL_X509_check_ip_asc().

Unfortunately, this succeeds where it should not, as wolfSSL internally
used the same check code for both cases. So, skip the test case until
wolfSSL fixes that.

Reported-by: Joshua Rogers
Closes #19252

lib/vquic/vquic-tls.c
lib/vtls/wolfssl.c
tests/http/test_02_download.py
tests/http/test_07_upload.py
tests/http/test_17_ssl_use.py
tests/http/testenv/certs.py
tests/http/testenv/env.py

index fa89c0b80967eea1d5618b3e3d32933d8cd493e9..f4ef06c33b72e4b153b0212931e3d62a450172ce 100644 (file)
@@ -178,12 +178,17 @@ CURLcode Curl_vquic_tls_verify_peer(struct curl_tls_ctx *ctx,
 #elif defined(USE_WOLFSSL)
   (void)data;
   if(conn_config->verifyhost) {
-    char *snihost = peer->sni ? peer->sni : peer->hostname;
     WOLFSSL_X509* cert = wolfSSL_get_peer_certificate(ctx->wssl.ssl);
-    if(wolfSSL_X509_check_host(cert, snihost, strlen(snihost), 0, NULL)
-          == WOLFSSL_FAILURE) {
+    if(!cert)
+      result = CURLE_OUT_OF_MEMORY;
+    else if(peer->sni &&
+      (wolfSSL_X509_check_host(cert, peer->sni, strlen(peer->sni), 0, NULL)
+       == WOLFSSL_FAILURE))
+      result = CURLE_PEER_FAILED_VERIFICATION;
+    else if(!peer->sni &&
+      (wolfSSL_X509_check_ip_asc(cert, peer->hostname, 0)
+       == WOLFSSL_FAILURE))
       result = CURLE_PEER_FAILED_VERIFICATION;
-    }
     wolfSSL_X509_free(cert);
   }
   if(!result)
index fe63097f9c3a8efc68e85ca524f636eaac37e893..1efb5dc370943530488261999a893a59360eebcd 100644 (file)
@@ -1491,11 +1491,10 @@ wssl_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data)
   }
 #endif
 
-  /* Enable RFC2818 checks */
-  if(conn_config->verifyhost) {
-    char *snihost = connssl->peer.sni ?
-      connssl->peer.sni : connssl->peer.hostname;
-    if(wolfSSL_check_domain_name(wssl->ssl, snihost) !=
+  /* Enable RFC2818 checks on domain names. This cannot check
+   * IP addresses which we need to do extra after the handshake. */
+  if(conn_config->verifyhost && connssl->peer.sni) {
+    if(wolfSSL_check_domain_name(wssl->ssl, connssl->peer.sni) !=
        WOLFSSL_SUCCESS) {
       return CURLE_SSL_CONNECT_ERROR;
     }
@@ -1719,6 +1718,25 @@ static CURLcode wssl_handshake(struct Curl_cfilter *cf,
   detail = wolfSSL_get_error(wssl->ssl, ret);
   CURL_TRC_CF(data, cf, "wolfSSL_connect() -> %d, detail=%d", ret, detail);
 
+  /* On a successful handshake with an IP address, do an extra check
+   * on the peer certificate */
+  if(ret == WOLFSSL_SUCCESS &&
+     conn_config->verifyhost &&
+     !connssl->peer.sni) {
+    /* we have an IP address as host name. */
+    WOLFSSL_X509* cert = wolfSSL_get_peer_certificate(wssl->ssl);
+    if(!cert) {
+      failf(data, "unable to get peer certificate");
+      return CURLE_PEER_FAILED_VERIFICATION;
+    }
+    ret = wolfSSL_X509_check_ip_asc(cert, connssl->peer.hostname, 0);
+    CURL_TRC_CF(data, cf, "check peer certificate for IP match on %s -> %d",
+                connssl->peer.hostname, ret);
+    if(ret != WOLFSSL_SUCCESS)
+      detail = DOMAIN_NAME_MISMATCH;
+    wolfSSL_X509_free(cert);
+  }
+
   if(ret == WOLFSSL_SUCCESS) {
     return CURLE_OK;
   }
index 26da1d2feee9b5d1fc4d96d3e36d9128df2cc6e2..574401cb72974a171144be40218712effa7bd86a 100644 (file)
@@ -610,6 +610,8 @@ class TestDownload:
             pytest.skip("h3 not supported")
         if proto != 'h3' and sys.platform.startswith('darwin') and env.ci_run:
             pytest.skip('failing on macOS CI runners')
+        if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('wolfssl'):
+            pytest.skip('h3 wolfssl early data failing on macOS')
         if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('gnutls'):
             pytest.skip('h3 gnutls early data failing on macOS')
         count = 2
index c2377d41e422cefb13b3ca28d42726aa3d8cd602..fa2ef272d7c6e2b7b9257449441f0725fb7500b8 100644 (file)
@@ -712,6 +712,8 @@ class TestUpload:
             pytest.skip("h3 not supported")
         if proto != 'h3' and sys.platform.startswith('darwin') and env.ci_run:
             pytest.skip('failing on macOS CI runners')
+        if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('wolfssl'):
+            pytest.skip('h3 wolfssl early data failing on macOS')
         if proto == 'h3' and sys.platform.startswith('darwin') and env.curl_uses_lib('gnutls'):
             pytest.skip('h3 gnutls early data failing on macOS')
         count = 2
index 619ecd25e6f1c01e747d3cea9c7d0c55127d12f2..d0af093af0b6b624395be3739269c44e4f780293 100644 (file)
@@ -186,6 +186,27 @@ class TestSSLUse:
         r = curl.http_get(url=url, alpn_proto=proto)
         assert r.exit_code == 60, f'{r}'
 
+    # use IP address that is in cert as DNS name (not really legal)
+    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
+    def test_17_05_very_bad_ip_addr(self, env: Env, proto,
+                                    httpd, configures_httpd,
+                                    nghttpx, configures_nghttpx):
+        if proto == 'h3' and not env.have_h3():
+            pytest.skip("h3 not supported")
+        if env.curl_uses_lib('mbedtls'):
+            pytest.skip("mbedtls falsely verifies a DNS: altname as IP address")
+        if env.curl_uses_lib('wolfssl'):
+            pytest.skip("wolfSSL falsely verifies a DNS: altname as IP address")
+        httpd.set_domain1_cred_name('domain1-very-bad')
+        httpd.reload_if_config_changed()
+        if proto == 'h3':
+            nghttpx.set_cred_name('domain1-very-bad')
+            nghttpx.reload_if_config_changed()
+        curl = CurlClient(env=env)
+        url = f'https://127.0.0.1:{env.port_for(proto)}/curltest/sslinfo'
+        r = curl.http_get(url=url, alpn_proto=proto)
+        assert r.exit_code == 60, f'{r}'
+
     # use localhost for connect
     @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
     def test_17_06_localhost(self, env: Env, proto, httpd, nghttpx):
index bfb7287eeee5f0f15a8694ddf2f60dbd6cc90c79..e59b1ea147e1492ba5d3966841f08c9c099fbb0b 100644 (file)
@@ -459,11 +459,15 @@ class TestCA:
     def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any:
         names = []
         for name in domains:
-            try:
-                names.append(x509.IPAddress(ipaddress.ip_address(name)))
-            # TODO: specify specific exceptions here
-            except:  # noqa: E722
-                names.append(x509.DNSName(name))
+            m = re.match(r'dns:(.+)', name)
+            if m:
+                names.append(x509.DNSName(m.group(1)))
+            else:
+                try:
+                    names.append(x509.IPAddress(ipaddress.ip_address(name)))
+                # TODO: specify specific exceptions here
+                except:  # noqa: E722
+                    names.append(x509.DNSName(name))
 
         return csr.add_extension(
             x509.BasicConstraints(ca=False, path_length=None),
index 0cd9101342d1de12a2676d83b62297b5048b2a60..ff8741530b7030701302ac181c1fe1aff801f5ea 100644 (file)
@@ -188,6 +188,7 @@ class EnvConfig:
         self.cert_specs = [
             CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost', '127.0.0.1'], key_type='rsa2048'),
             CertificateSpec(name='domain1-no-ip', domains=[self.domain1, self.domain1brotli], key_type='rsa2048'),
+            CertificateSpec(name='domain1-very-bad', domains=[self.domain1, 'dns:127.0.0.1'], key_type='rsa2048'),
             CertificateSpec(domains=[self.domain2], key_type='rsa2048'),
             CertificateSpec(domains=[self.ftp_domain], key_type='rsa2048'),
             CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'),