quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

set_psa_test_dependencies.py (11338B)


      1 #!/usr/bin/env python3
      2 
      3 """Edit test cases to use PSA dependencies instead of classic dependencies.
      4 """
      5 
      6 # Copyright The Mbed TLS Contributors
      7 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
      8 
      9 import os
     10 import re
     11 import sys
     12 
     13 CLASSIC_DEPENDENCIES = frozenset([
     14     # This list is manually filtered from mbedtls_config.h.
     15 
     16     # Mbed TLS feature support.
     17     # Only features that affect what can be done are listed here.
     18     # Options that control optimizations or alternative implementations
     19     # are omitted.
     20     'MBEDTLS_CIPHER_MODE_CBC',
     21     'MBEDTLS_CIPHER_MODE_CFB',
     22     'MBEDTLS_CIPHER_MODE_CTR',
     23     'MBEDTLS_CIPHER_MODE_OFB',
     24     'MBEDTLS_CIPHER_MODE_XTS',
     25     'MBEDTLS_CIPHER_NULL_CIPHER',
     26     'MBEDTLS_CIPHER_PADDING_PKCS7',
     27     'MBEDTLS_CIPHER_PADDING_ONE_AND_ZEROS',
     28     'MBEDTLS_CIPHER_PADDING_ZEROS_AND_LEN',
     29     'MBEDTLS_CIPHER_PADDING_ZEROS',
     30     #curve#'MBEDTLS_ECP_DP_SECP192R1_ENABLED',
     31     #curve#'MBEDTLS_ECP_DP_SECP224R1_ENABLED',
     32     #curve#'MBEDTLS_ECP_DP_SECP256R1_ENABLED',
     33     #curve#'MBEDTLS_ECP_DP_SECP384R1_ENABLED',
     34     #curve#'MBEDTLS_ECP_DP_SECP521R1_ENABLED',
     35     #curve#'MBEDTLS_ECP_DP_SECP192K1_ENABLED',
     36     #curve#'MBEDTLS_ECP_DP_SECP224K1_ENABLED',
     37     #curve#'MBEDTLS_ECP_DP_SECP256K1_ENABLED',
     38     #curve#'MBEDTLS_ECP_DP_BP256R1_ENABLED',
     39     #curve#'MBEDTLS_ECP_DP_BP384R1_ENABLED',
     40     #curve#'MBEDTLS_ECP_DP_BP512R1_ENABLED',
     41     #curve#'MBEDTLS_ECP_DP_CURVE25519_ENABLED',
     42     #curve#'MBEDTLS_ECP_DP_CURVE448_ENABLED',
     43     'MBEDTLS_ECDSA_DETERMINISTIC',
     44     #'MBEDTLS_GENPRIME', #needed for RSA key generation
     45     'MBEDTLS_PKCS1_V15',
     46     'MBEDTLS_PKCS1_V21',
     47 
     48     # Mbed TLS modules.
     49     # Only modules that provide cryptographic mechanisms are listed here.
     50     # Platform, data formatting, X.509 or TLS modules are omitted.
     51     'MBEDTLS_AES_C',
     52     'MBEDTLS_BIGNUM_C',
     53     'MBEDTLS_CAMELLIA_C',
     54     'MBEDTLS_ARIA_C',
     55     'MBEDTLS_CCM_C',
     56     'MBEDTLS_CHACHA20_C',
     57     'MBEDTLS_CHACHAPOLY_C',
     58     'MBEDTLS_CMAC_C',
     59     'MBEDTLS_CTR_DRBG_C',
     60     'MBEDTLS_DES_C',
     61     'MBEDTLS_DHM_C',
     62     'MBEDTLS_ECDH_C',
     63     'MBEDTLS_ECDSA_C',
     64     'MBEDTLS_ECJPAKE_C',
     65     'MBEDTLS_ECP_C',
     66     'MBEDTLS_ENTROPY_C',
     67     'MBEDTLS_GCM_C',
     68     'MBEDTLS_HKDF_C',
     69     'MBEDTLS_HMAC_DRBG_C',
     70     'MBEDTLS_NIST_KW_C',
     71     'MBEDTLS_MD5_C',
     72     'MBEDTLS_PKCS5_C',
     73     'MBEDTLS_PKCS12_C',
     74     'MBEDTLS_POLY1305_C',
     75     'MBEDTLS_RIPEMD160_C',
     76     'MBEDTLS_RSA_C',
     77     'MBEDTLS_SHA1_C',
     78     'MBEDTLS_SHA256_C',
     79     'MBEDTLS_SHA512_C',
     80 ])
     81 
     82 def is_classic_dependency(dep):
     83     """Whether dep is a classic dependency that PSA test cases should not use."""
     84     if dep.startswith('!'):
     85         dep = dep[1:]
     86     return dep in CLASSIC_DEPENDENCIES
     87 
     88 def is_systematic_dependency(dep):
     89     """Whether dep is a PSA dependency which is determined systematically."""
     90     if dep.startswith('PSA_WANT_ECC_'):
     91         return False
     92     return dep.startswith('PSA_WANT_')
     93 
     94 WITHOUT_SYSTEMATIC_DEPENDENCIES = frozenset([
     95     'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # only a modifier
     96     'PSA_ALG_ANY_HASH', # only meaningful in policies
     97     'PSA_ALG_KEY_AGREEMENT', # only a way to combine algorithms
     98     'PSA_ALG_TRUNCATED_MAC', # only a modifier
     99     'PSA_KEY_TYPE_NONE', # not a real key type
    100     'PSA_KEY_TYPE_DERIVE', # always supported, don't list it to reduce noise
    101     'PSA_KEY_TYPE_RAW_DATA', # always supported, don't list it to reduce noise
    102     'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', #only a modifier
    103     'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', #only a modifier
    104 ])
    105 
    106 SPECIAL_SYSTEMATIC_DEPENDENCIES = {
    107     'PSA_ALG_ECDSA_ANY': frozenset(['PSA_WANT_ALG_ECDSA']),
    108     'PSA_ALG_RSA_PKCS1V15_SIGN_RAW': frozenset(['PSA_WANT_ALG_RSA_PKCS1V15_SIGN']),
    109 }
    110 
    111 def dependencies_of_symbol(symbol):
    112     """Return the dependencies for a symbol that designates a cryptographic mechanism."""
    113     if symbol in WITHOUT_SYSTEMATIC_DEPENDENCIES:
    114         return frozenset()
    115     if symbol in SPECIAL_SYSTEMATIC_DEPENDENCIES:
    116         return SPECIAL_SYSTEMATIC_DEPENDENCIES[symbol]
    117     if symbol.startswith('PSA_ALG_CATEGORY_') or \
    118        symbol.startswith('PSA_KEY_TYPE_CATEGORY_'):
    119         # Categories are used in test data when an unsupported but plausible
    120         # mechanism number needed. They have no associated dependency.
    121         return frozenset()
    122     return {symbol.replace('_', '_WANT_', 1)}
    123 
    124 def systematic_dependencies(file_name, function_name, arguments):
    125     """List the systematically determined dependency for a test case."""
    126     deps = set()
    127 
    128     # Run key policy negative tests even if the algorithm to attempt performing
    129     # is not supported but in the case where the test is to check an
    130     # incompatibility between a requested algorithm for a cryptographic
    131     # operation and a key policy. In the latter, we want to filter out the
    132     # cases # where PSA_ERROR_NOT_SUPPORTED is returned instead of
    133     # PSA_ERROR_NOT_PERMITTED.
    134     if function_name.endswith('_key_policy') and \
    135        arguments[-1].startswith('PSA_ERROR_') and \
    136        arguments[-1] != ('PSA_ERROR_NOT_PERMITTED'):
    137         arguments[-2] = ''
    138     if function_name == 'copy_fail' and \
    139        arguments[-1].startswith('PSA_ERROR_'):
    140         arguments[-2] = ''
    141         arguments[-3] = ''
    142 
    143     # Storage format tests that only look at how the file is structured and
    144     # don't care about the format of the key material don't depend on any
    145     # cryptographic mechanisms.
    146     if os.path.basename(file_name) == 'test_suite_psa_crypto_persistent_key.data' and \
    147        function_name in {'format_storage_data_check',
    148                          'parse_storage_data_check'}:
    149         return []
    150 
    151     for arg in arguments:
    152         for symbol in re.findall(r'PSA_(?:ALG|KEY_TYPE)_\w+', arg):
    153             deps.update(dependencies_of_symbol(symbol))
    154     return sorted(deps)
    155 
    156 def updated_dependencies(file_name, function_name, arguments, dependencies):
    157     """Rework the list of dependencies into PSA_WANT_xxx.
    158 
    159     Remove classic crypto dependencies such as MBEDTLS_RSA_C,
    160     MBEDTLS_PKCS1_V15, etc.
    161 
    162     Add systematic PSA_WANT_xxx dependencies based on the called function and
    163     its arguments, replacing existing PSA_WANT_xxx dependencies.
    164     """
    165     automatic = systematic_dependencies(file_name, function_name, arguments)
    166     manual = [dep for dep in dependencies
    167               if not (is_systematic_dependency(dep) or
    168                       is_classic_dependency(dep))]
    169     return automatic + manual
    170 
    171 def keep_manual_dependencies(file_name, function_name, arguments):
    172     #pylint: disable=unused-argument
    173     """Declare test functions with unusual dependencies here."""
    174     # If there are no arguments, we can't do any useful work. Assume that if
    175     # there are dependencies, they are warranted.
    176     if not arguments:
    177         return True
    178     # When PSA_ERROR_NOT_SUPPORTED is expected, usually, at least one of the
    179     # constants mentioned in the test should not be supported. It isn't
    180     # possible to determine which one in a systematic way. So let the programmer
    181     # decide.
    182     if arguments[-1] == 'PSA_ERROR_NOT_SUPPORTED':
    183         return True
    184     return False
    185 
    186 def process_data_stanza(stanza, file_name, test_case_number):
    187     """Update PSA crypto dependencies in one Mbed TLS test case.
    188 
    189     stanza is the test case text (including the description, the dependencies,
    190     the line with the function and arguments, and optionally comments). Return
    191     a new stanza with an updated dependency line, preserving everything else
    192     (description, comments, arguments, etc.).
    193     """
    194     if not stanza.lstrip('\n'):
    195         # Just blank lines
    196         return stanza
    197     # Expect 2 or 3 non-comment lines: description, optional dependencies,
    198     # function-and-arguments.
    199     content_matches = list(re.finditer(r'^[\t ]*([^\t #].*)$', stanza, re.M))
    200     if len(content_matches) < 2:
    201         raise Exception('Not enough content lines in paragraph {} in {}'
    202                         .format(test_case_number, file_name))
    203     if len(content_matches) > 3:
    204         raise Exception('Too many content lines in paragraph {} in {}'
    205                         .format(test_case_number, file_name))
    206     arguments = content_matches[-1].group(0).split(':')
    207     function_name = arguments.pop(0)
    208     if keep_manual_dependencies(file_name, function_name, arguments):
    209         return stanza
    210     if len(content_matches) == 2:
    211         # Insert a line for the dependencies. If it turns out that there are
    212         # no dependencies, we'll remove that empty line below.
    213         dependencies_location = content_matches[-1].start()
    214         text_before = stanza[:dependencies_location]
    215         text_after = '\n' + stanza[dependencies_location:]
    216         old_dependencies = []
    217         dependencies_leader = 'depends_on:'
    218     else:
    219         dependencies_match = content_matches[-2]
    220         text_before = stanza[:dependencies_match.start()]
    221         text_after = stanza[dependencies_match.end():]
    222         old_dependencies = dependencies_match.group(0).split(':')
    223         dependencies_leader = old_dependencies.pop(0) + ':'
    224         if dependencies_leader != 'depends_on:':
    225             raise Exception('Next-to-last line does not start with "depends_on:"'
    226                             ' in paragraph {} in {}'
    227                             .format(test_case_number, file_name))
    228     new_dependencies = updated_dependencies(file_name, function_name, arguments,
    229                                             old_dependencies)
    230     if new_dependencies:
    231         stanza = (text_before +
    232                   dependencies_leader + ':'.join(new_dependencies) +
    233                   text_after)
    234     else:
    235         # The dependencies have become empty. Remove the depends_on: line.
    236         assert text_after[0] == '\n'
    237         stanza = text_before + text_after[1:]
    238     return stanza
    239 
    240 def process_data_file(file_name, old_content):
    241     """Update PSA crypto dependencies in an Mbed TLS test suite data file.
    242 
    243     Process old_content (the old content of the file) and return the new content.
    244     """
    245     old_stanzas = old_content.split('\n\n')
    246     new_stanzas = [process_data_stanza(stanza, file_name, n)
    247                    for n, stanza in enumerate(old_stanzas, start=1)]
    248     return '\n\n'.join(new_stanzas)
    249 
    250 def update_file(file_name, old_content, new_content):
    251     """Update the given file with the given new content.
    252 
    253     Replace the existing file. The previous version is renamed to *.bak.
    254     Don't modify the file if the content was unchanged.
    255     """
    256     if new_content == old_content:
    257         return
    258     backup = file_name + '.bak'
    259     tmp = file_name + '.tmp'
    260     with open(tmp, 'w', encoding='utf-8') as new_file:
    261         new_file.write(new_content)
    262     os.replace(file_name, backup)
    263     os.replace(tmp, file_name)
    264 
    265 def process_file(file_name):
    266     """Update PSA crypto dependencies in an Mbed TLS test suite data file.
    267 
    268     Replace the existing file. The previous version is renamed to *.bak.
    269     Don't modify the file if the content was unchanged.
    270     """
    271     old_content = open(file_name, encoding='utf-8').read()
    272     if file_name.endswith('.data'):
    273         new_content = process_data_file(file_name, old_content)
    274     else:
    275         raise Exception('File type not recognized: {}'
    276                         .format(file_name))
    277     update_file(file_name, old_content, new_content)
    278 
    279 def main(args):
    280     for file_name in args:
    281         process_file(file_name)
    282 
    283 if __name__ == '__main__':
    284     main(sys.argv[1:])