iat-verifier: Import iat-verifier from tf-m repo
Change-Id: I93446f3f2ce06e73958833ae50d9349cf4acd369
Signed-off-by: Mate Toth-Pal <mate.toth-pal@arm.com>
diff --git a/iat-verifier/README.rst b/iat-verifier/README.rst
new file mode 100644
index 0000000..39f3fa0
--- /dev/null
+++ b/iat-verifier/README.rst
@@ -0,0 +1,235 @@
+############################
+Initial Attestation Verifier
+############################
+This is a set of utility scripts for working with PSA Initial Attestation
+Token, the structure of which is described here:
+
+ https://tools.ietf.org/html/draft-tschofenig-rats-psa-token-05
+
+The following utilities are provided:
+
+check_iat
+ Verifies the structure, and optionally the signature, of a token.
+
+compile_token
+ Creates a (optionally, signed) token from a YAML descriptions of the claims.
+
+decompile_token
+ Generates a YAML descriptions of the claims contained within a token. (Note:
+ this description can then be compiled back into a token using compile_token.)
+
+
+************
+Installation
+************
+You can install the script using pip:
+
+.. code:: bash
+
+ # Inside the directory containg this README
+ pip3 install .
+
+This should automatically install all the required dependencies. Please
+see ``setup.py`` for the list of said dependencies.
+
+*****
+Usage
+*****
+
+.. note::
+ You can use ``-h`` flag with any of the scripts to see their usage help.
+
+check_iat
+---------
+
+After installing, you should have ``check_iat`` script in your ``PATH``. The
+script expects a single parameter – a path to the signed IAT in COSE
+format.
+
+You can find an example in the “sample” directory.
+
+The script will extract the COSE payload and make sure that it is a
+valid IAT (i.e. all mandatory fields are present, and all known
+fields have correct size/type):
+
+.. code:: bash
+
+ $ check_iat sample/cbor/iat.cbor
+ Token format OK
+
+If you want the script to verify the signature, you need to specify the
+file containing the signing key in PEM format using -k option. The key
+used to sign sample/iat.cbor is inside sample/key.pem.
+
+::
+
+ $ check_iat -k sample/key.pem sample/cbor/iat.cbor
+ Signature OK
+ Token format OK
+
+You can add a -p flag to the invocation in order to have the script
+print out the decoded IAT in JSON format. It should look something like
+this:
+
+.. code:: json
+
+ {
+ "INSTANCE_ID": "\u0001\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "IMPLEMENTATION_ID": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "CHALLEGE": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "CLIENT_ID": 2,
+ "SECURITY_LIFECYCLE": 2,
+ "VERSION": 1,
+ "BOOT_SEED": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018"
+ "SUBMOD": [
+ {
+ "SUBMOD_NAME": "BL",
+ "SIGNER_ID": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "SUBMOD_VERSION": "3.4.2",
+ "MEASUREMENT": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018"
+ },
+ {
+ "SUBMOD_NAME": "M1",
+ "SIGNER_ID": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "SUBMOD_VERSION": "3.4.2",
+ "MEASUREMENT": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018"
+ },
+ {
+ "SUBMOD_NAME": "M2",
+ "SIGNER_ID": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "SUBMOD_VERSION": "3.4.2",
+ "MEASUREMENT": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018"
+ },
+ {
+ "SUBMOD_NAME": "M3",
+ "SIGNER_ID": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018",
+ "SUBMOD_VERSION": "3.4.2",
+ "MEASUREMENT": "\u0007\u0006\u0005\u0004\u0003\u0002\u0001\u0000\u000f\u000e\r\f\u000b\n\t\b\u0017\u0016\u0015\u0014\u0013\u0012\u0011\u0010\u001f\u001e\u001d\u001c\u001b\u001a\u0019\u0018"
+ }
+ ]
+ }
+
+compile_token
+-------------
+
+You can use this script to compile a YAML claims description into a COSE-wrapped
+CBOR token:
+
+.. code:: bash
+
+ $ compile_token -k sample/key.pem sample/yaml/iat.yaml > sample_token.cbor
+
+*No validation* is performed as part of this, so there is no guarantee that a
+valid IAT will be produced.
+
+You can omit the ``-k`` option, in which case, the resulting token will not be
+signed, however it will still be wrapped in COSE "envelope". If you would like
+to produce a pure CBOR encoding of the claims without a COSE wrapper, you can
+use ``-r`` flag.
+
+
+decompile_token
+---------------
+
+Decompile an IAT (or any COSE-wrapped CBOR object -- *no validation* is performed
+as part of this) into a YAML description of its claims.
+
+
+.. code:: bash
+
+ $decompile_token sample/cbor/iat.cbor
+ boot_seed: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ challenge: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ client_id: 2
+ implementation_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ instance_id: !!binary |
+ AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+ profile_id: http://example.com
+ security_lifecycle: SL_SECURED
+ sw_components:
+ - measurement_description: TF-M_SHA256MemPreXIP
+ measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: BL
+ sw_component_version: 3.4.2
+ - measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M1
+ sw_component_version: 1.2
+ - measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M2
+ sw_component_version: 1.2.3
+ - measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M3
+ sw_component_version: 1
+
+This description can then be compiled back into CBOR using ``compile_token``.
+
+
+***********
+Mac0Message
+***********
+
+By default, the expectation is that the message will be wrapped using
+Sign1Message COSE structure, however, the alternative Mac0Message structure
+that uses HMAC with SHA256 algorithm rather than a signature is supported via
+the ``-m mac`` flag:
+
+::
+
+ $ check_iat -m mac -k sample/hmac.key sample/iat-hmac.cbor
+ Signature OK
+ Token format OK
+
+*******
+Testing
+*******
+Tests can be run using ``nose2``:
+
+.. code:: bash
+
+ pip install nose2
+
+Then run by executing ``nose2`` in the root directory.
+
+
+*******************
+Development Scripts
+*******************
+The following utility scripts are contained within ``dev_scripts``
+subdirectory and were utilized in development of this tool. They are not
+need to use the iat-verifier script, and can generally be ignored.
+
+.. code:: bash
+
+ ./dev_scripts/generate-key.py OUTFILE
+
+Generate an ECDSA (NIST256p curve) signing key and write it in PEM
+format to the specified file.
+
+.. code:: bash
+
+ ./dev_scripts/generate-sample-iat.py KEYFILE OUTFILE
+
+Generate a sample token, signing it with the specified key, and writing
+the output to the specified file.
+
+.. note::
+ This script is deprecated -- use ``compile_token`` (see above) instead.
+
+--------------
+
+*Copyright (c) 2019-2020, Arm Limited. All rights reserved.*
diff --git a/iat-verifier/dev_scripts/generate-key.py b/iat-verifier/dev_scripts/generate-key.py
new file mode 100755
index 0000000..6c1eee7
--- /dev/null
+++ b/iat-verifier/dev_scripts/generate-key.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import sys
+
+from ecdsa import SigningKey, NIST256p
+
+
+if __name__ == '__main__':
+ outfile = sys.argv[1]
+
+ sk = SigningKey.generate(curve=NIST256p)
+ with open(outfile, 'wb') as wfh:
+ wfh.write(sk.to_pem())
diff --git a/iat-verifier/dev_scripts/generate-sample-iat.py b/iat-verifier/dev_scripts/generate-sample-iat.py
new file mode 100755
index 0000000..5b5c56f
--- /dev/null
+++ b/iat-verifier/dev_scripts/generate-sample-iat.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import base64
+import struct
+
+import cbor2
+from ecdsa import SigningKey
+from pycose.sign1message import Sign1Message
+
+from iatverifier.util import sign_eat
+
+from iatverifier.verifiers import InstanceIdClaim, ImplementationIdClaim, ChallengeClaim
+from iatverifier.verifiers import ClientIdClaim, SecurityLifecycleClaim, ProfileIdClaim
+from iatverifier.verifiers import BootSeedClaim, SWComponentsClaim, SWComponentTypeClaim
+from iatverifier.verifiers import SignerIdClaim, SwComponentVersionClaim
+from iatverifier.verifiers import MeasurementValueClaim, MeasurementDescriptionClaim
+
+# First byte indicates "GUID"
+GUID = b'\x01' + struct.pack('QQQQ', 0x0001020304050607, 0x08090A0B0C0D0E0F,
+ 0x1011121314151617, 0x18191A1B1C1D1E1F)
+NONCE = struct.pack('QQQQ', 0X0001020304050607, 0X08090A0B0C0D0E0F,
+ 0X1011121314151617, 0X18191A1B1C1D1E1F)
+ORIGIN = struct.pack('QQQQ', 0X0001020304050607, 0X08090A0B0C0D0E0F,
+ 0X1011121314151617, 0X18191A1B1C1D1E1F)
+BOOT_SEED = struct.pack('QQQQ', 0X0001020304050607, 0X08090A0B0C0D0E0F,
+ 0X1011121314151617, 0X18191A1B1C1D1E1F)
+SIGNER_ID = struct.pack('QQQQ', 0X0001020304050607, 0X08090A0B0C0D0E0F,
+ 0X1011121314151617, 0X18191A1B1C1D1E1F)
+MEASUREMENT = struct.pack('QQQQ', 0X0001020304050607, 0X08090A0B0C0D0E0F,
+ 0X1011121314151617, 0X18191A1B1C1D1E1F)
+
+token_map = {
+ InstanceIdClaim.get_claim_key(): GUID,
+ ImplementationIdClaim.get_claim_key(): ORIGIN,
+ ChallengeClaim.get_claim_key(): NONCE,
+ ClientIdClaim.get_claim_key(): 2,
+ SecurityLifecycleClaim.get_claim_key(): SecurityLifecycleClaim.SL_SECURED,
+ ProfileIdClaim.get_claim_key(): 'http://example.com',
+ BootSeedClaim.get_claim_key(): BOOT_SEED,
+ SWComponentsClaim.get_claim_key(): [
+ {
+ # bootloader
+ SWComponentTypeClaim.get_claim_key(): 'BL',
+ SignerIdClaim.get_claim_key(): SIGNER_ID,
+ SwComponentVersionClaim.get_claim_key(): '3.4.2',
+ MeasurementValueClaim.get_claim_key(): MEASUREMENT,
+ MeasurementDescriptionClaim.get_claim_key(): 'TF-M_SHA256MemPreXIP',
+ },
+ {
+ # mod1
+ SWComponentTypeClaim.get_claim_key(): 'M1',
+ SignerIdClaim.get_claim_key(): SIGNER_ID,
+ SwComponentVersionClaim.get_claim_key(): '3.4.2',
+ MeasurementValueClaim.get_claim_key(): MEASUREMENT,
+ },
+ {
+ # mod2
+ SWComponentTypeClaim.get_claim_key(): 'M2',
+ SignerIdClaim.get_claim_key(): SIGNER_ID,
+ SwComponentVersionClaim.get_claim_key(): '3.4.2',
+ MeasurementValueClaim.get_claim_key(): MEASUREMENT,
+ },
+ {
+ # mod3
+ SWComponentTypeClaim.get_claim_key(): 'M3',
+ SignerIdClaim.get_claim_key(): SIGNER_ID,
+ SwComponentVersionClaim.get_claim_key(): '3.4.2',
+ MeasurementValueClaim.get_claim_key(): MEASUREMENT,
+ },
+ ],
+}
+
+
+if __name__ == '__main__':
+ import sys
+ if len(sys.argv) != 3:
+ print('Usage: {} KEYFILE OUTFILE'.format(sys.argv[0]))
+ sys.exit(1)
+ keyfile = sys.argv[1]
+ outfile = sys.argv[2]
+
+ sk = SigningKey.from_pem(open(keyfile, 'rb').read())
+ token = cbor2.dumps(token_map)
+ signed_token = sign_eat(token, sk)
+
+ with open(outfile, 'wb') as wfh:
+ wfh.write(signed_token)
diff --git a/iat-verifier/iatverifier/__init__.py b/iat-verifier/iatverifier/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/iat-verifier/iatverifier/__init__.py
diff --git a/iat-verifier/iatverifier/psa_iot_profile1_token_verifier.py b/iat-verifier/iatverifier/psa_iot_profile1_token_verifier.py
new file mode 100644
index 0000000..10c339a
--- /dev/null
+++ b/iat-verifier/iatverifier/psa_iot_profile1_token_verifier.py
@@ -0,0 +1,46 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+from iatverifier.verifiers import AttestationTokenVerifier as Verifier
+from iatverifier.verifiers import AttestationClaim as Claim
+from iatverifier.verifiers import ProfileIdClaim, ClientIdClaim, SecurityLifecycleClaim
+from iatverifier.verifiers import ImplementationIdClaim, BootSeedClaim, HardwareVersionClaim
+from iatverifier.verifiers import NoMeasurementsClaim, ChallengeClaim
+from iatverifier.verifiers import InstanceIdClaim, OriginatorClaim, SWComponentsClaim
+from iatverifier.verifiers import SWComponentTypeClaim, SwComponentVersionClaim
+from iatverifier.verifiers import MeasurementValueClaim, MeasurementDescriptionClaim, SignerIdClaim
+
+class PSAIoTProfile1TokenVerifier(Verifier):
+ @staticmethod
+ def get_verifier(configuration=None):
+ verifier = PSAIoTProfile1TokenVerifier(
+ method=Verifier.SIGN_METHOD_SIGN1,
+ cose_alg=Verifier.COSE_ALG_ES256,
+ configuration=configuration)
+
+ sw_component_claims = [
+ (SWComponentTypeClaim, (Claim.OPTIONAL, )),
+ (SwComponentVersionClaim, (Claim.OPTIONAL, )),
+ (MeasurementValueClaim, (Claim.MANDATORY, )),
+ (MeasurementDescriptionClaim, (Claim.OPTIONAL, )),
+ (SignerIdClaim, (Claim.RECOMMENDED, )),
+ ]
+
+ verifier.add_claims([
+ ProfileIdClaim(verifier, Claim.OPTIONAL),
+ ClientIdClaim(verifier, Claim.MANDATORY),
+ SecurityLifecycleClaim(verifier, Claim.MANDATORY),
+ ImplementationIdClaim(verifier, Claim.MANDATORY),
+ BootSeedClaim(verifier, Claim.MANDATORY),
+ HardwareVersionClaim(verifier, Claim.OPTIONAL),
+ SWComponentsClaim(verifier, sw_component_claims, Claim.OPTIONAL),
+ NoMeasurementsClaim(verifier, Claim.OPTIONAL),
+ ChallengeClaim(verifier, Claim.MANDATORY),
+ InstanceIdClaim(verifier, 33, Claim.MANDATORY),
+ OriginatorClaim(verifier, Claim.OPTIONAL),
+ ])
+ return verifier
diff --git a/iat-verifier/iatverifier/util.py b/iat-verifier/iatverifier/util.py
new file mode 100644
index 0000000..aa4e0ba
--- /dev/null
+++ b/iat-verifier/iatverifier/util.py
@@ -0,0 +1,247 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+from collections.abc import Iterable
+from copy import deepcopy
+import logging
+
+import base64
+import cbor2
+import yaml
+from ecdsa import SigningKey, VerifyingKey
+from pycose.sign1message import Sign1Message
+from pycose.mac0message import Mac0Message
+from iatverifier.verifiers import AttestationTokenVerifier
+from cbor2 import CBORTag
+
+_logger = logging.getLogger("util")
+
+def sign_eat(token, key=None):
+ signed_msg = Sign1Message()
+ signed_msg.payload = token
+ if key:
+ signed_msg.key = key
+ signed_msg.signature = signed_msg.compute_signature()
+ return signed_msg.encode()
+
+
+def hmac_eat(token, verifier, key=None):
+ hmac_msg = Mac0Message(payload=token, key=key)
+ hmac_msg.compute_auth_tag(verifier.cose_alg)
+ return hmac_msg.encode()
+
+
+def convert_map_to_token_files(mapfile, keyfile, verifier, outfile):
+ token_map = read_token_map(mapfile)
+
+ if verifier.method == 'sign':
+ with open(keyfile) as fh:
+ signing_key = SigningKey.from_pem(fh.read())
+ else:
+ with open(keyfile, 'rb') as fh:
+ signing_key = fh.read()
+
+ with open(outfile, 'wb') as wfh:
+ convert_map_to_token(token_map, signing_key, verifier, wfh)
+
+
+def convert_map_to_token(token_map, signing_key, verifier, wfh):
+ wrapping_tag = verifier.get_wrapping_tag()
+ if wrapping_tag is not None:
+ token = cbor2.dumps(CBORTag(wrapping_tag, token_map))
+ else:
+ token = cbor2.dumps(token_map)
+
+ if verifier.method == AttestationTokenVerifier.SIGN_METHOD_RAW:
+ signed_token = token
+ elif verifier.method == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
+ signed_token = sign_eat(token, signing_key)
+ elif verifier.method == AttestationTokenVerifier.SIGN_METHOD_MAC0:
+ signed_token = hmac_eat(token, verifier, signing_key)
+ else:
+ err_msg = 'Unexpected method "{}"; must be one of: raw, sign, mac'
+ raise ValueError(err_msg.format(method))
+
+ wfh.write(signed_token)
+
+
+def convert_token_to_map(raw_data, verifier):
+ payload = get_cose_payload(raw_data, verifier)
+ token_map = cbor2.loads(payload)
+ return _relabel_keys(token_map)
+
+
+def read_token_map(f):
+ if hasattr(f, 'read'):
+ raw = yaml.safe_load(f)
+ else:
+ with open(f) as fh:
+ raw = yaml.safe_load(fh)
+
+ return _parse_raw_token(raw)
+
+
+def extract_iat_from_cose(keyfile, tokenfile, verifier):
+ key = read_keyfile(keyfile, verifier.method)
+
+ try:
+ with open(tokenfile, 'rb') as wfh:
+ return get_cose_payload(wfh.read(), verifier, key)
+ except Exception as e:
+ msg = 'Bad COSE file "{}": {}'
+ raise ValueError(msg.format(tokenfile, e))
+
+
+def get_cose_payload(cose, verifier, key=None):
+ if verifier.method == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
+ return get_cose_sign1_payload(cose, verifier, key)
+ if verifier.method == AttestationTokenVerifier.SIGN_METHOD_MAC0:
+ return get_cose_mac0_pyload(cose, verifier, key)
+ err_msg = 'Unexpected method "{}"; must be one of: sign, mac'
+ raise ValueError(err_msg.format(method))
+
+
+def get_cose_sign1_payload(cose, verifier, key=None):
+ msg = Sign1Message.decode(cose)
+ if key:
+ msg.key = key
+ msg.signature = msg.signers
+ try:
+ msg.verify_signature(alg=verifier.cose_alg)
+ except Exception as e:
+ raise ValueError('Bad signature ({})'.format(e))
+ return msg.payload
+
+
+def get_cose_mac0_pyload(cose, verifier, key=None):
+ msg = Mac0Message.decode(cose)
+ if key:
+ msg.key = key
+ try:
+ msg.verify_auth_tag(alg=verifier.cose_alg)
+ except Exception as e:
+ raise ValueError('Bad signature ({})'.format(e))
+ return msg.payload
+
+def recursive_bytes_to_strings(d, in_place=False):
+ if in_place:
+ result = d
+ else:
+ result = deepcopy(d)
+
+ if hasattr(result, 'items'):
+ for k, v in result.items():
+ result[k] = recursive_bytes_to_strings(v, in_place=True)
+ elif (isinstance(result, Iterable) and
+ not isinstance(result, (str, bytes))):
+ result = [recursive_bytes_to_strings(r, in_place=True)
+ for r in result]
+ elif isinstance(result, bytes):
+ result = str(base64.b16encode(result))
+
+ return result
+
+
+def read_keyfile(keyfile, method=AttestationTokenVerifier.SIGN_METHOD_SIGN1):
+ if keyfile:
+ if method == AttestationTokenVerifier.SIGN_METHOD_SIGN1:
+ return read_sign1_key(keyfile)
+ if method == AttestationTokenVerifier.SIGN_METHOD_MAC0:
+ return read_hmac_key(keyfile)
+ err_msg = 'Unexpected method "{}"; must be one of: sign, mac'
+ raise ValueError(err_msg.format(method))
+
+ return None
+
+
+def read_sign1_key(keyfile):
+ try:
+ key = SigningKey.from_pem(open(keyfile, 'rb').read())
+ except Exception as e:
+ signing_key_error = str(e)
+
+ try:
+ key = VerifyingKey.from_pem(open(keyfile, 'rb').read())
+ except Exception as e:
+ verifying_key_error = str(e)
+
+ msg = 'Bad key file "{}":\n\tpubkey error: {}\n\tprikey error: {}'
+ raise ValueError(msg.format(keyfile, verifying_key_error, signing_key_error))
+ return key
+
+
+def read_hmac_key(keyfile):
+ return open(keyfile, 'rb').read()
+
+def _get_known_claims():
+ if logging.DEBUG >= logging.root.level:
+ _logger.debug("Known claims are:")
+ for _, claim_class in AttestationTokenVerifier.all_known_claims.items():
+ _logger.debug(f" {claim_class.get_claim_key():8} '{claim_class.get_claim_name()}'")
+ for _, claim_class in AttestationTokenVerifier.all_known_claims.items():
+ yield claim_class
+
+def _parse_raw_token(raw):
+ result = {}
+ field_names = {cc.get_claim_name(): cc for cc in _get_known_claims()}
+ for raw_key, raw_value in raw.items():
+ if isinstance(raw_key, int):
+ key = raw_key
+ else:
+ field_name = raw_key.upper()
+ try:
+ claim_class = field_names[field_name]
+ key = claim_class.get_claim_key()
+ except KeyError:
+ msg = 'Unknown field "{}" in token.'.format(field_name)
+ raise ValueError(msg)
+
+ if hasattr(raw_value, 'items'):
+ value = _parse_raw_token(raw_value)
+ elif (isinstance(raw_value, Iterable) and
+ not isinstance(raw_value, (str, bytes))):
+ # TODO -- asumes dict elements
+ value = [_parse_raw_token(v) for v in raw_value]
+ else:
+ value = claim_class.parse_raw(raw_value)
+
+ result[key] = value
+
+ return result
+
+def _format_value(names, key, value):
+ if key in names:
+ value = names[key].get_formatted_value(value)
+ return value
+
+def _relabel_keys(token_map):
+ result = {}
+ while not hasattr(token_map, 'items'):
+ # TODO: token map is not a map. We are assuming that it is a tag
+ token_map = token_map.value
+ names = {v.get_claim_key(): v for v in _get_known_claims()}
+ for key, value in token_map.items():
+ if hasattr(value, 'items'):
+ value = _relabel_keys(value)
+ elif (isinstance(value, Iterable) and
+ not isinstance(value, (str, bytes))):
+ new_value = []
+ for item in value:
+ if hasattr(item, 'items'):
+ new_value.append(_relabel_keys(item))
+ else:
+ new_value.append(_format_value(names, key, item))
+ value = new_value
+ else:
+ value = _format_value(names, key, value)
+
+ if key in names:
+ new_key = names[key].get_claim_name().lower()
+ else:
+ new_key = key
+ result[new_key] = value
+ return result
diff --git a/iat-verifier/iatverifier/verifiers.py b/iat-verifier/iatverifier/verifiers.py
new file mode 100644
index 0000000..a68cf8b
--- /dev/null
+++ b/iat-verifier/iatverifier/verifiers.py
@@ -0,0 +1,566 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import logging
+import string
+
+import cbor2
+
+logger = logging.getLogger('iat-verifiers')
+
+# IAT custom claims
+ARM_RANGE = -75000
+
+# SW component IDs
+SW_COMPONENT_RANGE = 0
+
+class AttestationClaim:
+ MANDATORY = 0
+ RECOMMENDED = 1
+ OPTIONAL = 2
+
+ def __init__(self, verifier, necessity=MANDATORY):
+ self.config = verifier.config
+ self.verifier = verifier
+ self.necessity = necessity
+ self.verify_count = 0
+
+ def verify(self, value):
+ raise NotImplementedError
+
+ def get_claim_key(self=None):
+ raise NotImplementedError
+
+ def get_claim_name(self=None):
+ raise NotImplementedError
+
+ def get_contained_claim_key_list(self):
+ return {}
+
+ def decode(self, value):
+ if self.is_utf_8():
+ try:
+ return value.decode()
+ except UnicodeDecodeError as e:
+ msg = 'Error decodeing value for "{}": {}'
+ self.verifier.error(msg.format(self.get_claim_name(), e))
+ return str(value)[2:-1]
+ else: # not a UTF-8 value, i.e. a bytestring
+ return value
+
+ def add_tokens_to_dict(self, token, value):
+ entry_name = self.get_claim_name()
+ if isinstance(value, bytes):
+ value = self.decode(value)
+ token[entry_name] = value
+
+ def claim_found(self):
+ return self.verify_count>0
+
+ def _check_type(self, name, value, expected_type):
+ if not isinstance(value, expected_type):
+ msg = 'Invalid {}: must be a(n) {}: found {}'
+ self.verifier.error(msg.format(name, expected_type, type(value)))
+ return False
+ return True
+
+ def _validate_bytestring_length_equals(self, value, name, expected_len):
+ self._check_type(name, value, bytes)
+
+ value_len = len(value)
+ if value_len != expected_len:
+ msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
+ self.verifier.error(msg.format(name, expected_len, value_len))
+
+ def _validate_bytestring_length_is_at_least(self, value, name, minimal_length):
+ self._check_type(name, value, bytes)
+
+ value_len = len(value)
+ if value_len < minimal_length:
+ msg = 'Invalid {} length: must be at least {} bytes, found {} bytes'
+ self.verifier.error(msg.format(name, minimal_length, value_len))
+
+ @staticmethod
+ def parse_raw(raw_value):
+ return raw_value
+
+ @staticmethod
+ def get_formatted_value(value):
+ return value
+
+ def is_utf_8(self):
+ return False
+
+ def check_cross_claim_requirements(self):
+ pass
+
+
+# ----------------------------------------------------------------------------
+# Validation classes
+#
+class InstanceIdClaim(AttestationClaim):
+ def __init__(self, verifier, expected_len, necessity=AttestationClaim.MANDATORY):
+ super().__init__(verifier, necessity)
+ self.expected_len = expected_len
+
+ def get_claim_key(self=None):
+ return ARM_RANGE - 9 # UEID
+
+ 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):
+
+ HASH_SIZES = [32, 48, 64]
+
+ def get_claim_key(self=None):
+ return ARM_RANGE - 8 # nonce
+
+ 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 NonVerifiedClaim(AttestationClaim):
+ def verify(self, value):
+ self.verify_count += 1
+
+ def get_claim_key(self=None):
+ raise NotImplementedError
+
+ def get_claim_name(self=None):
+ raise NotImplementedError
+
+
+class ImplementationIdClaim(NonVerifiedClaim):
+ def get_claim_key(self=None):
+ return ARM_RANGE - 3
+
+ def get_claim_name(self=None):
+ return 'IMPLEMENTATION_ID'
+
+
+class HardwareVersionClaim(AttestationClaim):
+ def verify(self, value):
+ self._check_type('HARDWARE_VERSION', value, str)
+
+ value_len = len(value)
+ expected_len = 13
+ if len(value) != expected_len:
+ msg = 'Invalid HARDWARE_VERSION length; must be {} digits, found {} characters'
+ self.verifier.error(msg.format(expected_len, value_len))
+ for idx, character in enumerate(value):
+ if character not in string.digits:
+ msg = 'Invalid digit {} 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'
+
+ def is_utf_8(self):
+ return True
+
+
+class OriginatorClaim(NonVerifiedClaim):
+ def get_claim_key(self=None):
+ return ARM_RANGE - 10 # originator
+
+ def get_claim_name(self=None):
+ return 'ORIGINATOR'
+
+ def is_utf_8(self):
+ return True
+
+
+
+class SWComponentsClaim(AttestationClaim):
+
+ def __init__(self, verifier, claims, necessity=AttestationClaim.MANDATORY):
+ super().__init__(verifier, necessity)
+ self.claims = claims
+
+
+ def get_claim_key(self=None):
+ return ARM_RANGE - 6
+
+ def get_claim_name(self=None):
+ return 'SW_COMPONENTS'
+
+ def get_sw_component_claims(self):
+ return [claim(self.verifier, *args) for claim, args in self.claims]
+
+ def get_contained_claim_key_list(self):
+ ret = {}
+ for claim in self.get_sw_component_claims():
+ ret[claim.get_claim_key()] = claim.__class__
+ return ret
+
+ def verify(self, value):
+ if not self._check_type('SW_COMPONENTS', value, list):
+ return
+
+ for entry_number, sw_component in enumerate(value):
+ if not self._check_type('SW_COMPONENTS', sw_component, dict):
+ return
+
+ claims = {v.get_claim_key(): v for v in self.get_sw_component_claims()}
+ for k, v in sw_component.items():
+ if k not in claims.keys():
+ if self.config.strict:
+ msg = 'Unexpected SW_COMPONENT claim: {}'
+ self.verifier.error(msg.format(k))
+ else:
+ continue
+ try:
+ claims[k].verify(v)
+ except Exception:
+ if not self.config.keep_going:
+ raise
+
+ # Check claims' necessity
+ for claim in claims.values():
+ if not claim.claim_found():
+ if claim.necessity==AttestationClaim.MANDATORY:
+ msg = ('Invalid IAT: missing MANDATORY claim "{}" '
+ 'from sw_component at index {}')
+ self.verifier.error(msg.format(claim.get_claim_name(),
+ entry_number))
+ elif claim.necessity==AttestationClaim.RECOMMENDED:
+ msg = ('Missing RECOMMENDED claim "{}" '
+ 'from sw_component at index {}')
+ self.verifier.warning(msg.format(claim.get_claim_name(),
+ entry_number))
+
+ self.verify_count += 1
+
+ def decode_sw_component(self, raw_sw_component):
+ sw_component = {}
+ names = {claim.get_claim_key(): claim.get_claim_name() for claim in self.get_sw_component_claims()}
+ for k, v in raw_sw_component.items():
+ if isinstance(v, bytes):
+ v = self.decode(v)
+ try:
+ sw_component[names[k]] = v
+ except KeyError:
+ if self.config.strict:
+ if not self.config.keep_going:
+ raise
+ else:
+ sw_component[k] = v
+ return sw_component
+
+ def add_tokens_to_dict(self, token, value):
+ entry_name = self.get_claim_name()
+ try:
+ token[entry_name] = []
+ for raw_sw_component in value:
+ decoded_component = self.decode_sw_component(raw_sw_component)
+ token[entry_name].append(decoded_component)
+ except TypeError:
+ self.verifier.error('Invalid SW_COMPONENT value: {}'.format(value))
+
+class SWComponentTypeClaim(NonVerifiedClaim):
+ def get_claim_key(self=None):
+ return SW_COMPONENT_RANGE + 1
+
+ def get_claim_name(self=None):
+ return 'SW_COMPONENT_TYPE'
+
+ def is_utf_8(self):
+ return True
+
+
+class NoMeasurementsClaim(NonVerifiedClaim):
+ def get_claim_key(self=None):
+ return ARM_RANGE - 7
+
+ def get_claim_name(self=None):
+ return 'NO_MEASUREMENTS'
+
+
+class ClientIdClaim(AttestationClaim):
+ 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):
+
+ 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
+
+ def add_tokens_to_dict(self, token, value):
+ entry_name = self.get_claim_name()
+ try:
+ name_idx = (value >> SecurityLifecycleClaim.SL_SHIFT) - 1
+ token[entry_name] = SecurityLifecycleClaim.SL_NAMES[name_idx]
+ except IndexError:
+ token[entry_name] = 'CUSTOM({})'.format(value)
+
+ @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):
+ def get_claim_key(self=None):
+ return ARM_RANGE
+
+ def get_claim_name(self=None):
+ return 'PROFILE_ID'
+
+ def verify(self, value):
+ self._check_type('PROFILE_ID', value, str)
+ self.verify_count += 1
+
+ def is_utf_8(self):
+ return True
+
+
+class BootSeedClaim(AttestationClaim):
+ 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):
+ def get_claim_key(self=None):
+ return ARM_RANGE - 10
+
+ def get_claim_name(self=None):
+ return 'VERIFICATION_SERVICE'
+
+ def is_utf_8(self):
+ return True
+
+
+class SignerIdClaim(AttestationClaim):
+ 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):
+ def get_claim_key(self=None):
+ return SW_COMPONENT_RANGE + 4
+
+ def get_claim_name(self=None):
+ return 'SW_COMPONENT_VERSION'
+
+ def is_utf_8(self):
+ return True
+
+
+class MeasurementValueClaim(AttestationClaim):
+ 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):
+ def get_claim_key(self=None):
+ return SW_COMPONENT_RANGE + 6
+
+ def get_claim_name(self=None):
+ return 'MEASUREMENT_DESCRIPTION'
+
+ def is_utf_8(self):
+ return True
+
+
+# ----------------------------------------------------------------------------
+
+class VerifierConfiguration:
+ def __init__(self, keep_going=False, strict=False):
+ self.keep_going=keep_going
+ self.strict=strict
+
+class AttestationTokenVerifier:
+
+ all_known_claims = {}
+
+ SIGN_METHOD_SIGN1 = "sign"
+ SIGN_METHOD_MAC0 = "mac"
+ SIGN_METHOD_RAW = "raw"
+
+ COSE_ALG_ES256="ES256"
+ COSE_ALG_ES384="ES384"
+ COSE_ALG_ES512="ES512"
+ COSE_ALG_HS256_64="HS256/64"
+ COSE_ALG_HS256="HS256"
+ COSE_ALG_HS384="HS384"
+ COSE_ALG_HS512="HS512"
+
+ def __init__(self, method, cose_alg, configuration=None):
+ self.method = method
+ self.cose_alg = cose_alg
+ self.config = configuration if configuration is not None else VerifierConfiguration()
+ self.claims = []
+
+ self.seen_errors = False
+
+ def add_claims(self, claims):
+ for claim in claims:
+ key = claim.get_claim_key()
+ if key not in AttestationTokenVerifier.all_known_claims:
+ AttestationTokenVerifier.all_known_claims[key] = claim.__class__
+
+ AttestationTokenVerifier.all_known_claims.update(claim.get_contained_claim_key_list())
+ self.claims.extend(claims)
+
+ def check_cross_claim_requirements(self):
+ claims = {v.get_claim_key(): v for v in self.claims}
+
+ if SWComponentsClaim.get_claim_key() in claims:
+ sw_component_present = claims[SWComponentsClaim.get_claim_key()].verify_count > 0
+ else:
+ sw_component_present = False
+
+ if NoMeasurementsClaim.get_claim_key() in claims:
+ no_measurement_present = claims[NoMeasurementsClaim.get_claim_key()].verify_count > 0
+ else:
+ no_measurement_present = False
+
+ if not sw_component_present and not no_measurement_present:
+ self.error('Invalid IAT: no software measurements defined and '
+ 'NO_MEASUREMENTS claim is not present.')
+
+ def decode_and_validate_iat(self, encoded_iat):
+ try:
+ raw_token = cbor2.loads(encoded_iat)
+ except Exception as e:
+ msg = 'Invalid CBOR: {}'
+ raise ValueError(msg.format(e))
+
+ claims = {v.get_claim_key(): v for v in self.claims}
+
+ token = {}
+ while not hasattr(raw_token, 'items'):
+ # TODO: token map is not a map. We are assuming that it is a tag
+ raw_token = raw_token.value
+ for entry in raw_token.keys():
+ value = raw_token[entry]
+
+ try:
+ claim = claims[entry]
+ except KeyError:
+ if self.config.strict:
+ self.error('Invalid IAT claim: {}'.format(entry))
+ token[entry] = value
+ continue
+
+ claim.verify(value)
+ claim.add_tokens_to_dict(token, value)
+
+ # Check claims' necessity
+ for claim in claims.values():
+ if not claim.claim_found():
+ if claim.necessity==AttestationClaim.MANDATORY:
+ msg = 'Invalid IAT: missing MANDATORY claim "{}"'
+ self.error(msg.format(claim.get_claim_name()))
+ elif claim.necessity==AttestationClaim.RECOMMENDED:
+ msg = 'Missing RECOMMENDED claim "{}"'
+ self.warning(msg.format(claim.get_claim_name()))
+
+ claim.check_cross_claim_requirements()
+
+ self.check_cross_claim_requirements()
+
+ return token
+
+
+ def get_wrapping_tag(self=None):
+ """The value of the tag that the token is wrapped in.
+
+ The function should return None if the token is not wrapped.
+ """
+ return None
+
+ def error(self, message):
+ self.seen_errors = True
+ if self.config.keep_going:
+ logger.error(message)
+ else:
+ raise ValueError(message)
+
+ def warning(self, message):
+ logger.warning(message)
diff --git a/iat-verifier/iatverifier/verify.py b/iat-verifier/iatverifier/verify.py
new file mode 100644
index 0000000..eab1ea6
--- /dev/null
+++ b/iat-verifier/iatverifier/verify.py
@@ -0,0 +1,82 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import argparse
+import json
+import logging
+import sys
+
+from iatverifier.util import extract_iat_from_cose, recursive_bytes_to_strings
+from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+from iatverifier.util import recursive_bytes_to_strings
+from iatverifier.verifiers import VerifierConfiguration, AttestationTokenVerifier
+
+logger = logging.getLogger('iat-verify')
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='''
+ Validates a signed Initial Attestation Token (IAT), checking
+ that the signature is valid, the token contian the required
+ fields, and those fields are in a valid format.
+ ''')
+ parser.add_argument('-k', '--keyfile',
+ help='''
+ Path to a file containing signing key in PEM format.
+ ''')
+ parser.add_argument('tokenfile',
+ help='''
+ path to a file containing a signed IAT.
+ ''')
+ parser.add_argument('-K', '--keep-going', action='store_true',
+ help='''
+ Do not stop upon encountering a validation error.
+ ''')
+ parser.add_argument('-p', '--print-iat', action='store_true',
+ help='''
+ Print the decoded token in JSON format.
+ ''')
+ parser.add_argument('-s', '--strict', action='store_true',
+ help='''
+ Report failure if unknown claim is encountered.
+ ''')
+ parser.add_argument('-m', '--method', choices=['sign', 'mac'], default='sign',
+ help='''
+ Specify how this token is wrapped -- whether Sign1Message or
+ Mac0Message COSE structure is used.
+ ''')
+ args = parser.parse_args()
+
+ logging.basicConfig(level=logging.INFO)
+
+ config = VerifierConfiguration(keep_going=args.keep_going, strict=args.strict)
+ verifier = PSAIoTProfile1TokenVerifier.get_verifier(config)
+ if args.method == 'mac':
+ verifier.method = AttestationTokenVerifier.SIGN_METHOD_MAC0
+ verifier.cose_alg = AttestationTokenVerifier.COSE_ALG_HS256
+
+ try:
+ raw_iat = extract_iat_from_cose(args.keyfile, args.tokenfile, verifier)
+ if args.keyfile:
+ print('Signature OK')
+ except ValueError as e:
+ logger.error('Could not extract IAT from COSE:\n\t{}'.format(e))
+ sys.exit(1)
+
+ try:
+ token = verifier.decode_and_validate_iat(raw_iat)
+ if not verifier.seen_errors:
+ print('Token format OK')
+ except ValueError as e:
+ logger.error('Could not validate IAT:\n\t{}'.format(e))
+ sys.exit(1)
+
+ if args.print_iat:
+ print('Token:')
+ json.dump(recursive_bytes_to_strings(token, in_place=True),
+ sys.stdout, indent=4)
+ print('')
diff --git a/iat-verifier/sample/cbor/badsig.cbor b/iat-verifier/sample/cbor/badsig.cbor
new file mode 100644
index 0000000..a38dead
--- /dev/null
+++ b/iat-verifier/sample/cbor/badsig.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/iat.cbor b/iat-verifier/sample/cbor/iat.cbor
new file mode 100644
index 0000000..38d2ec4
--- /dev/null
+++ b/iat-verifier/sample/cbor/iat.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/invalid-profile-id.cbor b/iat-verifier/sample/cbor/invalid-profile-id.cbor
new file mode 100644
index 0000000..a8fcbcb
--- /dev/null
+++ b/iat-verifier/sample/cbor/invalid-profile-id.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/malformed.cbor b/iat-verifier/sample/cbor/malformed.cbor
new file mode 100644
index 0000000..ac144ed
--- /dev/null
+++ b/iat-verifier/sample/cbor/malformed.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/missing-claim.cbor b/iat-verifier/sample/cbor/missing-claim.cbor
new file mode 100644
index 0000000..8374469
--- /dev/null
+++ b/iat-verifier/sample/cbor/missing-claim.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/missing-sw-comps.cbor b/iat-verifier/sample/cbor/missing-sw-comps.cbor
new file mode 100644
index 0000000..67f797b
--- /dev/null
+++ b/iat-verifier/sample/cbor/missing-sw-comps.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/no-sw-measurements.cbor b/iat-verifier/sample/cbor/no-sw-measurements.cbor
new file mode 100644
index 0000000..b66f4b0
--- /dev/null
+++ b/iat-verifier/sample/cbor/no-sw-measurements.cbor
Binary files differ
diff --git a/iat-verifier/sample/cbor/submod-missing-claim.cbor b/iat-verifier/sample/cbor/submod-missing-claim.cbor
new file mode 100644
index 0000000..6da7dd5
--- /dev/null
+++ b/iat-verifier/sample/cbor/submod-missing-claim.cbor
Binary files differ
diff --git a/iat-verifier/sample/hmac.key b/iat-verifier/sample/hmac.key
new file mode 100644
index 0000000..e9569f1
--- /dev/null
+++ b/iat-verifier/sample/hmac.key
Binary files differ
diff --git a/iat-verifier/sample/iat-hmac.cbor b/iat-verifier/sample/iat-hmac.cbor
new file mode 100644
index 0000000..1ea3018
--- /dev/null
+++ b/iat-verifier/sample/iat-hmac.cbor
Binary files differ
diff --git a/iat-verifier/sample/key.pem b/iat-verifier/sample/key.pem
new file mode 100644
index 0000000..df1e6ec
--- /dev/null
+++ b/iat-verifier/sample/key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIEP//suV+AhafEDh0+p5C+9Ot4zdd9WFA6ZMFgD5GzPnoAoGCCqGSM49
+AwEHoUQDQgAETl4iCZ47zrRbRG0TVf0dw7VFlHtv18HInYhnmMNybo+A1wuECyVq
+rDSmLt4QQzZPBECV8ANHS5HgGCCSr7E/Lg==
+-----END EC PRIVATE KEY-----
diff --git a/iat-verifier/sample/key.pub.pem b/iat-verifier/sample/key.pub.pem
new file mode 100644
index 0000000..924db30
--- /dev/null
+++ b/iat-verifier/sample/key.pub.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETl4iCZ47zrRbRG0TVf0dw7VFlHtv
+18HInYhnmMNybo+A1wuECyVqrDSmLt4QQzZPBECV8ANHS5HgGCCSr7E/Lg==
+-----END PUBLIC KEY-----
diff --git a/iat-verifier/sample/yaml/iat.yaml b/iat-verifier/sample/yaml/iat.yaml
new file mode 100644
index 0000000..c71e962
--- /dev/null
+++ b/iat-verifier/sample/yaml/iat.yaml
@@ -0,0 +1,37 @@
+boot_seed: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+implementation_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+instance_id: !!binary |
+ AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+profile_id: http://example.com
+security_lifecycle: SL_SECURED
+sw_components:
+- measurement_description: TF-M_SHA256MemPreXIP
+ measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: BL
+ sw_component_version: 3.4.2
+- measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M1
+ sw_component_version: 1.2
+- measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M2
+ sw_component_version: 1.2.3
+- measurement_value: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ signer_id: !!binary |
+ BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_type: M3
+ sw_component_version: 1
diff --git a/iat-verifier/scripts/check_iat b/iat-verifier/scripts/check_iat
new file mode 100755
index 0000000..d12275f
--- /dev/null
+++ b/iat-verifier/scripts/check_iat
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+from iatverifier.verify import main
+main()
diff --git a/iat-verifier/scripts/compile_token b/iat-verifier/scripts/compile_token
new file mode 100755
index 0000000..609e2c2
--- /dev/null
+++ b/iat-verifier/scripts/compile_token
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import argparse
+import logging
+import os
+import sys
+
+from ecdsa import SigningKey
+from iatverifier.util import read_token_map, convert_map_to_token
+from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+from iatverifier.verifiers import AttestationTokenVerifier
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('source', help='Token source in YAML format')
+ parser.add_argument('-o', '--outfile',
+ help='''Output file for the compiled token. If this is not
+ specified, the token will be written to standard output.''')
+ parser.add_argument('-k', '--keyfile',
+ help='''Path to the key in PEM format that should be used to
+ sign the token. If this is not specified, the token will be
+ unsigned.''')
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('-r', '--raw', action='store_true',
+ help='''Generate raw CBOR and do not create a signature
+ or COSE wrapper.''')
+ group.add_argument('-m', '--hmac', action='store_true',
+ help='''Generate a token wrapped in a Mac0 rather than
+ Sign1 COSE structure.''')
+
+ args = parser.parse_args()
+ signing_key = None
+
+ cose_alg = None
+ if args.hmac:
+ method = AttestationTokenVerifier.SIGN_METHOD_MAC0
+ cose_alg = AttestationTokenVerifier.COSE_ALG_HS256
+
+ if args.keyfile:
+ with open(args.keyfile, 'rb') as fh:
+ signing_key = fh.read()
+ elif args.raw:
+ if args.keyfile:
+ raise ValueError('A keyfile cannot be specified with --raw.')
+ method = AttestationTokenVerifier.SIGN_METHOD_RAW
+ else:
+ method = AttestationTokenVerifier.SIGN_METHOD_SIGN1
+ if args.keyfile:
+ with open(args.keyfile) as fh:
+ signing_key = SigningKey.from_pem(fh.read())
+
+ verifier = PSAIoTProfile1TokenVerifier.get_verifier()
+ if verifier.method != method:
+ verifier.method = method
+ if cose_alg is not None and verifier.cose_alg != cose_alg:
+ verifier.cose_alg = cose_alg
+ token_map = read_token_map(args.source)
+
+ if args.outfile:
+ with open(args.outfile, 'wb') as wfh:
+ convert_map_to_token(token_map, signing_key, verifier, wfh)
+ else:
+ with os.fdopen(sys.stdout.fileno(), 'wb') as wfh:
+ convert_map_to_token(token_map, signing_key, verifier, wfh)
diff --git a/iat-verifier/scripts/decompile_token b/iat-verifier/scripts/decompile_token
new file mode 100755
index 0000000..cc74e12
--- /dev/null
+++ b/iat-verifier/scripts/decompile_token
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import argparse
+import sys
+
+import yaml
+from iatverifier.util import convert_token_to_map
+from iatverifier.psa_iot_profile1_token_verifier import PSAIoTProfile1TokenVerifier
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('source', help='A compiled COSE IAT token.')
+ parser.add_argument('-o', '--outfile',
+ help='''Output file for the depompiled claims. If this is not
+ specified, the claims will be written to standard output.''')
+ args = parser.parse_args()
+
+ verifier = PSAIoTProfile1TokenVerifier.get_verifier()
+ with open(args.source, 'rb') as fh:
+ token_map = convert_token_to_map(fh.read(), verifier)
+
+ if args.outfile:
+ with open(args.outfile, 'w') as wfh:
+ yaml.dump(token_map, wfh)
+ else:
+ yaml.dump(token_map, sys.stdout)
+
+
diff --git a/iat-verifier/setup.py b/iat-verifier/setup.py
new file mode 100644
index 0000000..db7c1ef
--- /dev/null
+++ b/iat-verifier/setup.py
@@ -0,0 +1,29 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+from setuptools import setup
+
+setup(
+ name='iatverifier',
+ version='0.1',
+ packages=[
+ 'iatverifier',
+ ],
+ scripts=[
+ 'scripts/check_iat',
+ 'scripts/compile_token',
+ 'scripts/decompile_token',
+ ],
+ python_requires='>=3.6',
+ install_requires=[
+ 'cbor2',
+ 'cryptography',
+ 'ecdsa',
+ 'pycose>=0.1.2',
+ 'pyyaml',
+ ],
+)
diff --git a/iat-verifier/tests/data/hmac.key b/iat-verifier/tests/data/hmac.key
new file mode 100644
index 0000000..e9569f1
--- /dev/null
+++ b/iat-verifier/tests/data/hmac.key
Binary files differ
diff --git a/iat-verifier/tests/data/invalid-hw-version.yaml b/iat-verifier/tests/data/invalid-hw-version.yaml
new file mode 100644
index 0000000..16078e0
--- /dev/null
+++ b/iat-verifier/tests/data/invalid-hw-version.yaml
@@ -0,0 +1,34 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+hardware_version: ' 23-56789a'
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/invalid-profile-id.yaml b/iat-verifier/tests/data/invalid-profile-id.yaml
new file mode 100644
index 0000000..42fba85
--- /dev/null
+++ b/iat-verifier/tests/data/invalid-profile-id.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: 42
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/invalid-type-length.yaml b/iat-verifier/tests/data/invalid-type-length.yaml
new file mode 100644
index 0000000..5375755
--- /dev/null
+++ b/iat-verifier/tests/data/invalid-type-length.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: 12
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: Invalid_data
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRgX
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQ8OCgwLCgkIFxYVFBMSERAfHh0cGw==
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/key-alt.pem b/iat-verifier/tests/data/key-alt.pem
new file mode 100644
index 0000000..83f86bc
--- /dev/null
+++ b/iat-verifier/tests/data/key-alt.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEINLnDlSiV4jEYODZyVl+QpFxh/0braJXy0Urqq1IoAnboAoGCCqGSM49
+AwEHoUQDQgAEXQ+SyqaVGq7UZ4rnCV7gfjJbhPcTREdfxxK/UDhifr7XltyBN5jG
+HgJOv0bqOuORlfObC1+1f74W1LGCnjSgPg==
+-----END EC PRIVATE KEY-----
diff --git a/iat-verifier/tests/data/key.pem b/iat-verifier/tests/data/key.pem
new file mode 100644
index 0000000..df1e6ec
--- /dev/null
+++ b/iat-verifier/tests/data/key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIEP//suV+AhafEDh0+p5C+9Ot4zdd9WFA6ZMFgD5GzPnoAoGCCqGSM49
+AwEHoUQDQgAETl4iCZ47zrRbRG0TVf0dw7VFlHtv18HInYhnmMNybo+A1wuECyVq
+rDSmLt4QQzZPBECV8ANHS5HgGCCSr7E/Lg==
+-----END EC PRIVATE KEY-----
diff --git a/iat-verifier/tests/data/malformed.cbor b/iat-verifier/tests/data/malformed.cbor
new file mode 100644
index 0000000..ff094d4
--- /dev/null
+++ b/iat-verifier/tests/data/malformed.cbor
Binary files differ
diff --git a/iat-verifier/tests/data/missing-claim.yaml b/iat-verifier/tests/data/missing-claim.yaml
new file mode 100644
index 0000000..b148c0b
--- /dev/null
+++ b/iat-verifier/tests/data/missing-claim.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+#instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/missing-signer-id.yaml b/iat-verifier/tests/data/missing-signer-id.yaml
new file mode 100644
index 0000000..75d3c12
--- /dev/null
+++ b/iat-verifier/tests/data/missing-signer-id.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/missing-sw-comps.yaml b/iat-verifier/tests/data/missing-sw-comps.yaml
new file mode 100644
index 0000000..1863403
--- /dev/null
+++ b/iat-verifier/tests/data/missing-sw-comps.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+#sw_components:
+ #- sw_component_type: BL
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #sw_component_version: 3.4.2
+ #measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #measurement_description: TF-M_SHA256MemPreXIP
+ #- sw_component_type: M1
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #sw_component_version: 1.2
+ #measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #- sw_component_type: M2
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #sw_component_version: 1.2.3
+ #measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #- sw_component_type: M3
+ #signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ #sw_component_version: 1
+ #measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/submod-missing-claim.yaml b/iat-verifier/tests/data/submod-missing-claim.yaml
new file mode 100644
index 0000000..9fba392
--- /dev/null
+++ b/iat-verifier/tests/data/submod-missing-claim.yaml
@@ -0,0 +1,33 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ #measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/unexpected-claim.yaml b/iat-verifier/tests/data/unexpected-claim.yaml
new file mode 100644
index 0000000..6c5c586
--- /dev/null
+++ b/iat-verifier/tests/data/unexpected-claim.yaml
@@ -0,0 +1,34 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ 3: 1
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/data/valid-iat.yaml b/iat-verifier/tests/data/valid-iat.yaml
new file mode 100644
index 0000000..46df118
--- /dev/null
+++ b/iat-verifier/tests/data/valid-iat.yaml
@@ -0,0 +1,34 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+instance_id: !!binary AQcGBQQDAgEADw4NDAsKCQgXFhUUExIREB8eHRwbGhkY
+implementation_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+challenge: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+client_id: 2
+hardware_version: '1234567890123'
+security_lifecycle: sl_secured
+profile_id: http://example.com
+boot_seed: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+sw_components:
+ - sw_component_type: BL
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 3.4.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ measurement_description: TF-M_SHA256MemPreXIP
+ - sw_component_type: M1
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M2
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1.2.3
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ - sw_component_type: M3
+ signer_id: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+ sw_component_version: 1
+ measurement_value: !!binary BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=
+
diff --git a/iat-verifier/tests/test_verifier.py b/iat-verifier/tests/test_verifier.py
new file mode 100644
index 0000000..5e15895
--- /dev/null
+++ b/iat-verifier/tests/test_verifier.py
@@ -0,0 +1,119 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2019-2022, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import os
+import tempfile
+import unittest
+
+from iatverifier.util import convert_map_to_token_files
+from iatverifier.verify import extract_iat_from_cose, PSAIoTProfile1TokenVerifier
+from iatverifier.verifiers import VerifierConfiguration
+
+
+THIS_DIR = os.path.dirname(__file__)
+
+DATA_DIR = os.path.join(THIS_DIR, 'data')
+KEYFILE = os.path.join(DATA_DIR, 'key.pem')
+KEYFILE_ALT = os.path.join(DATA_DIR, 'key-alt.pem')
+
+
+def create_token(source_name, keyfile, verifier):
+ source_path = os.path.join(DATA_DIR, source_name)
+ fd, dest_path = tempfile.mkstemp()
+ os.close(fd)
+ convert_map_to_token_files(source_path, keyfile, verifier, dest_path)
+ return dest_path
+
+
+def read_iat(filename, keyfile, verifier):
+ filepath = os.path.join(DATA_DIR, filename)
+ raw_iat = extract_iat_from_cose(keyfile, filepath, verifier)
+ return verifier.decode_and_validate_iat(raw_iat)
+
+
+def create_and_read_iat(source_name, keyfile, verifier):
+ token_file = create_token(source_name, keyfile, verifier)
+ return read_iat(token_file, keyfile, verifier)
+
+
+class TestIatVerifier(unittest.TestCase):
+
+ def setUp(self):
+ self.config = VerifierConfiguration()
+
+ def test_validate_signature(self):
+ verifier = PSAIoTProfile1TokenVerifier.get_verifier(self.config)
+ good_sig = create_token('valid-iat.yaml', KEYFILE, verifier)
+ bad_sig = create_token('valid-iat.yaml', KEYFILE_ALT, verifier)
+
+ raw_iat = extract_iat_from_cose(KEYFILE, good_sig, verifier)
+
+ with self.assertRaises(ValueError) as cm:
+ raw_iat = extract_iat_from_cose(KEYFILE, bad_sig, verifier)
+
+ self.assertIn('Bad signature', cm.exception.args[0])
+
+ def test_validate_iat_structure(self):
+ keep_going_conf = VerifierConfiguration(keep_going=True)
+
+ iat = create_and_read_iat('valid-iat.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+
+ with self.assertRaises(ValueError) as cm:
+ iat = create_and_read_iat('invalid-profile-id.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('Invalid PROFILE_ID', cm.exception.args[0])
+
+ with self.assertRaises(ValueError) as cm:
+ iat = read_iat('malformed.cbor', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('Bad COSE', cm.exception.args[0])
+
+ with self.assertRaises(ValueError) as cm:
+ iat = create_and_read_iat('missing-claim.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('missing MANDATORY claim', cm.exception.args[0])
+
+ with self.assertRaises(ValueError) as cm:
+ iat = create_and_read_iat('submod-missing-claim.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('missing MANDATORY claim', cm.exception.args[0])
+
+ with self.assertRaises(ValueError) as cm:
+ iat = create_and_read_iat('missing-sw-comps.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('NO_MEASUREMENTS claim is not present',
+ cm.exception.args[0])
+
+ with self.assertLogs() as cm:
+ iat = create_and_read_iat('missing-signer-id.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertIn('Missing RECOMMENDED claim "SIGNER_ID" from sw_component',
+ cm.records[0].getMessage())
+
+ with self.assertLogs() as cm:
+ iat = create_and_read_iat('invalid-type-length.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(keep_going_conf))
+ self.assertIn("Invalid PROFILE_ID: must be a(n) <class 'str'>: found <class 'int'>",
+ cm.records[0].getMessage())
+ self.assertIn("Invalid SIGNER_ID: must be a(n) <class 'bytes'>: found <class 'str'>",
+ cm.records[1].getMessage())
+ self.assertIn("Invalid SIGNER_ID length: must be at least 32 bytes, found 12 bytes",
+ cm.records[2].getMessage())
+ self.assertIn("Invalid MEASUREMENT length: must be at least 32 bytes, found 28 bytes",
+ cm.records[3].getMessage())
+
+ with self.assertLogs() as cm:
+ iat = create_and_read_iat('invalid-hw-version.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(keep_going_conf))
+ self.assertIn("Invalid HARDWARE_VERSION length; must be 13 digits, found 10 characters",
+ cm.records[0].getMessage())
+ self.assertIn("Invalid digit at position 1",
+ cm.records[1].getMessage())
+ self.assertIn("Invalid digit - at position 4",
+ cm.records[2].getMessage())
+ self.assertIn("Invalid digit a at position 10",
+ cm.records[3].getMessage())
+
+ def test_binary_string_decoding(self):
+ iat = create_and_read_iat('valid-iat.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertEqual(iat['SECURITY_LIFECYCLE'], 'SL_SECURED')
+
+ def test_security_lifecycle_decoding(self):
+ iat = create_and_read_iat('valid-iat.yaml', KEYFILE, PSAIoTProfile1TokenVerifier.get_verifier(self.config))
+ self.assertEqual(iat['SECURITY_LIFECYCLE'], 'SL_SECURED')