curl-curl/tests/http/test_17_ssl_use.py
Stefan Eissing 065b149df0
OpenSSL: check reuse of sessions for verify status
OpenSSL records its peer verification status inside its SSL_SESSION
objects. When a session is later reused, the SSL connection inherits
this verify status.

Session keys prevent reuse of sessions between connections that verify
the peer and those who do not. However, when Apple SecTrust is used
to verify a connection, this does not update the Sessions verify
status (and there is no setter). On session reuse, OpenSSL fails
the verification and Apple SecTrust cannot verify either since the
certificate peer chain is not available.

Fix this by checking the verification status on session reuse and
remove the session again if the peer needs to be verified, but the
session is not.

Reported-by: Christian Schmitza
Fixes #20435
Closes #20446
2026-01-27 14:03:47 +01:00

605 lines
29 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
#
import json
import logging
import os
import re
import pytest
from testenv import Env, CurlClient, LocalClient
log = logging.getLogger(__name__)
class TLSDefs:
TLS_VERSIONS = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']
TLS_VERSION_IDS = {
'TLSv1': 0x301,
'TLSv1.1': 0x302,
'TLSv1.2': 0x303,
'TLSv1.3': 0x304
}
CURL_ARG_MIN_VERSION_ID = {
'none': 0x0,
'tlsv1': 0x301,
'tlsv1.0': 0x301,
'tlsv1.1': 0x302,
'tlsv1.2': 0x303,
'tlsv1.3': 0x304,
}
CURL_ARG_MAX_VERSION_ID = {
'none': 0x0,
'1.0': 0x301,
'1.1': 0x302,
'1.2': 0x303,
'1.3': 0x304,
}
class TestSSLUse:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd, nghttpx):
env.make_data_file(indir=httpd.docs_dir, fname="data-10k", fsize=10*1024)
def test_17_01_sslinfo_plain(self, env: Env, httpd):
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto)
assert r.json['HTTPS'] == 'on', f'{r.json}'
assert 'SSL_SESSION_ID' in r.json, f'{r.json}'
assert 'SSL_SESSION_RESUMED' in r.json, f'{r.json}'
assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}'
@pytest.mark.parametrize("tls_max", ['1.2', '1.3'])
def test_17_02_sslinfo_reconnect(self, env: Env, tls_max, httpd):
proto = 'http/1.1'
count = 3
exp_resumed = 'Resumed'
xargs = ['--sessionid', '--tls-max', tls_max, f'--tlsv{tls_max}']
if env.curl_uses_lib('libressl'):
if tls_max == '1.3':
exp_resumed = 'Initial' # 1.2 works in LibreSSL, but 1.3 does not, TODO
if env.curl_uses_lib('rustls-ffi'):
exp_resumed = 'Initial' # Rustls does not support sessions, TODO
if env.curl_uses_lib('mbedtls') and tls_max == '1.3' and \
not env.curl_lib_version_at_least('mbedtls', '3.6.0'):
pytest.skip('mbedtls TLSv1.3 session resume not working in 3.6.0')
run_env = os.environ.copy()
run_env['CURL_DEBUG'] = 'ssl'
curl = CurlClient(env=env, run_env=run_env)
# tell the server to close the connection after each request
urln = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo?'\
f'id=[0-{count-1}]&close'
r = curl.http_download(urls=[urln], alpn_proto=proto, with_stats=True,
extra_args=xargs)
r.check_response(count=count, http_status=200)
# should have used one connection for each request, sessions after
# first should have been resumed
assert r.total_connects == count, r.dump_logs()
for i in range(count):
dfile = curl.download_file(i)
assert os.path.exists(dfile)
with open(dfile) as f:
djson = json.load(f)
assert djson['HTTPS'] == 'on', f'{i}: {djson}'
if i == 0:
assert djson['SSL_SESSION_RESUMED'] == 'Initial', f'{i}: {djson}\n{r.dump_logs()}'
else:
assert djson['SSL_SESSION_RESUMED'] == exp_resumed, f'{i}: {djson}\n{r.dump_logs()}'
# use hostname with trailing dot, verify handshake
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_03_trailing_dot(self, env: Env, proto, httpd, nghttpx):
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 hostname with double trailing dot, verify handshake
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_04_double_dot(self, env: Env, proto, httpd, nghttpx):
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", Env.http_protos())
def test_17_05_good_ip_addr(self, env: Env, proto, httpd, nghttpx):
if env.curl_uses_lib('mbedtls'):
pytest.skip("mbedTLS does use IP addresses in SNI")
curl = CurlClient(env=env)
domain = '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 IP address that is not in cert
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_05_bad_ip_addr(self, env: Env, proto,
httpd, configures_httpd,
nghttpx, configures_nghttpx):
httpd.set_domain1_cred_name('domain1-no-ip')
httpd.reload_if_config_changed()
if proto == 'h3':
nghttpx.set_cred_name('domain1-no-ip')
nghttpx.reload_if_config_changed()
curl = CurlClient(env=env)
url = f'https://127.0.0.1:{env.port_for(proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto)
assert r.exit_code == 60, f'{r}'
# use IP address that is in cert as DNS name (not really legal)
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_05_very_bad_ip_addr(self, env: Env, proto,
httpd, configures_httpd,
nghttpx, configures_nghttpx):
if env.curl_uses_lib('mbedtls'):
pytest.skip("mbedtls falsely verifies a DNS: altname as IP address")
if env.curl_uses_lib('wolfssl') and \
env.curl_lib_version_before('wolfssl', '5.8.4'):
pytest.skip("wolfSSL falsely verifies a DNS: altname as IP address in 5.8.2 and before")
httpd.set_domain1_cred_name('domain1-very-bad')
httpd.reload_if_config_changed()
if proto == 'h3':
nghttpx.set_cred_name('domain1-very-bad')
nghttpx.reload_if_config_changed()
curl = CurlClient(env=env)
url = f'https://127.0.0.1:{env.port_for(proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto)
assert r.exit_code == 60, f'{r}'
# use localhost for connect
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_06_localhost(self, env: Env, proto, httpd, nghttpx):
curl = CurlClient(env=env)
domain = '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}'
@staticmethod
def gen_test_17_07_list():
tls13_tests = [
['def', None, True],
['AES128SHA256', ['TLS_AES_128_GCM_SHA256'], True],
['AES128SHA384', ['TLS_AES_256_GCM_SHA384'], False],
['CHACHA20SHA256', ['TLS_CHACHA20_POLY1305_SHA256'], True],
['AES128SHA384+CHACHA20SHA256', ['TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256'], True],
]
tls12_tests = [
['def', None, True],
['AES128ish', ['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'], True],
['AES256ish', ['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384'], False],
['CHACHA20ish', ['ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
['AES256ish+CHACHA20ish', ['ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305'], True],
]
ret = []
for tls_id, tls_proto in {
'TLSv1.2+3': 'TLSv1.3 +TLSv1.2',
'TLSv1.3': 'TLSv1.3',
'TLSv1.2': 'TLSv1.2'}.items():
for [cid13, ciphers13, succeed13] in tls13_tests:
for [cid12, ciphers12, succeed12] in tls12_tests:
id = f'{tls_id}-{cid13}-{cid12}'
ret.append(pytest.param(tls_proto, ciphers13, ciphers12, succeed13, succeed12, id=id))
return ret
@pytest.mark.parametrize(
"tls_proto, ciphers13, ciphers12, succeed13, succeed12",
gen_test_17_07_list())
def test_17_07_ssl_ciphers(self, env: Env, httpd, configures_httpd,
tls_proto, ciphers13, ciphers12,
succeed13, succeed12):
# to test setting cipher suites, the AES 256 ciphers are disabled in the test server
httpd.set_extra_config('base', [
'SSLCipherSuite SSL'
' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
'SSLCipherSuite TLSv1.3'
' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
f'SSLProtocol {tls_proto}'
])
httpd.reload_if_config_changed()
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
# SSL backend specifics
# see wolfSSL/wolfssl#9462
if env.curl_uses_lib('wolfssl') and env.curl_lib_version('wolfssl') == '5.8.4' \
and ciphers13 and 'TLS_CHACHA20_POLY1305_SHA256' in ciphers13:
pytest.skip('wolfSSL 5.8.4 is borked on ARM with CHACHA20')
if env.curl_uses_lib('gnutls'):
pytest.skip('GnuTLS does not support setting ciphers')
elif env.curl_uses_lib('boringssl'):
if ciphers13 is not None:
pytest.skip('BoringSSL does not support setting TLSv1.3 ciphers')
elif env.curl_uses_lib('schannel'): # not in CI, so untested
if ciphers12 is not None:
pytest.skip('Schannel does not support setting TLSv1.2 ciphers by name')
elif env.curl_uses_lib('mbedtls') and not env.curl_lib_version_at_least('mbedtls', '3.6.0'):
if tls_proto == 'TLSv1.3':
pytest.skip('mbedTLS < 3.6.0 does not support TLSv1.3')
# test
extra_args = ['--tls13-ciphers', ':'.join(ciphers13)] if ciphers13 else []
extra_args += ['--ciphers', ':'.join(ciphers12)] if ciphers12 else []
r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
if tls_proto != 'TLSv1.2' and succeed13:
assert r.exit_code == 0, r.dump_logs()
assert r.json['HTTPS'] == 'on', r.dump_logs()
assert r.json['SSL_PROTOCOL'] == 'TLSv1.3', r.dump_logs()
assert ciphers13 is None or r.json['SSL_CIPHER'] in ciphers13, r.dump_logs()
elif tls_proto == 'TLSv1.2' and succeed12:
assert r.exit_code == 0, r.dump_logs()
assert r.json['HTTPS'] == 'on', r.dump_logs()
assert r.json['SSL_PROTOCOL'] == 'TLSv1.2', r.dump_logs()
assert ciphers12 is None or r.json['SSL_CIPHER'] in ciphers12, r.dump_logs()
else:
assert r.exit_code != 0, r.dump_logs()
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_08_cert_status(self, env: Env, proto, httpd, nghttpx):
if not env.curl_can_cert_status():
pytest.skip("TLS library does not support --cert-status")
curl = CurlClient(env=env)
domain = 'localhost'
url = f'https://{env.authority_for(domain, proto)}/'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--cert-status'
])
# CURLE_SSL_INVALIDCERTSTATUS, our certs have no OCSP info
assert r.exit_code == 91, f'{r}'
@staticmethod
def gen_test_17_09_list():
return [
[server_tls, min_arg, max_arg]
for server_tls in TLSDefs.TLS_VERSIONS
for min_arg in TLSDefs.CURL_ARG_MIN_VERSION_ID
for max_arg in TLSDefs.CURL_ARG_MAX_VERSION_ID
]
@pytest.mark.parametrize("server_tls, min_arg, max_arg", gen_test_17_09_list())
def test_17_09_ssl_min_max(self, env: Env, httpd, configures_httpd, server_tls, min_arg, max_arg):
# We test if curl using min/max versions arguments (and defaults) can connect
# to a server using 'server_tls' version only
httpd.set_extra_config('base', [
f'SSLProtocol {server_tls}',
'SSLCipherSuite ALL:@SECLEVEL=0',
])
httpd.reload_if_config_changed()
# curl's TLS backend supported version
if env.curl_uses_lib('gnutls') or \
env.curl_uses_lib('quiche') or \
env.curl_uses_lib('aws-lc') or \
env.curl_uses_lib('boringssl'):
curl_supported = [0x301, 0x302, 0x303, 0x304]
elif env.curl_uses_lib('openssl') and \
env.curl_lib_version_before('openssl', '3.0.0'):
curl_supported = [0x301, 0x302, 0x303, 0x304]
else: # most SSL backends dropped support for TLSv1.0, TLSv1.1
curl_supported = [0x303, 0x304]
extra_args = ['--trace-config', 'ssl']
# determine effective min/max version used by curl with these args
if max_arg != 'none':
extra_args.extend(['--tls-max', max_arg])
curl_max_ver = TLSDefs.CURL_ARG_MAX_VERSION_ID[max_arg]
else:
curl_max_ver = max(TLSDefs.TLS_VERSION_IDS.values())
if min_arg != 'none':
extra_args.append(f'--{min_arg}')
curl_min_ver = TLSDefs.CURL_ARG_MIN_VERSION_ID[min_arg]
else:
curl_min_ver = min(0x303, curl_max_ver) # TLSv1.2 is the default now
# collect all versions that curl is allowed with this command lines and supports
curl_allowed = [tid for tid in sorted(TLSDefs.TLS_VERSION_IDS.values())
if curl_min_ver <= tid <= curl_max_ver and
tid in curl_supported]
# we expect a successful transfer, when the server TLS version is allowed
server_ver = TLSDefs.TLS_VERSION_IDS[server_tls]
# do the transfer
proto = 'http/1.1'
run_env = os.environ.copy()
if env.curl_uses_lib('gnutls'):
# we need to override any default system configuration since
# we want to test all protocol versions. Ubuntu (or the GH image)
# disable TSL1.0 and TLS1.1 system wide. We do not want.
our_config = os.path.join(env.gen_dir, 'gnutls_config')
if not os.path.exists(our_config):
with open(our_config, 'w') as fd:
fd.write('# empty\n')
run_env['GNUTLS_SYSTEM_PRIORITY_FILE'] = our_config
curl = CurlClient(env=env, run_env=run_env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
if server_ver in curl_allowed:
assert r.exit_code == 0, f'should succeed, server={server_ver:04x}, curl=[{curl_min_ver:04x}, {curl_max_ver:04x}], allowed={curl_allowed}\n{r.dump_logs()}'
assert r.json['HTTPS'] == 'on', r.dump_logs()
assert r.json['SSL_PROTOCOL'] == server_tls, r.dump_logs()
else:
assert r.exit_code != 0, f'should fail, server={server_ver:04x}, curl=[{curl_min_ver:04x}, {curl_max_ver:04x}]\n{r.dump_logs()}'
@pytest.mark.skipif(condition=not Env.curl_is_debug(), reason="needs curl debug")
@pytest.mark.skipif(condition=not Env.curl_is_verbose(), reason="needs curl verbose strings")
def test_17_10_h3_session_reuse(self, env: Env, httpd, nghttpx):
if not env.have_h3():
pytest.skip("h3 not supported")
if not env.curl_uses_lib('quictls') and \
not env.curl_uses_lib('openssl') and \
not env.curl_uses_lib('gnutls') and \
not env.curl_uses_lib('wolfssl'):
pytest.skip("QUIC session reuse not implemented")
count = 2
docname = 'data-10k'
url = f'https://localhost:{env.https_port}/{docname}'
client = LocalClient(name='cli_hx_download', env=env)
if not client.exists():
pytest.skip(f'example client not built: {client.name}')
r = client.run(args=[
'-n', f'{count}',
'-f', # forbid reuse of connections
'-C', env.ca.cert_file,
'-r', f'{env.domain1}:{env.port_for("h3")}:127.0.0.1',
'-V', 'h3', url
])
r.check_exit_code(0)
# check that TLS session was reused as expected
reused_session = False
for line in r.trace_lines:
if re.match(r'.*\[1-1] (\* )?SSL reusing session.*', line):
reused_session = True
assert reused_session, f'{r}\n{r.dump_logs()}'
# use hostname server has no certificate for
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_11_wrong_host(self, env: Env, proto, httpd, nghttpx):
curl = CurlClient(env=env)
domain = f'insecure.{env.tld}'
url = f'https://{domain}:{env.port_for(proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto)
assert r.exit_code == 60, f'{r}'
# use hostname server has no cert for with --insecure
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_12_insecure(self, env: Env, proto, httpd, nghttpx):
curl = CurlClient(env=env)
domain = f'insecure.{env.tld}'
url = f'https://{domain}:{env.port_for(proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--insecure'
])
assert r.exit_code == 0, f'{r}'
assert r.json, f'{r}'
# connect to an expired certificate
@pytest.mark.parametrize("proto", Env.http_h1_h2_protos())
def test_17_14_expired_cert(self, env: Env, proto, httpd):
curl = CurlClient(env=env)
url = f'https://{env.expired_domain}:{env.port_for(proto)}/'
r = curl.http_get(url=url, alpn_proto=proto)
assert r.exit_code == 60, f'{r}' # peer failed verification
exp_trace = None
match_trace = None
if env.curl_uses_lib('openssl') or env.curl_uses_lib('quictls'):
exp_trace = r'.*SSL certificate OpenSSL verify result: certificate has expired.*$'
elif env.curl_uses_lib('gnutls'):
exp_trace = r'.*SSL certificate verification failed: certificate has expired\..*'
elif env.curl_uses_lib('wolfssl'):
exp_trace = r'.*server verification failed: certificate has expired\.$'
if exp_trace is not None:
for line in r.trace_lines:
if re.match(exp_trace, line):
match_trace = line
break
assert match_trace, f'Did not find "{exp_trace}" in trace\n{r.dump_logs()}'
@pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'),
reason='curl lacks SSL session export support')
def test_17_15_session_export(self, env: Env, httpd):
proto = 'http/1.1'
if env.curl_uses_lib('libressl'):
pytest.skip('Libressl resumption does not work inTLSv1.3')
if env.curl_uses_lib('rustls-ffi'):
pytest.skip('rustsls does not expose sessions')
if env.curl_uses_lib('mbedtls') and \
not env.curl_lib_version_at_least('mbedtls', '3.6.0'):
pytest.skip('mbedtls TLSv1.3 session resume not working before 3.6.0')
run_env = os.environ.copy()
run_env['CURL_DEBUG'] = 'ssl,ssls'
# clean session file first, then reuse
session_file = os.path.join(env.gen_dir, 'test_17_15.sessions')
if os.path.exists(session_file):
return os.remove(session_file)
xargs = ['--tls-max', '1.3', '--tlsv1.3', '--ssl-sessions', session_file]
curl = CurlClient(env=env, run_env=run_env)
# tell the server to close the connection after each request
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=xargs)
assert r.exit_code == 0, f'{r}'
assert r.json['HTTPS'] == 'on', f'{r.json}'
assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}\n{r.dump_logs()}'
# ok, run again, sessions should be imported
run_dir2 = os.path.join(env.gen_dir, 'curl2')
curl = CurlClient(env=env, run_env=run_env, run_dir=run_dir2)
r = curl.http_get(url=url, alpn_proto=proto, extra_args=xargs)
assert r.exit_code == 0, f'{r}'
assert r.json['SSL_SESSION_RESUMED'] == 'Resumed', f'{r.json}\n{r.dump_logs()}'
# verify the ciphers are ignored when talking TLSv1.3 only
# see issue #16232
def test_17_16_h3_ignore_ciphers12(self, env: Env, httpd, nghttpx):
proto = 'h3'
if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported")
if env.curl_uses_lib('gnutls'):
pytest.skip("gnutls does not ignore --ciphers on TLSv1.3")
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--ciphers', 'NONSENSE'
])
assert r.exit_code == 0, f'{r}'
def test_17_17_h1_ignore_ciphers13(self, env: Env, httpd):
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--tls13-ciphers', 'NONSENSE', '--tls-max', '1.2'
])
assert r.exit_code == 0, f'{r}'
@pytest.mark.parametrize("priority, tls_proto, ciphers, success", [
pytest.param("", "", [], False, id='prio-empty'),
pytest.param("NONSENSE", "", [], False, id='nonsense'),
pytest.param("+NONSENSE", "", [], False, id='+nonsense'),
pytest.param("NORMAL:-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-CHACHA20-POLY1305'], True, id='TLSv1.2-normal-only'),
pytest.param("-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-CHACHA20-POLY1305'], True, id='TLSv1.2-only'),
pytest.param("NORMAL", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-normal'),
pytest.param("NORMAL:-VERS-ALL:+VERS-TLS1.3", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-normal-only'),
pytest.param("-VERS-ALL:+VERS-TLS1.3", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-only'),
pytest.param("!CHACHA20-POLY1305", "TLSv1.3", ['TLS_AES_128_GCM_SHA256'], True, id='TLSv1.3-no-chacha'),
pytest.param("-CIPHER-ALL:+CHACHA20-POLY1305", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-only-chacha'),
pytest.param("-CIPHER-ALL:+AES-256-GCM", "", [], False, id='only-AES256'),
pytest.param("-CIPHER-ALL:+AES-128-GCM", "TLSv1.3", ['TLS_AES_128_GCM_SHA256'], True, id='TLSv1.3-only-AES128'),
pytest.param("SECURE:-CIPHER-ALL:+AES-128-GCM:-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-AES128-GCM-SHA256'], True, id='TLSv1.2-secure'),
pytest.param("-MAC-ALL:+SHA256", "", [], False, id='MAC-only-SHA256'),
pytest.param("-MAC-ALL:+AEAD", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-MAC-only-AEAD'),
pytest.param("-GROUP-ALL:+GROUP-X25519", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True, id='TLSv1.3-group-only-X25519'),
pytest.param("-GROUP-ALL:+GROUP-SECP192R1", "", [], False, id='group-only-SECP192R1'),
])
def test_17_18_gnutls_priority(self, env: Env, httpd, configures_httpd, priority, tls_proto, ciphers, success):
# to test setting cipher suites, the AES 256 ciphers are disabled in the test server
httpd.set_extra_config('base', [
'SSLCipherSuite SSL'
' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
'SSLCipherSuite TLSv1.3'
' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
])
httpd.reload_if_config_changed()
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
# SSL backend specifics
if not env.curl_uses_lib('gnutls'):
pytest.skip('curl not build with GnuTLS')
# test
extra_args = ['--ciphers', f'{priority}']
r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
if success:
assert r.exit_code == 0, r.dump_logs()
assert r.json['HTTPS'] == 'on', r.dump_logs()
if tls_proto:
assert r.json['SSL_PROTOCOL'] == tls_proto, r.dump_logs()
assert r.json['SSL_CIPHER'] in ciphers, r.dump_logs()
else:
assert r.exit_code != 0, r.dump_logs()
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_19_wrong_pin(self, env: Env, proto, httpd, nghttpx):
if env.curl_uses_lib('rustls-ffi'):
pytest.skip('TLS backend ignores --pinnedpubkey')
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--pinnedpubkey', 'sha256//ffff'
])
# expect NOT_IMPLEMENTED or CURLE_SSL_PINNEDPUBKEYNOTMATCH
assert r.exit_code in [2, 90], f'{r.dump_logs()}'
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_20_correct_pin(self, env: Env, proto, httpd, nghttpx):
curl = CurlClient(env=env)
creds = env.get_credentials(env.domain1)
assert creds
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--pinnedpubkey', f'sha256//{creds.pub_sha256_b64()}'
])
# expect NOT_IMPLEMENTED or OK
assert r.exit_code in [0, 2], f'{r.dump_logs()}'
@pytest.mark.skipif(condition=not Env.have_openssl(), reason="needs openssl command")
def test_17_21_capath_valid(self, env: Env, httpd):
if env.curl_uses_lib('rustls-ffi'):
pytest.skip('rustls does not support CURLOPT_CAPATH')
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--capath', env.ca.hashdir
])
assert r.exit_code == 0, f'{r.dump_logs()}'
assert r.json['HTTPS'] == 'on', f'{r.json}'
@pytest.mark.skipif(condition=not Env.have_openssl(), reason="needs openssl command")
def test_17_22_capath_invalid(self, env: Env, httpd):
# we can test all TLS backends here. the ones not supporting CAPATH
# need to fail as well as the ones which do, but get an invalid path.
proto = 'http/1.1'
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
'--capath', os.path.join(env.gen_dir, 'ca/invalid')
])
# CURLE_PEER_FAILED_VERIFICATION or CURLE_SSL_CACERT_BADFILE
assert r.exit_code in [60, 77], f'{r.dump_logs()}'