blob: ad1bc9012c1da73eaefdc315c6df3975e96fed15 [file] [log] [blame]
Gilles Peskinebdffaea2021-01-12 00:37:38 +01001#!/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
Gilles Peskine82ebaa42021-01-12 00:45:14 +010022import re
Gilles Peskinebdffaea2021-01-12 00:37:38 +010023import sys
24
Gilles Peskine9bbba5e2021-01-12 00:55:55 +010025CLASSIC_DEPENDENCIES = frozenset([
Gilles Peskine81dec002021-01-12 00:59:09 +010026 # This list is manually filtered from 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 #cipher#'MBEDTLS_CIPHER_MODE_CBC',
33 #cipher#'MBEDTLS_CIPHER_MODE_CFB',
34 #cipher#'MBEDTLS_CIPHER_MODE_CTR',
35 #cipher#'MBEDTLS_CIPHER_MODE_OFB',
36 #cipher#'MBEDTLS_CIPHER_MODE_XTS',
37 #cipher#'MBEDTLS_CIPHER_NULL_CIPHER',
38 #cipher#'MBEDTLS_CIPHER_PADDING_PKCS7',
39 #cipher#'MBEDTLS_CIPHER_PADDING_ONE_AND_ZEROS',
40 #cipher#'MBEDTLS_CIPHER_PADDING_ZEROS_AND_LEN',
41 #cipher#'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 'MBEDTLS_SHA512_NO_SHA384',
60
61 # Mbed TLS modules.
62 # Only modules that provide cryptographic mechanisms are listed here.
63 # Platform, data formatting, X.509 or TLS modules are omitted.
64 #cipher#'MBEDTLS_AES_C',
65 #cipher#'MBEDTLS_ARC4_C',
66 'MBEDTLS_BIGNUM_C',
67 #cipher#'MBEDTLS_BLOWFISH_C',
68 #cipher#'MBEDTLS_CAMELLIA_C',
69 #cipher#'MBEDTLS_ARIA_C',
70 #cipher#'MBEDTLS_CCM_C',
71 #cipher#'MBEDTLS_CHACHA20_C',
72 #cipher#'MBEDTLS_CHACHAPOLY_C',
73 #cipher#'MBEDTLS_CMAC_C',
74 'MBEDTLS_CTR_DRBG_C',
75 #cipher#'MBEDTLS_DES_C',
76 'MBEDTLS_DHM_C',
77 'MBEDTLS_ECDH_C',
78 'MBEDTLS_ECDSA_C',
79 'MBEDTLS_ECJPAKE_C',
80 'MBEDTLS_ECP_C',
81 'MBEDTLS_ENTROPY_C',
82 #cipher#'MBEDTLS_GCM_C',
83 'MBEDTLS_HKDF_C',
84 'MBEDTLS_HMAC_DRBG_C',
85 #cipher#'MBEDTLS_NIST_KW_C',
86 'MBEDTLS_MD2_C',
87 'MBEDTLS_MD4_C',
88 'MBEDTLS_MD5_C',
89 'MBEDTLS_PKCS5_C',
90 'MBEDTLS_PKCS12_C',
91 #cipher#'MBEDTLS_POLY1305_C',
92 #cipher#'MBEDTLS_RIPEMD160_C',
93 'MBEDTLS_RSA_C',
94 'MBEDTLS_SHA1_C',
95 'MBEDTLS_SHA256_C',
96 'MBEDTLS_SHA512_C',
97 'MBEDTLS_XTEA_C',
Gilles Peskine9bbba5e2021-01-12 00:55:55 +010098])
99
100def is_classic_dependency(dep):
101 """Whether dep is a classic dependency that PSA test cases should not use."""
102 if dep.startswith('!'):
103 dep = dep[1:]
104 return dep in CLASSIC_DEPENDENCIES
105
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100106def is_systematic_dependency(dep):
107 """Whether dep is a PSA dependency which is determined systematically."""
Ronald Cron6ac020d2021-03-23 17:40:47 +0100108 if dep.startswith('PSA_WANT_ECC_'):
109 return False
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100110 return dep.startswith('PSA_WANT_')
111
Gilles Peskinefa379612021-01-12 21:14:46 +0100112WITHOUT_SYSTEMATIC_DEPENDENCIES = frozenset([
Bence Szépkútia63b20d2020-12-16 11:36:46 +0100113 'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # only a modifier
Gilles Peskinef032fa92021-01-12 01:01:26 +0100114 'PSA_ALG_ANY_HASH', # only meaningful in policies
115 'PSA_ALG_KEY_AGREEMENT', # only a way to combine algorithms
116 'PSA_ALG_TRUNCATED_MAC', # only a modifier
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100117 'PSA_KEY_TYPE_NONE', # not a real key type
118 'PSA_KEY_TYPE_DERIVE', # always supported, don't list it to reduce noise
119 'PSA_KEY_TYPE_RAW_DATA', # always supported, don't list it to reduce noise
Steven Cooreman2c2efa42021-02-23 09:36:42 +0100120 'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', #only a modifier
121 'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', #only a modifier
Gilles Peskinef032fa92021-01-12 01:01:26 +0100122
Gilles Peskinee4f539c2021-01-12 00:58:36 +0100123 # Not implemented yet: cipher-related key types and algorithms.
124 # Manually extracted from crypto_values.h.
125 'PSA_KEY_TYPE_AES',
126 'PSA_KEY_TYPE_DES',
127 'PSA_KEY_TYPE_CAMELLIA',
128 'PSA_KEY_TYPE_ARC4',
129 'PSA_KEY_TYPE_CHACHA20',
130 'PSA_ALG_CBC_MAC',
131 'PSA_ALG_CMAC',
132 'PSA_ALG_STREAM_CIPHER',
133 'PSA_ALG_CTR',
134 'PSA_ALG_CFB',
135 'PSA_ALG_OFB',
136 'PSA_ALG_XTS',
137 'PSA_ALG_ECB_NO_PADDING',
138 'PSA_ALG_CBC_NO_PADDING',
139 'PSA_ALG_CBC_PKCS7',
140 'PSA_ALG_CCM',
141 'PSA_ALG_GCM',
142 'PSA_ALG_CHACHA20_POLY1305',
143])
144
Gilles Peskinef032fa92021-01-12 01:01:26 +0100145SPECIAL_SYSTEMATIC_DEPENDENCIES = {
146 'PSA_ALG_ECDSA_ANY': frozenset(['PSA_WANT_ALG_ECDSA']),
147 'PSA_ALG_RSA_PKCS1V15_SIGN_RAW': frozenset(['PSA_WANT_ALG_RSA_PKCS1V15_SIGN']),
148}
149
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100150def dependencies_of_symbol(symbol):
151 """Return the dependencies for a symbol that designates a cryptographic mechanism."""
Gilles Peskinefa379612021-01-12 21:14:46 +0100152 if symbol in WITHOUT_SYSTEMATIC_DEPENDENCIES:
Gilles Peskinee4f539c2021-01-12 00:58:36 +0100153 return frozenset()
Gilles Peskinef032fa92021-01-12 01:01:26 +0100154 if symbol in SPECIAL_SYSTEMATIC_DEPENDENCIES:
155 return SPECIAL_SYSTEMATIC_DEPENDENCIES[symbol]
Gilles Peskine20987b92021-01-12 01:11:32 +0100156 if symbol.startswith('PSA_ALG_CATEGORY_') or \
157 symbol.startswith('PSA_KEY_TYPE_CATEGORY_'):
158 # Categories are used in test data when an unsupported but plausible
159 # mechanism number needed. They have no associated dependency.
160 return frozenset()
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100161 return {symbol.replace('_', '_WANT_', 1)}
162
163def systematic_dependencies(file_name, function_name, arguments):
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100164 """List the systematically determined dependency for a test case."""
165 deps = set()
Gilles Peskine72d8e0a2021-01-12 01:11:42 +0100166
167 # Run key policy negative tests even if the algorithm to attempt performing
168 # is not supported.
169 if function_name.endswith('_key_policy') and \
Gilles Peskine07945722021-01-12 12:57:23 +0100170 arguments[-1].startswith('PSA_ERROR_'):
Gilles Peskine72d8e0a2021-01-12 01:11:42 +0100171 arguments[-2] = ''
Gilles Peskineb51d72f2021-01-12 21:15:52 +0100172 if function_name == 'copy_fail' and \
173 arguments[-1].startswith('PSA_ERROR_'):
174 arguments[-2] = ''
175 arguments[-3] = ''
Gilles Peskine72d8e0a2021-01-12 01:11:42 +0100176
Gilles Peskine80a97082021-01-12 21:18:36 +0100177 # Storage format tests that only look at how the file is structured and
178 # don't care about the format of the key material don't depend on any
179 # cryptographic mechanisms.
180 if os.path.basename(file_name) == 'test_suite_psa_crypto_persistent_key.data' and \
181 function_name in {'format_storage_data_check',
182 'parse_storage_data_check'}:
183 return []
184
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100185 for arg in arguments:
186 for symbol in re.findall(r'PSA_(?:ALG|KEY_TYPE)_\w+', arg):
187 deps.update(dependencies_of_symbol(symbol))
188 return sorted(deps)
189
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100190def updated_dependencies(file_name, function_name, arguments, dependencies):
191 """Rework the list of dependencies into PSA_WANT_xxx.
192
193 Remove classic crypto dependencies such as MBEDTLS_RSA_C,
194 MBEDTLS_PKCS1_V15, etc.
195
196 Add systematic PSA_WANT_xxx dependencies based on the called function and
197 its arguments, replacing existing PSA_WANT_xxx dependencies.
198 """
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100199 automatic = systematic_dependencies(file_name, function_name, arguments)
200 manual = [dep for dep in dependencies
Gilles Peskine9bbba5e2021-01-12 00:55:55 +0100201 if not (is_systematic_dependency(dep) or
202 is_classic_dependency(dep))]
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100203 return automatic + manual
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100204
Gilles Peskine45e9e732021-01-12 00:47:03 +0100205def keep_manual_dependencies(file_name, function_name, arguments):
206 #pylint: disable=unused-argument
207 """Declare test functions with unusual dependencies here."""
Gilles Peskinea87e1782021-01-12 01:13:39 +0100208 # If there are no arguments, we can't do any useful work. Assume that if
209 # there are dependencies, they are warranted.
210 if not arguments:
211 return True
Gilles Peskine20987b92021-01-12 01:11:32 +0100212 # When PSA_ERROR_NOT_SUPPORTED is expected, usually, at least one of the
213 # constants mentioned in the test should not be supported. It isn't
214 # possible to determine which one in a systematic way. So let the programmer
215 # decide.
216 if arguments[-1] == 'PSA_ERROR_NOT_SUPPORTED':
217 return True
Gilles Peskine45e9e732021-01-12 00:47:03 +0100218 return False
219
Gilles Peskinebdffaea2021-01-12 00:37:38 +0100220def process_data_stanza(stanza, file_name, test_case_number):
221 """Update PSA crypto dependencies in one Mbed TLS test case.
222
223 stanza is the test case text (including the description, the dependencies,
224 the line with the function and arguments, and optionally comments). Return
225 a new stanza with an updated dependency line, preserving everything else
226 (description, comments, arguments, etc.).
227 """
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100228 if not stanza.lstrip('\n'):
229 # Just blank lines
230 return stanza
231 # Expect 2 or 3 non-comment lines: description, optional dependencies,
232 # function-and-arguments.
233 content_matches = list(re.finditer(r'^[\t ]*([^\t #].*)$', stanza, re.M))
234 if len(content_matches) < 2:
235 raise Exception('Not enough content lines in paragraph {} in {}'
236 .format(test_case_number, file_name))
237 if len(content_matches) > 3:
238 raise Exception('Too many content lines in paragraph {} in {}'
239 .format(test_case_number, file_name))
240 arguments = content_matches[-1].group(0).split(':')
241 function_name = arguments.pop(0)
Gilles Peskine45e9e732021-01-12 00:47:03 +0100242 if keep_manual_dependencies(file_name, function_name, arguments):
243 return stanza
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100244 if len(content_matches) == 2:
245 # Insert a line for the dependencies. If it turns out that there are
246 # no dependencies, we'll remove that empty line below.
247 dependencies_location = content_matches[-1].start()
248 text_before = stanza[:dependencies_location]
249 text_after = '\n' + stanza[dependencies_location:]
250 old_dependencies = []
251 dependencies_leader = 'depends_on:'
252 else:
253 dependencies_match = content_matches[-2]
254 text_before = stanza[:dependencies_match.start()]
255 text_after = stanza[dependencies_match.end():]
256 old_dependencies = dependencies_match.group(0).split(':')
257 dependencies_leader = old_dependencies.pop(0) + ':'
258 if dependencies_leader != 'depends_on:':
259 raise Exception('Next-to-last line does not start with "depends_on:"'
260 ' in paragraph {} in {}'
261 .format(test_case_number, file_name))
262 new_dependencies = updated_dependencies(file_name, function_name, arguments,
263 old_dependencies)
264 if new_dependencies:
265 stanza = (text_before +
266 dependencies_leader + ':'.join(new_dependencies) +
267 text_after)
268 else:
269 # The dependencies have become empty. Remove the depends_on: line.
270 assert text_after[0] == '\n'
271 stanza = text_before + text_after[1:]
Gilles Peskinebdffaea2021-01-12 00:37:38 +0100272 return stanza
273
274def process_data_file(file_name, old_content):
275 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
276
277 Process old_content (the old content of the file) and return the new content.
278 """
279 old_stanzas = old_content.split('\n\n')
280 new_stanzas = [process_data_stanza(stanza, file_name, n)
281 for n, stanza in enumerate(old_stanzas, start=1)]
282 return '\n\n'.join(new_stanzas)
283
284def update_file(file_name, old_content, new_content):
285 """Update the given file with the given new content.
286
287 Replace the existing file. The previous version is renamed to *.bak.
288 Don't modify the file if the content was unchanged.
289 """
290 if new_content == old_content:
291 return
292 backup = file_name + '.bak'
293 tmp = file_name + '.tmp'
294 with open(tmp, 'w', encoding='utf-8') as new_file:
295 new_file.write(new_content)
296 os.replace(file_name, backup)
297 os.replace(tmp, file_name)
298
299def process_file(file_name):
300 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
301
302 Replace the existing file. The previous version is renamed to *.bak.
303 Don't modify the file if the content was unchanged.
304 """
305 old_content = open(file_name, encoding='utf-8').read()
306 if file_name.endswith('.data'):
307 new_content = process_data_file(file_name, old_content)
308 else:
309 raise Exception('File type not recognized: {}'
310 .format(file_name))
311 update_file(file_name, old_content, new_content)
312
313def main(args):
314 for file_name in args:
315 process_file(file_name)
316
317if __name__ == '__main__':
318 main(sys.argv[1:])