2
0

test_17_ssl_use.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. #
  27. import json
  28. import logging
  29. import os
  30. import re
  31. import pytest
  32. from testenv import Env, CurlClient, LocalClient
  33. log = logging.getLogger(__name__)
  34. class TestSSLUse:
  35. @pytest.fixture(autouse=True, scope='class')
  36. def _class_scope(self, env, httpd, nghttpx):
  37. env.make_data_file(indir=httpd.docs_dir, fname="data-10k", fsize=10*1024)
  38. if env.have_h3():
  39. nghttpx.start_if_needed()
  40. @pytest.fixture(autouse=True, scope='function')
  41. def _function_scope(self, request, env, httpd):
  42. httpd.clear_extra_configs()
  43. if 'httpd' not in request.node._fixtureinfo.argnames:
  44. httpd.reload_if_config_changed()
  45. def test_17_01_sslinfo_plain(self, env: Env, nghttpx, repeat):
  46. proto = 'http/1.1'
  47. curl = CurlClient(env=env)
  48. url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
  49. r = curl.http_get(url=url, alpn_proto=proto)
  50. assert r.json['HTTPS'] == 'on', f'{r.json}'
  51. assert 'SSL_SESSION_ID' in r.json, f'{r.json}'
  52. assert 'SSL_SESSION_RESUMED' in r.json, f'{r.json}'
  53. assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}'
  54. @pytest.mark.parametrize("tls_max", ['1.2', '1.3'])
  55. def test_17_02_sslinfo_reconnect(self, env: Env, tls_max):
  56. proto = 'http/1.1'
  57. count = 3
  58. exp_resumed = 'Resumed'
  59. xargs = ['--sessionid', '--tls-max', tls_max, f'--tlsv{tls_max}']
  60. if env.curl_uses_lib('libressl'):
  61. if tls_max == '1.3':
  62. exp_resumed = 'Initial' # 1.2 works in LibreSSL, but 1.3 does not, TODO
  63. if env.curl_uses_lib('rustls-ffi'):
  64. exp_resumed = 'Initial' # Rustls does not support sessions, TODO
  65. if env.curl_uses_lib('bearssl') and tls_max == '1.3':
  66. pytest.skip('BearSSL does not support TLSv1.3')
  67. if env.curl_uses_lib('mbedtls') and tls_max == '1.3' and \
  68. not env.curl_lib_version_at_least('mbedtls', '3.6.0'):
  69. pytest.skip('mbedtls TLSv1.3 session resume not working in 3.6.0')
  70. run_env = os.environ.copy()
  71. run_env['CURL_DEBUG'] = 'ssl'
  72. curl = CurlClient(env=env, run_env=run_env)
  73. # tell the server to close the connection after each request
  74. urln = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo?'\
  75. f'id=[0-{count-1}]&close'
  76. r = curl.http_download(urls=[urln], alpn_proto=proto, with_stats=True,
  77. extra_args=xargs)
  78. r.check_response(count=count, http_status=200)
  79. # should have used one connection for each request, sessions after
  80. # first should have been resumed
  81. assert r.total_connects == count, r.dump_logs()
  82. for i in range(count):
  83. dfile = curl.download_file(i)
  84. assert os.path.exists(dfile)
  85. with open(dfile) as f:
  86. djson = json.load(f)
  87. assert djson['HTTPS'] == 'on', f'{i}: {djson}'
  88. if i == 0:
  89. assert djson['SSL_SESSION_RESUMED'] == 'Initial', f'{i}: {djson}\n{r.dump_logs()}'
  90. else:
  91. assert djson['SSL_SESSION_RESUMED'] == exp_resumed, f'{i}: {djson}\n{r.dump_logs()}'
  92. # use host name with trailing dot, verify handshake
  93. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  94. def test_17_03_trailing_dot(self, env: Env, proto):
  95. if proto == 'h3' and not env.have_h3():
  96. pytest.skip("h3 not supported")
  97. curl = CurlClient(env=env)
  98. domain = f'{env.domain1}.'
  99. url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo'
  100. r = curl.http_get(url=url, alpn_proto=proto)
  101. assert r.exit_code == 0, f'{r}'
  102. assert r.json, f'{r}'
  103. if proto != 'h3': # we proxy h3
  104. # the SNI the server received is without trailing dot
  105. assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}'
  106. # use host name with double trailing dot, verify handshake
  107. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  108. def test_17_04_double_dot(self, env: Env, proto):
  109. if proto == 'h3' and not env.have_h3():
  110. pytest.skip("h3 not supported")
  111. curl = CurlClient(env=env)
  112. domain = f'{env.domain1}..'
  113. url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo'
  114. r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
  115. '-H', f'Host: {env.domain1}',
  116. ])
  117. if r.exit_code == 0:
  118. assert r.json, f'{r.stdout}'
  119. # the SNI the server received is without trailing dot
  120. if proto != 'h3': # we proxy h3
  121. assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}'
  122. assert False, f'should not have succeeded: {r.json}'
  123. # 7 - Rustls rejects a servername with .. during setup
  124. # 35 - LibreSSL rejects setting an SNI name with trailing dot
  125. # 60 - peer name matching failed against certificate
  126. assert r.exit_code in [7, 35, 60], f'{r}'
  127. # use ip address for connect
  128. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  129. def test_17_05_ip_addr(self, env: Env, proto):
  130. if env.curl_uses_lib('bearssl'):
  131. pytest.skip("BearSSL does not support cert verification with IP addresses")
  132. if env.curl_uses_lib('mbedtls'):
  133. pytest.skip("mbedTLS does use IP addresses in SNI")
  134. if proto == 'h3' and not env.have_h3():
  135. pytest.skip("h3 not supported")
  136. curl = CurlClient(env=env)
  137. domain = '127.0.0.1'
  138. url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo'
  139. r = curl.http_get(url=url, alpn_proto=proto)
  140. assert r.exit_code == 0, f'{r}'
  141. assert r.json, f'{r}'
  142. if proto != 'h3': # we proxy h3
  143. # the SNI should not have been used
  144. assert 'SSL_TLS_SNI' not in r.json, f'{r.json}'
  145. # use localhost for connect
  146. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  147. def test_17_06_localhost(self, env: Env, proto):
  148. if proto == 'h3' and not env.have_h3():
  149. pytest.skip("h3 not supported")
  150. curl = CurlClient(env=env)
  151. domain = 'localhost'
  152. url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo'
  153. r = curl.http_get(url=url, alpn_proto=proto)
  154. assert r.exit_code == 0, f'{r}'
  155. assert r.json, f'{r}'
  156. if proto != 'h3': # we proxy h3
  157. assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}'
  158. @staticmethod
  159. def gen_test_17_07_list():
  160. tls13_tests = [
  161. [None, True],
  162. [['TLS_AES_128_GCM_SHA256'], True],
  163. [['TLS_AES_256_GCM_SHA384'], False],
  164. [['TLS_CHACHA20_POLY1305_SHA256'], True],
  165. [['TLS_AES_256_GCM_SHA384',
  166. 'TLS_CHACHA20_POLY1305_SHA256'], True],
  167. ]
  168. tls12_tests = [
  169. [None, True],
  170. [['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'], True],
  171. [['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384'], False],
  172. [['ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
  173. [['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384',
  174. 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
  175. ]
  176. ret = []
  177. for tls_proto in ['TLSv1.3 +TLSv1.2', 'TLSv1.3', 'TLSv1.2']:
  178. for [ciphers13, succeed13] in tls13_tests:
  179. for [ciphers12, succeed12] in tls12_tests:
  180. ret.append([tls_proto, ciphers13, ciphers12, succeed13, succeed12])
  181. return ret
  182. @pytest.mark.parametrize("tls_proto, ciphers13, ciphers12, succeed13, succeed12", gen_test_17_07_list())
  183. def test_17_07_ssl_ciphers(self, env: Env, httpd, tls_proto, ciphers13, ciphers12, succeed13, succeed12):
  184. # to test setting cipher suites, the AES 256 ciphers are disabled in the test server
  185. httpd.set_extra_config('base', [
  186. 'SSLCipherSuite SSL'
  187. ' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
  188. ':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
  189. 'SSLCipherSuite TLSv1.3'
  190. ' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
  191. f'SSLProtocol {tls_proto}'
  192. ])
  193. httpd.reload_if_config_changed()
  194. proto = 'http/1.1'
  195. curl = CurlClient(env=env)
  196. url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
  197. # SSL backend specifics
  198. if env.curl_uses_lib('gnutls'):
  199. pytest.skip('GnuTLS does not support setting ciphers')
  200. elif env.curl_uses_lib('boringssl'):
  201. if ciphers13 is not None:
  202. pytest.skip('BoringSSL does not support setting TLSv1.3 ciphers')
  203. elif env.curl_uses_lib('schannel'): # not in CI, so untested
  204. if ciphers12 is not None:
  205. pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name')
  206. elif env.curl_uses_lib('bearssl'):
  207. if tls_proto == 'TLSv1.3':
  208. pytest.skip('BearSSL does not support TLSv1.3')
  209. tls_proto = 'TLSv1.2'
  210. elif env.curl_uses_lib('mbedtls') and not env.curl_lib_version_at_least('mbedtls', '3.6.0'):
  211. if tls_proto == 'TLSv1.3':
  212. pytest.skip('mbedTLS < 3.6.0 does not support TLSv1.3')
  213. elif env.curl_uses_lib('sectransp'): # not in CI, so untested
  214. if tls_proto == 'TLSv1.3':
  215. pytest.skip('Secure Transport does not support TLSv1.3')
  216. tls_proto = 'TLSv1.2'
  217. # test
  218. extra_args = ['--tls13-ciphers', ':'.join(ciphers13)] if ciphers13 else []
  219. extra_args += ['--ciphers', ':'.join(ciphers12)] if ciphers12 else []
  220. r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
  221. if tls_proto != 'TLSv1.2' and succeed13:
  222. assert r.exit_code == 0, r.dump_logs()
  223. assert r.json['HTTPS'] == 'on', r.dump_logs()
  224. assert r.json['SSL_PROTOCOL'] == 'TLSv1.3', r.dump_logs()
  225. assert ciphers13 is None or r.json['SSL_CIPHER'] in ciphers13, r.dump_logs()
  226. elif tls_proto == 'TLSv1.2' and succeed12:
  227. assert r.exit_code == 0, r.dump_logs()
  228. assert r.json['HTTPS'] == 'on', r.dump_logs()
  229. assert r.json['SSL_PROTOCOL'] == 'TLSv1.2', r.dump_logs()
  230. assert ciphers12 is None or r.json['SSL_CIPHER'] in ciphers12, r.dump_logs()
  231. else:
  232. assert r.exit_code != 0, r.dump_logs()
  233. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  234. def test_17_08_cert_status(self, env: Env, proto):
  235. if proto == 'h3' and not env.have_h3():
  236. pytest.skip("h3 not supported")
  237. if not env.curl_uses_lib('openssl') and \
  238. not env.curl_uses_lib('gnutls') and \
  239. not env.curl_uses_lib('quictls'):
  240. pytest.skip("TLS library does not support --cert-status")
  241. curl = CurlClient(env=env)
  242. domain = 'localhost'
  243. url = f'https://{env.authority_for(domain, proto)}/'
  244. r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
  245. '--cert-status'
  246. ])
  247. # CURLE_SSL_INVALIDCERTSTATUS, our certs have no OCSP info
  248. assert r.exit_code == 91, f'{r}'
  249. @staticmethod
  250. def gen_test_17_09_list():
  251. return [[tls_proto, max_ver, min_ver]
  252. for tls_proto in ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']
  253. for max_ver in range(5)
  254. for min_ver in range(-2, 4)]
  255. @pytest.mark.parametrize("tls_proto, max_ver, min_ver", gen_test_17_09_list())
  256. def test_17_09_ssl_min_max(self, env: Env, httpd, tls_proto, max_ver, min_ver):
  257. httpd.set_extra_config('base', [
  258. f'SSLProtocol {tls_proto}',
  259. 'SSLCipherSuite ALL:@SECLEVEL=0',
  260. ])
  261. httpd.reload_if_config_changed()
  262. proto = 'http/1.1'
  263. run_env = os.environ.copy()
  264. if env.curl_uses_lib('gnutls'):
  265. # we need to override any default system configuration since
  266. # we want to test all protocol versions. Ubuntu (or the GH image)
  267. # disable TSL1.0 and TLS1.1 system wide. We do not want.
  268. our_config = os.path.join(env.gen_dir, 'gnutls_config')
  269. if not os.path.exists(our_config):
  270. with open(our_config, 'w') as fd:
  271. fd.write('# empty\n')
  272. run_env['GNUTLS_SYSTEM_PRIORITY_FILE'] = our_config
  273. curl = CurlClient(env=env, run_env=run_env)
  274. url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
  275. # SSL backend specifics
  276. if env.curl_uses_lib('bearssl'):
  277. supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', None]
  278. elif env.curl_uses_lib('sectransp'): # not in CI, so untested
  279. supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', None]
  280. elif env.curl_uses_lib('gnutls'):
  281. supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']
  282. elif env.curl_uses_lib('quiche'):
  283. supported = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']
  284. else: # most SSL backends dropped support for TLSv1.0, TLSv1.1
  285. supported = [None, None, 'TLSv1.2', 'TLSv1.3']
  286. # test
  287. extra_args = [[], ['--tlsv1'], ['--tlsv1.0'], ['--tlsv1.1'], ['--tlsv1.2'], ['--tlsv1.3']][min_ver+2] + \
  288. [['--tls-max', '1.0'], ['--tls-max', '1.1'], ['--tls-max', '1.2'], ['--tls-max', '1.3'], []][max_ver]
  289. extra_args.extend(['--trace-config', 'ssl'])
  290. r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
  291. if max_ver >= min_ver and tls_proto in supported[max(0, min_ver):min(max_ver, 3)+1]:
  292. assert r.exit_code == 0, f'extra_args={extra_args}\n{r.dump_logs()}'
  293. assert r.json['HTTPS'] == 'on', r.dump_logs()
  294. assert r.json['SSL_PROTOCOL'] == tls_proto, r.dump_logs()
  295. else:
  296. assert r.exit_code != 0, f'extra_args={extra_args}\n{r.dump_logs()}'
  297. def test_17_10_h3_session_reuse(self, env: Env, httpd, nghttpx):
  298. if not env.have_h3():
  299. pytest.skip("h3 not supported")
  300. if not env.curl_uses_lib('quictls') and \
  301. not env.curl_uses_lib('gnutls') and \
  302. not env.curl_uses_lib('wolfssl'):
  303. pytest.skip("QUIC session reuse not implemented")
  304. count = 2
  305. docname = 'data-10k'
  306. url = f'https://localhost:{env.https_port}/{docname}'
  307. client = LocalClient(name='hx-download', env=env)
  308. if not client.exists():
  309. pytest.skip(f'example client not built: {client.name}')
  310. r = client.run(args=[
  311. '-n', f'{count}',
  312. '-f', # forbid reuse of connections
  313. '-r', f'{env.domain1}:{env.port_for("h3")}:127.0.0.1',
  314. '-V', 'h3', url
  315. ])
  316. r.check_exit_code(0)
  317. # check that TLS session was reused as expected
  318. reused_session = False
  319. for line in r.trace_lines:
  320. m = re.match(r'\[1-1] \* SSL reusing session.*', line)
  321. if m:
  322. reused_session = True
  323. assert reused_session, f'{r}\n{r.dump_logs()}'
  324. # use host name server has no certificate for
  325. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  326. def test_17_11_wrong_host(self, env: Env, proto):
  327. if proto == 'h3' and not env.have_h3():
  328. pytest.skip("h3 not supported")
  329. curl = CurlClient(env=env)
  330. domain = f'insecure.{env.tld}'
  331. url = f'https://{domain}:{env.port_for(proto)}/curltest/sslinfo'
  332. r = curl.http_get(url=url, alpn_proto=proto)
  333. assert r.exit_code == 60, f'{r}'
  334. # use host name server has no cert for with --insecure
  335. @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
  336. def test_17_12_insecure(self, env: Env, proto):
  337. if proto == 'h3' and not env.have_h3():
  338. pytest.skip("h3 not supported")
  339. curl = CurlClient(env=env)
  340. domain = f'insecure.{env.tld}'
  341. url = f'https://{domain}:{env.port_for(proto)}/curltest/sslinfo'
  342. r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
  343. '--insecure'
  344. ])
  345. assert r.exit_code == 0, f'{r}'
  346. assert r.json, f'{r}'
  347. # connect to an expired certificate
  348. @pytest.mark.parametrize("proto", ['http/1.1', 'h2'])
  349. def test_17_14_expired_cert(self, env: Env, proto):
  350. if proto == 'h3' and not env.have_h3():
  351. pytest.skip("h3 not supported")
  352. curl = CurlClient(env=env)
  353. url = f'https://{env.expired_domain}:{env.port_for(proto)}/'
  354. r = curl.http_get(url=url, alpn_proto=proto)
  355. assert r.exit_code == 60, f'{r}' # peer failed verification
  356. exp_trace = None
  357. match_trace = None
  358. if env.curl_uses_lib('openssl') or env.curl_uses_lib('quictls'):
  359. exp_trace = r'.*SSL certificate problem: certificate has expired$'
  360. elif env.curl_uses_lib('gnutls'):
  361. exp_trace = r'.*server verification failed: certificate has expired\..*'
  362. elif env.curl_uses_lib('wolfssl'):
  363. exp_trace = r'.*server verification failed: certificate has expired\.$'
  364. if exp_trace is not None:
  365. for line in r.trace_lines:
  366. if re.match(exp_trace, line):
  367. match_trace = line
  368. break
  369. assert match_trace, f'Did not find "{exp_trace}" in trace\n{r.dump_logs()}'