diff --git a/scripts/generate_psa_tests.py b/scripts/generate_psa_tests.py index ee2d8e7ea..fd71ec68a 100755 --- a/scripts/generate_psa_tests.py +++ b/scripts/generate_psa_tests.py @@ -106,6 +106,9 @@ def test_cases_for_key_type_not_supported( 'PSA_KEY_TYPE_ECC_PUBLIC_KEY') DH_KEY_TYPES = ('PSA_KEY_TYPE_DH_KEY_PAIR', 'PSA_KEY_TYPE_DH_PUBLIC_KEY') + # SPAKE2+ key types are also parametrised by an ECC family. + SPAKE2P_KEY_TYPES = ('PSA_KEY_TYPE_SPAKE2P_KEY_PAIR', + 'PSA_KEY_TYPE_SPAKE2P_PUBLIC_KEY') def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]: """Generate test cases that exercise the creation of keys of unsupported types.""" @@ -114,6 +117,8 @@ def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]: continue if key_type in self.DH_KEY_TYPES: continue + if key_type in self.SPAKE2P_KEY_TYPES: + continue kt = crypto_knowledge.KeyType(key_type) yield from self.test_cases_for_key_type_not_supported(kt) for curve_family in sorted(self.constructors.ecc_curves): @@ -123,6 +128,14 @@ def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]: kt, param_descr='type') yield from self.test_cases_for_key_type_not_supported( kt, 0, param_descr='curve') + # SPAKE2+ shares the ECC family parametrisation; sizes_to_test() + # yields nothing for families SPAKE2+ does not support. + for constr in self.SPAKE2P_KEY_TYPES: + kt = crypto_knowledge.KeyType(constr, [curve_family]) + yield from self.test_cases_for_key_type_not_supported( + kt, param_descr='type') + yield from self.test_cases_for_key_type_not_supported( + kt, 0, param_descr='curve') for dh_family in sorted(self.constructors.dh_groups): for constr in self.DH_KEY_TYPES: kt = crypto_knowledge.KeyType(constr, [dh_family]) @@ -181,6 +194,12 @@ def test_cases_for_key_type_key_generation( tc.set_dependencies([]) yield tc + # SPAKE2+ keys are password-derived registration material, not randomly + # generated, so key generation is not supported and no positive generate + # test cases are emitted for them. + SPAKE2P_KEY_TYPES = ('PSA_KEY_TYPE_SPAKE2P_KEY_PAIR', + 'PSA_KEY_TYPE_SPAKE2P_PUBLIC_KEY') + def test_cases_for_key_generation(self) -> Iterator[test_case.TestCase]: """Generate test cases that exercise the generation of keys.""" for key_type in sorted(self.constructors.key_types): @@ -188,6 +207,8 @@ def test_cases_for_key_generation(self) -> Iterator[test_case.TestCase]: continue if key_type in self.DH_KEY_TYPES: continue + if key_type in self.SPAKE2P_KEY_TYPES: + continue kt = crypto_knowledge.KeyType(key_type) yield from self.test_cases_for_key_type_key_generation(kt) for curve_family in sorted(self.constructors.ecc_curves): @@ -214,8 +235,18 @@ def __init__(self, info: psa_information.Information) -> None: key_type_expressions = self.constructors.generate_expressions( sorted(self.constructors.key_types) ) - self.key_types = [crypto_knowledge.KeyType(kt_expr) - for kt_expr in key_type_expressions] + # Skip key types with no testable sizes (e.g. SPAKE2+ over an ECC + # family it does not support): make_test_case() needs a concrete size. + # Also skip SPAKE2+ key types altogether: they are PAKE-only, so their + # permitted-algorithm policy must be a SPAKE2+ (PAKE) algorithm, and + # importing them with the incompatible non-PAKE policies this generator + # assigns is rejected at import time rather than at the operation. PAKE + # algorithms are themselves excluded from op-fail generation. + self.key_types = [kt for kt in + (crypto_knowledge.KeyType(kt_expr) + for kt_expr in key_type_expressions) + if kt.sizes_to_test() + and not kt.name.startswith('PSA_KEY_TYPE_SPAKE2P')] def make_test_case( self, @@ -775,7 +806,9 @@ def gather_key_types_for_sign_alg(self) -> Dict[str, List[str]]: # To create a valid combination both the algorithms and key types # must be filtered. Pair them with keywords created from its names. incompatible_alg_keyword = frozenset(['RAW', 'ANY', 'PURE']) - incompatible_key_type_keywords = frozenset(['MONTGOMERY']) + # SPAKE2+ key types carry the ECC family keyword but are PAKE keys, not + # signature keys, so exclude them from sign-algorithm matching. + incompatible_key_type_keywords = frozenset(['MONTGOMERY', 'SPAKE2P']) keyword_translation = { 'ECDSA': 'ECC', 'ED[0-9]*.*' : 'EDWARDS' @@ -832,6 +865,10 @@ def all_keys_for_implicit_usage(self) -> Iterator[StorageTestData]: if kt.is_public() and '_SIGN_' in usage: # Can't sign with a public key continue + if not kt.sizes_to_test(): + # No testable size (e.g. SPAKE2+ over an unsupported + # ECC family). + continue yield self.keys_for_implicit_usage(usage, alg, kt) def generate_all_keys(self) -> Iterator[StorageTestData]: diff --git a/scripts/mbedtls_framework/crypto_knowledge.py b/scripts/mbedtls_framework/crypto_knowledge.py index d406c8a9a..7fe67c3fc 100644 --- a/scripts/mbedtls_framework/crypto_knowledge.py +++ b/scripts/mbedtls_framework/crypto_knowledge.py @@ -143,6 +143,26 @@ def is_public(self) -> bool: 'PSA_ECC_FAMILY_MONTGOMERY': (255, 448), 'PSA_ECC_FAMILY_TWISTED_EDWARDS': (255, 448), } # type: Dict[str, Tuple[int, ...]] + # SPAKE2+ (RFC 9383) is defined over secp_r1 P-256/P-384/P-521. The key + # size is the curve bit size, shared by the key pair (w0||w1) and the + # public key (w0||L). + SPAKE2P_KEY_SIZES = { + 'PSA_ECC_FAMILY_SECP_R1': (256, 384, 521), + } # type: Dict[str, Tuple[int, ...]] + # Valid SPAKE2+ registration material (RFC 9383 Appendix C): a key pair is + # w0 || w1 and a public key is w0 || L. Used as the export representation. + SPAKE2P_KEY_DATA = { + 'PSA_KEY_TYPE_SPAKE2P_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)': { + 256: bytes.fromhex("bb8e1bbcf3c48f62c08db243652ae55d3e5586053fca77102994f23ad95491b37e945f34d78785b8a3ef44d0df5a1a97d6b3b460409a345ca7830387a74b1dba"), + 384: bytes.fromhex("097a61cbb1cee72bb654be96d80f46e0e3531151003903b572fc193f233772c23c22228884a0d5447d0ab49a656ce1d218772816140e6c3c3938a693c600b2191118a34c7956e1f1cd5b0d519b56ea5858060966cfaf27679c9182129949e74f"), + 521: bytes.fromhex("009c79bcd7656716314fca5a6e2c5cda7ef86131399438e012a043051e863f60b5aeb3c101731e1505e721580f48535a9b0456b231b9266ae6fff49ee90d25f72f5f01632c15f51fcd916cd79e19075f8a69b72b0099922ad62ff8d540b469569f0aa027047aed2b3f242ea0ac4288b4e4db6a4e5946d8ad32b42192c5aa66d9ef8e1b33"), + }, + 'PSA_KEY_TYPE_SPAKE2P_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1)': { + 256: bytes.fromhex("bb8e1bbcf3c48f62c08db243652ae55d3e5586053fca77102994f23ad95491b304eb7c9db3d9a9eb1f8adab81b5794c1f13ae3e225efbe91ea487425854c7fc00f00bfedcbd09b2400142d40a14f2064ef31dfaa903b91d1faea7093d835966efd"), + 384: bytes.fromhex("097a61cbb1cee72bb654be96d80f46e0e3531151003903b572fc193f233772c23c22228884a0d5447d0ab49a656ce1d204f27dd5384d6b9beb4c5022c94b1978d632779e1d3abe458611e734a529d004e25053398e5dc9eeaa4ffa59743ca7ddbc0e7ce69155295cb2b846da83ee6a44490dd8e96bb0b0f6645281bfd978dd5f6836561ea0d8b2c045ff04cef2e5873d2c"), + 521: bytes.fromhex("009c79bcd7656716314fca5a6e2c5cda7ef86131399438e012a043051e863f60b5aeb3c101731e1505e721580f48535a9b0456b231b9266ae6fff49ee90d25f72f5f040135072d0fa36f9e80031294cef5c3c35b882a0efa2c66570d64a49f8bec6c66435bf65bb7c7b2a3e7dece491e02b4d567e7087dbc32fe0fae8af417dcb50be6d704012a194588b690e6d3db492656f72ddea01fc1c7fcec0f5d34a5af0102939f6fdeae39c20cff74fcdb7f09855f0fc9520d20b0520b0b096b8d42c7c3d68b4a66f751"), + }, + } # type: Dict[str, Dict[int, bytes]] KEY_TYPE_SIZES = { 'PSA_KEY_TYPE_AES': (128, 192, 256), # exhaustive 'PSA_KEY_TYPE_ARC4': (8, 128, 2048), # extremes + sensible @@ -176,6 +196,11 @@ def sizes_to_test(self) -> Tuple[int, ...]: if self.private_type == 'PSA_KEY_TYPE_DH_KEY_PAIR': assert self.params is not None return self.DH_KEY_SIZES[self.params[0]] + if self.private_type == 'PSA_KEY_TYPE_SPAKE2P_KEY_PAIR': + assert self.params is not None + # SPAKE2+ is only defined over a subset of ECC families; other + # families yield no sizes (hence no generated test cases). + return self.SPAKE2P_KEY_SIZES.get(self.params[0], ()) return self.KEY_TYPE_SIZES[self.private_type] # "48657265006973206b6579a064617461" @@ -193,6 +218,11 @@ def key_material(self, bits: int) -> bytes: psa_export_key(id, `material`, ...); ``` """ + if self.expression in self.SPAKE2P_KEY_DATA: + if bits not in self.SPAKE2P_KEY_DATA[self.expression]: + raise ValueError('No key data for {}-bit {}' + .format(bits, self.expression)) + return self.SPAKE2P_KEY_DATA[self.expression][bits] if self.expression in ASYMMETRIC_KEY_DATA: if bits not in ASYMMETRIC_KEY_DATA[self.expression]: raise ValueError('No key data for {}-bit {}' diff --git a/scripts/mbedtls_framework/psa_information.py b/scripts/mbedtls_framework/psa_information.py index cac16b5da..8a1f72a11 100644 --- a/scripts/mbedtls_framework/psa_information.py +++ b/scripts/mbedtls_framework/psa_information.py @@ -26,18 +26,16 @@ def remove_unwanted_macros( """Remove constructors that should be exckuded from systematic testing.""" # Mbed TLS does not support finite-field DSA, but 3.6 defines DSA # identifiers for historical reasons. - # Mbed TLS and TF-PSA-Crypto 1.0 do not support SPAKE2+, although - # TF-PSA-Crypto 1.0 defines SPAKE2+ identifiers to be able to build - # the psa-arch-tests compliance test suite. - # - # Don't attempt to generate any related test case. - # The corresponding test cases would be commented out anyway, - # but for these types, we don't have enough support in the test scripts - # to generate these test cases. constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR') constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY') - constructors.key_types.discard('PSA_KEY_TYPE_SPAKE2P_KEY_PAIR') - constructors.key_types.discard('PSA_KEY_TYPE_SPAKE2P_PUBLIC_KEY') + # SPAKE2+ (RFC 9383) is implemented in TF-PSA-Crypto, with the curve + # bit size as the key size and PSA_EXPORT_KEY_OUTPUT_SIZE defined for + # its key types; crypto_knowledge.KeyType has the matching size/material + # model. So its key types take part in systematic key-type generation + # (not-supported and storage). Key *generation* of SPAKE2+ keys is not + # supported (the keys are password-derived registration material, not + # random), so PSA_WANT_KEY_TYPE_SPAKE2P_KEY_PAIR_GENERATE is off and the + # KeyGenerate generator skips them accordingly. def read_psa_interface(self) -> macro_collector.PSAMacroEnumerator: """Return the list of known key types, algorithms, etc.""" diff --git a/tests/src/psa_exercise_key.c b/tests/src/psa_exercise_key.c index ffed3becf..053804dbc 100644 --- a/tests/src/psa_exercise_key.c +++ b/tests/src/psa_exercise_key.c @@ -1125,7 +1125,17 @@ int mbedtls_test_psa_exported_key_sanity_check( PSA_EXPORT_PUBLIC_KEY_OUTPUT_SIZE(type, bits)); TEST_ASSERT(exported_length <= PSA_EXPORT_PUBLIC_KEY_MAX_SIZE); - } else { + } else +#if defined(PSA_WANT_ALG_SPAKE2P_HMAC) || \ + defined(PSA_WANT_ALG_SPAKE2P_CMAC) || \ + defined(PSA_WANT_ALG_SPAKE2P_MATTER) + if (PSA_KEY_TYPE_IS_SPAKE2P(type)) { + /* The export representation is the registration material: w0 || w1 for + * a key pair, or w0 || L for a public key. */ + TEST_EQUAL(exported_length, PSA_EXPORT_KEY_OUTPUT_SIZE(type, bits)); + } else +#endif + { (void) exported; TEST_FAIL("Sanity check not implemented for this key type"); }