1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3#*************************************************************************** 4# _ _ ____ _ 5# Project ___| | | | _ \| | 6# / __| | | | |_) | | 7# | (__| |_| | _ <| |___ 8# \___|\___/|_| \_\_____| 9# 10# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. 11# 12# This software is licensed as described in the file COPYING, which 13# you should have received as part of this distribution. The terms 14# are also available at https://curl.se/docs/copyright.html. 15# 16# You may opt to use, copy, modify, merge, publish, distribute and/or sell 17# copies of the Software, and permit persons to whom the Software is 18# furnished to do so, under the terms of the COPYING file. 19# 20# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 21# KIND, either express or implied. 22# 23# SPDX-License-Identifier: curl 24# 25########################################################################### 26# 27import difflib 28import filecmp 29import json 30import logging 31import os 32from datetime import timedelta 33import pytest 34 35from testenv import Env, CurlClient, LocalClient, ExecResult 36 37 38log = logging.getLogger(__name__) 39 40 41class TestSSLUse: 42 43 @pytest.fixture(autouse=True, scope='class') 44 def _class_scope(self, env, httpd, nghttpx): 45 if env.have_h3(): 46 nghttpx.start_if_needed() 47 httpd.clear_extra_configs() 48 httpd.reload() 49 50 def test_17_01_sslinfo_plain(self, env: Env, httpd, nghttpx, repeat): 51 proto = 'http/1.1' 52 curl = CurlClient(env=env) 53 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' 54 r = curl.http_get(url=url, alpn_proto=proto) 55 assert r.json['HTTPS'] == 'on', f'{r.json}' 56 assert 'SSL_SESSION_ID' in r.json, f'{r.json}' 57 assert 'SSL_SESSION_RESUMED' in r.json, f'{r.json}' 58 assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}' 59 60 @pytest.mark.parametrize("tls_max", ['1.2', '1.3']) 61 def test_17_02_sslinfo_reconnect(self, env: Env, httpd, nghttpx, tls_max, repeat): 62 proto = 'http/1.1' 63 count = 3 64 exp_resumed = 'Resumed' 65 xargs = ['--sessionid', '--tls-max', tls_max, f'--tlsv{tls_max}'] 66 if env.curl_uses_lib('gnutls'): 67 if tls_max == '1.3': 68 exp_resumed = 'Initial' # 1.2 works in gnutls, but 1.3 does not, TODO 69 if env.curl_uses_lib('libressl'): 70 if tls_max == '1.3': 71 exp_resumed = 'Initial' # 1.2 works in libressl, but 1.3 does not, TODO 72 if env.curl_uses_lib('wolfssl'): 73 xargs = ['--sessionid', f'--tlsv{tls_max}'] 74 if tls_max == '1.3': 75 exp_resumed = 'Initial' # 1.2 works in wolfssl, but 1.3 does not, TODO 76 if env.curl_uses_lib('rustls-ffi'): 77 exp_resumed = 'Initial' # rustls does not support sessions, TODO 78 if env.curl_uses_lib('bearssl') and tls_max == '1.3': 79 pytest.skip('BearSSL does not support TLSv1.3') 80 if env.curl_uses_lib('mbedtls') and tls_max == '1.3' and \ 81 not env.curl_lib_version_at_least('mbedtls', '3.6.0'): 82 pytest.skip('mbedtls does not support TLSv1.3') 83 84 curl = CurlClient(env=env) 85 # tell the server to close the connection after each request 86 urln = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo?'\ 87 f'id=[0-{count-1}]&close' 88 r = curl.http_download(urls=[urln], alpn_proto=proto, with_stats=True, 89 extra_args=xargs) 90 r.check_response(count=count, http_status=200) 91 # should have used one connection for each request, sessions after 92 # first should have been resumed 93 assert r.total_connects == count, r.dump_logs() 94 for i in range(count): 95 dfile = curl.download_file(i) 96 assert os.path.exists(dfile) 97 with open(dfile) as f: 98 djson = json.load(f) 99 assert djson['HTTPS'] == 'on', f'{i}: {djson}' 100 if i == 0: 101 assert djson['SSL_SESSION_RESUMED'] == 'Initial', f'{i}: {djson}' 102 else: 103 assert djson['SSL_SESSION_RESUMED'] == exp_resumed, f'{i}: {djson}' 104 105 # use host name with trailing dot, verify handshake 106 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 107 def test_17_03_trailing_dot(self, env: Env, httpd, nghttpx, repeat, proto): 108 if env.curl_uses_lib('gnutls'): 109 pytest.skip("gnutls does not match hostnames with trailing dot") 110 if proto == 'h3' and not env.have_h3(): 111 pytest.skip("h3 not supported") 112 curl = CurlClient(env=env) 113 domain = f'{env.domain1}.' 114 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 115 r = curl.http_get(url=url, alpn_proto=proto) 116 assert r.exit_code == 0, f'{r}' 117 assert r.json, f'{r}' 118 if proto != 'h3': # we proxy h3 119 # the SNI the server received is without trailing dot 120 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 121 122 # use host name with double trailing dot, verify handshake 123 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 124 def test_17_04_double_dot(self, env: Env, httpd, nghttpx, repeat, proto): 125 if proto == 'h3' and not env.have_h3(): 126 pytest.skip("h3 not supported") 127 if proto == 'h3' and env.curl_uses_lib('wolfssl'): 128 pytest.skip("wolfSSL HTTP/3 peer verification does not properly check") 129 curl = CurlClient(env=env) 130 domain = f'{env.domain1}..' 131 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 132 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ 133 '-H', f'Host: {env.domain1}', 134 ]) 135 if r.exit_code == 0: 136 assert r.json, f'{r.stdout}' 137 # the SNI the server received is without trailing dot 138 if proto != 'h3': # we proxy h3 139 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 140 assert False, f'should not have succeeded: {r.json}' 141 # 7 - rustls rejects a servername with .. during setup 142 # 35 - libressl rejects setting an SNI name with trailing dot 143 # 60 - peer name matching failed against certificate 144 assert r.exit_code in [7, 35, 60], f'{r}' 145 146 # use ip address for connect 147 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 148 def test_17_05_ip_addr(self, env: Env, httpd, nghttpx, repeat, proto): 149 if env.curl_uses_lib('bearssl'): 150 pytest.skip("bearssl does not support cert verification with IP addresses") 151 if env.curl_uses_lib('mbedtls'): 152 pytest.skip("mbedtls does not support cert verification with IP addresses") 153 if proto == 'h3' and not env.have_h3(): 154 pytest.skip("h3 not supported") 155 curl = CurlClient(env=env) 156 domain = f'127.0.0.1' 157 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 158 r = curl.http_get(url=url, alpn_proto=proto) 159 assert r.exit_code == 0, f'{r}' 160 assert r.json, f'{r}' 161 if proto != 'h3': # we proxy h3 162 # the SNI should not have been used 163 assert 'SSL_TLS_SNI' not in r.json, f'{r.json}' 164 165 # use localhost for connect 166 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 167 def test_17_06_localhost(self, env: Env, httpd, nghttpx, repeat, proto): 168 if proto == 'h3' and not env.have_h3(): 169 pytest.skip("h3 not supported") 170 curl = CurlClient(env=env) 171 domain = f'localhost' 172 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 173 r = curl.http_get(url=url, alpn_proto=proto) 174 assert r.exit_code == 0, f'{r}' 175 assert r.json, f'{r}' 176 if proto != 'h3': # we proxy h3 177 assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}' 178