blob: 34084a7205a762e8a8fcdf0f0650c962c44ff661 [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."""
108 return dep.startswith('PSA_WANT_')
109
Gilles Peskinee4f539c2021-01-12 00:58:36 +0100110OMITTED_SYSTEMATIC_DEPENDENCIES = frozenset([
111 # Not implemented yet: cipher-related key types and algorithms.
112 # Manually extracted from crypto_values.h.
113 'PSA_KEY_TYPE_AES',
114 'PSA_KEY_TYPE_DES',
115 'PSA_KEY_TYPE_CAMELLIA',
116 'PSA_KEY_TYPE_ARC4',
117 'PSA_KEY_TYPE_CHACHA20',
118 'PSA_ALG_CBC_MAC',
119 'PSA_ALG_CMAC',
120 'PSA_ALG_STREAM_CIPHER',
121 'PSA_ALG_CTR',
122 'PSA_ALG_CFB',
123 'PSA_ALG_OFB',
124 'PSA_ALG_XTS',
125 'PSA_ALG_ECB_NO_PADDING',
126 'PSA_ALG_CBC_NO_PADDING',
127 'PSA_ALG_CBC_PKCS7',
128 'PSA_ALG_CCM',
129 'PSA_ALG_GCM',
130 'PSA_ALG_CHACHA20_POLY1305',
131])
132
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100133def dependencies_of_symbol(symbol):
134 """Return the dependencies for a symbol that designates a cryptographic mechanism."""
Gilles Peskinee4f539c2021-01-12 00:58:36 +0100135 if symbol in OMITTED_SYSTEMATIC_DEPENDENCIES:
136 return frozenset()
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100137 return {symbol.replace('_', '_WANT_', 1)}
138
139def systematic_dependencies(file_name, function_name, arguments):
140 #pylint: disable=unused-argument
141 """List the systematically determined dependency for a test case."""
142 deps = set()
143 for arg in arguments:
144 for symbol in re.findall(r'PSA_(?:ALG|KEY_TYPE)_\w+', arg):
145 deps.update(dependencies_of_symbol(symbol))
146 return sorted(deps)
147
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100148def updated_dependencies(file_name, function_name, arguments, dependencies):
149 """Rework the list of dependencies into PSA_WANT_xxx.
150
151 Remove classic crypto dependencies such as MBEDTLS_RSA_C,
152 MBEDTLS_PKCS1_V15, etc.
153
154 Add systematic PSA_WANT_xxx dependencies based on the called function and
155 its arguments, replacing existing PSA_WANT_xxx dependencies.
156 """
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100157 automatic = systematic_dependencies(file_name, function_name, arguments)
158 manual = [dep for dep in dependencies
Gilles Peskine9bbba5e2021-01-12 00:55:55 +0100159 if not (is_systematic_dependency(dep) or
160 is_classic_dependency(dep))]
Gilles Peskine2d2e9242021-01-12 00:52:31 +0100161 return automatic + manual
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100162
Gilles Peskine45e9e732021-01-12 00:47:03 +0100163def keep_manual_dependencies(file_name, function_name, arguments):
164 #pylint: disable=unused-argument
165 """Declare test functions with unusual dependencies here."""
166 return False
167
Gilles Peskinebdffaea2021-01-12 00:37:38 +0100168def process_data_stanza(stanza, file_name, test_case_number):
169 """Update PSA crypto dependencies in one Mbed TLS test case.
170
171 stanza is the test case text (including the description, the dependencies,
172 the line with the function and arguments, and optionally comments). Return
173 a new stanza with an updated dependency line, preserving everything else
174 (description, comments, arguments, etc.).
175 """
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100176 if not stanza.lstrip('\n'):
177 # Just blank lines
178 return stanza
179 # Expect 2 or 3 non-comment lines: description, optional dependencies,
180 # function-and-arguments.
181 content_matches = list(re.finditer(r'^[\t ]*([^\t #].*)$', stanza, re.M))
182 if len(content_matches) < 2:
183 raise Exception('Not enough content lines in paragraph {} in {}'
184 .format(test_case_number, file_name))
185 if len(content_matches) > 3:
186 raise Exception('Too many content lines in paragraph {} in {}'
187 .format(test_case_number, file_name))
188 arguments = content_matches[-1].group(0).split(':')
189 function_name = arguments.pop(0)
Gilles Peskine45e9e732021-01-12 00:47:03 +0100190 if keep_manual_dependencies(file_name, function_name, arguments):
191 return stanza
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100192 if len(content_matches) == 2:
193 # Insert a line for the dependencies. If it turns out that there are
194 # no dependencies, we'll remove that empty line below.
195 dependencies_location = content_matches[-1].start()
196 text_before = stanza[:dependencies_location]
197 text_after = '\n' + stanza[dependencies_location:]
198 old_dependencies = []
199 dependencies_leader = 'depends_on:'
200 else:
201 dependencies_match = content_matches[-2]
202 text_before = stanza[:dependencies_match.start()]
203 text_after = stanza[dependencies_match.end():]
204 old_dependencies = dependencies_match.group(0).split(':')
205 dependencies_leader = old_dependencies.pop(0) + ':'
206 if dependencies_leader != 'depends_on:':
207 raise Exception('Next-to-last line does not start with "depends_on:"'
208 ' in paragraph {} in {}'
209 .format(test_case_number, file_name))
210 new_dependencies = updated_dependencies(file_name, function_name, arguments,
211 old_dependencies)
212 if new_dependencies:
213 stanza = (text_before +
214 dependencies_leader + ':'.join(new_dependencies) +
215 text_after)
216 else:
217 # The dependencies have become empty. Remove the depends_on: line.
218 assert text_after[0] == '\n'
219 stanza = text_before + text_after[1:]
Gilles Peskinebdffaea2021-01-12 00:37:38 +0100220 return stanza
221
222def process_data_file(file_name, old_content):
223 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
224
225 Process old_content (the old content of the file) and return the new content.
226 """
227 old_stanzas = old_content.split('\n\n')
228 new_stanzas = [process_data_stanza(stanza, file_name, n)
229 for n, stanza in enumerate(old_stanzas, start=1)]
230 return '\n\n'.join(new_stanzas)
231
232def update_file(file_name, old_content, new_content):
233 """Update the given file with the given new content.
234
235 Replace the existing file. The previous version is renamed to *.bak.
236 Don't modify the file if the content was unchanged.
237 """
238 if new_content == old_content:
239 return
240 backup = file_name + '.bak'
241 tmp = file_name + '.tmp'
242 with open(tmp, 'w', encoding='utf-8') as new_file:
243 new_file.write(new_content)
244 os.replace(file_name, backup)
245 os.replace(tmp, file_name)
246
247def process_file(file_name):
248 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
249
250 Replace the existing file. The previous version is renamed to *.bak.
251 Don't modify the file if the content was unchanged.
252 """
253 old_content = open(file_name, encoding='utf-8').read()
254 if file_name.endswith('.data'):
255 new_content = process_data_file(file_name, old_content)
256 else:
257 raise Exception('File type not recognized: {}'
258 .format(file_name))
259 update_file(file_name, old_content, new_content)
260
261def main(args):
262 for file_name in args:
263 process_file(file_name)
264
265if __name__ == '__main__':
266 main(sys.argv[1:])