Skip to content

Commit 8296a73

Browse files
Merge pull request #104 from gilles-peskine-arm/psa-storage-test-cases-never-supported-negative-framework
Switch generate_psa_test.py to automatic dependencies for negative test cases
2 parents 1ead596 + 89cc06d commit 8296a73

File tree

3 files changed

+116
-72
lines changed

3 files changed

+116
-72
lines changed

scripts/generate_psa_tests.py

+51-46
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import enum
1212
import re
1313
import sys
14-
from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional
14+
from typing import Callable, Dict, Iterable, Iterator, List, Optional
1515

1616
from mbedtls_framework import crypto_data_tests
1717
from mbedtls_framework import crypto_knowledge
@@ -26,7 +26,7 @@
2626

2727
def test_case_for_key_type_not_supported(
2828
verb: str, key_type: str, bits: int,
29-
dependencies: List[str],
29+
not_supported_mechanism: str,
3030
*args: str,
3131
param_descr: str = ''
3232
) -> test_case.TestCase:
@@ -35,17 +35,16 @@ def test_case_for_key_type_not_supported(
3535
"""
3636
tc = psa_test_case.TestCase()
3737
short_key_type = crypto_knowledge.short_expression(key_type)
38-
adverb = 'not' if dependencies else 'never'
39-
if param_descr:
40-
adverb = param_descr + ' ' + adverb
41-
tc.set_description('PSA {} {} {}-bit {} supported'
42-
.format(verb, short_key_type, bits, adverb))
38+
tc.set_description('PSA {} {} {}-bit{} not supported'
39+
.format(verb, short_key_type, bits,
40+
' ' + param_descr if param_descr else ''))
41+
# if tc.description == 'PSA import RSA_KEY_PAIR 1024-bit not supported':
42+
# import pdb; pdb.set_trace()
4343
tc.set_function(verb + '_not_supported')
4444
tc.set_key_bits(bits)
45-
tc.set_key_pair_usage(verb.upper())
45+
tc.set_key_pair_usage([verb.upper()])
46+
tc.assumes_not_supported(not_supported_mechanism)
4647
tc.set_arguments([key_type] + list(args))
47-
tc.set_dependencies(dependencies)
48-
tc.skip_if_any_not_implemented(dependencies)
4948
return tc
5049

5150
class KeyTypeNotSupported:
@@ -77,37 +76,27 @@ def test_cases_for_key_type_not_supported(
7776
# Don't generate test cases for key types that are always supported.
7877
# They would be skipped in all configurations, which is noise.
7978
return
80-
import_dependencies = [('!' if param is None else '') +
81-
psa_information.psa_want_symbol(kt.name)]
82-
if kt.params is not None:
83-
import_dependencies += [('!' if param == i else '') +
84-
psa_information.psa_want_symbol(sym)
85-
for i, sym in enumerate(kt.params)]
86-
if kt.name.endswith('_PUBLIC_KEY'):
87-
generate_dependencies = []
79+
if param is None:
80+
not_supported_mechanism = kt.name
8881
else:
89-
generate_dependencies = \
90-
psa_information.fix_key_pair_dependencies(import_dependencies, 'GENERATE')
91-
import_dependencies = \
92-
psa_information.fix_key_pair_dependencies(import_dependencies, 'BASIC')
82+
assert kt.params is not None
83+
not_supported_mechanism = kt.params[param]
9384
for bits in kt.sizes_to_test():
9485
yield test_case_for_key_type_not_supported(
9586
'import', kt.expression, bits,
96-
psa_information.finish_family_dependencies(import_dependencies, bits),
87+
not_supported_mechanism,
9788
test_case.hex_string(kt.key_material(bits)),
9889
param_descr=param_descr,
9990
)
100-
if not generate_dependencies and param is not None:
101-
# If generation is impossible for this key type, rather than
102-
# supported or not depending on implementation capabilities,
103-
# only generate the test case once.
104-
continue
105-
# For public key we expect that key generation fails with
106-
# INVALID_ARGUMENT. It is handled by KeyGenerate class.
91+
# Don't generate not-supported test cases for key generation of
92+
# public keys. Our implementation always returns
93+
# PSA_ERROR_INVALID_ARGUMENT when attempting to generate a
94+
# public key, so we cover this together with the positive cases
95+
# in the KeyGenerate class.
10796
if not kt.is_public():
10897
yield test_case_for_key_type_not_supported(
10998
'generate', kt.expression, bits,
110-
psa_information.finish_family_dependencies(generate_dependencies, bits),
99+
not_supported_mechanism,
111100
str(bits),
112101
param_descr=param_descr,
113102
)
@@ -155,7 +144,7 @@ def test_case_for_key_generation(
155144
.format(short_key_type, bits))
156145
tc.set_function('generate_key')
157146
tc.set_key_bits(bits)
158-
tc.set_key_pair_usage('GENERATE')
147+
tc.set_key_pair_usage(['GENERATE'])
159148
tc.set_arguments([key_type] + list(args) + [result])
160149
return tc
161150

@@ -234,16 +223,19 @@ def make_test_case(
234223
category: crypto_knowledge.AlgorithmCategory,
235224
reason: 'Reason',
236225
kt: Optional[crypto_knowledge.KeyType] = None,
237-
not_deps: FrozenSet[str] = frozenset(),
226+
not_supported: Optional[str] = None,
238227
) -> test_case.TestCase:
239-
"""Construct a failure test case for a one-key or keyless operation."""
228+
"""Construct a failure test case for a one-key or keyless operation.
229+
230+
If `reason` is `Reason.NOT_SUPPORTED`, pass the not-supported
231+
dependency symbol as the `not_supported` argument.
232+
"""
240233
#pylint: disable=too-many-arguments,too-many-locals
241234
tc = psa_test_case.TestCase()
242235
pretty_alg = alg.short_expression()
243236
if reason == self.Reason.NOT_SUPPORTED:
244-
short_deps = [re.sub(r'PSA_WANT_ALG_', r'', dep)
245-
for dep in not_deps]
246-
pretty_reason = '!' + '&'.join(sorted(short_deps))
237+
assert not_supported is not None
238+
pretty_reason = '!' + re.sub(r'PSA_WANT_[A-Z]+_', r'', not_supported)
247239
else:
248240
pretty_reason = reason.name.lower()
249241
if kt:
@@ -257,16 +249,12 @@ def make_test_case(
257249
pretty_alg,
258250
pretty_reason,
259251
' with ' + pretty_type if pretty_type else ''))
260-
dependencies = psa_information.automatic_dependencies(alg.base_expression, key_type)
261-
dependencies = psa_information.fix_key_pair_dependencies(dependencies, 'BASIC')
262-
for i, dep in enumerate(dependencies):
263-
if dep in not_deps:
264-
dependencies[i] = '!' + dep
265252
tc.set_function(category.name.lower() + '_fail')
266253
arguments = [] # type: List[str]
267254
if kt:
268255
bits = kt.sizes_to_test()[0]
269256
tc.set_key_bits(bits)
257+
tc.set_key_pair_usage(['IMPORT'])
270258
key_material = kt.key_material(bits)
271259
arguments += [key_type, test_case.hex_string(key_material)]
272260
arguments.append(alg.expression)
@@ -275,8 +263,25 @@ def make_test_case(
275263
error = ('NOT_SUPPORTED' if reason == self.Reason.NOT_SUPPORTED else
276264
'INVALID_ARGUMENT')
277265
arguments.append('PSA_ERROR_' + error)
266+
if reason == self.Reason.NOT_SUPPORTED:
267+
assert not_supported is not None
268+
tc.assumes_not_supported(not_supported)
269+
# Special case: if one of deterministic/randomized
270+
# ECDSA is supported but not the other, then the one
271+
# that is not supported in the signature direction is
272+
# still supported in the verification direction,
273+
# because the two verification algorithms are
274+
# identical. This property is how Mbed TLS chooses to
275+
# behave, the specification would also allow it to
276+
# reject the algorithm. In the generated test cases,
277+
# we avoid this difficulty by not running the
278+
# not-supported test case when exactly one of the
279+
# two variants is supported.
280+
if not_supported == 'PSA_WANT_ALG_ECDSA':
281+
tc.add_dependencies(['!PSA_WANT_ALG_DETERMINISTIC_ECDSA'])
282+
if not_supported == 'PSA_WANT_ALG_DETERMINISTIC_ECDSA':
283+
tc.add_dependencies(['!PSA_WANT_ALG_ECDSA'])
278284
tc.set_arguments(arguments)
279-
tc.set_dependencies(dependencies)
280285
return tc
281286

282287
def no_key_test_cases(
@@ -290,7 +295,7 @@ def no_key_test_cases(
290295
for dep in psa_information.automatic_dependencies(alg.base_expression):
291296
yield self.make_test_case(alg, category,
292297
self.Reason.NOT_SUPPORTED,
293-
not_deps=frozenset([dep]))
298+
not_supported=dep)
294299
else:
295300
# Incompatible operation, supported algorithm
296301
yield self.make_test_case(alg, category, self.Reason.INVALID)
@@ -308,7 +313,7 @@ def one_key_test_cases(
308313
for dep in psa_information.automatic_dependencies(alg.base_expression):
309314
yield self.make_test_case(alg, category,
310315
self.Reason.NOT_SUPPORTED,
311-
kt=kt, not_deps=frozenset([dep]))
316+
kt=kt, not_supported=dep)
312317
# Public key for a private-key operation
313318
if category.is_asymmetric() and kt.is_public():
314319
yield self.make_test_case(alg, category,
@@ -481,7 +486,7 @@ def make_test_case(self, key: StorageTestData) -> test_case.TestCase:
481486
tc.add_dependencies(psa_information.generate_deps_from_description(key.description))
482487
tc.set_function('key_storage_' + verb)
483488
tc.set_key_bits(key.bits)
484-
tc.set_key_pair_usage('BASIC')
489+
tc.set_key_pair_usage(['IMPORT'] if self.forward else ['EXPORT'])
485490
if self.forward:
486491
extra_arguments = []
487492
else:

scripts/mbedtls_framework/psa_information.py

+5-16
Original file line numberDiff line numberDiff line change
@@ -139,29 +139,18 @@ def generate_deps_from_description(
139139

140140
return dep_list
141141

142-
def tweak_key_pair_dependency(dep: str, usage: str):
142+
def tweak_key_pair_dependency(dep: str, usages: List[str]) -> List[str]:
143143
"""
144144
This helper function add the proper suffix to PSA_WANT_KEY_TYPE_xxx_KEY_PAIR
145145
symbols according to the required usage.
146146
"""
147-
ret_list = list()
148147
if dep.endswith('KEY_PAIR'):
149-
if usage == "BASIC":
150-
# BASIC automatically includes IMPORT and EXPORT for test purposes (see
151-
# config_psa.h).
152-
ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_BASIC', dep))
153-
ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_IMPORT', dep))
154-
ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_EXPORT', dep))
155-
elif usage == "GENERATE":
156-
ret_list.append(re.sub(r'KEY_PAIR', r'KEY_PAIR_GENERATE', dep))
157-
else:
158-
# No replacement to do in this case
159-
ret_list.append(dep)
160-
return ret_list
148+
return [dep + '_' + usage for usage in usages]
149+
return [dep]
161150

162-
def fix_key_pair_dependencies(dep_list: List[str], usage: str):
151+
def fix_key_pair_dependencies(dep_list: List[str], usages: List[str]) -> List[str]:
163152
new_list = [new_deps
164153
for dep in dep_list
165-
for new_deps in tweak_key_pair_dependency(dep, usage)]
154+
for new_deps in tweak_key_pair_dependency(dep, usages)]
166155

167156
return new_list

scripts/mbedtls_framework/psa_test_case.py

+60-10
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
from . import test_case
1515

1616

17-
# A temporary hack: at the time of writing, not all dependency symbols
18-
# are implemented yet. Skip test cases for which the dependency symbols are
19-
# not available. Once all dependency symbols are available, this hack must
20-
# be removed so that a bug in the dependency symbols properly leads to a test
21-
# failure.
17+
# Skip test cases for which the dependency symbols are not defined.
18+
# We assume that this means that a required mechanism is not implemented.
19+
# Note that if we erroneously skip generating test cases for
20+
# mechanisms that are not implemented, this should be caught
21+
# by the NOT_SUPPORTED test cases generated by generate_psa_tests.py
22+
# in test_suite_psa_crypto_not_supported and test_suite_psa_crypto_op_fail:
23+
# those emit tests with negative dependencies, which will not be skipped here.
24+
2225
def read_implemented_dependencies(acc: Set[str], filename: str) -> None:
2326
with open(filename) as input_stream:
2427
for line in input_stream:
@@ -51,8 +54,8 @@ def find_dependencies_not_implemented(dependencies: List[str]) -> List[str]:
5154
_implemented_dependencies = frozenset(acc)
5255
return [dep
5356
for dep in dependencies
54-
if (dep.lstrip('!') not in _implemented_dependencies and
55-
dep.lstrip('!').startswith('PSA_WANT'))]
57+
if (dep not in _implemented_dependencies and
58+
dep.startswith('PSA_WANT'))]
5659

5760

5861
class TestCase(test_case.TestCase):
@@ -75,8 +78,9 @@ def __init__(self, dependency_prefix: Optional[str] = None) -> None:
7578
self.manual_dependencies = [] #type: List[str]
7679
self.automatic_dependencies = set() #type: Set[str]
7780
self.dependency_prefix = dependency_prefix #type: Optional[str]
81+
self.negated_dependencies = set() #type: Set[str]
7882
self.key_bits = None #type: Optional[int]
79-
self.key_pair_usage = None #type: Optional[str]
83+
self.key_pair_usage = None #type: Optional[List[str]]
8084

8185
def set_key_bits(self, key_bits: Optional[int]) -> None:
8286
"""Use the given key size for automatic dependency generation.
@@ -88,8 +92,8 @@ def set_key_bits(self, key_bits: Optional[int]) -> None:
8892
"""
8993
self.key_bits = key_bits
9094

91-
def set_key_pair_usage(self, key_pair_usage: Optional[str]) -> None:
92-
"""Use the given suffix for key pair dependencies.
95+
def set_key_pair_usage(self, key_pair_usage: Optional[List[str]]) -> None:
96+
"""Use the given suffixes for key pair dependencies.
9397
9498
Call this function before set_arguments() if relevant.
9599
@@ -109,16 +113,62 @@ def infer_dependencies(self, arguments: List[str]) -> List[str]:
109113
dependencies = psa_information.fix_key_pair_dependencies(dependencies,
110114
self.key_pair_usage)
111115
if 'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE' in dependencies and \
116+
'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE' not in self.negated_dependencies and \
112117
self.key_bits is not None:
113118
size_dependency = ('PSA_VENDOR_RSA_GENERATE_MIN_KEY_BITS <= ' +
114119
str(self.key_bits))
115120
dependencies.append(size_dependency)
116121
return dependencies
117122

123+
def assumes_not_supported(self, name: str) -> None:
124+
"""Negate the given mechanism for automatic dependency generation.
125+
126+
`name` can be either a dependency symbol (``PSA_WANT_xxx``) or
127+
a mechanism name (``PSA_KEY_TYPE_xxx``, etc.).
128+
129+
Call this function before set_arguments() for a test case that should
130+
run if the given mechanism is not supported.
131+
132+
Call modifiers such as set_key_bits() and set_key_pair_usage() before
133+
calling this method, if applicable.
134+
135+
A mechanism is a PSA_XXX symbol, e.g. PSA_KEY_TYPE_AES, PSA_ALG_HMAC,
136+
etc. For mechanisms like ECC curves where the support status includes
137+
the key bit-size, this class assumes that only one bit-size is
138+
involved in a given test case.
139+
"""
140+
if name.startswith('PSA_WANT_'):
141+
self.negated_dependencies.add(name)
142+
return
143+
if name == 'PSA_KEY_TYPE_RSA_KEY_PAIR' and \
144+
self.key_bits is not None and \
145+
self.key_pair_usage == ['GENERATE']:
146+
# When RSA key pair generation is not supported, it could be
147+
# due to the specific key size is out of range, or because
148+
# RSA key pair generation itself is not supported. Assume the
149+
# latter.
150+
dep = psa_information.psa_want_symbol(name, prefix=self.dependency_prefix)
151+
152+
self.negated_dependencies.add(dep + '_GENERATE')
153+
return
154+
dependencies = self.infer_dependencies([name])
155+
# * If we have more than one dependency to negate, the result would
156+
# say that all of the dependencies are disabled, which is not
157+
# a desirable outcome: the negation of (A and B) is (!A or !B),
158+
# not (!A and !B).
159+
# * If we have no dependency to negate, the result wouldn't be a
160+
# not-supported case.
161+
# Assert that we don't reach either such case.
162+
assert len(dependencies) == 1
163+
self.negated_dependencies.add(dependencies[0])
164+
118165
def set_arguments(self, arguments: List[str]) -> None:
119166
"""Set test case arguments and automatically infer dependencies."""
120167
super().set_arguments(arguments)
121168
dependencies = self.infer_dependencies(arguments)
169+
for i in range(len(dependencies)): #pylint: disable=consider-using-enumerate
170+
if dependencies[i] in self.negated_dependencies:
171+
dependencies[i] = '!' + dependencies[i]
122172
self.skip_if_any_not_implemented(dependencies)
123173
self.automatic_dependencies.update(dependencies)
124174

0 commit comments

Comments
 (0)