Răsfoiți Sursa

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
Stefan Eissing 6 luni în urmă
părinte
comite
b06619d0a3

+ 6 - 0
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

+ 5 - 1
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;
   }

+ 2 - 7
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);

+ 73 - 0
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}'
 

+ 1 - 1
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=[

+ 1 - 0
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 */