Don't exercise OAEP with small key and large hash

RSA-OAEP requires the key to be larger than a function of the hash size.
Ideally such combinations would be detected as a key/algorithm
incompatibility. However key/algorithm compatibility is currently tested
between the key type and the algorithm without considering the key size, and
this is inconvenient to change. So as a workaround, dispense
OAEP-with-too-small-hash from exercising, without including it in the
automatic operation-failure test generation.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
diff --git a/scripts/mbedtls_dev/crypto_knowledge.py b/scripts/mbedtls_dev/crypto_knowledge.py
index 55eb54d..1491a84 100644
--- a/scripts/mbedtls_dev/crypto_knowledge.py
+++ b/scripts/mbedtls_dev/crypto_knowledge.py
@@ -213,7 +213,7 @@
 
         This function does not currently handle key derivation or PAKE.
         """
-        #pylint: disable=too-many-return-statements
+        #pylint: disable=too-many-branches,too-many-return-statements
         if alg.is_wildcard:
             return False
         if alg.is_invalid_truncation():
@@ -425,28 +425,41 @@
         """
         return short_expression(self.expression, level=level)
 
+    HASH_LENGTH = {
+        'PSA_ALG_MD5': 16,
+        'PSA_ALG_SHA_1': 20,
+    }
+    HASH_LENGTH_BITS_RE = re.compile(r'([0-9]+)\Z')
+    @classmethod
+    def hash_length(cls, alg: str) -> int:
+        """The length of the given hash algorithm, in bytes."""
+        if alg in cls.HASH_LENGTH:
+            return cls.HASH_LENGTH[alg]
+        m = cls.HASH_LENGTH_BITS_RE.search(alg)
+        if m:
+            return int(m.group(1)) // 8
+        raise ValueError('Unknown hash length for ' + alg)
+
     PERMITTED_TAG_LENGTHS = {
         'PSA_ALG_CCM': frozenset([4, 6, 8, 10, 12, 14, 16]),
         'PSA_ALG_CHACHA20_POLY1305': frozenset([16]),
         'PSA_ALG_GCM': frozenset([4, 8, 12, 13, 14, 15, 16]),
     }
     MAC_LENGTH = {
-        'PSA_ALG_CBC_MAC': 16,
-        'PSA_ALG_CMAC': 16,
-        'PSA_ALG_HMAC(PSA_ALG_MD5)': 16,
-        'PSA_ALG_HMAC(PSA_ALG_SHA_1)': 20,
+        'PSA_ALG_CBC_MAC': 16, # actually the block cipher length
+        'PSA_ALG_CMAC': 16, # actually the block cipher length
     }
-    HMAC_WITH_NOMINAL_LENGTH_RE = re.compile(r'PSA_ALG_HMAC\(\w+([0-9])+\)\Z')
+    HMAC_RE = re.compile(r'PSA_ALG_HMAC\((.*)\)\Z')
     @classmethod
-    def mac_or_tag_length(cls, base: str) -> FrozenSet[int]:
+    def mac_or_tag_lengths(cls, base: str) -> FrozenSet[int]:
         """Return the set of permitted lengths for the given MAC or AEAD tag."""
         if base in cls.PERMITTED_TAG_LENGTHS:
             return cls.PERMITTED_TAG_LENGTHS[base]
         max_length = cls.MAC_LENGTH.get(base, None)
         if max_length is None:
-            m = cls.HMAC_WITH_NOMINAL_LENGTH_RE.match(base)
+            m = cls.HMAC_RE.match(base)
             if m:
-                max_length = int(m.group(1)) // 8
+                max_length = cls.hash_length(m.group(1))
         if max_length is None:
             raise ValueError('Unknown permitted lengths for ' + base)
         return frozenset(range(4, max_length + 1))
@@ -466,7 +479,7 @@
         if m:
             base = m.group('base')
             to_length = int(m.group('length'), 0)
-            permitted_lengths = self.mac_or_tag_length(base)
+            permitted_lengths = self.mac_or_tag_lengths(base)
             if to_length not in permitted_lengths:
                 return True
         return False
diff --git a/tests/scripts/generate_psa_tests.py b/tests/scripts/generate_psa_tests.py
index 27ee52d..6fd311e 100755
--- a/tests/scripts/generate_psa_tests.py
+++ b/tests/scripts/generate_psa_tests.py
@@ -518,6 +518,32 @@
         self.version = version #type: int
         self.forward = forward #type: bool
 
+    RSA_OAEP_RE = re.compile(r'PSA_ALG_RSA_OAEP\((.*)\)\Z')
+    @classmethod
+    def valid_key_size_for_algorithm(
+            cls,
+            key_type: psa_storage.Expr, bits: int,
+            alg: psa_storage.Expr
+    ) -> bool:
+        """Whether the given key type and size are valid for the algorithm.
+
+        Normally only the type and algorithm matter for compatibility, and
+        this is handled in crypto_knowledge.KeyType.can_do(). This function
+        exists to detect exceptional cases. Exceptional cases detected here
+        are not tested in OpFail and should therefore have manually written
+        test cases.
+        """
+        #pylint: disable=unused-argument
+        # OAEP requires room for two hashes plus wrapping
+        m = cls.RSA_OAEP_RE.match(alg.string)
+        if m:
+            hash_alg = m.group(1)
+            hash_length = crypto_knowledge.Algorithm.hash_length(hash_alg)
+            key_length = (bits + 7) // 8
+            # Leave enough room for at least one byte of plaintext
+            return key_length > 2 * hash_length + 2
+        return True
+
     def make_test_case(self, key: StorageTestData) -> test_case.TestCase:
         """Construct a storage format test case for the given key.
 
@@ -546,7 +572,8 @@
             # encodings of the attributes.
             # Raw data keys have no useful exercise anyway so there is no
             # loss of test coverage.
-            if key.type.string != 'PSA_KEY_TYPE_RAW_DATA':
+            if key.type.string != 'PSA_KEY_TYPE_RAW_DATA' and \
+               self.valid_key_size_for_algorithm(key.type, key.bits, key.alg):
                 flags.append('TEST_FLAG_EXERCISE')
             if 'READ_ONLY' in key.lifetime.string:
                 flags.append('TEST_FLAG_READ_ONLY')