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