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