Add PSA 2.0.0 profile

An updated version of the PSA IoT Profile is available:
 - https://www.ietf.org/archive/id/draft-tschofenig-rats-psa-token-09.html
 - Profile name: PSA_2_0_0

Signed-off-by: Tamas Ban <tamas.ban@arm.com>
Change-Id: Ifb64f39b0b7965d3af408e52289916e487f560fb
diff --git a/iat-verifier/iatverifier/psa_2_0_0_token_claims.py b/iat-verifier/iatverifier/psa_2_0_0_token_claims.py
new file mode 100644
index 0000000..63d49ee
--- /dev/null
+++ b/iat-verifier/iatverifier/psa_2_0_0_token_claims.py
@@ -0,0 +1,265 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import string
+
+from iatverifier.attest_token_verifier import AttestationClaim, NonVerifiedClaim
+from iatverifier.attest_token_verifier import CompositeAttestClaim
+
+# IAT custom claims
+ARM_RANGE = 2393
+
+# SW component IDs
+SW_COMPONENT_RANGE = 0
+
+class InstanceIdClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Instance ID claim"""
+    def __init__(self, verifier, *, expected_len, necessity=AttestationClaim.MANDATORY):
+        super().__init__(verifier=verifier, necessity=necessity)
+        self.expected_len = expected_len
+
+    def get_claim_key(self=None):
+        return 256  # EAT UEID label
+
+    def get_claim_name(self=None):
+        return 'INSTANCE_ID'
+
+    def verify(self, value):
+        self._validate_bytestring_length_equals(value, 'INSTANCE_ID', self.expected_len)
+        if value[0] != 0x01:
+            msg = 'Invalid INSTANCE_ID: first byte must be 0x01, found: 0x{}'
+            self.verifier.error(msg.format(value[0]))
+        self.verify_count += 1
+
+
+class ChallengeClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Challenge claim"""
+    HASH_SIZES = [32, 48, 64]
+
+    def get_claim_key(self=None):
+        return 10  # EAT nonce label
+
+    def get_claim_name(self=None):
+        return 'CHALLENGE'
+
+    def verify(self, value):
+        self._check_type('CHALLENGE', value, bytes)
+
+        value_len = len(value)
+        if value_len not in ChallengeClaim.HASH_SIZES:
+            msg = 'Invalid CHALLENGE length; must one of {}, found {} bytes'
+            self.verifier.error(msg.format(ChallengeClaim.HASH_SIZES, value_len))
+        self.verify_count += 1
+
+
+class ImplementationIdClaim(NonVerifiedClaim):
+    """Class representing a PSA Attestation Token Implementation ID claim"""
+    def get_claim_key(self=None):
+        return ARM_RANGE + 3
+
+    def get_claim_name(self=None):
+        return 'IMPLEMENTATION_ID'
+
+
+class CertificationReference(AttestationClaim):
+    """Class representing a PSA Attestation Token Certification Reference claim"""
+    def verify(self, value):
+        self._check_type('CERTIFICATION_REFERENCE', value, str)
+
+        value_len = len(value)
+        expected_len = 19 # 'EAN13-Version' 13 + '-' + 5. e.g.:0604565272829-10010
+        if len(value) != expected_len:
+            msg = 'Invalid CERTIFICATION_REFERENCE length; must be {} characters, found {} characters'
+            self.verifier.error(msg.format(expected_len, value_len))
+        for idx, character in enumerate(value):
+            if character not in string.digits and character not in '-':
+                msg = 'Invalid character {} at position {}'
+                self.verifier.error(msg.format(character, idx+1))
+
+        self.verify_count += 1
+
+    def get_claim_key(self=None):
+        return ARM_RANGE + 5
+
+    def get_claim_name(self=None):
+        return 'HARDWARE_VERSION'
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
+
+
+class SWComponentsClaim(CompositeAttestClaim):
+    """Class representing a PSA Attestation Token Software Components claim"""
+    def get_claim_key(self=None):
+        return ARM_RANGE + 6
+
+    def get_claim_name(self=None):
+        return 'SW_COMPONENTS'
+
+class SWComponentTypeClaim(NonVerifiedClaim):
+    """Class representing a PSA Attestation Token Software Component Measurement Type claim"""
+    def get_claim_key(self=None):
+        return SW_COMPONENT_RANGE + 1
+
+    def get_claim_name(self=None):
+        return 'SW_COMPONENT_TYPE'
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
+
+class ClientIdClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Client ID claim"""
+    def get_claim_key(self=None):
+        return ARM_RANGE + 1
+
+    def get_claim_name(self=None):
+        return 'CLIENT_ID'
+
+    def verify(self, value):
+        self._check_type('CLIENT_ID', value, int)
+        self.verify_count += 1
+
+class SecurityLifecycleClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Security Lifecycle claim"""
+    SL_SHIFT = 12
+
+    SL_NAMES = [
+        'SL_UNKNOWN',
+        'SL_PSA_ROT_PROVISIONING',
+        'SL_SECURED',
+        'SL_NON_PSA_ROT_DEBUG',
+        'SL_RECOVERABLE_PSA_ROT_DEBUG',
+        'SL_PSA_LIFECYCLE_DECOMMISSIONED',
+    ]
+
+    # Security Lifecycle claims
+    SL_UNKNOWN = 0x1000
+    SL_PSA_ROT_PROVISIONING = 0x2000
+    SL_SECURED = 0x3000
+    SL_NON_PSA_ROT_DEBUG = 0x4000
+    SL_RECOVERABLE_PSA_ROT_DEBUG = 0x5000
+    SL_PSA_LIFECYCLE_DECOMMISSIONED = 0x6000
+
+    def get_claim_key(self=None):
+        return ARM_RANGE + 2
+
+    def get_claim_name(self=None):
+        return 'SECURITY_LIFECYCLE'
+
+    def verify(self, value):
+        self._check_type('SECURITY_LIFECYCLE', value, int)
+        self.verify_count += 1
+
+    @staticmethod
+    def parse_raw(raw_value):
+        name_idx = SecurityLifecycleClaim.SL_NAMES.index(raw_value.upper())
+        return (name_idx + 1) << SecurityLifecycleClaim.SL_SHIFT
+
+    @staticmethod
+    def get_formatted_value(value):
+        return SecurityLifecycleClaim.SL_NAMES[(value >> SecurityLifecycleClaim.SL_SHIFT) - 1]
+
+
+class ProfileIdClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Profile Definition claim"""
+    def get_claim_key(self=None):
+        return 265 #EAT profile
+
+    def get_claim_name(self=None):
+        return 'PROFILE_ID'
+
+    def verify(self, value):
+        expected_value = "http://arm.com/psa/2.0.0"
+        self._check_type(self.get_claim_name(), value, str)
+        if value != expected_value:
+            msg = 'Invalid Attest profile "{}": must be "{}"'
+            self.verifier.error(msg.format(value, expected_value))
+        self.verify_count += 1
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
+
+
+class BootSeedClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Boot Seed claim"""
+    def get_claim_key(self=None):
+        return ARM_RANGE + 4
+
+    def get_claim_name(self=None):
+        return 'BOOT_SEED'
+
+    def verify(self, value):
+        self._validate_bytestring_length_is_at_least(value, 'BOOT_SEED', 32)
+        self.verify_count += 1
+
+
+class VerificationServiceClaim(NonVerifiedClaim):
+    """Class representing a PSA Attestation Token Verification Service Indicator claim"""
+    def get_claim_key(self=None):
+        return ARM_RANGE + 7 # originator
+
+    def get_claim_name(self=None):
+        return 'VERIFICATION_SERVICE'
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
+
+
+class SignerIdClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Software Component Signer ID claim"""
+    def get_claim_key(self=None):
+        return SW_COMPONENT_RANGE + 5
+
+    def get_claim_name(self=None):
+        return 'SIGNER_ID'
+
+    def verify(self, value):
+        self._validate_bytestring_length_is_at_least(value, 'SIGNER_ID', 32)
+        self.verify_count += 1
+
+
+class SwComponentVersionClaim(NonVerifiedClaim):
+    """Class representing a PSA Attestation Token Software Component Version claim"""
+    def get_claim_key(self=None):
+        return SW_COMPONENT_RANGE + 4
+
+    def get_claim_name(self=None):
+        return 'SW_COMPONENT_VERSION'
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
+
+
+class MeasurementValueClaim(AttestationClaim):
+    """Class representing a PSA Attestation Token Software Component Measurement value claim"""
+    def get_claim_key(self=None):
+        return SW_COMPONENT_RANGE + 2
+
+    def get_claim_name(self=None):
+        return 'MEASUREMENT_VALUE'
+
+    def verify(self, value):
+        self._validate_bytestring_length_is_at_least(value, 'MEASUREMENT', 32)
+        self.verify_count += 1
+
+
+class MeasurementDescriptionClaim(NonVerifiedClaim):
+    """Class representing a PSA Attestation Token Software Component Measurement description claim"""
+    def get_claim_key(self=None):
+        return SW_COMPONENT_RANGE + 6
+
+    def get_claim_name(self=None):
+        return 'MEASUREMENT_DESCRIPTION'
+
+    @classmethod
+    def is_utf_8(cls):
+        return True
diff --git a/iat-verifier/iatverifier/psa_2_0_0_token_verifier.py b/iat-verifier/iatverifier/psa_2_0_0_token_verifier.py
new file mode 100644
index 0000000..fc8d1b4
--- /dev/null
+++ b/iat-verifier/iatverifier/psa_2_0_0_token_verifier.py
@@ -0,0 +1,81 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+from iatverifier.attest_token_verifier import AttestationTokenVerifier as Verifier
+from iatverifier.attest_token_verifier import AttestationClaim as Claim
+from iatverifier.psa_2_0_0_token_claims import ProfileIdClaim, ClientIdClaim, SecurityLifecycleClaim
+from iatverifier.psa_2_0_0_token_claims import ImplementationIdClaim, BootSeedClaim, CertificationReference
+from iatverifier.psa_2_0_0_token_claims import ChallengeClaim
+from iatverifier.psa_2_0_0_token_claims import InstanceIdClaim, VerificationServiceClaim, SWComponentsClaim
+from iatverifier.psa_2_0_0_token_claims import SWComponentTypeClaim, SwComponentVersionClaim
+from iatverifier.psa_2_0_0_token_claims import MeasurementValueClaim, MeasurementDescriptionClaim, SignerIdClaim
+
+class PSA_2_0_0_TokenVerifier(Verifier):
+    """Verifier class for PSA Attestation Token profile PSA_IOT_PROFILE_1"""
+
+    def get_claim_key(self=None):
+        return 0xb5a101bc  #TODO: some made up claim. Change claim indexing to use name
+                           #      and this should return None
+
+    def get_claim_name(self=None):
+        return 'PSA_IOT_PROFILE1_TOKEN'
+
+    def _get_p_header(self):
+        return {'alg': self._get_cose_alg()}
+
+    def _get_wrapping_tag(self):
+        return None
+
+    def _parse_p_header(self, msg):
+        alg = self._get_cose_alg()
+        try:
+            msg_alg = msg.protected_header['alg']
+        except KeyError:
+            raise ValueError('Missing alg from protected header (expected {})'.format(alg))
+        if alg != msg_alg:
+            raise ValueError('Unexpected alg in protected header (expected {} instead of {})'.format(alg, msg_alg))
+
+    def __init__(self, *, method, cose_alg, signing_key, configuration):
+
+        # First prepare the claim hierarchy for this token
+        sw_component_claims = [
+            (SWComponentTypeClaim, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+            (SwComponentVersionClaim, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+            (MeasurementValueClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (MeasurementDescriptionClaim, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+            (SignerIdClaim, {'verifier': self, 'necessity': Claim.RECOMMENDED}),
+        ]
+
+        verifier_claims = [
+            (ProfileIdClaim, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+            (ClientIdClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (SecurityLifecycleClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (ImplementationIdClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (BootSeedClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (CertificationReference, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+            (SWComponentsClaim, {'verifier': self, 'claims': sw_component_claims, 'is_list': True, 'cross_claim_requirement_checker':None, 'necessity': Claim.MANDATORY}),
+            (ChallengeClaim, {'verifier': self, 'necessity': Claim.MANDATORY}),
+            (InstanceIdClaim, {'verifier': self, 'expected_len': 33, 'necessity': Claim.MANDATORY}),
+            (VerificationServiceClaim, {'verifier': self, 'necessity': Claim.OPTIONAL}),
+        ]
+
+        # initialise the base part of the token
+        super().__init__(
+            claims=verifier_claims,
+            configuration=configuration,
+            necessity=Claim.MANDATORY,
+            method=method,
+            cose_alg=cose_alg,
+            signing_key=signing_key)
+
+        # Set the properties of this token:
+        self.signing_key_set = False # None is a valid value for signing_key
+        self.signing_key = signing_key
+
+    @staticmethod
+    def check_cross_claim_requirements(verifier, claims):
+        pass
diff --git a/iat-verifier/sample/cbor/psa-2_0_0_sign1-token.cbor b/iat-verifier/sample/cbor/psa-2_0_0_sign1-token.cbor
new file mode 100644
index 0000000..fbd6a72
--- /dev/null
+++ b/iat-verifier/sample/cbor/psa-2_0_0_sign1-token.cbor
Binary files differ
diff --git a/iat-verifier/sample/yaml/psa-2_0_0_token.yaml b/iat-verifier/sample/yaml/psa-2_0_0_token.yaml
new file mode 100644
index 0000000..166ab3b
--- /dev/null
+++ b/iat-verifier/sample/yaml/psa-2_0_0_token.yaml
@@ -0,0 +1,29 @@
+boot_seed: !!binary |
+  oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8=
+challenge: !!binary |
+  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+  AAAAAAAAAA==
+client_id: 3002
+hardware_version: 0604565272829-10010
+implementation_id: !!binary |
+  qqqqqqqqqqq7u7u7u7u7u8zMzMzMzMzM3d3d3d3d3d0=
+instance_id: !!binary |
+  AfpYdV9lhifOVGDym3UpZxMkjK562eKYS5AoDvy8tQJI
+profile_id: http://arm.com/psa/2.0.0
+security_lifecycle: SL_SECURED
+sw_components:
+- measurement_description: sha-256
+  measurement_value: !!binary |
+    4z6h4ALS/nlNGhZ521i7aiOo9lm7d/icRYzs+dWZX/0=
+  signer_id: !!binary |
+    v+bYb4gm9P+X+5bE5vvEmT5GGfxWXaJq3zTDKUia3Dg=
+  sw_component_type: SPE
+  sw_component_version: "1.6.0"
+- measurement_description: sha-256
+  measurement_value: !!binary |
+    CH0Txo8yqq+4xPwKIlNEVDIAl2XiFvuFw5jJWAUiwb8=
+  signer_id: !!binary |
+    s2DK9cmMa5QqSIL6nUgj77Fmqe9qbkqjfBkZ7R/MwEk=
+  sw_component_type: NSPE
+  sw_component_version: "0.0.0"
+verification_service: www.trustedfirmware.org
diff --git a/iat-verifier/scripts/check_iat b/iat-verifier/scripts/check_iat
index e9adf48..80a1dfc 100755
--- a/iat-verifier/scripts/check_iat
+++ b/iat-verifier/scripts/check_iat
@@ -13,9 +13,9 @@
 import logging
 import sys
 
-from iatverifier.attest_token_verifier import AttestationClaim as Claim
 from iatverifier.util import recursive_bytes_to_strings, read_keyfile, get_cose_alg_from_key
 from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+from iatverifier.psa_2_0_0_token_verifier import PSA_2_0_0_TokenVerifier
 from iatverifier.attest_token_verifier import VerifierConfiguration, AttestationTokenVerifier
 from iatverifier.cca_token_verifier import CCATokenVerifier, CCAPlatformTokenVerifier
 
@@ -28,6 +28,7 @@
         "PSA-IoT-Profile1-token": PSAIoTProfile1TokenVerifier,
         "CCA-token": CCATokenVerifier,
         "CCA-plat-token": CCAPlatformTokenVerifier,
+        "PSA-2.0.0-token": PSA_2_0_0_TokenVerifier,
     }
 
     parser = argparse.ArgumentParser(
@@ -136,6 +137,14 @@
             signing_key=key,
             configuration=config,
             necessity=None)
+    elif verifier_class == PSA_2_0_0_TokenVerifier:
+        key_checked = args.psa_iot_profile1_keyfile
+        key = read_keyfile(keyfile=args.psa_iot_profile1_keyfile, method=method)
+        if method == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
+            cose_alg = get_cose_alg_from_key(key, AttestationTokenVerifier.COSE_ALG_ES256)
+        else:
+            cose_alg = AttestationTokenVerifier.COSE_ALG_HS256
+        verifier = PSA_2_0_0_TokenVerifier(method=method, cose_alg=cose_alg, signing_key=key, configuration=config)
     else:
         logger.error(f'Invalid token type:{verifier_class}\n\t')
         sys.exit(1)
diff --git a/iat-verifier/scripts/compile_token b/iat-verifier/scripts/compile_token
index 2783cf6..647ed0d 100755
--- a/iat-verifier/scripts/compile_token
+++ b/iat-verifier/scripts/compile_token
@@ -16,6 +16,7 @@
 from iatverifier.util import read_token_map, convert_map_to_token, read_keyfile
 from iatverifier.util import get_cose_alg_from_key
 from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+from iatverifier.psa_2_0_0_token_verifier import PSA_2_0_0_TokenVerifier
 from iatverifier.attest_token_verifier import AttestationTokenVerifier, VerifierConfiguration
 from iatverifier.cca_token_verifier import CCATokenVerifier, CCAPlatformTokenVerifier
 
@@ -26,6 +27,7 @@
         "PSA-IoT-Profile1-token": PSAIoTProfile1TokenVerifier,
         "CCA-token": CCATokenVerifier,
         "CCA-plat-token": CCAPlatformTokenVerifier,
+        "PSA-2.0.0-token": PSA_2_0_0_TokenVerifier,
     }
 
     parser = argparse.ArgumentParser()
@@ -120,6 +122,18 @@
             signing_key=key,
             configuration=configuration,
             necessity=None)
+    elif verifier_class == PSA_2_0_0_TokenVerifier:
+        key_checked = args.psa_iot_profile1_keyfile
+        key = read_keyfile(keyfile=args.psa_iot_profile1_keyfile, method=METHOD)
+        if METHOD == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
+            cose_alg = get_cose_alg_from_key(key, AttestationTokenVerifier.COSE_ALG_ES256)
+        else:
+            cose_alg = AttestationTokenVerifier.COSE_ALG_HS256
+        verifier = PSA_2_0_0_TokenVerifier(
+            method=METHOD,
+            cose_alg=cose_alg,
+            signing_key=key,
+            configuration=configuration)
     else:
         logging.error(f'Invalid token type:{verifier_class}\n\t')
         sys.exit(1)
diff --git a/iat-verifier/scripts/decompile_token b/iat-verifier/scripts/decompile_token
index 58bc9cf..c9b7f37 100755
--- a/iat-verifier/scripts/decompile_token
+++ b/iat-verifier/scripts/decompile_token
@@ -14,6 +14,7 @@
 
 import yaml
 from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+from iatverifier.psa_2_0_0_token_verifier import PSA_2_0_0_TokenVerifier
 from iatverifier.attest_token_verifier import AttestationTokenVerifier
 from iatverifier.cca_token_verifier import CCATokenVerifier, CCAPlatformTokenVerifier
 
@@ -25,6 +26,7 @@
         "PSA-IoT-Profile1-token": PSAIoTProfile1TokenVerifier,
         "CCA-token": CCATokenVerifier,
         "CCA-plat-token": CCAPlatformTokenVerifier,
+        "PSA-2.0.0-token": PSA_2_0_0_TokenVerifier,
     }
 
     parser = argparse.ArgumentParser()
@@ -66,6 +68,12 @@
             signing_key=None,
             configuration=None,
             necessity=None)
+    elif verifier_class == PSA_2_0_0_TokenVerifier:
+        verifier = PSA_2_0_0_TokenVerifier(
+            method=AttestationTokenVerifier.SIGN_METHOD_SIGN1,
+            cose_alg=AttestationTokenVerifier.COSE_ALG_ES256,
+            signing_key=None,
+            configuration=None)
     else:
         logging.error(f'Invalid token type:{verifier_class}\n\t')
         sys.exit(1)