diff --git a/scripts/generate_ssl_debug_helpers.py b/scripts/generate_ssl_debug_helpers.py new file mode 100755 index 000000000..df3d9b93a --- /dev/null +++ b/scripts/generate_ssl_debug_helpers.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 + +"""Generate library/ssl_debug_helpers_generated.c + +The code generated by this module includes debug helper functions that can not be +implemented by fixed codes. + +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later +import sys +import re +import os +import textwrap +import argparse + +import framework_scripts_path # pylint: disable=unused-import +from mbedtls_framework import build_tree + + +def remove_c_comments(string): + """ + Remove C style comments from input string + """ + string_pattern = r"(?P\".*?\"|\'.*?\')" + comment_pattern = r"(?P/\*.*?\*/|//[^\r\n]*$)" + pattern = re.compile(string_pattern + r'|' + comment_pattern, + re.MULTILINE | re.DOTALL) + + def replacer(match): + if match.lastgroup == 'comment': + return "" + return match.group() + return pattern.sub(replacer, string) + + +class CondDirectiveNotMatch(Exception): + pass + + +def preprocess_c_source_code(source, *classes): + """ + Simple preprocessor for C source code. + + Only processes condition directives without expanding them. + Yield object according to the classes input. Most match firstly + + If the directive pair does not match , raise CondDirectiveNotMatch. + + Assume source code does not include comments and compile pass. + + """ + + pattern = re.compile(r"^[ \t]*#[ \t]*" + + r"(?P(if[ \t]|ifndef[ \t]|ifdef[ \t]|else|endif))" + + r"[ \t]*(?P(.*\\\n)*.*$)", + re.MULTILINE) + stack = [] + + def _yield_objects(s, d, p, st, end): + """ + Output matched source piece + """ + nonlocal stack + start_line, end_line = '', '' + if stack: + start_line = '#{} {}'.format(d, p) + if d == 'if': + end_line = '#endif /* {} */'.format(p) + elif d == 'ifdef': + end_line = '#endif /* defined({}) */'.format(p) + else: + end_line = '#endif /* !defined({}) */'.format(p) + has_instance = False + for cls in classes: + for instance in cls.extract(s, st, end): + if has_instance is False: + has_instance = True + yield pair_start, start_line + yield instance.span()[0], instance + if has_instance: + yield start, end_line + + for match in pattern.finditer(source): + + directive = match.groupdict()['directive'].strip() + param = match.groupdict()['param'] + start, end = match.span() + + if directive in ('if', 'ifndef', 'ifdef'): + stack.append((directive, param, start, end)) + continue + + if not stack: + raise CondDirectiveNotMatch() + + pair_directive, pair_param, pair_start, pair_end = stack.pop() + yield from _yield_objects(source, + pair_directive, + pair_param, + pair_end, + start) + + if directive == 'endif': + continue + + if pair_directive == 'if': + directive = 'if' + param = "!( {} )".format(pair_param) + elif pair_directive == 'ifdef': + directive = 'ifndef' + param = pair_param + else: + directive = 'ifdef' + param = pair_param + + stack.append((directive, param, start, end)) + assert not stack, len(stack) + + +class EnumDefinition: + """ + Generate helper functions around enumeration. + + Currently, it generate translation function from enum value to string. + Enum definition looks like: + [typedef] enum [prefix name] { [body] } [suffix name]; + + Known limitation: + - the '}' and ';' SHOULD NOT exist in different macro blocks. Like + ``` + enum test { + .... + #if defined(A) + .... + }; + #else + .... + }; + #endif + ``` + """ + + @classmethod + def extract(cls, source_code, start=0, end=-1): + enum_pattern = re.compile(r'enum\s*(?P\w*)\s*' + + r'{\s*(?P[^}]*)}' + + r'\s*(?P\w*)\s*;', + re.MULTILINE | re.DOTALL) + + for match in enum_pattern.finditer(source_code, start, end): + yield EnumDefinition(source_code, + span=match.span(), + group=match.groupdict()) + + def __init__(self, source_code, span=None, group=None): + assert isinstance(group, dict) + prefix_name = group.get('prefix_name', None) + suffix_name = group.get('suffix_name', None) + body = group.get('body', None) + assert prefix_name or suffix_name + assert body + assert span + # If suffix_name exists, it is a typedef + self._prototype = suffix_name if suffix_name else 'enum ' + prefix_name + self._name = suffix_name if suffix_name else prefix_name + self._body = body + self._source = source_code + self._span = span + + def __repr__(self): + return 'Enum({},{})'.format(self._name, self._span) + + def __str__(self): + return repr(self) + + def span(self): + return self._span + + def generate_translation_function(self): + """ + Generate function for translating value to string + """ + translation_table = [] + + for line in self._body.splitlines(): + + if line.strip().startswith('#'): + # Preprocess directive, keep it in table + translation_table.append(line.strip()) + continue + + if not line.strip(): + continue + + for field in line.strip().split(','): + if not field.strip(): + continue + member = field.strip().split()[0] + translation_table.append( + '{space}case {member}:\n{space} return "{member}";' + .format(member=member, space=' '*8) + ) + + body = textwrap.dedent('''\ + const char *{name}_str( {prototype} in ) + {{ + switch (in) {{ + {translation_table} + default: + return "UNKNOWN_VALUE"; + }} + }} + ''') + body = body.format(translation_table='\n'.join(translation_table), + name=self._name, + prototype=self._prototype) + return body + + +class SignatureAlgorithmDefinition: + """ + Generate helper functions for signature algorithms. + + It generates translation function from signature algorithm define to string. + Signature algorithm definition looks like: + #define MBEDTLS_TLS1_3_SIG_[ upper case signature algorithm ] [ value(hex) ] + + Known limitation: + - the definitions SHOULD exist in same macro blocks. + """ + + @classmethod + def extract(cls, source_code, start=0, end=-1): + sig_alg_pattern = re.compile(r'#define\s+(?PMBEDTLS_TLS1_3_SIG_\w+)\s+' + + r'(?P0[xX][0-9a-fA-F]+)$', + re.MULTILINE | re.DOTALL) + matches = list(sig_alg_pattern.finditer(source_code, start, end)) + if matches: + yield SignatureAlgorithmDefinition(source_code, definitions=matches) + + def __init__(self, source_code, definitions=None): + if definitions is None: + definitions = [] + assert isinstance(definitions, list) and definitions + self._definitions = definitions + self._source = source_code + + def __repr__(self): + return 'SigAlgs({})'.format(self._definitions[0].span()) + + def span(self): + return self._definitions[0].span() + + def __str__(self): + """ + Generate function for translating value to string + """ + translation_table = [] + for m in self._definitions: + name = m.groupdict()['name'] + return_val = name[len('MBEDTLS_TLS1_3_SIG_'):].lower() + translation_table.append( + ' case {}:\n return "{}";'.format(name, return_val)) + + body = textwrap.dedent('''\ + const char *mbedtls_ssl_sig_alg_to_str( uint16_t in ) + {{ + switch( in ) + {{ + {translation_table} + }}; + + return "UNKNOWN"; + }}''') + body = body.format(translation_table='\n'.join(translation_table)) + return body + + +class NamedGroupDefinition: + """ + Generate helper functions for named group + + It generates translation function from named group define to string. + Named group definition looks like: + #define MBEDTLS_SSL_IANA_TLS_GROUP_[ upper case named group ] [ value(hex) ] + + Known limitation: + - the definitions SHOULD exist in same macro blocks. + """ + + @classmethod + def extract(cls, source_code, start=0, end=-1): + named_group_pattern = re.compile(r'#define\s+(?PMBEDTLS_SSL_IANA_TLS_GROUP_\w+)\s+' + + r'(?P0[xX][0-9a-fA-F]+)$', + re.MULTILINE | re.DOTALL) + matches = list(named_group_pattern.finditer(source_code, start, end)) + if matches: + yield NamedGroupDefinition(source_code, definitions=matches) + + def __init__(self, source_code, definitions=None): + if definitions is None: + definitions = [] + assert isinstance(definitions, list) and definitions + self._definitions = definitions + self._source = source_code + + def __repr__(self): + return 'NamedGroup({})'.format(self._definitions[0].span()) + + def span(self): + return self._definitions[0].span() + + def __str__(self): + """ + Generate function for translating value to string + """ + translation_table = [] + for m in self._definitions: + name = m.groupdict()['name'] + iana_name = name[len('MBEDTLS_SSL_IANA_TLS_GROUP_'):].lower() + translation_table.append(' case {}:\n return "{}";'.format(name, iana_name)) + + body = textwrap.dedent('''\ + const char *mbedtls_ssl_named_group_to_str( uint16_t in ) + {{ + switch( in ) + {{ + {translation_table} + }}; + + return "UNKNOWN"; + }}''') + body = body.format(translation_table='\n'.join(translation_table)) + return body + + +OUTPUT_C_TEMPLATE = '''\ +/* Automatically generated by generate_ssl_debug_helpers.py. DO NOT EDIT. */ + +/** + * \\file ssl_debug_helpers_generated.c + * + * \\brief Automatically generated helper functions for debugging + */ +/* + * Copyright The Mbed TLS Contributors + * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + * + */ + +#include "ssl_misc.h" + +#if defined(MBEDTLS_DEBUG_C) + +#include "ssl_debug_helpers.h" + +{functions} + +#endif /* MBEDTLS_DEBUG_C */ +/* End of automatically generated file. */ + +''' + + +def generate_ssl_debug_helpers(output_directory, mbedtls_root): + """ + Generate functions of debug helps + """ + mbedtls_root = os.path.abspath( + mbedtls_root or build_tree.guess_mbedtls_root()) + with open(os.path.join(mbedtls_root, 'include/mbedtls/ssl.h')) as f: + source_code = remove_c_comments(f.read()) + + definitions = dict() + for start, instance in preprocess_c_source_code(source_code, + EnumDefinition, + SignatureAlgorithmDefinition, + NamedGroupDefinition): + if start in definitions: + continue + if isinstance(instance, EnumDefinition): + definition = instance.generate_translation_function() + else: + definition = instance + definitions[start] = definition + + function_definitions = [str(v) for _, v in sorted(definitions.items())] + if output_directory == sys.stdout: + sys.stdout.write(OUTPUT_C_TEMPLATE.format( + functions='\n'.join(function_definitions))) + else: + with open(os.path.join(output_directory, 'ssl_debug_helpers_generated.c'), 'w') as f: + f.write(OUTPUT_C_TEMPLATE.format( + functions='\n'.join(function_definitions))) + + +def main(): + """ + Command line entry + """ + parser = argparse.ArgumentParser() + parser.add_argument('--mbedtls-root', nargs='?', default=None, + help='root directory of mbedtls source code') + parser.add_argument('output_directory', nargs='?', + default='library', help='source/header files location') + + args = parser.parse_args() + + generate_ssl_debug_helpers(args.output_directory, args.mbedtls_root) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/generate_tls13_compat_tests.py b/scripts/generate_tls13_compat_tests.py new file mode 100755 index 000000000..b9dcff4e1 --- /dev/null +++ b/scripts/generate_tls13_compat_tests.py @@ -0,0 +1,649 @@ +#!/usr/bin/env python3 + +# generate_tls13_compat_tests.py +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +""" +Generate TLSv1.3 Compat test cases + +""" + +import sys +import os +import argparse +import itertools +from collections import namedtuple + +# define certificates configuration entry +Certificate = namedtuple("Certificate", ['cafile', 'certfile', 'keyfile']) +# define the certificate parameters for signature algorithms +CERTIFICATES = { + 'ecdsa_secp256r1_sha256': Certificate('$DATA_FILES_PATH/test-ca2.crt', + '$DATA_FILES_PATH/ecdsa_secp256r1.crt', + '$DATA_FILES_PATH/ecdsa_secp256r1.key'), + 'ecdsa_secp384r1_sha384': Certificate('$DATA_FILES_PATH/test-ca2.crt', + '$DATA_FILES_PATH/ecdsa_secp384r1.crt', + '$DATA_FILES_PATH/ecdsa_secp384r1.key'), + 'ecdsa_secp521r1_sha512': Certificate('$DATA_FILES_PATH/test-ca2.crt', + '$DATA_FILES_PATH/ecdsa_secp521r1.crt', + '$DATA_FILES_PATH/ecdsa_secp521r1.key'), + 'rsa_pss_rsae_sha256': Certificate('$DATA_FILES_PATH/test-ca_cat12.crt', + '$DATA_FILES_PATH/server2-sha256.crt', + '$DATA_FILES_PATH/server2.key') +} + +CIPHER_SUITE_IANA_VALUE = { + "TLS_AES_128_GCM_SHA256": 0x1301, + "TLS_AES_256_GCM_SHA384": 0x1302, + "TLS_CHACHA20_POLY1305_SHA256": 0x1303, + "TLS_AES_128_CCM_SHA256": 0x1304, + "TLS_AES_128_CCM_8_SHA256": 0x1305 +} + +SIG_ALG_IANA_VALUE = { + "ecdsa_secp256r1_sha256": 0x0403, + "ecdsa_secp384r1_sha384": 0x0503, + "ecdsa_secp521r1_sha512": 0x0603, + 'rsa_pss_rsae_sha256': 0x0804, +} + +NAMED_GROUP_IANA_VALUE = { + 'secp256r1': 0x17, + 'secp384r1': 0x18, + 'secp521r1': 0x19, + 'x25519': 0x1d, + 'x448': 0x1e, + # Only one finite field group to keep testing time within reasonable bounds. + 'ffdhe2048': 0x100, +} + +class TLSProgram: + """ + Base class for generate server/client command. + """ + + # pylint: disable=too-many-arguments + def __init__(self, ciphersuite=None, signature_algorithm=None, named_group=None, + cert_sig_alg=None): + self._ciphers = [] + self._sig_algs = [] + self._named_groups = [] + self._cert_sig_algs = [] + if ciphersuite: + self.add_ciphersuites(ciphersuite) + if named_group: + self.add_named_groups(named_group) + if signature_algorithm: + self.add_signature_algorithms(signature_algorithm) + if cert_sig_alg: + self.add_cert_signature_algorithms(cert_sig_alg) + + # add_ciphersuites should not override by sub class + def add_ciphersuites(self, *ciphersuites): + self._ciphers.extend( + [cipher for cipher in ciphersuites if cipher not in self._ciphers]) + + # add_signature_algorithms should not override by sub class + def add_signature_algorithms(self, *signature_algorithms): + self._sig_algs.extend( + [sig_alg for sig_alg in signature_algorithms if sig_alg not in self._sig_algs]) + + # add_named_groups should not override by sub class + def add_named_groups(self, *named_groups): + self._named_groups.extend( + [named_group for named_group in named_groups if named_group not in self._named_groups]) + + # add_cert_signature_algorithms should not override by sub class + def add_cert_signature_algorithms(self, *signature_algorithms): + self._cert_sig_algs.extend( + [sig_alg for sig_alg in signature_algorithms if sig_alg not in self._cert_sig_algs]) + + # pylint: disable=no-self-use + def pre_checks(self): + return [] + + # pylint: disable=no-self-use + def cmd(self): + if not self._cert_sig_algs: + self._cert_sig_algs = list(CERTIFICATES.keys()) + return self.pre_cmd() + + # pylint: disable=no-self-use + def post_checks(self): + return [] + + # pylint: disable=no-self-use + def pre_cmd(self): + return ['false'] + + # pylint: disable=unused-argument,no-self-use + def hrr_post_checks(self, named_group): + return [] + + +class OpenSSLBase(TLSProgram): + """ + Generate base test commands for OpenSSL. + """ + + NAMED_GROUP = { + 'secp256r1': 'P-256', + 'secp384r1': 'P-384', + 'secp521r1': 'P-521', + 'x25519': 'X25519', + 'x448': 'X448', + 'ffdhe2048': 'ffdhe2048', + } + + def cmd(self): + ret = super().cmd() + + if self._ciphers: + ciphersuites = ':'.join(self._ciphers) + ret += ["-ciphersuites {ciphersuites}".format(ciphersuites=ciphersuites)] + + if self._sig_algs: + signature_algorithms = set(self._sig_algs + self._cert_sig_algs) + signature_algorithms = ':'.join(signature_algorithms) + ret += ["-sigalgs {signature_algorithms}".format( + signature_algorithms=signature_algorithms)] + + if self._named_groups: + named_groups = ':'.join( + map(lambda named_group: self.NAMED_GROUP[named_group], self._named_groups)) + ret += ["-groups {named_groups}".format(named_groups=named_groups)] + + ret += ['-msg -tls1_3'] + + return ret + + def pre_checks(self): + ret = ["requires_openssl_tls1_3"] + + # ffdh groups require at least openssl 3.0 + ffdh_groups = ['ffdhe2048'] + + if any(x in ffdh_groups for x in self._named_groups): + ret = ["requires_openssl_tls1_3_with_ffdh"] + + return ret + + +class OpenSSLServ(OpenSSLBase): + """ + Generate test commands for OpenSSL server. + """ + + def cmd(self): + ret = super().cmd() + ret += ['-num_tickets 0 -no_resume_ephemeral -no_cache'] + return ret + + def post_checks(self): + return ['-c "HTTP/1.0 200 ok"'] + + def pre_cmd(self): + ret = ['$O_NEXT_SRV_NO_CERT'] + for _, cert, key in map(lambda sig_alg: CERTIFICATES[sig_alg], self._cert_sig_algs): + ret += ['-cert {cert} -key {key}'.format(cert=cert, key=key)] + return ret + + +class OpenSSLCli(OpenSSLBase): + """ + Generate test commands for OpenSSL client. + """ + + def pre_cmd(self): + return ['$O_NEXT_CLI_NO_CERT', + '-CAfile {cafile}'.format(cafile=CERTIFICATES[self._cert_sig_algs[0]].cafile)] + + +class GnuTLSBase(TLSProgram): + """ + Generate base test commands for GnuTLS. + """ + + CIPHER_SUITE = { + 'TLS_AES_256_GCM_SHA384': [ + 'AES-256-GCM', + 'SHA384', + 'AEAD'], + 'TLS_AES_128_GCM_SHA256': [ + 'AES-128-GCM', + 'SHA256', + 'AEAD'], + 'TLS_CHACHA20_POLY1305_SHA256': [ + 'CHACHA20-POLY1305', + 'SHA256', + 'AEAD'], + 'TLS_AES_128_CCM_SHA256': [ + 'AES-128-CCM', + 'SHA256', + 'AEAD'], + 'TLS_AES_128_CCM_8_SHA256': [ + 'AES-128-CCM-8', + 'SHA256', + 'AEAD']} + + SIGNATURE_ALGORITHM = { + 'ecdsa_secp256r1_sha256': ['SIGN-ECDSA-SECP256R1-SHA256'], + 'ecdsa_secp521r1_sha512': ['SIGN-ECDSA-SECP521R1-SHA512'], + 'ecdsa_secp384r1_sha384': ['SIGN-ECDSA-SECP384R1-SHA384'], + 'rsa_pss_rsae_sha256': ['SIGN-RSA-PSS-RSAE-SHA256']} + + NAMED_GROUP = { + 'secp256r1': ['GROUP-SECP256R1'], + 'secp384r1': ['GROUP-SECP384R1'], + 'secp521r1': ['GROUP-SECP521R1'], + 'x25519': ['GROUP-X25519'], + 'x448': ['GROUP-X448'], + 'ffdhe2048': ['GROUP-FFDHE2048'], + } + + def pre_checks(self): + return ["requires_gnutls_tls1_3", + "requires_gnutls_next_no_ticket"] + + def cmd(self): + ret = super().cmd() + + priority_string_list = [] + + def update_priority_string_list(items, map_table): + for item in items: + for i in map_table[item]: + if i not in priority_string_list: + yield i + + if self._ciphers: + priority_string_list.extend(update_priority_string_list( + self._ciphers, self.CIPHER_SUITE)) + else: + priority_string_list.extend(['CIPHER-ALL', 'MAC-ALL']) + + if self._sig_algs: + signature_algorithms = set(self._sig_algs + self._cert_sig_algs) + priority_string_list.extend(update_priority_string_list( + signature_algorithms, self.SIGNATURE_ALGORITHM)) + else: + priority_string_list.append('SIGN-ALL') + + + if self._named_groups: + priority_string_list.extend(update_priority_string_list( + self._named_groups, self.NAMED_GROUP)) + else: + priority_string_list.append('GROUP-ALL') + + priority_string_list = ['NONE'] + \ + priority_string_list + ['VERS-TLS1.3'] + + priority_string = ':+'.join(priority_string_list) + priority_string += ':%NO_TICKETS' + + ret += ['--priority={priority_string}'.format( + priority_string=priority_string)] + return ret + +class GnuTLSServ(GnuTLSBase): + """ + Generate test commands for GnuTLS server. + """ + + def pre_cmd(self): + ret = ['$G_NEXT_SRV_NO_CERT', '--http', '--disable-client-cert', '--debug=4'] + + for _, cert, key in map(lambda sig_alg: CERTIFICATES[sig_alg], self._cert_sig_algs): + ret += ['--x509certfile {cert} --x509keyfile {key}'.format( + cert=cert, key=key)] + return ret + + def post_checks(self): + return ['-c "HTTP/1.0 200 OK"'] + + +class GnuTLSCli(GnuTLSBase): + """ + Generate test commands for GnuTLS client. + """ + + def pre_cmd(self): + return ['$G_NEXT_CLI_NO_CERT', '--debug=4', '--single-key-share', + '--x509cafile {cafile}'.format(cafile=CERTIFICATES[self._cert_sig_algs[0]].cafile)] + + +class MbedTLSBase(TLSProgram): + """ + Generate base test commands for mbedTLS. + """ + + CIPHER_SUITE = { + 'TLS_AES_256_GCM_SHA384': 'TLS1-3-AES-256-GCM-SHA384', + 'TLS_AES_128_GCM_SHA256': 'TLS1-3-AES-128-GCM-SHA256', + 'TLS_CHACHA20_POLY1305_SHA256': 'TLS1-3-CHACHA20-POLY1305-SHA256', + 'TLS_AES_128_CCM_SHA256': 'TLS1-3-AES-128-CCM-SHA256', + 'TLS_AES_128_CCM_8_SHA256': 'TLS1-3-AES-128-CCM-8-SHA256'} + + def cmd(self): + ret = super().cmd() + ret += ['debug_level=4'] + + + if self._ciphers: + ciphers = ','.join( + map(lambda cipher: self.CIPHER_SUITE[cipher], self._ciphers)) + ret += ["force_ciphersuite={ciphers}".format(ciphers=ciphers)] + + if self._sig_algs + self._cert_sig_algs: + ret += ['sig_algs={sig_algs}'.format( + sig_algs=','.join(set(self._sig_algs + self._cert_sig_algs)))] + + if self._named_groups: + named_groups = ','.join(self._named_groups) + ret += ["groups={named_groups}".format(named_groups=named_groups)] + return ret + + #pylint: disable=missing-function-docstring + def add_ffdh_group_requirements(self, requirement_list): + if 'ffdhe2048' in self._named_groups: + requirement_list.append('requires_config_enabled PSA_WANT_DH_RFC7919_2048') + if 'ffdhe3072' in self._named_groups: + requirement_list.append('requires_config_enabled PSA_WANT_DH_RFC7919_2048') + if 'ffdhe4096' in self._named_groups: + requirement_list.append('requires_config_enabled PSA_WANT_DH_RFC7919_2048') + if 'ffdhe6144' in self._named_groups: + requirement_list.append('requires_config_enabled PSA_WANT_DH_RFC7919_2048') + if 'ffdhe8192' in self._named_groups: + requirement_list.append('requires_config_enabled PSA_WANT_DH_RFC7919_2048') + + def pre_checks(self): + ret = ['requires_config_enabled MBEDTLS_DEBUG_C', + 'requires_config_enabled MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED'] + + if 'rsa_pss_rsae_sha256' in self._sig_algs + self._cert_sig_algs: + ret.append( + 'requires_config_enabled MBEDTLS_X509_RSASSA_PSS_SUPPORT') + + ec_groups = ['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448'] + ffdh_groups = ['ffdhe2048', 'ffdhe3072', 'ffdhe4096', 'ffdhe6144', 'ffdhe8192'] + + if any(x in ec_groups for x in self._named_groups): + ret.append('requires_config_enabled PSA_WANT_ALG_ECDH') + + if any(x in ffdh_groups for x in self._named_groups): + ret.append('requires_config_enabled PSA_WANT_ALG_FFDH') + self.add_ffdh_group_requirements(ret) + + return ret + + +class MbedTLSServ(MbedTLSBase): + """ + Generate test commands for mbedTLS server. + """ + + def cmd(self): + ret = super().cmd() + ret += ['tls13_kex_modes=ephemeral cookies=0 tickets=0'] + return ret + + def pre_checks(self): + return ['requires_config_enabled MBEDTLS_SSL_SRV_C'] + super().pre_checks() + + def post_checks(self): + check_strings = ["Protocol is TLSv1.3"] + if self._ciphers: + check_strings.append( + "server hello, chosen ciphersuite: {} ( id={:04d} )".format( + self.CIPHER_SUITE[self._ciphers[0]], + CIPHER_SUITE_IANA_VALUE[self._ciphers[0]])) + if self._sig_algs: + check_strings.append( + "received signature algorithm: 0x{:x}".format( + SIG_ALG_IANA_VALUE[self._sig_algs[0]])) + + for named_group in self._named_groups: + check_strings += ['got named group: {named_group}({iana_value:04x})'.format( + named_group=named_group, + iana_value=NAMED_GROUP_IANA_VALUE[named_group])] + + check_strings.append("Certificate verification was skipped") + return ['-s "{}"'.format(i) for i in check_strings] + + def pre_cmd(self): + ret = ['$P_SRV'] + for _, cert, key in map(lambda sig_alg: CERTIFICATES[sig_alg], self._cert_sig_algs): + ret += ['crt_file={cert} key_file={key}'.format(cert=cert, key=key)] + return ret + + def hrr_post_checks(self, named_group): + return ['-s "HRR selected_group: {:s}"'.format(named_group)] + + +class MbedTLSCli(MbedTLSBase): + """ + Generate test commands for mbedTLS client. + """ + + def pre_cmd(self): + return ['$P_CLI', + 'ca_file={cafile}'.format(cafile=CERTIFICATES[self._cert_sig_algs[0]].cafile)] + + def pre_checks(self): + return ['requires_config_enabled MBEDTLS_SSL_CLI_C'] + super().pre_checks() + + def hrr_post_checks(self, named_group): + ret = ['-c "received HelloRetryRequest message"'] + ret += ['-c "selected_group ( {:d} )"'.format(NAMED_GROUP_IANA_VALUE[named_group])] + return ret + + def post_checks(self): + check_strings = ["Protocol is TLSv1.3"] + if self._ciphers: + check_strings.append( + "server hello, chosen ciphersuite: ( {:04x} ) - {}".format( + CIPHER_SUITE_IANA_VALUE[self._ciphers[0]], + self.CIPHER_SUITE[self._ciphers[0]])) + if self._sig_algs: + check_strings.append( + "Certificate Verify: Signature algorithm ( {:04x} )".format( + SIG_ALG_IANA_VALUE[self._sig_algs[0]])) + + for named_group in self._named_groups: + check_strings += ['NamedGroup: {named_group} ( {iana_value:x} )'.format( + named_group=named_group, + iana_value=NAMED_GROUP_IANA_VALUE[named_group])] + + check_strings.append("Verifying peer X.509 certificate... ok") + return ['-c "{}"'.format(i) for i in check_strings] + + +SERVER_CLASSES = {'OpenSSL': OpenSSLServ, 'GnuTLS': GnuTLSServ, 'mbedTLS': MbedTLSServ} +CLIENT_CLASSES = {'OpenSSL': OpenSSLCli, 'GnuTLS': GnuTLSCli, 'mbedTLS': MbedTLSCli} + + +def generate_compat_test(client=None, server=None, cipher=None, named_group=None, sig_alg=None): + """ + Generate test case with `ssl-opt.sh` format. + """ + name = 'TLS 1.3 {client[0]}->{server[0]}: {cipher},{named_group},{sig_alg}'.format( + client=client, server=server, cipher=cipher[4:], sig_alg=sig_alg, named_group=named_group) + + server_object = SERVER_CLASSES[server](ciphersuite=cipher, + named_group=named_group, + signature_algorithm=sig_alg, + cert_sig_alg=sig_alg) + client_object = CLIENT_CLASSES[client](ciphersuite=cipher, + named_group=named_group, + signature_algorithm=sig_alg, + cert_sig_alg=sig_alg) + + cmd = ['run_test "{}"'.format(name), + '"{}"'.format(' '.join(server_object.cmd())), + '"{}"'.format(' '.join(client_object.cmd())), + '0'] + cmd += server_object.post_checks() + cmd += client_object.post_checks() + cmd += ['-C "received HelloRetryRequest message"'] + prefix = ' \\\n' + (' '*9) + cmd = prefix.join(cmd) + return '\n'.join(server_object.pre_checks() + client_object.pre_checks() + [cmd]) + + +def generate_hrr_compat_test(client=None, server=None, + client_named_group=None, server_named_group=None, + cert_sig_alg=None): + """ + Generate Hello Retry Request test case with `ssl-opt.sh` format. + """ + name = 'TLS 1.3 {client[0]}->{server[0]}: HRR {c_named_group} -> {s_named_group}'.format( + client=client, server=server, c_named_group=client_named_group, + s_named_group=server_named_group) + server_object = SERVER_CLASSES[server](named_group=server_named_group, + cert_sig_alg=cert_sig_alg) + + client_object = CLIENT_CLASSES[client](named_group=client_named_group, + cert_sig_alg=cert_sig_alg) + client_object.add_named_groups(server_named_group) + + cmd = ['run_test "{}"'.format(name), + '"{}"'.format(' '.join(server_object.cmd())), + '"{}"'.format(' '.join(client_object.cmd())), + '0'] + cmd += server_object.post_checks() + cmd += client_object.post_checks() + cmd += server_object.hrr_post_checks(server_named_group) + cmd += client_object.hrr_post_checks(server_named_group) + prefix = ' \\\n' + (' '*9) + cmd = prefix.join(cmd) + return '\n'.join(server_object.pre_checks() + + client_object.pre_checks() + + [cmd]) + +SSL_OUTPUT_HEADER = '''\ +# TLS 1.3 interoperability test cases (equivalent of compat.sh for TLS 1.3). +# +# Automatically generated by {cmd}. Do not edit! + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later +''' +DATA_FILES_PATH_VAR = ''' +DATA_FILES_PATH=../framework/data_files +''' + +def main(): + """ + Main function of this program + """ + parser = argparse.ArgumentParser() + + parser.add_argument('-o', '--output', + default='tests/opt-testcases/tls13-compat.sh', + help='Output file path (not used with -1)') + + parser.add_argument('-1', '--single', action='store_true', + help='Print a single test case') + # Single mode used to be the default. + parser.add_argument('-a', '--generate-all-tls13-compat-tests', + action='store_false', dest='single', + help='Generate all test cases (negates -1) (default)') + + parser.add_argument('--list-ciphers', action='store_true', + default=False, help='List supported ciphersuites') + + parser.add_argument('--list-sig-algs', action='store_true', + default=False, help='List supported signature algorithms') + + parser.add_argument('--list-named-groups', action='store_true', + default=False, help='List supported named groups') + + parser.add_argument('--list-servers', action='store_true', + default=False, help='List supported TLS servers') + + parser.add_argument('--list-clients', action='store_true', + default=False, help='List supported TLS Clients') + + parser.add_argument('server', choices=SERVER_CLASSES.keys(), nargs='?', + default=list(SERVER_CLASSES.keys())[0], + help='Choose TLS server program for test') + parser.add_argument('client', choices=CLIENT_CLASSES.keys(), nargs='?', + default=list(CLIENT_CLASSES.keys())[0], + help='Choose TLS client program for test') + parser.add_argument('cipher', choices=CIPHER_SUITE_IANA_VALUE.keys(), nargs='?', + default=list(CIPHER_SUITE_IANA_VALUE.keys())[0], + help='Choose cipher suite for test') + parser.add_argument('sig_alg', choices=SIG_ALG_IANA_VALUE.keys(), nargs='?', + default=list(SIG_ALG_IANA_VALUE.keys())[0], + help='Choose cipher suite for test') + parser.add_argument('named_group', choices=NAMED_GROUP_IANA_VALUE.keys(), nargs='?', + default=list(NAMED_GROUP_IANA_VALUE.keys())[0], + help='Choose cipher suite for test') + + args = parser.parse_args() + + def get_all_test_cases(): + # Generate normal compat test cases + for client, server, cipher, named_group, sig_alg in \ + itertools.product(CLIENT_CLASSES.keys(), + SERVER_CLASSES.keys(), + CIPHER_SUITE_IANA_VALUE.keys(), + NAMED_GROUP_IANA_VALUE.keys(), + SIG_ALG_IANA_VALUE.keys()): + if server == 'mbedTLS' or client == 'mbedTLS': + yield generate_compat_test(client=client, server=server, + cipher=cipher, named_group=named_group, + sig_alg=sig_alg) + + + # Generate Hello Retry Request compat test cases + for client, server, client_named_group, server_named_group in \ + itertools.product(CLIENT_CLASSES.keys(), + SERVER_CLASSES.keys(), + NAMED_GROUP_IANA_VALUE.keys(), + NAMED_GROUP_IANA_VALUE.keys()): + + if (client == 'mbedTLS' or server == 'mbedTLS') and \ + client_named_group != server_named_group: + yield generate_hrr_compat_test(client=client, server=server, + client_named_group=client_named_group, + server_named_group=server_named_group, + cert_sig_alg="ecdsa_secp256r1_sha256") + + if not args.single: + if args.output: + with open(args.output, 'w', encoding="utf-8") as f: + f.write(SSL_OUTPUT_HEADER.format( + filename=os.path.basename(args.output), + cmd=os.path.basename(sys.argv[0]))) + f.write(DATA_FILES_PATH_VAR) + f.write('\n\n'.join(get_all_test_cases())) + f.write('\n') + else: + print('\n\n'.join(get_all_test_cases())) + return 0 + + if args.list_ciphers or args.list_sig_algs or args.list_named_groups \ + or args.list_servers or args.list_clients: + if args.list_ciphers: + print(*CIPHER_SUITE_IANA_VALUE.keys()) + if args.list_sig_algs: + print(*SIG_ALG_IANA_VALUE.keys()) + if args.list_named_groups: + print(*NAMED_GROUP_IANA_VALUE.keys()) + if args.list_servers: + print(*SERVER_CLASSES.keys()) + if args.list_clients: + print(*CLIENT_CLASSES.keys()) + return 0 + + print(generate_compat_test(server=args.server, client=args.client, sig_alg=args.sig_alg, + cipher=args.cipher, named_group=args.named_group)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/translate_ciphers.py b/scripts/translate_ciphers.py new file mode 100755 index 000000000..90514fca1 --- /dev/null +++ b/scripts/translate_ciphers.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +# translate_ciphers.py +# +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +""" +Translate standard ciphersuite names to GnuTLS, OpenSSL and Mbed TLS standards. + +To test the translation functions run: +python3 -m unittest translate_cipher.py +""" + +import re +import argparse +import unittest + +class TestTranslateCiphers(unittest.TestCase): + """ + Ensure translate_ciphers.py translates and formats ciphersuite names + correctly + """ + def test_translate_all_cipher_names(self): + """ + Translate standard ciphersuite names to GnuTLS, OpenSSL and + Mbed TLS counterpart. Use only a small subset of ciphers + that exercise each step of the translation functions + """ + ciphers = [ + ("TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "+ECDHE-ECDSA:+NULL:+SHA1", + "ECDHE-ECDSA-NULL-SHA", + "TLS-ECDHE-ECDSA-WITH-NULL-SHA"), + ("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "+ECDHE-ECDSA:+AES-128-GCM:+AEAD", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256"), + ("TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "+DHE-RSA:+3DES-CBC:+SHA1", + "EDH-RSA-DES-CBC3-SHA", + "TLS-DHE-RSA-WITH-3DES-EDE-CBC-SHA"), + ("TLS_RSA_WITH_AES_256_CBC_SHA", + "+RSA:+AES-256-CBC:+SHA1", + "AES256-SHA", + "TLS-RSA-WITH-AES-256-CBC-SHA"), + ("TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "+PSK:+3DES-CBC:+SHA1", + "PSK-3DES-EDE-CBC-SHA", + "TLS-PSK-WITH-3DES-EDE-CBC-SHA"), + ("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + None, + "ECDHE-ECDSA-CHACHA20-POLY1305", + "TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256"), + ("TLS_ECDHE_ECDSA_WITH_AES_128_CCM", + "+ECDHE-ECDSA:+AES-128-CCM:+AEAD", + None, + "TLS-ECDHE-ECDSA-WITH-AES-128-CCM"), + ("TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", + None, + "ECDHE-ARIA256-GCM-SHA384", + "TLS-ECDHE-RSA-WITH-ARIA-256-GCM-SHA384"), + ] + + for s, g_exp, o_exp, m_exp in ciphers: + + if g_exp is not None: + g = translate_gnutls(s) + self.assertEqual(g, g_exp) + + if o_exp is not None: + o = translate_ossl(s) + self.assertEqual(o, o_exp) + + if m_exp is not None: + m = translate_mbedtls(s) + self.assertEqual(m, m_exp) + +def translate_gnutls(s_cipher): + """ + Translate s_cipher from standard ciphersuite naming convention + and return the GnuTLS naming convention + """ + + # Replace "_" with "-" to handle ciphersuite names based on Mbed TLS + # naming convention + s_cipher = s_cipher.replace("_", "-") + + s_cipher = re.sub(r'\ATLS-', '+', s_cipher) + s_cipher = s_cipher.replace("-WITH-", ":+") + s_cipher = s_cipher.replace("-EDE", "") + + # SHA in Mbed TLS == SHA1 GnuTLS, + # if the last 3 chars are SHA append 1 + if s_cipher[-3:] == "SHA": + s_cipher = s_cipher+"1" + + # CCM or CCM-8 should be followed by ":+AEAD" + # Replace "GCM:+SHAxyz" with "GCM:+AEAD" + if "CCM" in s_cipher or "GCM" in s_cipher: + s_cipher = re.sub(r"GCM-SHA\d\d\d", "GCM", s_cipher) + s_cipher = s_cipher+":+AEAD" + + # Replace the last "-" with ":+" + else: + index = s_cipher.rindex("-") + s_cipher = s_cipher[:index] + ":+" + s_cipher[index+1:] + + return s_cipher + +def translate_ossl(s_cipher): + """ + Translate s_cipher from standard ciphersuite naming convention + and return the OpenSSL naming convention + """ + + # Replace "_" with "-" to handle ciphersuite names based on Mbed TLS + # naming convention + s_cipher = s_cipher.replace("_", "-") + + s_cipher = re.sub(r'^TLS-', '', s_cipher) + s_cipher = s_cipher.replace("-WITH", "") + + # Remove the "-" from "ABC-xyz" + s_cipher = s_cipher.replace("AES-", "AES") + s_cipher = s_cipher.replace("CAMELLIA-", "CAMELLIA") + s_cipher = s_cipher.replace("ARIA-", "ARIA") + + # Remove "RSA" if it is at the beginning + s_cipher = re.sub(r'^RSA-', r'', s_cipher) + + # For all circumstances outside of PSK + if "PSK" not in s_cipher: + s_cipher = s_cipher.replace("-EDE", "") + s_cipher = s_cipher.replace("3DES-CBC", "DES-CBC3") + + # Remove "CBC" if it is not prefixed by DES + s_cipher = re.sub(r'(?