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