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
This commit is contained in:
Stefan Eissing 2025-10-27 12:16:59 +01:00 committed by Daniel Stenberg
parent 2bb33c18a7
commit 692c7f133e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
7 changed files with 67 additions and 14 deletions

View 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)

View 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;
}

View 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

View 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

View 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):

View 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),

View 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'),