]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
tests: add SNI and peer name checks
authorStefan Eissing <stefan@eissing.org>
Fri, 26 Apr 2024 12:13:23 +0000 (14:13 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Mon, 29 Apr 2024 06:20:35 +0000 (08:20 +0200)
- 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

docs/TODO
lib/vquic/vquic-tls.c
lib/vtls/rustls.c
tests/http/test_17_ssl_use.py
tests/http/testenv/env.py
tests/http/testenv/mod_curltest/mod_curltest.c

index e5bf092433fb0c96f7132ae2f647c8c5eb89ca1b..f5838afedf5ff0ed3b5aefee931ff0d05b66f3f1 100644 (file)
--- a/docs/TODO
+++ b/docs/TODO
  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
 
  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
index 1ad8cb7f0a11ffddabc1e069bd70b66fe279a274..90a5044b5c923cd5f5cfbaf7d8f8d6569a929919 100644 (file)
@@ -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;
   }
index 7a989c95e19da2415097774ce3b9f295bd802aed..49f8c008bbde864a2d9300438cf4e7a881cf6dae 100644 (file)
@@ -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);
index f9d24652ec7600904bcbe9de88df8994de879719..dfe8673e2b72f9e6ead542f16314ef1132232d88 100644 (file)
@@ -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}'
 
index ef240bc5c7a8a70ad9e9f0d1eab8fbd80be72203..539bffe3842358bc997f308265acdf000b5dea37 100644 (file)
@@ -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=[
index 911103bcf485d88cffde740ba874edd9c9a3a82e..4443e0021e9e295658aa4660a15d1d860776e812 100644 (file)
@@ -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 */