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 json 28import logging 29import os 30import pytest 31 32from testenv import Env, CurlClient 33 34 35log = logging.getLogger(__name__) 36 37 38class TestSSLUse: 39 40 @pytest.fixture(autouse=True, scope='class') 41 def _class_scope(self, env, nghttpx): 42 if env.have_h3(): 43 nghttpx.start_if_needed() 44 45 @pytest.fixture(autouse=True, scope='function') 46 def _function_scope(self, request, env, httpd): 47 httpd.clear_extra_configs() 48 if 'httpd' not in request.node._fixtureinfo.argnames: 49 httpd.reload_if_config_changed() 50 51 def test_17_01_sslinfo_plain(self, env: Env, nghttpx, repeat): 52 proto = 'http/1.1' 53 curl = CurlClient(env=env) 54 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' 55 r = curl.http_get(url=url, alpn_proto=proto) 56 assert r.json['HTTPS'] == 'on', f'{r.json}' 57 assert 'SSL_SESSION_ID' in r.json, f'{r.json}' 58 assert 'SSL_SESSION_RESUMED' in r.json, f'{r.json}' 59 assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}' 60 61 @pytest.mark.parametrize("tls_max", ['1.2', '1.3']) 62 def test_17_02_sslinfo_reconnect(self, env: Env, tls_max): 63 proto = 'http/1.1' 64 count = 3 65 exp_resumed = 'Resumed' 66 xargs = ['--sessionid', '--tls-max', tls_max, f'--tlsv{tls_max}'] 67 if env.curl_uses_lib('gnutls'): 68 if tls_max == '1.3': 69 exp_resumed = 'Initial' # 1.2 works in GnuTLS, but 1.3 does not, TODO 70 if env.curl_uses_lib('libressl'): 71 if tls_max == '1.3': 72 exp_resumed = 'Initial' # 1.2 works in LibreSSL, but 1.3 does not, TODO 73 if env.curl_uses_lib('wolfssl'): 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': 81 pytest.skip('mbedtls TLSv1.3 session resume not working in 3.6.0') 82 83 curl = CurlClient(env=env) 84 # tell the server to close the connection after each request 85 urln = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo?'\ 86 f'id=[0-{count-1}]&close' 87 r = curl.http_download(urls=[urln], alpn_proto=proto, with_stats=True, 88 extra_args=xargs) 89 r.check_response(count=count, http_status=200) 90 # should have used one connection for each request, sessions after 91 # first should have been resumed 92 assert r.total_connects == count, r.dump_logs() 93 for i in range(count): 94 dfile = curl.download_file(i) 95 assert os.path.exists(dfile) 96 with open(dfile) as f: 97 djson = json.load(f) 98 assert djson['HTTPS'] == 'on', f'{i}: {djson}' 99 if i == 0: 100 assert djson['SSL_SESSION_RESUMED'] == 'Initial', f'{i}: {djson}' 101 else: 102 assert djson['SSL_SESSION_RESUMED'] == exp_resumed, f'{i}: {djson}' 103 104 # use host name with trailing dot, verify handshake 105 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 106 def test_17_03_trailing_dot(self, env: Env, proto): 107 if proto == 'h3' and not env.have_h3(): 108 pytest.skip("h3 not supported") 109 curl = CurlClient(env=env) 110 domain = f'{env.domain1}.' 111 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 112 r = curl.http_get(url=url, alpn_proto=proto) 113 assert r.exit_code == 0, f'{r}' 114 assert r.json, f'{r}' 115 if proto != 'h3': # we proxy h3 116 # the SNI the server received is without trailing dot 117 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 118 119 # use host name with double trailing dot, verify handshake 120 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 121 def test_17_04_double_dot(self, env: Env, proto): 122 if proto == 'h3' and not env.have_h3(): 123 pytest.skip("h3 not supported") 124 if proto == 'h3' and env.curl_uses_lib('wolfssl'): 125 pytest.skip("wolfSSL HTTP/3 peer verification does not properly check") 126 curl = CurlClient(env=env) 127 domain = f'{env.domain1}..' 128 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 129 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ 130 '-H', f'Host: {env.domain1}', 131 ]) 132 if r.exit_code == 0: 133 assert r.json, f'{r.stdout}' 134 # the SNI the server received is without trailing dot 135 if proto != 'h3': # we proxy h3 136 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 137 assert False, f'should not have succeeded: {r.json}' 138 # 7 - Rustls rejects a servername with .. during setup 139 # 35 - LibreSSL rejects setting an SNI name with trailing dot 140 # 60 - peer name matching failed against certificate 141 assert r.exit_code in [7, 35, 60], f'{r}' 142 143 # use ip address for connect 144 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 145 def test_17_05_ip_addr(self, env: Env, proto): 146 if env.curl_uses_lib('bearssl'): 147 pytest.skip("BearSSL does not support cert verification with IP addresses") 148 if env.curl_uses_lib('mbedtls'): 149 pytest.skip("mbedTLS does not support cert verification with IP addresses") 150 if proto == 'h3' and not env.have_h3(): 151 pytest.skip("h3 not supported") 152 curl = CurlClient(env=env) 153 domain = '127.0.0.1' 154 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 155 r = curl.http_get(url=url, alpn_proto=proto) 156 assert r.exit_code == 0, f'{r}' 157 assert r.json, f'{r}' 158 if proto != 'h3': # we proxy h3 159 # the SNI should not have been used 160 assert 'SSL_TLS_SNI' not in r.json, f'{r.json}' 161 162 # use localhost for connect 163 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 164 def test_17_06_localhost(self, env: Env, proto): 165 if proto == 'h3' and not env.have_h3(): 166 pytest.skip("h3 not supported") 167 curl = CurlClient(env=env) 168 domain = 'localhost' 169 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 170 r = curl.http_get(url=url, alpn_proto=proto) 171 assert r.exit_code == 0, f'{r}' 172 assert r.json, f'{r}' 173 if proto != 'h3': # we proxy h3 174 assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}' 175 176 @staticmethod 177 def gen_test_17_07_list(): 178 tls13_tests = [ 179 [None, True], 180 [['TLS_AES_128_GCM_SHA256'], True], 181 [['TLS_AES_256_GCM_SHA384'], False], 182 [['TLS_CHACHA20_POLY1305_SHA256'], True], 183 [['TLS_AES_256_GCM_SHA384', 184 'TLS_CHACHA20_POLY1305_SHA256'], True], 185 ] 186 tls12_tests = [ 187 [None, True], 188 [['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'], True], 189 [['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384'], False], 190 [['ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True], 191 [['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384', 192 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True], 193 ] 194 ret = [] 195 for tls_proto in ['TLSv1.3 +TLSv1.2', 'TLSv1.3', 'TLSv1.2']: 196 for [ciphers13, succeed13] in tls13_tests: 197 for [ciphers12, succeed12] in tls12_tests: 198 ret.append([tls_proto, ciphers13, ciphers12, succeed13, succeed12]) 199 return ret 200 201 @pytest.mark.parametrize("tls_proto, ciphers13, ciphers12, succeed13, succeed12", gen_test_17_07_list()) 202 def test_17_07_ssl_ciphers(self, env: Env, httpd, tls_proto, ciphers13, ciphers12, succeed13, succeed12): 203 # to test setting cipher suites, the AES 256 ciphers are disabled in the test server 204 httpd.set_extra_config('base', [ 205 'SSLCipherSuite SSL' 206 ' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256' 207 ':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305', 208 'SSLCipherSuite TLSv1.3' 209 ' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256', 210 f'SSLProtocol {tls_proto}' 211 ]) 212 httpd.reload_if_config_changed() 213 proto = 'http/1.1' 214 curl = CurlClient(env=env) 215 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' 216 # SSL backend specifics 217 if env.curl_uses_lib('gnutls'): 218 pytest.skip('GnuTLS does not support setting ciphers') 219 elif env.curl_uses_lib('boringssl'): 220 if ciphers13 is not None: 221 pytest.skip('BoringSSL does not support setting TLSv1.3 ciphers') 222 elif env.curl_uses_lib('schannel'): # not in CI, so untested 223 if ciphers12 is not None: 224 pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name') 225 elif env.curl_uses_lib('bearssl'): 226 if tls_proto == 'TLSv1.3': 227 pytest.skip('BearSSL does not support TLSv1.3') 228 tls_proto = 'TLSv1.2' 229 elif env.curl_uses_lib('sectransp'): # not in CI, so untested 230 if tls_proto == 'TLSv1.3': 231 pytest.skip('Secure Transport does not support TLSv1.3') 232 tls_proto = 'TLSv1.2' 233 # test 234 extra_args = ['--tls13-ciphers', ':'.join(ciphers13)] if ciphers13 else [] 235 extra_args += ['--ciphers', ':'.join(ciphers12)] if ciphers12 else [] 236 r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args) 237 if tls_proto != 'TLSv1.2' and succeed13: 238 assert r.exit_code == 0, r.dump_logs() 239 assert r.json['HTTPS'] == 'on', r.dump_logs() 240 assert r.json['SSL_PROTOCOL'] == 'TLSv1.3', r.dump_logs() 241 assert ciphers13 is None or r.json['SSL_CIPHER'] in ciphers13, r.dump_logs() 242 elif tls_proto == 'TLSv1.2' and succeed12: 243 assert r.exit_code == 0, r.dump_logs() 244 assert r.json['HTTPS'] == 'on', r.dump_logs() 245 assert r.json['SSL_PROTOCOL'] == 'TLSv1.2', r.dump_logs() 246 assert ciphers12 is None or r.json['SSL_CIPHER'] in ciphers12, r.dump_logs() 247 else: 248 assert r.exit_code != 0, r.dump_logs() 249 250 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 251 def test_17_08_cert_status(self, env: Env, proto): 252 if proto == 'h3' and not env.have_h3(): 253 pytest.skip("h3 not supported") 254 if not env.curl_uses_lib('openssl') and \ 255 not env.curl_uses_lib('gnutls') and \ 256 not env.curl_uses_lib('quictls'): 257 pytest.skip("TLS library does not support --cert-status") 258 curl = CurlClient(env=env) 259 domain = 'localhost' 260 url = f'https://{env.authority_for(domain, proto)}/' 261 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ 262 '--cert-status' 263 ]) 264 # CURLE_SSL_INVALIDCERTSTATUS, our certs have no OCSP info 265 assert r.exit_code == 91, f'{r}' 266 267 @staticmethod 268 def gen_test_17_09_list(): 269 return [[tls_proto, max_ver, min_ver] 270 for tls_proto in ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'] 271 for max_ver in range(5) 272 for min_ver in range(-2, 4)] 273 274 @pytest.mark.parametrize("tls_proto, max_ver, min_ver", gen_test_17_09_list()) 275 def test_17_09_ssl_min_max(self, env: Env, httpd, tls_proto, max_ver, min_ver): 276 httpd.set_extra_config('base', [ 277 f'SSLProtocol {tls_proto}', 278 'SSLCipherSuite ALL:@SECLEVEL=0', 279 ]) 280 httpd.reload_if_config_changed() 281 proto = 'http/1.1' 282 curl = CurlClient(env=env) 283 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' 284 # SSL backend specifics 285 if env.curl_uses_lib('bearssl'): 286 supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', None] 287 elif env.curl_uses_lib('sectransp'): # not in CI, so untested 288 supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', None] 289 elif env.curl_uses_lib('gnutls'): 290 supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'] 291 elif env.curl_uses_lib('quiche'): 292 supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'] 293 else: # most SSL backends dropped support for TLSv1.0, TLSv1.1 294 supported = [None, None, 'TLSv1.2', 'TLSv1.3'] 295 # test 296 extra_args = [[], ['--tlsv1'], ['--tlsv1.0'], ['--tlsv1.1'], ['--tlsv1.2'], ['--tlsv1.3']][min_ver+2] + \ 297 [['--tls-max', '1.0'], ['--tls-max', '1.1'], ['--tls-max', '1.2'], ['--tls-max', '1.3'], []][max_ver] 298 r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args) 299 if max_ver >= min_ver and tls_proto in supported[max(0, min_ver):min(max_ver, 3)+1]: 300 assert r.exit_code == 0 , r.dump_logs() 301 assert r.json['HTTPS'] == 'on', r.dump_logs() 302 assert r.json['SSL_PROTOCOL'] == tls_proto, r.dump_logs() 303 else: 304 assert r.exit_code != 0, r.dump_logs() 305