From: Stefan Eissing Date: Fri, 26 Apr 2024 12:13:23 +0000 (+0200) Subject: tests: add SNI and peer name checks X-Git-Tag: curl-8_8_0~145 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b06619d0a33b957301d306097a7932ef77fcb979;p=thirdparty%2Fcurl.git tests: add SNI and peer name checks - connect to DNS names with trailing dot - connect to DNS names with double trailing dot - rustls, always give `peer->hostname` and let it figure out SNI itself - add SNI tests for ip address and localhost - document in code and TODO that QUIC with ngtcp2+wolfssl does not do proper peer verification of the certificate - mbedtls, skip tests with ip address verification as not supported by the library Closes #13486 --- diff --git a/docs/TODO b/docs/TODO index e5bf092433..f5838afedf 100644 --- a/docs/TODO +++ b/docs/TODO @@ -126,6 +126,7 @@ 13.13 Make sure we forbid TLS 1.3 post-handshake authentication 13.14 Support the clienthello extension 13.15 Select signature algorithms + 13.16 QUIC peer verification with wolfSSL 14. GnuTLS 14.2 check connection @@ -921,6 +922,11 @@ https://github.com/curl/curl/issues/12982 +13.16 QUIC peer verification with wolfSSL + + Peer certificate verification is missing in the QUIC (ngtcp2) implementation + using wolfSSL. + 14. GnuTLS 14.2 check connection diff --git a/lib/vquic/vquic-tls.c b/lib/vquic/vquic-tls.c index 1ad8cb7f0a..90a5044b5c 100644 --- a/lib/vquic/vquic-tls.c +++ b/lib/vquic/vquic-tls.c @@ -324,7 +324,11 @@ CURLcode Curl_vquic_tls_verify_peer(struct curl_tls_ctx *ctx, #elif defined(USE_WOLFSSL) (void)data; if(conn_config->verifyhost) { - if(!peer->sni || + /* TODO: this does not really verify the peer certificate. + * On TCP connection this works as it is wired into the wolfSSL + * connect() implementation and gives a special return code on + * such a fail. */ + if(peer->sni && wolfSSL_check_domain_name(ctx->ssl, peer->sni) == SSL_FAILURE) return CURLE_PEER_FAILED_VERIFICATION; } diff --git a/lib/vtls/rustls.c b/lib/vtls/rustls.c index 7a989c95e1..49f8c008bb 100644 --- a/lib/vtls/rustls.c +++ b/lib/vtls/rustls.c @@ -479,13 +479,8 @@ cr_init_backend(struct Curl_cfilter *cf, struct Curl_easy *data, backend->config = rustls_client_config_builder_build(config_builder); DEBUGASSERT(rconn == NULL); - { - /* rustls claims to manage ip address hostnames as well here. So, - * if we have an SNI, we use it, otherwise we pass the hostname */ - char *server = connssl->peer.sni? - connssl->peer.sni : connssl->peer.hostname; - result = rustls_client_connection_new(backend->config, server, &rconn); - } + result = rustls_client_connection_new(backend->config, + connssl->peer.hostname, &rconn); if(result != RUSTLS_RESULT_OK) { rustls_error(result, errorbuf, sizeof(errorbuf), &errorlen); failf(data, "rustls_client_connection_new: %.*s", (int)errorlen, errorbuf); diff --git a/tests/http/test_17_ssl_use.py b/tests/http/test_17_ssl_use.py index f9d24652ec..dfe8673e2b 100644 --- a/tests/http/test_17_ssl_use.py +++ b/tests/http/test_17_ssl_use.py @@ -101,4 +101,77 @@ class TestSSLUse: else: assert djson['SSL_SESSION_RESUMED'] == exp_resumed, f'{i}: {djson}' + # use host name with trailing dot, verify handshake + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + def test_17_03_trailing_dot(self, env: Env, httpd, nghttpx, repeat, proto): + if env.curl_uses_lib('gnutls'): + pytest.skip("gnutls does not match hostnames with trailing dot") + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + curl = CurlClient(env=env) + domain = f'{env.domain1}.' + url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' + r = curl.http_get(url=url, alpn_proto=proto) + assert r.exit_code == 0, f'{r}' + assert r.json, f'{r}' + if proto != 'h3': # we proxy h3 + # the SNI the server received is without trailing dot + assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' + + # use host name with double trailing dot, verify handshake + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + def test_17_04_double_dot(self, env: Env, httpd, nghttpx, repeat, proto): + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + if proto == 'h3' and env.curl_uses_lib('wolfssl'): + pytest.skip("wolfSSL HTTP/3 peer verification does not properly check") + curl = CurlClient(env=env) + domain = f'{env.domain1}..' + url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' + r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ + '-H', f'Host: {env.domain1}', + ]) + if r.exit_code == 0: + assert r.json, f'{r.stdout}' + # the SNI the server received is without trailing dot + if proto != 'h3': # we proxy h3 + assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' + assert False, f'should not have succeeded: {r.json}' + # 7 - rustls rejects a servername with .. during setup + # 35 - libressl rejects setting an SNI name with trailing dot + # 60 - peer name matching failed against certificate + assert r.exit_code in [7, 35, 60], f'{r}' + + # use ip address for connect + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + def test_17_05_ip_addr(self, env: Env, httpd, nghttpx, repeat, proto): + if env.curl_uses_lib('bearssl'): + pytest.skip("bearssl does not support cert verification with IP addresses") + if env.curl_uses_lib('mbedtls'): + pytest.skip("mbedtls does not support cert verification with IP addresses") + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + curl = CurlClient(env=env) + domain = f'127.0.0.1' + url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' + r = curl.http_get(url=url, alpn_proto=proto) + assert r.exit_code == 0, f'{r}' + assert r.json, f'{r}' + if proto != 'h3': # we proxy h3 + # the SNI should not have been used + assert 'SSL_TLS_SNI' not in r.json, f'{r.json}' + + # use localhost for connect + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) + def test_17_06_localhost(self, env: Env, httpd, nghttpx, repeat, proto): + if proto == 'h3' and not env.have_h3(): + pytest.skip("h3 not supported") + curl = CurlClient(env=env) + domain = f'localhost' + url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' + r = curl.http_get(url=url, alpn_proto=proto) + assert r.exit_code == 0, f'{r}' + assert r.json, f'{r}' + if proto != 'h3': # we proxy h3 + assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}' diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py index ef240bc5c7..539bffe384 100644 --- a/tests/http/testenv/env.py +++ b/tests/http/testenv/env.py @@ -133,7 +133,7 @@ class EnvConfig: self.domain2 = f"two.{self.tld}" self.proxy_domain = f"proxy.{self.tld}" self.cert_specs = [ - CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost'], key_type='rsa2048'), + CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost', '127.0.0.1'], key_type='rsa2048'), CertificateSpec(domains=[self.domain2], key_type='rsa2048'), CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'), CertificateSpec(name="clientsX", sub_specs=[ diff --git a/tests/http/testenv/mod_curltest/mod_curltest.c b/tests/http/testenv/mod_curltest/mod_curltest.c index 911103bcf4..4443e0021e 100644 --- a/tests/http/testenv/mod_curltest/mod_curltest.c +++ b/tests/http/testenv/mod_curltest/mod_curltest.c @@ -709,6 +709,7 @@ static int curltest_sslinfo_handler(request_rec *r) brigade_env_var(r, bb, "SSL_SESSION_RESUMED"); brigade_env_var(r, bb, "SSL_SRP_USER"); brigade_env_var(r, bb, "SSL_SRP_USERINFO"); + brigade_env_var(r, bb, "SSL_TLS_SNI"); apr_brigade_puts(bb, NULL, NULL, "}\n"); /* flush response */