From: Stefan Eissing Date: Mon, 27 Oct 2025 11:16:59 +0000 (+0100) Subject: TLS: IP address verification, extend test X-Git-Tag: curl-8_17_0~86 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=692c7f133e6f9a5053a87b1fffbf3c41697a7742;p=thirdparty%2Fcurl.git TLS: IP address verification, extend test 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 --- diff --git a/lib/vquic/vquic-tls.c b/lib/vquic/vquic-tls.c index fa89c0b809..f4ef06c33b 100644 --- a/lib/vquic/vquic-tls.c +++ b/lib/vquic/vquic-tls.c @@ -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) diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c index fe63097f9c..1efb5dc370 100644 --- a/lib/vtls/wolfssl.c +++ b/lib/vtls/wolfssl.c @@ -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; } diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py index 26da1d2fee..574401cb72 100644 --- a/tests/http/test_02_download.py +++ b/tests/http/test_02_download.py @@ -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 diff --git a/tests/http/test_07_upload.py b/tests/http/test_07_upload.py index c2377d41e4..fa2ef272d7 100644 --- a/tests/http/test_07_upload.py +++ b/tests/http/test_07_upload.py @@ -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 diff --git a/tests/http/test_17_ssl_use.py b/tests/http/test_17_ssl_use.py index 619ecd25e6..d0af093af0 100644 --- a/tests/http/test_17_ssl_use.py +++ b/tests/http/test_17_ssl_use.py @@ -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): diff --git a/tests/http/testenv/certs.py b/tests/http/testenv/certs.py index bfb7287eee..e59b1ea147 100644 --- a/tests/http/testenv/certs.py +++ b/tests/http/testenv/certs.py @@ -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), diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py index 0cd9101342..ff8741530b 100644 --- a/tests/http/testenv/env.py +++ b/tests/http/testenv/env.py @@ -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'),