1#!/usr/bin/env python3 2 3# generate_tls13_compat_tests.py 4# 5# Copyright The Mbed TLS Contributors 6# SPDX-License-Identifier: Apache-2.0 7# 8# Licensed under the Apache License, Version 2.0 (the "License"); you may 9# not use this file except in compliance with the License. 10# You may obtain a copy of the License at 11# 12# http://www.apache.org/licenses/LICENSE-2.0 13# 14# Unless required by applicable law or agreed to in writing, software 15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17# See the License for the specific language governing permissions and 18# limitations under the License. 19 20""" 21Generate TLSv1.3 Compat test cases 22 23""" 24 25import sys 26import os 27import abc 28import argparse 29import itertools 30from collections import namedtuple 31# pylint: disable=useless-super-delegation 32 33# define certificates configuration entry 34Certificate = namedtuple("Certificate", ['cafile', 'certfile', 'keyfile']) 35# define the certificate parameters for signature algorithms 36CERTIFICATES = { 37 'ecdsa_secp256r1_sha256': Certificate('data_files/test-ca2.crt', 38 'data_files/ecdsa_secp256r1.crt', 39 'data_files/ecdsa_secp256r1.key'), 40 'ecdsa_secp384r1_sha384': Certificate('data_files/test-ca2.crt', 41 'data_files/ecdsa_secp384r1.crt', 42 'data_files/ecdsa_secp384r1.key'), 43 'ecdsa_secp521r1_sha512': Certificate('data_files/test-ca2.crt', 44 'data_files/ecdsa_secp521r1.crt', 45 'data_files/ecdsa_secp521r1.key'), 46 'rsa_pss_rsae_sha256': Certificate('data_files/test-ca_cat12.crt', 47 'data_files/server2-sha256.crt', 'data_files/server2.key' 48 ) 49} 50 51CIPHER_SUITE_IANA_VALUE = { 52 "TLS_AES_128_GCM_SHA256": 0x1301, 53 "TLS_AES_256_GCM_SHA384": 0x1302, 54 "TLS_CHACHA20_POLY1305_SHA256": 0x1303, 55 "TLS_AES_128_CCM_SHA256": 0x1304, 56 "TLS_AES_128_CCM_8_SHA256": 0x1305 57} 58 59SIG_ALG_IANA_VALUE = { 60 "ecdsa_secp256r1_sha256": 0x0403, 61 "ecdsa_secp384r1_sha384": 0x0503, 62 "ecdsa_secp521r1_sha512": 0x0603, 63 'rsa_pss_rsae_sha256': 0x0804, 64} 65 66NAMED_GROUP_IANA_VALUE = { 67 'secp256r1': 0x17, 68 'secp384r1': 0x18, 69 'secp521r1': 0x19, 70 'x25519': 0x1d, 71 'x448': 0x1e, 72} 73 74class TLSProgram(metaclass=abc.ABCMeta): 75 """ 76 Base class for generate server/client command. 77 """ 78 79 def __init__(self, ciphersuite, signature_algorithm, named_group, compat_mode=True): 80 self._ciphers = [] 81 self._sig_algs = [] 82 self._named_groups = [] 83 self.add_ciphersuites(ciphersuite) 84 self.add_named_groups(named_group) 85 self.add_signature_algorithms(signature_algorithm) 86 self._compat_mode = compat_mode 87 88 # add_ciphersuites should not override by sub class 89 def add_ciphersuites(self, *ciphersuites): 90 self._ciphers.extend( 91 [cipher for cipher in ciphersuites if cipher not in self._ciphers]) 92 93 # add_signature_algorithms should not override by sub class 94 def add_signature_algorithms(self, *signature_algorithms): 95 self._sig_algs.extend( 96 [sig_alg for sig_alg in signature_algorithms if sig_alg not in self._sig_algs]) 97 98 # add_signature_algorithms should not override by sub class 99 def add_named_groups(self, *named_groups): 100 self._named_groups.extend( 101 [named_group for named_group in named_groups if named_group not in self._named_groups]) 102 103 @abc.abstractmethod 104 def pre_checks(self): 105 return [] 106 107 @abc.abstractmethod 108 def cmd(self): 109 pass 110 111 @abc.abstractmethod 112 def post_checks(self): 113 return [] 114 115 116class OpenSSLServ(TLSProgram): 117 """ 118 Generate test commands for OpenSSL server. 119 """ 120 121 NAMED_GROUP = { 122 'secp256r1': 'P-256', 123 'secp384r1': 'P-384', 124 'secp521r1': 'P-521', 125 'x25519': 'X25519', 126 'x448': 'X448', 127 } 128 129 def cmd(self): 130 ret = ['$O_NEXT_SRV_NO_CERT'] 131 for _, cert, key in map(lambda sig_alg: CERTIFICATES[sig_alg], self._sig_algs): 132 ret += ['-cert {cert} -key {key}'.format(cert=cert, key=key)] 133 ret += ['-accept $SRV_PORT'] 134 ciphersuites = ','.join(self._ciphers) 135 signature_algorithms = ','.join(self._sig_algs) 136 named_groups = ','.join( 137 map(lambda named_group: self.NAMED_GROUP[named_group], self._named_groups)) 138 ret += ["-ciphersuites {ciphersuites}".format(ciphersuites=ciphersuites), 139 "-sigalgs {signature_algorithms}".format( 140 signature_algorithms=signature_algorithms), 141 "-groups {named_groups}".format(named_groups=named_groups)] 142 ret += ['-msg -tls1_3 -num_tickets 0 -no_resume_ephemeral -no_cache'] 143 if not self._compat_mode: 144 ret += ['-no_middlebox'] 145 146 return ' '.join(ret) 147 148 def pre_checks(self): 149 return ["requires_openssl_tls1_3"] 150 151 def post_checks(self): 152 return ['-c "HTTP/1.0 200 ok"'] 153 154 155class GnuTLSServ(TLSProgram): 156 """ 157 Generate test commands for GnuTLS server. 158 """ 159 160 CIPHER_SUITE = { 161 'TLS_AES_256_GCM_SHA384': [ 162 'AES-256-GCM', 163 'SHA384', 164 'AEAD'], 165 'TLS_AES_128_GCM_SHA256': [ 166 'AES-128-GCM', 167 'SHA256', 168 'AEAD'], 169 'TLS_CHACHA20_POLY1305_SHA256': [ 170 'CHACHA20-POLY1305', 171 'SHA256', 172 'AEAD'], 173 'TLS_AES_128_CCM_SHA256': [ 174 'AES-128-CCM', 175 'SHA256', 176 'AEAD'], 177 'TLS_AES_128_CCM_8_SHA256': [ 178 'AES-128-CCM-8', 179 'SHA256', 180 'AEAD']} 181 182 SIGNATURE_ALGORITHM = { 183 'ecdsa_secp256r1_sha256': ['SIGN-ECDSA-SECP256R1-SHA256'], 184 'ecdsa_secp521r1_sha512': ['SIGN-ECDSA-SECP521R1-SHA512'], 185 'ecdsa_secp384r1_sha384': ['SIGN-ECDSA-SECP384R1-SHA384'], 186 'rsa_pss_rsae_sha256': ['SIGN-RSA-PSS-RSAE-SHA256']} 187 188 NAMED_GROUP = { 189 'secp256r1': ['GROUP-SECP256R1'], 190 'secp384r1': ['GROUP-SECP384R1'], 191 'secp521r1': ['GROUP-SECP521R1'], 192 'x25519': ['GROUP-X25519'], 193 'x448': ['GROUP-X448'], 194 } 195 196 def pre_checks(self): 197 return ["requires_gnutls_tls1_3", 198 "requires_gnutls_next_no_ticket", 199 "requires_gnutls_next_disable_tls13_compat", ] 200 201 def post_checks(self): 202 return ['-c "HTTP/1.0 200 OK"'] 203 204 def cmd(self): 205 ret = ['$G_NEXT_SRV_NO_CERT', '--http', 206 '--disable-client-cert', '--debug=4'] 207 208 for _, cert, key in map(lambda sig_alg: CERTIFICATES[sig_alg], self._sig_algs): 209 ret += ['--x509certfile {cert} --x509keyfile {key}'.format( 210 cert=cert, key=key)] 211 212 priority_string_list = [] 213 214 def update_priority_string_list(items, map_table): 215 for item in items: 216 for i in map_table[item]: 217 if i not in priority_string_list: 218 yield i 219 priority_string_list.extend(update_priority_string_list( 220 self._sig_algs, self.SIGNATURE_ALGORITHM)) 221 priority_string_list.extend( 222 update_priority_string_list(self._ciphers, self.CIPHER_SUITE)) 223 priority_string_list.extend(update_priority_string_list( 224 self._named_groups, self.NAMED_GROUP)) 225 priority_string_list = ['NONE'] + sorted(priority_string_list) + ['VERS-TLS1.3'] 226 227 priority_string = ':+'.join(priority_string_list) 228 priority_string += ':%NO_TICKETS' 229 if not self._compat_mode: 230 priority_string += [':%DISABLE_TLS13_COMPAT_MODE'] 231 232 ret += ['--priority={priority_string}'.format( 233 priority_string=priority_string)] 234 ret = ' '.join(ret) 235 return ret 236 237 238class MbedTLSCli(TLSProgram): 239 """ 240 Generate test commands for mbedTLS client. 241 """ 242 243 CIPHER_SUITE = { 244 'TLS_AES_256_GCM_SHA384': 'TLS1-3-AES-256-GCM-SHA384', 245 'TLS_AES_128_GCM_SHA256': 'TLS1-3-AES-128-GCM-SHA256', 246 'TLS_CHACHA20_POLY1305_SHA256': 'TLS1-3-CHACHA20-POLY1305-SHA256', 247 'TLS_AES_128_CCM_SHA256': 'TLS1-3-AES-128-CCM-SHA256', 248 'TLS_AES_128_CCM_8_SHA256': 'TLS1-3-AES-128-CCM-8-SHA256'} 249 250 def cmd(self): 251 ret = ['$P_CLI'] 252 ret += ['server_addr=127.0.0.1', 'server_port=$SRV_PORT', 253 'debug_level=4', 'force_version=tls13'] 254 ret += ['ca_file={cafile}'.format( 255 cafile=CERTIFICATES[self._sig_algs[0]].cafile)] 256 257 if self._ciphers: 258 ciphers = ','.join( 259 map(lambda cipher: self.CIPHER_SUITE[cipher], self._ciphers)) 260 ret += ["force_ciphersuite={ciphers}".format(ciphers=ciphers)] 261 262 if self._sig_algs: 263 ret += ['sig_algs={sig_algs}'.format( 264 sig_algs=','.join(self._sig_algs))] 265 for sig_alg in self._sig_algs: 266 if sig_alg in ('ecdsa_secp256r1_sha256', 267 'ecdsa_secp384r1_sha384', 268 'ecdsa_secp521r1_sha512'): 269 self.add_named_groups(sig_alg.split('_')[1]) 270 271 if self._named_groups: 272 named_groups = ','.join(self._named_groups) 273 ret += ["curves={named_groups}".format(named_groups=named_groups)] 274 275 ret = ' '.join(ret) 276 return ret 277 278 def pre_checks(self): 279 ret = ['requires_config_enabled MBEDTLS_DEBUG_C', 280 'requires_config_enabled MBEDTLS_SSL_CLI_C', 281 'requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3', 282 'requires_config_disabled MBEDTLS_USE_PSA_CRYPTO'] 283 284 if self._compat_mode: 285 ret += ['requires_config_enabled MBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE'] 286 287 if 'rsa_pss_rsae_sha256' in self._sig_algs: 288 ret.append( 289 'requires_config_enabled MBEDTLS_X509_RSASSA_PSS_SUPPORT') 290 return ret 291 292 def post_checks(self): 293 check_strings = ["ECDH curve: {group}".format(group=self._named_groups[0]), 294 "server hello, chosen ciphersuite: ( {:04x} ) - {}".format( 295 CIPHER_SUITE_IANA_VALUE[self._ciphers[0]], 296 self.CIPHER_SUITE[self._ciphers[0]]), 297 "Certificate Verify: Signature algorithm ( {:04x} )".format( 298 SIG_ALG_IANA_VALUE[self._sig_algs[0]]), 299 "Verifying peer X.509 certificate... ok", ] 300 return ['-c "{}"'.format(i) for i in check_strings] 301 302 303SERVER_CLASSES = {'OpenSSL': OpenSSLServ, 'GnuTLS': GnuTLSServ} 304CLIENT_CLASSES = {'mbedTLS': MbedTLSCli} 305 306 307def generate_compat_test(server=None, client=None, cipher=None, sig_alg=None, named_group=None): 308 """ 309 Generate test case with `ssl-opt.sh` format. 310 """ 311 name = 'TLS 1.3 {client[0]}->{server[0]}: {cipher},{named_group},{sig_alg}'.format( 312 client=client, server=server, cipher=cipher, sig_alg=sig_alg, named_group=named_group) 313 server_object = SERVER_CLASSES[server](cipher, sig_alg, named_group) 314 client_object = CLIENT_CLASSES[client](cipher, sig_alg, named_group) 315 316 cmd = ['run_test "{}"'.format(name), '"{}"'.format( 317 server_object.cmd()), '"{}"'.format(client_object.cmd()), '0'] 318 cmd += server_object.post_checks() 319 cmd += client_object.post_checks() 320 prefix = ' \\\n' + (' '*9) 321 cmd = prefix.join(cmd) 322 return '\n'.join(server_object.pre_checks() + client_object.pre_checks() + [cmd]) 323 324 325SSL_OUTPUT_HEADER = '''#!/bin/sh 326 327# {filename} 328# 329# Copyright The Mbed TLS Contributors 330# SPDX-License-Identifier: Apache-2.0 331# 332# Licensed under the Apache License, Version 2.0 (the "License"); you may 333# not use this file except in compliance with the License. 334# You may obtain a copy of the License at 335# 336# http://www.apache.org/licenses/LICENSE-2.0 337# 338# Unless required by applicable law or agreed to in writing, software 339# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 340# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 341# See the License for the specific language governing permissions and 342# limitations under the License. 343# 344# Purpose 345# 346# List TLS1.3 compat test cases. They are generated by 347# `generate_tls13_compat_tests.py -a`. 348# 349# PLEASE DO NOT EDIT THIS FILE. IF NEEDED, PLEASE MODIFY `generate_tls13_compat_tests.py` 350# AND REGENERATE THIS FILE. 351# 352''' 353 354 355def main(): 356 """ 357 Main function of this program 358 """ 359 parser = argparse.ArgumentParser() 360 361 parser.add_argument('-o', '--output', nargs='?', 362 default=None, help='Output file path if `-a` was set') 363 364 parser.add_argument('-a', '--generate-all-tls13-compat-tests', action='store_true', 365 default=False, help='Generate all available tls13 compat tests') 366 367 parser.add_argument('--list-ciphers', action='store_true', 368 default=False, help='List supported ciphersuites') 369 370 parser.add_argument('--list-sig-algs', action='store_true', 371 default=False, help='List supported signature algorithms') 372 373 parser.add_argument('--list-named-groups', action='store_true', 374 default=False, help='List supported named groups') 375 376 parser.add_argument('--list-servers', action='store_true', 377 default=False, help='List supported TLS servers') 378 379 parser.add_argument('--list-clients', action='store_true', 380 default=False, help='List supported TLS Clients') 381 382 parser.add_argument('server', choices=SERVER_CLASSES.keys(), nargs='?', 383 default=list(SERVER_CLASSES.keys())[0], 384 help='Choose TLS server program for test') 385 parser.add_argument('client', choices=CLIENT_CLASSES.keys(), nargs='?', 386 default=list(CLIENT_CLASSES.keys())[0], 387 help='Choose TLS client program for test') 388 parser.add_argument('cipher', choices=CIPHER_SUITE_IANA_VALUE.keys(), nargs='?', 389 default=list(CIPHER_SUITE_IANA_VALUE.keys())[0], 390 help='Choose cipher suite for test') 391 parser.add_argument('sig_alg', choices=SIG_ALG_IANA_VALUE.keys(), nargs='?', 392 default=list(SIG_ALG_IANA_VALUE.keys())[0], 393 help='Choose cipher suite for test') 394 parser.add_argument('named_group', choices=NAMED_GROUP_IANA_VALUE.keys(), nargs='?', 395 default=list(NAMED_GROUP_IANA_VALUE.keys())[0], 396 help='Choose cipher suite for test') 397 398 args = parser.parse_args() 399 400 def get_all_test_cases(): 401 for cipher, sig_alg, named_group, server, client in \ 402 itertools.product(CIPHER_SUITE_IANA_VALUE.keys(), SIG_ALG_IANA_VALUE.keys(), 403 NAMED_GROUP_IANA_VALUE.keys(), SERVER_CLASSES.keys(), 404 CLIENT_CLASSES.keys()): 405 yield generate_compat_test(cipher=cipher, sig_alg=sig_alg, named_group=named_group, 406 server=server, client=client) 407 408 if args.generate_all_tls13_compat_tests: 409 if args.output: 410 with open(args.output, 'w', encoding="utf-8") as f: 411 f.write(SSL_OUTPUT_HEADER.format( 412 filename=os.path.basename(args.output))) 413 f.write('\n\n'.join(get_all_test_cases())) 414 f.write('\n') 415 else: 416 print('\n'.join(get_all_test_cases())) 417 return 0 418 419 if args.list_ciphers or args.list_sig_algs or args.list_named_groups \ 420 or args.list_servers or args.list_clients: 421 if args.list_ciphers: 422 print(*CIPHER_SUITE_IANA_VALUE.keys()) 423 if args.list_sig_algs: 424 print(*SIG_ALG_IANA_VALUE.keys()) 425 if args.list_named_groups: 426 print(*NAMED_GROUP_IANA_VALUE.keys()) 427 if args.list_servers: 428 print(*SERVER_CLASSES.keys()) 429 if args.list_clients: 430 print(*CLIENT_CLASSES.keys()) 431 return 0 432 433 print(generate_compat_test(server=args.server, client=args.client, sig_alg=args.sig_alg, 434 cipher=args.cipher, named_group=args.named_group)) 435 return 0 436 437 438if __name__ == "__main__": 439 sys.exit(main()) 440