From fdb722384b93675486fae98d7c1817fcf23c3637 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Mon, 19 Jun 2023 20:46:47 +0200 Subject: [PATCH 1/4] Move PSA information and dependency automation into their own module This will let us use these features from other modules (yet to be created). Signed-off-by: Gilles Peskine --- scripts/mbedtls_dev/psa_information.py | 162 ++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/Makefile | 1 + tests/scripts/generate_psa_tests.py | 202 +++++-------------------- 4 files changed, 199 insertions(+), 167 deletions(-) create mode 100644 scripts/mbedtls_dev/psa_information.py diff --git a/scripts/mbedtls_dev/psa_information.py b/scripts/mbedtls_dev/psa_information.py new file mode 100644 index 0000000000..a82df41df4 --- /dev/null +++ b/scripts/mbedtls_dev/psa_information.py @@ -0,0 +1,162 @@ +"""Collect information about PSA cryptographic mechanisms. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from typing import Dict, FrozenSet, List, Optional + +from . import macro_collector + + +class Information: + """Gather information about PSA constructors.""" + + def __init__(self) -> None: + self.constructors = self.read_psa_interface() + + @staticmethod + def remove_unwanted_macros( + constructors: macro_collector.PSAMacroEnumerator + ) -> None: + # Mbed TLS does not support finite-field DSA. + # Don't attempt to generate any related test case. + constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR') + constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY') + + def read_psa_interface(self) -> macro_collector.PSAMacroEnumerator: + """Return the list of known key types, algorithms, etc.""" + constructors = macro_collector.InputsForTest() + header_file_names = ['include/psa/crypto_values.h', + 'include/psa/crypto_extra.h'] + test_suites = ['tests/suites/test_suite_psa_crypto_metadata.data'] + for header_file_name in header_file_names: + constructors.parse_header(header_file_name) + for test_cases in test_suites: + constructors.parse_test_cases(test_cases) + self.remove_unwanted_macros(constructors) + constructors.gather_arguments() + return constructors + + +def psa_want_symbol(name: str) -> str: + """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature.""" + if name.startswith('PSA_'): + return name[:4] + 'WANT_' + name[4:] + else: + raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name) + +def finish_family_dependency(dep: str, bits: int) -> str: + """Finish dep if it's a family dependency symbol prefix. + + A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be + qualified by the key size. If dep is such a symbol, finish it by adjusting + the prefix and appending the key size. Other symbols are left unchanged. + """ + return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep) + +def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]: + """Finish any family dependency symbol prefixes. + + Apply `finish_family_dependency` to each element of `dependencies`. + """ + return [finish_family_dependency(dep, bits) for dep in dependencies] + +SYMBOLS_WITHOUT_DEPENDENCY = frozenset([ + 'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', # modifier, only in policies + 'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # modifier + 'PSA_ALG_ANY_HASH', # only in policies + 'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', # modifier, only in policies + 'PSA_ALG_KEY_AGREEMENT', # chaining + 'PSA_ALG_TRUNCATED_MAC', # modifier +]) +def automatic_dependencies(*expressions: str) -> List[str]: + """Infer dependencies of a test case by looking for PSA_xxx symbols. + + The arguments are strings which should be C expressions. Do not use + string literals or comments as this function is not smart enough to + skip them. + """ + used = set() + for expr in expressions: + used.update(re.findall(r'PSA_(?:ALG|ECC_FAMILY|KEY_TYPE)_\w+', expr)) + used.difference_update(SYMBOLS_WITHOUT_DEPENDENCY) + return sorted(psa_want_symbol(name) for name in used) + +# Define set of regular expressions and dependencies to optionally append +# extra dependencies for test case. +AES_128BIT_ONLY_DEP_REGEX = r'AES\s(192|256)' +AES_128BIT_ONLY_DEP = ["!MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH"] + +DEPENDENCY_FROM_KEY = { + AES_128BIT_ONLY_DEP_REGEX: AES_128BIT_ONLY_DEP +}#type: Dict[str, List[str]] +def generate_key_dependencies(description: str) -> List[str]: + """Return additional dependencies based on pairs of REGEX and dependencies. + """ + deps = [] + for regex, dep in DEPENDENCY_FROM_KEY.items(): + if re.search(regex, description): + deps += dep + + return deps + +# A temporary hack: at the time of writing, not all dependency symbols +# are implemented yet. Skip test cases for which the dependency symbols are +# not available. Once all dependency symbols are available, this hack must +# be removed so that a bug in the dependency symbols properly leads to a test +# failure. +def read_implemented_dependencies(filename: str) -> FrozenSet[str]: + return frozenset(symbol + for line in open(filename) + for symbol in re.findall(r'\bPSA_WANT_\w+\b', line)) +_implemented_dependencies = None #type: Optional[FrozenSet[str]] #pylint: disable=invalid-name +def hack_dependencies_not_implemented(dependencies: List[str]) -> None: + global _implemented_dependencies #pylint: disable=global-statement,invalid-name + if _implemented_dependencies is None: + _implemented_dependencies = \ + read_implemented_dependencies('include/psa/crypto_config.h') + if not all((dep.lstrip('!') in _implemented_dependencies or + not dep.lstrip('!').startswith('PSA_WANT')) + for dep in dependencies): + dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET') + +def tweak_key_pair_dependency(dep: str, usage: str): + """ + This helper function add the proper suffix to PSA_WANT_KEY_TYPE_xxx_KEY_PAIR + symbols according to the required usage. + """ + ret_list = list() + if dep.endswith('KEY_PAIR'): + if usage == "BASIC": + # BASIC automatically includes IMPORT and EXPORT for test purposes (see + # config_psa.h). + ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_BASIC', dep)) + ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_IMPORT', dep)) + ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_EXPORT', dep)) + elif usage == "GENERATE": + ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_GENERATE', dep)) + else: + # No replacement to do in this case + ret_list.append(dep) + return ret_list + +def fix_key_pair_dependencies(dep_list: List[str], usage: str): + new_list = [new_deps + for dep in dep_list + for new_deps in tweak_key_pair_dependency(dep, usage)] + + return new_list diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9bd93f1568..3274739e5b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -120,6 +120,7 @@ if(GEN_FILES) ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_psa_tests.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/crypto_knowledge.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/macro_collector.py + ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/psa_information.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/psa_storage.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_case.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/test_data_generation.py diff --git a/tests/Makefile b/tests/Makefile index 75dc3c6294..3a3425b185 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -123,6 +123,7 @@ $(GENERATED_PSA_DATA_FILES): generated_psa_test_data generated_psa_test_data: scripts/generate_psa_tests.py generated_psa_test_data: ../scripts/mbedtls_dev/crypto_knowledge.py generated_psa_test_data: ../scripts/mbedtls_dev/macro_collector.py +generated_psa_test_data: ../scripts/mbedtls_dev/psa_information.py generated_psa_test_data: ../scripts/mbedtls_dev/psa_storage.py generated_psa_test_data: ../scripts/mbedtls_dev/test_case.py generated_psa_test_data: ../scripts/mbedtls_dev/test_data_generation.py diff --git a/tests/scripts/generate_psa_tests.py b/tests/scripts/generate_psa_tests.py index 993457872f..5cdeb3bae4 100755 --- a/tests/scripts/generate_psa_tests.py +++ b/tests/scripts/generate_psa_tests.py @@ -27,150 +27,13 @@ from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional import scripts_path # pylint: disable=unused-import from mbedtls_dev import crypto_knowledge -from mbedtls_dev import macro_collector +from mbedtls_dev import macro_collector #pylint: disable=unused-import +from mbedtls_dev import psa_information from mbedtls_dev import psa_storage from mbedtls_dev import test_case from mbedtls_dev import test_data_generation -def psa_want_symbol(name: str) -> str: - """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature.""" - if name.startswith('PSA_'): - return name[:4] + 'WANT_' + name[4:] - else: - raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name) - -def finish_family_dependency(dep: str, bits: int) -> str: - """Finish dep if it's a family dependency symbol prefix. - - A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be - qualified by the key size. If dep is such a symbol, finish it by adjusting - the prefix and appending the key size. Other symbols are left unchanged. - """ - return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep) - -def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]: - """Finish any family dependency symbol prefixes. - - Apply `finish_family_dependency` to each element of `dependencies`. - """ - return [finish_family_dependency(dep, bits) for dep in dependencies] - -SYMBOLS_WITHOUT_DEPENDENCY = frozenset([ - 'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', # modifier, only in policies - 'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # modifier - 'PSA_ALG_ANY_HASH', # only in policies - 'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', # modifier, only in policies - 'PSA_ALG_KEY_AGREEMENT', # chaining - 'PSA_ALG_TRUNCATED_MAC', # modifier -]) -def automatic_dependencies(*expressions: str) -> List[str]: - """Infer dependencies of a test case by looking for PSA_xxx symbols. - - The arguments are strings which should be C expressions. Do not use - string literals or comments as this function is not smart enough to - skip them. - """ - used = set() - for expr in expressions: - used.update(re.findall(r'PSA_(?:ALG|ECC_FAMILY|KEY_TYPE)_\w+', expr)) - used.difference_update(SYMBOLS_WITHOUT_DEPENDENCY) - return sorted(psa_want_symbol(name) for name in used) - -# Define set of regular expressions and dependencies to optionally append -# extra dependencies for test case. -AES_128BIT_ONLY_DEP_REGEX = r'AES\s(192|256)' -AES_128BIT_ONLY_DEP = ["!MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH"] - -DEPENDENCY_FROM_KEY = { - AES_128BIT_ONLY_DEP_REGEX: AES_128BIT_ONLY_DEP -}#type: Dict[str, List[str]] -def generate_key_dependencies(description: str) -> List[str]: - """Return additional dependencies based on pairs of REGEX and dependencies. - """ - deps = [] - for regex, dep in DEPENDENCY_FROM_KEY.items(): - if re.search(regex, description): - deps += dep - - return deps - -# A temporary hack: at the time of writing, not all dependency symbols -# are implemented yet. Skip test cases for which the dependency symbols are -# not available. Once all dependency symbols are available, this hack must -# be removed so that a bug in the dependency symbols properly leads to a test -# failure. -def read_implemented_dependencies(filename: str) -> FrozenSet[str]: - return frozenset(symbol - for line in open(filename) - for symbol in re.findall(r'\bPSA_WANT_\w+\b', line)) -_implemented_dependencies = None #type: Optional[FrozenSet[str]] #pylint: disable=invalid-name -def hack_dependencies_not_implemented(dependencies: List[str]) -> None: - global _implemented_dependencies #pylint: disable=global-statement,invalid-name - if _implemented_dependencies is None: - _implemented_dependencies = \ - read_implemented_dependencies('include/psa/crypto_config.h') - if not all((dep.lstrip('!') in _implemented_dependencies or - not dep.lstrip('!').startswith('PSA_WANT')) - for dep in dependencies): - dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET') - -def tweak_key_pair_dependency(dep: str, usage: str): - """ - This helper function add the proper suffix to PSA_WANT_KEY_TYPE_xxx_KEY_PAIR - symbols according to the required usage. - """ - ret_list = list() - if dep.endswith('KEY_PAIR'): - if usage == "BASIC": - # BASIC automatically includes IMPORT and EXPORT for test purposes (see - # config_psa.h). - ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_BASIC', dep)) - ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_IMPORT', dep)) - ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_EXPORT', dep)) - elif usage == "GENERATE": - ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_GENERATE', dep)) - else: - # No replacement to do in this case - ret_list.append(dep) - return ret_list - -def fix_key_pair_dependencies(dep_list: List[str], usage: str): - new_list = [new_deps - for dep in dep_list - for new_deps in tweak_key_pair_dependency(dep, usage)] - - return new_list - -class Information: - """Gather information about PSA constructors.""" - - def __init__(self) -> None: - self.constructors = self.read_psa_interface() - - @staticmethod - def remove_unwanted_macros( - constructors: macro_collector.PSAMacroEnumerator - ) -> None: - # Mbed TLS does not support finite-field DSA. - # Don't attempt to generate any related test case. - constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR') - constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY') - - def read_psa_interface(self) -> macro_collector.PSAMacroEnumerator: - """Return the list of known key types, algorithms, etc.""" - constructors = macro_collector.InputsForTest() - header_file_names = ['include/psa/crypto_values.h', - 'include/psa/crypto_extra.h'] - test_suites = ['tests/suites/test_suite_psa_crypto_metadata.data'] - for header_file_name in header_file_names: - constructors.parse_header(header_file_name) - for test_cases in test_suites: - constructors.parse_test_cases(test_cases) - self.remove_unwanted_macros(constructors) - constructors.gather_arguments() - return constructors - def test_case_for_key_type_not_supported( verb: str, key_type: str, bits: int, @@ -181,7 +44,7 @@ def test_case_for_key_type_not_supported( """Return one test case exercising a key creation method for an unsupported key type or size. """ - hack_dependencies_not_implemented(dependencies) + psa_information.hack_dependencies_not_implemented(dependencies) tc = test_case.TestCase() short_key_type = crypto_knowledge.short_expression(key_type) adverb = 'not' if dependencies else 'never' @@ -197,7 +60,7 @@ def test_case_for_key_type_not_supported( class KeyTypeNotSupported: """Generate test cases for when a key type is not supported.""" - def __init__(self, info: Information) -> None: + def __init__(self, info: psa_information.Information) -> None: self.constructors = info.constructors ALWAYS_SUPPORTED = frozenset([ @@ -224,20 +87,22 @@ class KeyTypeNotSupported: # They would be skipped in all configurations, which is noise. return import_dependencies = [('!' if param is None else '') + - psa_want_symbol(kt.name)] + psa_information.psa_want_symbol(kt.name)] if kt.params is not None: import_dependencies += [('!' if param == i else '') + - psa_want_symbol(sym) + psa_information.psa_want_symbol(sym) for i, sym in enumerate(kt.params)] if kt.name.endswith('_PUBLIC_KEY'): generate_dependencies = [] else: - generate_dependencies = fix_key_pair_dependencies(import_dependencies, 'GENERATE') - import_dependencies = fix_key_pair_dependencies(import_dependencies, 'BASIC') + generate_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'GENERATE') + import_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'BASIC') for bits in kt.sizes_to_test(): yield test_case_for_key_type_not_supported( 'import', kt.expression, bits, - finish_family_dependencies(import_dependencies, bits), + psa_information.finish_family_dependencies(import_dependencies, bits), test_case.hex_string(kt.key_material(bits)), param_descr=param_descr, ) @@ -251,7 +116,7 @@ class KeyTypeNotSupported: if not kt.is_public(): yield test_case_for_key_type_not_supported( 'generate', kt.expression, bits, - finish_family_dependencies(generate_dependencies, bits), + psa_information.finish_family_dependencies(generate_dependencies, bits), str(bits), param_descr=param_descr, ) @@ -294,7 +159,7 @@ def test_case_for_key_generation( ) -> test_case.TestCase: """Return one test case exercising a key generation. """ - hack_dependencies_not_implemented(dependencies) + psa_information.hack_dependencies_not_implemented(dependencies) tc = test_case.TestCase() short_key_type = crypto_knowledge.short_expression(key_type) tc.set_description('PSA {} {}-bit' @@ -308,7 +173,7 @@ def test_case_for_key_generation( class KeyGenerate: """Generate positive and negative (invalid argument) test cases for key generation.""" - def __init__(self, info: Information) -> None: + def __init__(self, info: psa_information.Information) -> None: self.constructors = info.constructors ECC_KEY_TYPES = ('PSA_KEY_TYPE_ECC_KEY_PAIR', @@ -327,9 +192,9 @@ class KeyGenerate: """ result = 'PSA_SUCCESS' - import_dependencies = [psa_want_symbol(kt.name)] + import_dependencies = [psa_information.psa_want_symbol(kt.name)] if kt.params is not None: - import_dependencies += [psa_want_symbol(sym) + import_dependencies += [psa_information.psa_want_symbol(sym) for i, sym in enumerate(kt.params)] if kt.name.endswith('_PUBLIC_KEY'): # The library checks whether the key type is a public key generically, @@ -338,7 +203,8 @@ class KeyGenerate: generate_dependencies = [] result = 'PSA_ERROR_INVALID_ARGUMENT' else: - generate_dependencies = fix_key_pair_dependencies(import_dependencies, 'GENERATE') + generate_dependencies = \ + psa_information.fix_key_pair_dependencies(import_dependencies, 'GENERATE') for bits in kt.sizes_to_test(): if kt.name == 'PSA_KEY_TYPE_RSA_KEY_PAIR': size_dependency = "PSA_VENDOR_RSA_GENERATE_MIN_KEY_BITS <= " + str(bits) @@ -347,7 +213,7 @@ class KeyGenerate: test_dependencies = generate_dependencies yield test_case_for_key_generation( kt.expression, bits, - finish_family_dependencies(test_dependencies, bits), + psa_information.finish_family_dependencies(test_dependencies, bits), str(bits), result ) @@ -380,7 +246,7 @@ class OpFail: INCOMPATIBLE = 2 PUBLIC = 3 - def __init__(self, info: Information) -> None: + def __init__(self, info: psa_information.Information) -> None: self.constructors = info.constructors key_type_expressions = self.constructors.generate_expressions( sorted(self.constructors.key_types) @@ -417,8 +283,8 @@ class OpFail: pretty_alg, pretty_reason, ' with ' + pretty_type if pretty_type else '')) - dependencies = automatic_dependencies(alg.base_expression, key_type) - dependencies = fix_key_pair_dependencies(dependencies, 'BASIC') + dependencies = psa_information.automatic_dependencies(alg.base_expression, key_type) + dependencies = psa_information.fix_key_pair_dependencies(dependencies, 'BASIC') for i, dep in enumerate(dependencies): if dep in not_deps: dependencies[i] = '!' + dep @@ -445,7 +311,7 @@ class OpFail: """Generate failure test cases for keyless operations with the specified algorithm.""" if alg.can_do(category): # Compatible operation, unsupported algorithm - for dep in automatic_dependencies(alg.base_expression): + for dep in psa_information.automatic_dependencies(alg.base_expression): yield self.make_test_case(alg, category, self.Reason.NOT_SUPPORTED, not_deps=frozenset([dep])) @@ -463,7 +329,7 @@ class OpFail: key_is_compatible = kt.can_do(alg) if key_is_compatible and alg.can_do(category): # Compatible key and operation, unsupported algorithm - for dep in automatic_dependencies(alg.base_expression): + for dep in psa_information.automatic_dependencies(alg.base_expression): yield self.make_test_case(alg, category, self.Reason.NOT_SUPPORTED, kt=kt, not_deps=frozenset([dep])) @@ -569,7 +435,7 @@ class StorageTestData(StorageKey): class StorageFormat: """Storage format stability test cases.""" - def __init__(self, info: Information, version: int, forward: bool) -> None: + def __init__(self, info: psa_information.Information, version: int, forward: bool) -> None: """Prepare to generate test cases for storage format stability. * `info`: information about the API. See the `Information` class. @@ -636,13 +502,13 @@ class StorageFormat: verb = 'save' if self.forward else 'read' tc = test_case.TestCase() tc.set_description(verb + ' ' + key.description) - dependencies = automatic_dependencies( + dependencies = psa_information.automatic_dependencies( key.lifetime.string, key.type.string, key.alg.string, key.alg2.string, ) - dependencies = finish_family_dependencies(dependencies, key.bits) - dependencies += generate_key_dependencies(key.description) - dependencies = fix_key_pair_dependencies(dependencies, 'BASIC') + dependencies = psa_information.finish_family_dependencies(dependencies, key.bits) + dependencies += psa_information.generate_key_dependencies(key.description) + dependencies = psa_information.fix_key_pair_dependencies(dependencies, 'BASIC') tc.set_dependencies(dependencies) tc.set_function('key_storage_' + verb) if self.forward: @@ -847,13 +713,13 @@ class StorageFormat: class StorageFormatForward(StorageFormat): """Storage format stability test cases for forward compatibility.""" - def __init__(self, info: Information, version: int) -> None: + def __init__(self, info: psa_information.Information, version: int) -> None: super().__init__(info, version, True) class StorageFormatV0(StorageFormat): """Storage format stability test cases for version 0 compatibility.""" - def __init__(self, info: Information) -> None: + def __init__(self, info: psa_information.Information) -> None: super().__init__(info, 0, False) def all_keys_for_usage_flags(self) -> Iterator[StorageTestData]: @@ -963,6 +829,7 @@ class StorageFormatV0(StorageFormat): yield from super().generate_all_keys() yield from self.all_keys_for_implicit_usage() + class PSATestGenerator(test_data_generation.TestGenerator): """Test generator subclass including PSA targets and info.""" # Note that targets whose names contain 'test_format' have their content @@ -978,14 +845,15 @@ class PSATestGenerator(test_data_generation.TestGenerator): lambda info: StorageFormatForward(info, 0).all_test_cases(), 'test_suite_psa_crypto_storage_format.v0': lambda info: StorageFormatV0(info).all_test_cases(), - } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]] + } #type: Dict[str, Callable[[psa_information.Information], Iterable[test_case.TestCase]]] def __init__(self, options): super().__init__(options) - self.info = Information() + self.info = psa_information.Information() def generate_target(self, name: str, *target_args) -> None: super().generate_target(name, self.info) + if __name__ == '__main__': test_data_generation.main(sys.argv[1:], __doc__, PSATestGenerator) From c9187c5866d7d562b7e077cbe7ba6c03f76809d5 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 20 Jun 2023 15:22:53 +0200 Subject: [PATCH 2/4] New test suite for the low-level hash interface Some basic test coverage for now: * Nominal operation. * Larger output buffer. * Clone an operation and use it after the original operation stops. Generate test data automatically. For the time being, only do that for hashes that Python supports natively. Supporting all algorithms is future work. Signed-off-by: Gilles Peskine --- scripts/mbedtls_dev/crypto_data_tests.py | 123 ++++++++++ tests/CMakeLists.txt | 1 + tests/Makefile | 1 + tests/scripts/generate_psa_tests.py | 3 + .../test_suite_psa_crypto_low_hash.function | 225 ++++++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 scripts/mbedtls_dev/crypto_data_tests.py create mode 100644 tests/suites/test_suite_psa_crypto_low_hash.function diff --git a/scripts/mbedtls_dev/crypto_data_tests.py b/scripts/mbedtls_dev/crypto_data_tests.py new file mode 100644 index 0000000000..80051fa439 --- /dev/null +++ b/scripts/mbedtls_dev/crypto_data_tests.py @@ -0,0 +1,123 @@ +"""Generate test data for cryptographic mechanisms. + +This module is a work in progress, only implementing a few cases for now. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +from typing import Callable, Dict, Iterator, List, Optional #pylint: disable=unused-import + +from . import crypto_knowledge +from . import psa_information +from . import test_case + + +def psa_low_level_dependencies(*expressions: str) -> List[str]: + """Infer dependencies of a PSA low-level test case by looking for PSA_xxx symbols. + + This function generates MBEDTLS_PSA_BUILTIN_xxx symbols. + """ + high_level = psa_information.automatic_dependencies(*expressions) + for dep in high_level: + assert dep.startswith('PSA_WANT_') + return ['MBEDTLS_PSA_BUILTIN_' + dep[9:] for dep in high_level] + + +class HashPSALowLevel: + """Generate test cases for the PSA low-level hash interface.""" + + def __init__(self, info: psa_information.Information) -> None: + self.info = info + base_algorithms = sorted(info.constructors.algorithms) + all_algorithms = \ + [crypto_knowledge.Algorithm(expr) + for expr in info.constructors.generate_expressions(base_algorithms)] + self.algorithms = \ + [alg + for alg in all_algorithms + if (not alg.is_wildcard and + alg.can_do(crypto_knowledge.AlgorithmCategory.HASH))] + + # CALCULATE[alg] = function to return the hash of its argument in hex + # TO-DO: implement the None entries with a third-party library, because + # hashlib might not have everything, depending on the Python version and + # the underlying OpenSSL. On Ubuntu 16.04, truncated sha512 and sha3/shake + # are not available. On Ubuntu 22.04, md2, md4 and ripemd160 are not + # available. + CALCULATE = { + 'PSA_ALG_MD5': lambda data: hashlib.md5(data).hexdigest(), + 'PSA_ALG_RIPEMD160': None, #lambda data: hashlib.new('ripdemd160').hexdigest() + 'PSA_ALG_SHA_1': lambda data: hashlib.sha1(data).hexdigest(), + 'PSA_ALG_SHA_224': lambda data: hashlib.sha224(data).hexdigest(), + 'PSA_ALG_SHA_256': lambda data: hashlib.sha256(data).hexdigest(), + 'PSA_ALG_SHA_384': lambda data: hashlib.sha384(data).hexdigest(), + 'PSA_ALG_SHA_512': lambda data: hashlib.sha512(data).hexdigest(), + 'PSA_ALG_SHA_512_224': None, #lambda data: hashlib.new('sha512_224').hexdigest() + 'PSA_ALG_SHA_512_256': None, #lambda data: hashlib.new('sha512_256').hexdigest() + 'PSA_ALG_SHA3_224': None, #lambda data: hashlib.sha3_224(data).hexdigest(), + 'PSA_ALG_SHA3_256': None, #lambda data: hashlib.sha3_256(data).hexdigest(), + 'PSA_ALG_SHA3_384': None, #lambda data: hashlib.sha3_384(data).hexdigest(), + 'PSA_ALG_SHA3_512': None, #lambda data: hashlib.sha3_512(data).hexdigest(), + 'PSA_ALG_SHAKE256_512': None, #lambda data: hashlib.shake_256(data).hexdigest(64), + } #typing: Optional[Dict[str, Callable[[bytes], str]]] + + @staticmethod + def one_test_case(alg: crypto_knowledge.Algorithm, + function: str, note: str, + arguments: List[str]) -> test_case.TestCase: + """Construct one test case involving a hash.""" + tc = test_case.TestCase() + tc.set_description('{}{} {}' + .format(function, + ' ' + note if note else '', + alg.short_expression())) + tc.set_dependencies(psa_low_level_dependencies(alg.expression)) + tc.set_function(function) + tc.set_arguments([alg.expression] + + ['"{}"'.format(arg) for arg in arguments]) + return tc + + def test_cases_for_hash(self, + alg: crypto_knowledge.Algorithm + ) -> Iterator[test_case.TestCase]: + """Enumerate all test cases for one hash algorithm.""" + calc = self.CALCULATE[alg.expression] + if calc is None: + return # not implemented yet + + short = b'abc' + hash_short = calc(short) + long = (b'Hello, world. Here are 16 unprintable bytes: [' + b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a' + b'\x80\x81\x82\x83\xfe\xff]. ' + b' This message was brought to you by a natural intelligence. ' + b' If you can read this, good luck with your debugging!') + hash_long = calc(long) + + yield self.one_test_case(alg, 'hash_empty', '', [calc(b'')]) + yield self.one_test_case(alg, 'hash_valid_one_shot', '', + [short.hex(), hash_short]) + for n in [0, 1, 64, len(long) - 1, len(long)]: + yield self.one_test_case(alg, 'hash_valid_multipart', + '{} + {}'.format(n, len(long) - n), + [long[:n].hex(), calc(long[:n]), + long[n:].hex(), hash_long]) + + def all_test_cases(self) -> Iterator[test_case.TestCase]: + """Enumerate all test cases for all hash algorithms.""" + for alg in self.algorithms: + yield from self.test_cases_for_hash(alg) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3274739e5b..0869aaa018 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -118,6 +118,7 @@ if(GEN_FILES) --directory ${CMAKE_CURRENT_BINARY_DIR}/suites DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../tests/scripts/generate_psa_tests.py + ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/crypto_data_tests.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/crypto_knowledge.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/macro_collector.py ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/mbedtls_dev/psa_information.py diff --git a/tests/Makefile b/tests/Makefile index 3a3425b185..ec016d871f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -121,6 +121,7 @@ generated_ecp_test_data: $(GENERATED_PSA_DATA_FILES): generated_psa_test_data generated_psa_test_data: scripts/generate_psa_tests.py +generated_psa_test_data: ../scripts/mbedtls_dev/crypto_data_tests.py generated_psa_test_data: ../scripts/mbedtls_dev/crypto_knowledge.py generated_psa_test_data: ../scripts/mbedtls_dev/macro_collector.py generated_psa_test_data: ../scripts/mbedtls_dev/psa_information.py diff --git a/tests/scripts/generate_psa_tests.py b/tests/scripts/generate_psa_tests.py index 5cdeb3bae4..b6f83c111b 100755 --- a/tests/scripts/generate_psa_tests.py +++ b/tests/scripts/generate_psa_tests.py @@ -26,6 +26,7 @@ import sys from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional import scripts_path # pylint: disable=unused-import +from mbedtls_dev import crypto_data_tests from mbedtls_dev import crypto_knowledge from mbedtls_dev import macro_collector #pylint: disable=unused-import from mbedtls_dev import psa_information @@ -839,6 +840,8 @@ class PSATestGenerator(test_data_generation.TestGenerator): lambda info: KeyGenerate(info).test_cases_for_key_generation(), 'test_suite_psa_crypto_not_supported.generated': lambda info: KeyTypeNotSupported(info).test_cases_for_not_supported(), + 'test_suite_psa_crypto_low_hash.generated': + lambda info: crypto_data_tests.HashPSALowLevel(info).all_test_cases(), 'test_suite_psa_crypto_op_fail.generated': lambda info: OpFail(info).all_test_cases(), 'test_suite_psa_crypto_storage_format.current': diff --git a/tests/suites/test_suite_psa_crypto_low_hash.function b/tests/suites/test_suite_psa_crypto_low_hash.function new file mode 100644 index 0000000000..24cc90536d --- /dev/null +++ b/tests/suites/test_suite_psa_crypto_low_hash.function @@ -0,0 +1,225 @@ +/* BEGIN_HEADER */ +/* + * Test suite for the PSA hash built-in driver + * + * This test suite exercises some aspects of the built-in PSA driver for + * hash algorithms (psa_crypto_hash.c). This code is mostly tested via + * the application interface (above the PSA API layer) and via tests of + * individual hash modules. The goal of this test suite is to ensure that + * the driver dispatch layer behaves correctly even when not invoked via + * the API layer, but directly from another driver. + * + * This test suite is currently incomplete. It focuses on non-regression + * tests for past bugs or near misses. + */ + +#include + +/* END_HEADER */ + +/* BEGIN_DEPENDENCIES + * depends_on:MBEDTLS_PSA_BUILTIN_HASH + * END_DEPENDENCIES + */ + +/* BEGIN_CASE */ +void hash_valid_one_shot(int alg_arg, data_t *input, + data_t *expected) +{ + psa_algorithm_t alg = alg_arg; + uint8_t *output = NULL; + size_t output_size = expected->len; + size_t length = SIZE_MAX; + + /* Nominal case */ + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_compute(alg, input->x, input->len, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected->x, expected->len, output, length); + mbedtls_free(output); + output = NULL; + + /* Larger output buffer */ + output_size = expected->len + 1; + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_compute(alg, input->x, input->len, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected->x, expected->len, output, length); + mbedtls_free(output); + output = NULL; + +#if 0 + /* Smaller output buffer (does not have to work!) */ + output_size = expected->len - 1; + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_compute(alg, input->x, input->len, + output, output_size, &length), + PSA_ERROR_BUFFER_TOO_SMALL); + mbedtls_free(output); + output = NULL; +#endif + +exit: + mbedtls_free(output); +} +/* END_CASE */ + +/* BEGIN_CASE */ +void hash_valid_multipart(int alg_arg, + data_t *input1, data_t *expected1, + data_t *input2, data_t *expected2) +{ + psa_algorithm_t alg = alg_arg; + uint8_t *output = NULL; + size_t output_size = expected1->len; + size_t length = SIZE_MAX; + mbedtls_psa_hash_operation_t operation0; // original + memset(&operation0, 0, sizeof(operation0)); + mbedtls_psa_hash_operation_t clone_start; // cloned after setup + memset(&clone_start, 0, sizeof(clone_start)); + mbedtls_psa_hash_operation_t clone_middle; // cloned between updates + memset(&clone_middle, 0, sizeof(clone_middle)); + mbedtls_psa_hash_operation_t clone_end; // cloned before finish + memset(&clone_end, 0, sizeof(clone_end)); + mbedtls_psa_hash_operation_t clone_more; // cloned before finish + memset(&clone_more, 0, sizeof(clone_more)); + + /* Nominal case with two update calls */ + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_setup(&operation0, alg), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_clone(&operation0, &clone_start), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_update(&operation0, input1->x, input1->len), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_clone(&operation0, &clone_middle), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_update(&operation0, input2->x, input2->len), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_clone(&operation0, &clone_end), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_finish(&operation0, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected2->x, expected2->len, output, length); + + /* Nominal case with an operation cloned after setup */ + memset(output, 0, output_size); + TEST_EQUAL(mbedtls_psa_hash_update(&clone_start, input1->x, input1->len), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_start, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected1->x, expected1->len, output, length); + + /* Nominal case with an operation cloned between updates */ + memset(output, 0, output_size); + TEST_EQUAL(mbedtls_psa_hash_update(&clone_middle, input2->x, input2->len), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_middle, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected2->x, expected2->len, output, length); + + /* Nominal case with an operation cloned before finish */ + TEST_EQUAL(mbedtls_psa_hash_clone(&clone_end, &clone_more), + PSA_SUCCESS); + memset(output, 0, output_size); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_end, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected2->x, expected2->len, output, length); + mbedtls_free(output); + output = NULL; + + /* Larger output buffer */ + TEST_EQUAL(mbedtls_psa_hash_clone(&clone_more, &clone_end), + PSA_SUCCESS); + output_size = expected2->len + 1; + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_end, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected2->x, expected2->len, output, length); + mbedtls_free(output); + output = NULL; + +#if 0 + /* Smaller output buffer (does not have to work!) */ + TEST_EQUAL(mbedtls_psa_hash_clone(&clone_more, &clone_end), + PSA_SUCCESS); + output_size = expected2->len - 1; + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_end, + output, output_size, &length), + PSA_ERROR_BUFFER_TOO_SMALL); + mbedtls_free(output); + output = NULL; +#endif + + /* Nominal case again after an error in a cloned operation */ + output_size = expected2->len; + ASSERT_ALLOC(output, output_size); + TEST_EQUAL(mbedtls_psa_hash_finish(&clone_more, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected2->x, expected2->len, output, length); + mbedtls_free(output); + output = NULL; + +exit: + mbedtls_free(output); + mbedtls_psa_hash_abort(&operation0); + mbedtls_psa_hash_abort(&clone_start); + mbedtls_psa_hash_abort(&clone_middle); + mbedtls_psa_hash_abort(&clone_end); + mbedtls_psa_hash_abort(&clone_more); +} +/* END_CASE */ + +/* BEGIN_CASE */ +void hash_empty(int alg_arg, data_t *expected) +{ + psa_algorithm_t alg = alg_arg; + uint8_t *output = NULL; + size_t output_size = expected->len; + size_t length = SIZE_MAX; + mbedtls_psa_hash_operation_t operation; + memset(&operation, 0, sizeof(operation)); + + ASSERT_ALLOC(output, output_size); + + /* One-shot */ + TEST_EQUAL(mbedtls_psa_hash_compute(alg, NULL, 0, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected->x, expected->len, output, length); + + /* Multipart, no update */ + memset(output, 0, output_size); + TEST_EQUAL(mbedtls_psa_hash_setup(&operation, alg), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_finish(&operation, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected->x, expected->len, output, length); + + /* Multipart, one update */ + memset(output, 0, output_size); + memset(&operation, 0, sizeof(operation)); + TEST_EQUAL(mbedtls_psa_hash_setup(&operation, alg), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_update(&operation, NULL, 0), + PSA_SUCCESS); + TEST_EQUAL(mbedtls_psa_hash_finish(&operation, + output, output_size, &length), + PSA_SUCCESS); + ASSERT_COMPARE(expected->x, expected->len, output, length); + +exit: + mbedtls_free(output); + mbedtls_psa_hash_abort(&operation); +} +/* END_CASE */ From 6d14c2b858969c4861c865b2a2240ad0981c2a01 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Mon, 7 Aug 2023 21:21:21 +0200 Subject: [PATCH 3/4] Remove dead code Do explain why we don't test a smaller buffer in addition to testing the nominal size and a larger buffer. Signed-off-by: Gilles Peskine --- .../test_suite_psa_crypto_low_hash.function | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/tests/suites/test_suite_psa_crypto_low_hash.function b/tests/suites/test_suite_psa_crypto_low_hash.function index 24cc90536d..6dabceff9e 100644 --- a/tests/suites/test_suite_psa_crypto_low_hash.function +++ b/tests/suites/test_suite_psa_crypto_low_hash.function @@ -50,16 +50,9 @@ void hash_valid_one_shot(int alg_arg, data_t *input, mbedtls_free(output); output = NULL; -#if 0 - /* Smaller output buffer (does not have to work!) */ - output_size = expected->len - 1; - ASSERT_ALLOC(output, output_size); - TEST_EQUAL(mbedtls_psa_hash_compute(alg, input->x, input->len, - output, output_size, &length), - PSA_ERROR_BUFFER_TOO_SMALL); - mbedtls_free(output); - output = NULL; -#endif + /* We don't test with a smaller output buffer because this isn't + * guaranteed to work: the core must pass a sufficiently large + * output buffer to the driver. */ exit: mbedtls_free(output); @@ -146,18 +139,9 @@ void hash_valid_multipart(int alg_arg, mbedtls_free(output); output = NULL; -#if 0 - /* Smaller output buffer (does not have to work!) */ - TEST_EQUAL(mbedtls_psa_hash_clone(&clone_more, &clone_end), - PSA_SUCCESS); - output_size = expected2->len - 1; - ASSERT_ALLOC(output, output_size); - TEST_EQUAL(mbedtls_psa_hash_finish(&clone_end, - output, output_size, &length), - PSA_ERROR_BUFFER_TOO_SMALL); - mbedtls_free(output); - output = NULL; -#endif + /* We don't test with a smaller output buffer because this isn't + * guaranteed to work: the core must pass a sufficiently large + * output buffer to the driver. */ /* Nominal case again after an error in a cloned operation */ output_size = expected2->len; From ad7725d95d2d024034f12411192b1a5179e49c21 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Wed, 9 Aug 2023 10:50:58 +0200 Subject: [PATCH 4/4] Fix type annotation Signed-off-by: Gilles Peskine --- scripts/mbedtls_dev/crypto_data_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mbedtls_dev/crypto_data_tests.py b/scripts/mbedtls_dev/crypto_data_tests.py index 80051fa439..7593952da1 100644 --- a/scripts/mbedtls_dev/crypto_data_tests.py +++ b/scripts/mbedtls_dev/crypto_data_tests.py @@ -73,7 +73,7 @@ class HashPSALowLevel: 'PSA_ALG_SHA3_384': None, #lambda data: hashlib.sha3_384(data).hexdigest(), 'PSA_ALG_SHA3_512': None, #lambda data: hashlib.sha3_512(data).hexdigest(), 'PSA_ALG_SHAKE256_512': None, #lambda data: hashlib.shake_256(data).hexdigest(64), - } #typing: Optional[Dict[str, Callable[[bytes], str]]] + } #type: Dict[str, Optional[Callable[[bytes], str]]] @staticmethod def one_test_case(alg: crypto_knowledge.Algorithm,