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': 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, httpd, nghttpx, repeat, proto): 107 if env.curl_uses_lib('gnutls'): 108 pytest.skip("gnutls does not match hostnames with trailing dot") 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) 115 assert r.exit_code == 0, f'{r}' 116 assert r.json, f'{r}' 117 if proto != 'h3': # we proxy h3 118 # the SNI the server received is without trailing dot 119 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 120 121 # use host name with double trailing dot, verify handshake 122 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 123 def test_17_04_double_dot(self, env: Env, httpd, nghttpx, repeat, proto): 124 if proto == 'h3' and not env.have_h3(): 125 pytest.skip("h3 not supported") 126 if proto == 'h3' and env.curl_uses_lib('wolfssl'): 127 pytest.skip("wolfSSL HTTP/3 peer verification does not properly check") 128 curl = CurlClient(env=env) 129 domain = f'{env.domain1}..' 130 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 131 r = curl.http_get(url=url, alpn_proto=proto, extra_args=[ 132 '-H', f'Host: {env.domain1}', 133 ]) 134 if r.exit_code == 0: 135 assert r.json, f'{r.stdout}' 136 # the SNI the server received is without trailing dot 137 if proto != 'h3': # we proxy h3 138 assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}' 139 assert False, f'should not have succeeded: {r.json}' 140 # 7 - rustls rejects a servername with .. during setup 141 # 35 - libressl rejects setting an SNI name with trailing dot 142 # 60 - peer name matching failed against certificate 143 assert r.exit_code in [7, 35, 60], f'{r}' 144 145 # use ip address for connect 146 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 147 def test_17_05_ip_addr(self, env: Env, httpd, nghttpx, repeat, proto): 148 if env.curl_uses_lib('bearssl'): 149 pytest.skip("bearssl does not support cert verification with IP addresses") 150 if env.curl_uses_lib('mbedtls'): 151 pytest.skip("mbedtls does not support cert verification with IP addresses") 152 if proto == 'h3' and not env.have_h3(): 153 pytest.skip("h3 not supported") 154 curl = CurlClient(env=env) 155 domain = f'127.0.0.1' 156 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 157 r = curl.http_get(url=url, alpn_proto=proto) 158 assert r.exit_code == 0, f'{r}' 159 assert r.json, f'{r}' 160 if proto != 'h3': # we proxy h3 161 # the SNI should not have been used 162 assert 'SSL_TLS_SNI' not in r.json, f'{r.json}' 163 164 # use localhost for connect 165 @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 166 def test_17_06_localhost(self, env: Env, httpd, nghttpx, repeat, proto): 167 if proto == 'h3' and not env.have_h3(): 168 pytest.skip("h3 not supported") 169 curl = CurlClient(env=env) 170 domain = f'localhost' 171 url = f'https://{env.authority_for(domain, proto)}/curltest/sslinfo' 172 r = curl.http_get(url=url, alpn_proto=proto) 173 assert r.exit_code == 0, f'{r}' 174 assert r.json, f'{r}' 175 if proto != 'h3': # we proxy h3 176 assert r.json['SSL_TLS_SNI'] == domain, f'{r.json}' 177 178 # test setting cipher suites, the AES 256 ciphers are disabled in the test server 179 @pytest.mark.parametrize("ciphers, succeed", [ 180 [[0x1301], True], 181 [[0x1302], False], 182 [[0x1303], True], 183 [[0x1302, 0x1303], True], 184 [[0xC02B, 0xC02F], True], 185 [[0xC02C, 0xC030], False], 186 [[0xCCA9, 0xCCA8], True], 187 [[0xC02C, 0xC030, 0xCCA9, 0xCCA8], True], 188 ]) 189 def test_17_07_ssl_ciphers(self, env: Env, httpd, nghttpx, ciphers, succeed, repeat): 190 cipher_table = { 191 0x1301: 'TLS_AES_128_GCM_SHA256', 192 0x1302: 'TLS_AES_256_GCM_SHA384', 193 0x1303: 'TLS_CHACHA20_POLY1305_SHA256', 194 0xC02B: 'ECDHE-ECDSA-AES128-GCM-SHA256', 195 0xC02F: 'ECDHE-RSA-AES128-GCM-SHA256', 196 0xC02C: 'ECDHE-ECDSA-AES256-GCM-SHA384', 197 0xC030: 'ECDHE-RSA-AES256-GCM-SHA384', 198 0xCCA9: 'ECDHE-ECDSA-CHACHA20-POLY1305', 199 0xCCA8: 'ECDHE-RSA-CHACHA20-POLY1305', 200 } 201 cipher_names = list(map(cipher_table.get, ciphers)) 202 proto = 'http/1.1' 203 curl = CurlClient(env=env) 204 url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' 205 extra_args = [] 206 if env.curl_uses_lib('gnutls'): 207 pytest.skip('gnutls does not support setting ciphers by name') 208 if env.curl_uses_lib('rustls-ffi'): 209 pytest.skip('rustls-ffi does not support setting ciphers') 210 if ciphers[0] & 0xFF00 == 0x1300: 211 # test setting TLSv1.3 ciphers 212 if env.curl_uses_lib('bearssl'): 213 pytest.skip('bearssl does not support TLSv1.3') 214 elif env.curl_uses_lib('sectransp'): 215 pytest.skip('sectransp does not support TLSv1.3') 216 elif env.curl_uses_lib('boringssl'): 217 pytest.skip('boringssl does not support setting TLSv1.3 ciphers') 218 elif env.curl_uses_lib('mbedtls'): 219 if not env.curl_lib_version_at_least('mbedtls', '3.6.0'): 220 pytest.skip('mbedtls TLSv1.3 support requires at least 3.6.0') 221 extra_args = ['--ciphers', ':'.join(cipher_names)] 222 elif env.curl_uses_lib('wolfssl'): 223 extra_args = ['--ciphers', ':'.join(cipher_names)] 224 else: 225 extra_args = ['--tls13-ciphers', ':'.join(cipher_names)] 226 else: 227 # test setting TLSv1.2 ciphers 228 if env.curl_uses_lib('schannel'): 229 pytest.skip('schannel does not support setting TLSv1.2 ciphers by name') 230 elif env.curl_uses_lib('wolfssl'): 231 # setting tls version is botched with wolfssl: setting max (--tls-max) 232 # is not supported, setting min (--tlsv1.*) actually also sets max 233 extra_args = ['--tlsv1.2', '--ciphers', ':'.join(cipher_names)] 234 else: 235 # the server supports TLSv1.3, so to test TLSv1.2 ciphers we set tls-max 236 extra_args = ['--tls-max', '1.2', '--ciphers', ':'.join(cipher_names)] 237 r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args) 238 if succeed: 239 assert r.exit_code == 0, f'{r}' 240 assert r.json['HTTPS'] == 'on', f'{r.json}' 241 assert 'SSL_CIPHER' in r.json, f'{r.json}' 242 assert r.json['SSL_CIPHER'] in cipher_names, f'{r.json}' 243 else: 244 assert r.exit_code != 0, f'{r}' 245