Reject invalid MAC and AEAD truncations

Reject algorithms of the form PSA_ALG_TRUNCATED_MAC(...) or
PSA_ALG_AEAD_WITH_SHORTENED_TAG(...) when the truncation length is invalid
or not accepted by policy in Mbed TLS.

This is done in KeyType.can_do, so in generate_psa_tests.py, keys will be
tested for operation failure with this algorithm if the algorithm is
rejected, and for storage if the algorithm is accepted.

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 bd4d94e..7d2e755 100644
--- a/scripts/mbedtls_dev/crypto_knowledge.py
+++ b/scripts/mbedtls_dev/crypto_knowledge.py
@@ -20,7 +20,7 @@
 
 import enum
 import re
-from typing import Iterable, List, Optional, Tuple
+from typing import FrozenSet, Iterable, List, Optional, Tuple
 
 from mbedtls_dev.asymmetric_key_data import ASYMMETRIC_KEY_DATA
 
@@ -216,6 +216,8 @@
         #pylint: disable=too-many-return-statements
         if alg.is_wildcard:
             return False
+        if alg.is_invalid_truncation():
+            return False
         if self.head == 'HMAC' and alg.head == 'HMAC':
             return True
         if self.head == 'DES':
@@ -420,8 +422,55 @@
         """
         return short_expression(self.expression, level=level)
 
+    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,
+    }
+    HMAC_WITH_NOMINAL_LENGTH_RE = re.compile(r'PSA_ALG_HMAC\(\w+([0-9])+\)\Z')
+    @classmethod
+    def mac_or_tag_length(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)
+            if m:
+                max_length = int(m.group(1)) // 8
+        if max_length is None:
+            raise ValueError('Unknown permitted lengths for ' + base)
+        return frozenset(range(4, max_length + 1))
+
+    TRUNCATED_ALG_RE = re.compile(
+        r'(?P<face>PSA_ALG_(?:AEAD_WITH_SHORTENED_TAG|TRUNCATED_MAC))'
+        r'\((?P<base>.*),'
+        r'(?P<length>0[Xx][0-9A-Fa-f]+|[1-9][0-9]*|0[0-9]*)[LUlu]*\)\Z')
+    def is_invalid_truncation(self) -> bool:
+        """False for a MAC or AEAD algorithm truncated to an invalid length.
+
+        True for a MAC or AEAD algorithm truncated to a valid length or to
+        a length that cannot be determined. True for anything other than
+        a truncated MAC or AEAD.
+        """
+        m = self.TRUNCATED_ALG_RE.match(self.expression)
+        if m:
+            base = m.group('base')
+            to_length = int(m.group('length'), 0)
+            permitted_lengths = self.mac_or_tag_length(base)
+            if to_length not in permitted_lengths:
+                return True
+        return False
+
     def can_do(self, category: AlgorithmCategory) -> bool:
-        """Whether this algorithm fits the specified operation category."""
+        """Whether this algorithm can perform operations in the given category.
+        """
         if category == self.category:
             return True
         if category == AlgorithmCategory.KEY_DERIVATION and \