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')