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
9import os
10import re
11import sys
12
13CLASSIC_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
82def 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
88def 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
94WITHOUT_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
106SPECIAL_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
111def 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
124def 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
156def 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
171def 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
186def 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
240def 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
250def 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
265def 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
279def main(args):
280    for file_name in args:
281        process_file(file_name)
282
283if __name__ == '__main__':
284    main(sys.argv[1:])
285