Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c6fe12a
hack_dependencies_not_implemented: more verbose indication
gilles-peskine-arm Apr 10, 2024
c3b261a
Sort dependencies in automatically generated PSA test cases
gilles-peskine-arm Apr 10, 2024
c7b58d5
PSA test case generation: dependency inference class: base case
gilles-peskine-arm Apr 10, 2024
c113b42
hack_dependencies_not_implemented: Also read inferred PSA_WANT symbols
gilles-peskine-arm Apr 10, 2024
d3286af
hack_dependencies_not_implemented: apply to positive test cases
gilles-peskine-arm Apr 10, 2024
6281cf4
PSA test case generation: dependency inference class: key generation
gilles-peskine-arm Apr 10, 2024
1ae57ec
PSA test case generation: dependency inference class: key not supported
gilles-peskine-arm Apr 10, 2024
764c2d3
PSA test case generation: dependency inference class: operation fail
gilles-peskine-arm Apr 10, 2024
b6e362b
PSA sign/verify: more uniform error on an unsupported hash
gilles-peskine-arm Apr 10, 2024
995d7d4
Do run not-supported test cases on not-implemented mechanisms
gilles-peskine-arm Apr 10, 2024
519762b
Clean up not-implemented detection
gilles-peskine-arm Apr 10, 2024
696b7ee
TestCase: add mechanism to skip a test case
gilles-peskine-arm Apr 11, 2024
b8ddf6a
PSA test case generation: comment out always-skipped test cases
gilles-peskine-arm Apr 11, 2024
0311b21
Explain why DH and DSA are still explicitly excluded
gilles-peskine-arm Apr 11, 2024
47398e0
Add some missing test case dependencies
gilles-peskine-arm Apr 19, 2024
9ffffab
Fix edge case with half-supported ECDSA
gilles-peskine-arm Apr 19, 2024
fd9e506
Missing word
gilles-peskine-arm May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 81 additions & 9 deletions library/psa_crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -2259,6 +2259,50 @@ psa_status_t psa_copy_key(mbedtls_svc_key_id_t source_key,
/* Message digests */
/****************************************************************/

static int is_hash_supported(psa_algorithm_t alg)
{
switch (alg) {
#if defined(PSA_WANT_ALG_MD2)
case PSA_ALG_MD2:
return 1;
#endif
#if defined(PSA_WANT_ALG_MD4)
case PSA_ALG_MD4:
return 1;
#endif
#if defined(PSA_WANT_ALG_MD5)
case PSA_ALG_MD5:
return 1;
#endif
#if defined(PSA_WANT_ALG_RIPEMD160)
case PSA_ALG_RIPEMD160:
return 1;
#endif
#if defined(PSA_WANT_ALG_SHA_1)
case PSA_ALG_SHA_1:
return 1;
#endif
#if defined(PSA_WANT_ALG_SHA_224)
case PSA_ALG_SHA_224:
return 1;
#endif
#if defined(PSA_WANT_ALG_SHA_256)
case PSA_ALG_SHA_256:
return 1;
#endif
#if defined(PSA_WANT_ALG_SHA_384)
case PSA_ALG_SHA_384:
return 1;
#endif
#if defined(PSA_WANT_ALG_SHA_512)
case PSA_ALG_SHA_512:
return 1;
#endif
default:
return 0;
}
}

psa_status_t psa_hash_abort(psa_hash_operation_t *operation)
{
/* Aborting a non-active operation is allowed */
Expand Down Expand Up @@ -2913,16 +2957,44 @@ static psa_status_t psa_sign_verify_check_alg(int input_is_message,
if (!PSA_ALG_IS_SIGN_MESSAGE(alg)) {
return PSA_ERROR_INVALID_ARGUMENT;
}
}

if (PSA_ALG_IS_SIGN_HASH(alg)) {
if (!PSA_ALG_IS_HASH(PSA_ALG_SIGN_GET_HASH(alg))) {
return PSA_ERROR_INVALID_ARGUMENT;
}
}
} else {
if (!PSA_ALG_IS_SIGN_HASH(alg)) {
return PSA_ERROR_INVALID_ARGUMENT;
}
psa_algorithm_t hash_alg = 0;
if (PSA_ALG_IS_SIGN_HASH(alg)) {
hash_alg = PSA_ALG_SIGN_GET_HASH(alg);
}

/* Now hash_alg==0 if alg by itself doesn't need a hash.
* This is good enough for sign-hash, but a guaranteed failure for
* sign-message which needs to hash first for all algorithms
* supported at the moment. */

if (hash_alg == 0 && input_is_message) {
return PSA_ERROR_INVALID_ARGUMENT;
}
if (hash_alg == PSA_ALG_ANY_HASH) {
return PSA_ERROR_INVALID_ARGUMENT;
}
/* Give up immediately if the hash is not supported. This has
* several advantages:
* - For mechanisms that don't use the hash at all (e.g.
* ECDSA verification, randomized ECDSA signature), without
* this check, the operation would succeed even though it has
* been given an invalid argument. This would not be insecure
* since the hash was not necessary, but it would be weird.
* - For mechanisms that do use the hash, we avoid an error
* deep inside the execution. In principle this doesn't matter,
* but there is a little more risk of a bug in error handling
* deep inside than in this preliminary check.
* - When calling a driver, the driver might be capable of using
* a hash that the core doesn't support. This could potentially
* result in a buffer overflow if the hash is larger than the
* maximum hash size assumed by the core.
* - Returning a consistent error makes it possible to test
* not-supported hashes in a consistent way.
*/
if (hash_alg != 0 && !is_hash_supported(hash_alg)) {
return PSA_ERROR_NOT_SUPPORTED;
}

return PSA_SUCCESS;
Expand Down
123 changes: 100 additions & 23 deletions scripts/mbedtls_dev/psa_information.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@


import re
from typing import Dict, FrozenSet, List, Optional
from typing import Dict, FrozenSet, Iterator, List, Optional, Set

from . import macro_collector
from . import test_case


def psa_want_symbol(name: str) -> str:
Expand Down Expand Up @@ -53,26 +54,6 @@ def automatic_dependencies(*expressions: str) -> List[str]:
used.difference_update(SYMBOLS_WITHOUT_DEPENDENCY)
return sorted(psa_want_symbol(name) for name in used)

# 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')

class Information:
"""Gather information about PSA constructors."""
Expand All @@ -84,8 +65,13 @@ def __init__(self) -> None:
def remove_unwanted_macros(
constructors: macro_collector.PSAMacroEnumerator
) -> None:
# Mbed TLS doesn't support finite-field DH yet and will not support
# finite-field DSA. Don't attempt to generate any related test case.
"""Remove macros from consideration during value enumeration."""
# Remove some mechanisms that are declared but not implemented.
# The corresponding test cases would be commented out anyway
# thanks to the detect_not_implemented_dependencies mechanism,
# but for those particular key types, we don't even have enough
# support in the test scripts to construct test keys. So
# we arrange to not even attempt to generate test cases.
constructors.key_types.discard('PSA_KEY_TYPE_DH_KEY_PAIR')
constructors.key_types.discard('PSA_KEY_TYPE_DH_PUBLIC_KEY')
constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
Expand All @@ -104,3 +90,94 @@ def read_psa_interface(self) -> macro_collector.PSAMacroEnumerator:
self.remove_unwanted_macros(constructors)
constructors.gather_arguments()
return constructors


class TestCase(test_case.TestCase):
"""A PSA test case with automatically inferred dependencies.

For mechanisms like ECC curves where the support status includes
the key bit-size, this class assumes that only one bit-size is
involved in a given test case.
"""

# Use a class variable to cache the set of implemented dependencies.
# Call read_implemented_dependencies() to fill the cache.
_implemented_dependencies = None #type: Optional[FrozenSet[str]]

DEPENDENCY_SYMBOL_RE = re.compile(r'\bPSA_WANT_\w+\b')
@classmethod
def _yield_implemented_dependencies(cls) -> Iterator[str]:
for filename in ['include/psa/crypto_config.h',
'include/mbedtls/config_psa.h']:
Comment on lines +110 to +111
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite right. For example, mechanisms that are commented out in crypto_config.h will be considered as implemented, and conversely if you run this script with an edited crypto_config.h then some supported mechanisms might not be considered to be implemented. I'm considering some solutions, but unsure yet whether to apply them here or in a follow-up, because this pull request is already pretty long and I think I've reached a decent stepping point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would definitely agree that this PR is long enough already - I would have said a follow up would be better at this point.

As it stands this is exactly the same as it has been for some time, although the better detection implemented in this PR will possibly cause more issues. Is it worth documenting the issue, if a fix is forthcoming soon?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall issue remains #5390.

with open(filename) as inp:
content = inp.read()
yield from cls.DEPENDENCY_SYMBOL_RE.findall(content)

@classmethod
def read_implemented_dependencies(cls) -> FrozenSet[str]:
if cls._implemented_dependencies is None:
cls._implemented_dependencies = \
frozenset(cls._yield_implemented_dependencies())
# Redundant return to reassure pylint (mypy is fine without it).
# Known issue: https://github.com/pylint-dev/pylint/issues/3045
return cls._implemented_dependencies
return cls._implemented_dependencies

# We skip test cases for which the dependency symbols are not defined.
# We assume that this means that a required mechanism is not implemented.
# Note that if we erroneously skip generating test cases for
# mechanisms that are not implemented, this should be caught
# by the NOT_SUPPORTED test cases generated by generate_psa_tests.py
# in test_suite_psa_crypto_not_supported and test_suite_psa_crypto_op_fail:
# those emit negative tests, which will not be skipped here.
def detect_not_implemented_dependencies(self) -> None:
"""Detect dependencies that are not implemented."""
all_implemented_dependencies = self.read_implemented_dependencies()
not_implemented = [dep
for dep in self.dependencies
if (dep.startswith('PSA_WANT') and
dep not in all_implemented_dependencies)]
if not_implemented:
self.skip_because('not implemented: ' +
' '.join(not_implemented))

def __init__(self) -> None:
super().__init__()
self.key_bits = None #type: Optional[int]
self.negated_dependencies = set() #type: Set[str]

def assumes_not_supported(self, name: str) -> None:
"""Negate the given mechanism for automatic dependency generation.

Call this function before set_arguments() for a test case that should
run if the given mechanism is not supported.

A mechanism is either a PSA_XXX symbol (e.g. PSA_KEY_TYPE_AES,
PSA_ALG_HMAC, etc.) or a PSA_WANT_XXX symbol.
"""
symbol = name
if not symbol.startswith('PSA_WANT_'):
symbol = psa_want_symbol(name)
self.negated_dependencies.add(symbol)

def set_key_bits(self, key_bits: Optional[int]) -> None:
"""Use the given key size for automatic dependency generation.

Call this function before set_arguments() if relevant.

This is only relevant for ECC and DH keys. For other key types,
this information is ignored.
"""
self.key_bits = key_bits

def set_arguments(self, arguments: List[str]) -> None:
"""Set test case arguments and automatically infer dependencies."""
super().set_arguments(arguments)
dependencies = automatic_dependencies(*arguments)
for i in range(len(dependencies)): #pylint: disable=consider-using-enumerate
if dependencies[i] in self.negated_dependencies:
dependencies[i] = '!' + dependencies[i]
if self.key_bits is not None:
dependencies = finish_family_dependencies(dependencies, self.key_bits)
self.dependencies += sorted(dependencies)
self.detect_not_implemented_dependencies()
30 changes: 27 additions & 3 deletions scripts/mbedtls_dev/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(self, description: Optional[str] = None):
self.dependencies = [] #type: List[str]
self.function = None #type: Optional[str]
self.arguments = [] #type: List[str]
self.skip_reason = ''

def add_comment(self, *lines: str) -> None:
self.comments += lines
Expand All @@ -47,6 +48,23 @@ def set_function(self, function: str) -> None:
def set_arguments(self, arguments: List[str]) -> None:
self.arguments = arguments

def skip_because(self, reason: str) -> None:
"""Skip this test case.

It will be included in the output, but commented out.

This is intended for test cases that are obtained from a
systematic enumeration, but that have dependencies that cannot
be fulfilled. Since we don't want to have test cases that are
never executed, we arrange not to have actual test cases. But
we do include comments to make it easier to understand the output
of test case generation.

reason must be a non-empty string explaining to humans why this
test case is skipped.
"""
self.skip_reason = reason

def check_completeness(self) -> None:
if self.description is None:
raise MissingDescription
Expand All @@ -67,10 +85,16 @@ def write(self, out: typing_util.Writable) -> None:
out.write('\n')
for line in self.comments:
out.write('# ' + line + '\n')
out.write(self.description + '\n')
prefix = ''
if self.skip_reason:
prefix = '## '
out.write('## # skipped because: ' + self.skip_reason + '\n')
out.write(prefix + self.description + '\n')
if self.dependencies:
out.write('depends_on:' + ':'.join(self.dependencies) + '\n')
out.write(self.function + ':' + ':'.join(self.arguments) + '\n')
out.write(prefix + 'depends_on:' +
':'.join(self.dependencies) + '\n')
out.write(prefix + self.function + ':' +
':'.join(self.arguments) + '\n')

def write_data_file(filename: str,
test_cases: Iterable[TestCase],
Expand Down
Loading