Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 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 | |
| 21 | import os |
Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 22 | import re |
Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 23 | import sys |
| 24 | |
Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 25 | CLASSIC_DEPENDENCIES = frozenset([ |
Bence Szépkúti | bb0cfeb | 2021-05-28 09:42:25 +0200 | [diff] [blame] | 26 | # This list is manually filtered from mbedtls_config.h. |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 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. |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 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', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 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', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 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. |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 63 | 'MBEDTLS_AES_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 64 | 'MBEDTLS_BIGNUM_C', |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 65 | 'MBEDTLS_CAMELLIA_C', |
| 66 | 'MBEDTLS_ARIA_C', |
| 67 | 'MBEDTLS_CCM_C', |
| 68 | 'MBEDTLS_CHACHA20_C', |
| 69 | 'MBEDTLS_CHACHAPOLY_C', |
| 70 | 'MBEDTLS_CMAC_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 71 | 'MBEDTLS_CTR_DRBG_C', |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 72 | 'MBEDTLS_DES_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 73 | 'MBEDTLS_DHM_C', |
| 74 | 'MBEDTLS_ECDH_C', |
| 75 | 'MBEDTLS_ECDSA_C', |
| 76 | 'MBEDTLS_ECJPAKE_C', |
| 77 | 'MBEDTLS_ECP_C', |
| 78 | 'MBEDTLS_ENTROPY_C', |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 79 | 'MBEDTLS_GCM_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 80 | 'MBEDTLS_HKDF_C', |
| 81 | 'MBEDTLS_HMAC_DRBG_C', |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 82 | 'MBEDTLS_NIST_KW_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 83 | 'MBEDTLS_MD5_C', |
| 84 | 'MBEDTLS_PKCS5_C', |
| 85 | 'MBEDTLS_PKCS12_C', |
Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 86 | 'MBEDTLS_POLY1305_C', |
| 87 | 'MBEDTLS_RIPEMD160_C', |
Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 88 | 'MBEDTLS_RSA_C', |
| 89 | 'MBEDTLS_SHA1_C', |
| 90 | 'MBEDTLS_SHA256_C', |
| 91 | 'MBEDTLS_SHA512_C', |
Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 92 | ]) |
| 93 | |
| 94 | def 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 | |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 100 | def is_systematic_dependency(dep): |
| 101 | """Whether dep is a PSA dependency which is determined systematically.""" |
Ronald Cron | 6ac020d | 2021-03-23 17:40:47 +0100 | [diff] [blame] | 102 | if dep.startswith('PSA_WANT_ECC_'): |
| 103 | return False |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 104 | return dep.startswith('PSA_WANT_') |
| 105 | |
Gilles Peskine | fa37961 | 2021-01-12 21:14:46 +0100 | [diff] [blame] | 106 | WITHOUT_SYSTEMATIC_DEPENDENCIES = frozenset([ |
Bence Szépkúti | a63b20d | 2020-12-16 11:36:46 +0100 | [diff] [blame] | 107 | 'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # only a modifier |
Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 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 |
Gilles Peskine | 60b29fe | 2021-02-16 14:06:50 +0100 | [diff] [blame] | 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 |
Steven Cooreman | 2c2efa4 | 2021-02-23 09:36:42 +0100 | [diff] [blame] | 114 | 'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', #only a modifier |
| 115 | 'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', #only a modifier |
Gilles Peskine | e4f539c | 2021-01-12 00:58:36 +0100 | [diff] [blame] | 116 | ]) |
| 117 | |
Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 118 | SPECIAL_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 | |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 123 | def dependencies_of_symbol(symbol): |
| 124 | """Return the dependencies for a symbol that designates a cryptographic mechanism.""" |
Gilles Peskine | fa37961 | 2021-01-12 21:14:46 +0100 | [diff] [blame] | 125 | if symbol in WITHOUT_SYSTEMATIC_DEPENDENCIES: |
Gilles Peskine | e4f539c | 2021-01-12 00:58:36 +0100 | [diff] [blame] | 126 | return frozenset() |
Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 127 | if symbol in SPECIAL_SYSTEMATIC_DEPENDENCIES: |
| 128 | return SPECIAL_SYSTEMATIC_DEPENDENCIES[symbol] |
Gilles Peskine | 20987b9 | 2021-01-12 01:11:32 +0100 | [diff] [blame] | 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() |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 134 | return {symbol.replace('_', '_WANT_', 1)} |
| 135 | |
| 136 | def systematic_dependencies(file_name, function_name, arguments): |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 137 | """List the systematically determined dependency for a test case.""" |
| 138 | deps = set() |
Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 139 | |
| 140 | # Run key policy negative tests even if the algorithm to attempt performing |
Ronald Cron | 9838dc2 | 2021-03-24 09:18:23 +0100 | [diff] [blame] | 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. |
Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 146 | if function_name.endswith('_key_policy') and \ |
Ronald Cron | 9838dc2 | 2021-03-24 09:18:23 +0100 | [diff] [blame] | 147 | arguments[-1].startswith('PSA_ERROR_') and \ |
| 148 | arguments[-1] != ('PSA_ERROR_NOT_PERMITTED'): |
Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 149 | arguments[-2] = '' |
Gilles Peskine | b51d72f | 2021-01-12 21:15:52 +0100 | [diff] [blame] | 150 | if function_name == 'copy_fail' and \ |
| 151 | arguments[-1].startswith('PSA_ERROR_'): |
| 152 | arguments[-2] = '' |
| 153 | arguments[-3] = '' |
Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 154 | |
Gilles Peskine | 80a9708 | 2021-01-12 21:18:36 +0100 | [diff] [blame] | 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 | |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 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 | |
Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 168 | def 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 | """ |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 177 | automatic = systematic_dependencies(file_name, function_name, arguments) |
| 178 | manual = [dep for dep in dependencies |
Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 179 | if not (is_systematic_dependency(dep) or |
| 180 | is_classic_dependency(dep))] |
Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 181 | return automatic + manual |
Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 182 | |
Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 183 | def keep_manual_dependencies(file_name, function_name, arguments): |
| 184 | #pylint: disable=unused-argument |
| 185 | """Declare test functions with unusual dependencies here.""" |
Gilles Peskine | a87e178 | 2021-01-12 01:13:39 +0100 | [diff] [blame] | 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 |
Gilles Peskine | 20987b9 | 2021-01-12 01:11:32 +0100 | [diff] [blame] | 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 |
Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 196 | return False |
| 197 | |
Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 198 | def 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 | """ |
Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 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) |
Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 220 | if keep_manual_dependencies(file_name, function_name, arguments): |
| 221 | return stanza |
Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 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:] |
Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 250 | return stanza |
| 251 | |
| 252 | def 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 | |
| 262 | def 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 | |
| 277 | def 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 | |
| 291 | def main(args): |
| 292 | for file_name in args: |
| 293 | process_file(file_name) |
| 294 | |
| 295 | if __name__ == '__main__': |
| 296 | main(sys.argv[1:]) |