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)