Tools: add IAT verifier.

Add a script for verifying the signatures and structure of Initial
Attestation Tokens.

Change-Id: Ic3649f25c32edd9b08793eb8a77c8b40dd71e8c8
Signed-off-by: Sergei Trofimov <sergei.trofimov@arm.com>
diff --git a/docs/index.rst.in b/docs/index.rst.in
index 010443d..534118f 100644
--- a/docs/index.rst.in
+++ b/docs/index.rst.in
@@ -87,6 +87,13 @@
 
     @REJECTED_DD_LIST@
 
+.. toctree::
+    :maxdepth: 2
+    :caption: Tools
+    :glob:
+    :hidden:
+    tools/iat-verifier/*
+
 .. include:: readme.rst
 
 -----------
diff --git a/tools/iat-verifier/README.rst b/tools/iat-verifier/README.rst
new file mode 100644
index 0000000..b7dd8bc
--- /dev/null
+++ b/tools/iat-verifier/README.rst
@@ -0,0 +1,133 @@
+############################
+Initial Attestation Verifier
+############################
+This is a parser and verifier script for an Initial Attestation Token,
+the structure of which is described here:
+
+https://tools.ietf.org/html/draft-tschofenig-rats-psa-token-01
+
+
+************
+Installation
+************
+You can install the script using pip:
+
+.. code:: bash
+
+   # Inside the directory containg this README
+   pip install .
+
+This should automatically install all the required dependencies. Please
+see ``setup.py`` for the list of said dependencies.
+
+*****
+Usage
+*****
+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/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/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",
+           "EPOCH": 1,
+           "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",
+           "EPOCH": 1,
+           "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",
+           "EPOCH": 1,
+           "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",
+           "EPOCH": 1,
+           "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"
+       }
+       ]
+   }
+
+
+*******
+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.
+
+--------------
+
+*Copyright (c) 2019, Arm Limited. All rights reserved.*
diff --git a/tools/iat-verifier/dev_scripts/generate-key.py b/tools/iat-verifier/dev_scripts/generate-key.py
new file mode 100755
index 0000000..f653ffd
--- /dev/null
+++ b/tools/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/tools/iat-verifier/dev_scripts/generate-sample-iat.py b/tools/iat-verifier/dev_scripts/generate-sample-iat.py
new file mode 100755
index 0000000..3d69a12
--- /dev/null
+++ b/tools/iat-verifier/dev_scripts/generate-sample-iat.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import base64
+import struct
+
+import cbor
+from ecdsa import SigningKey
+from pycose.sign1message import Sign1Message
+
+from iatverifier import const
+from iatverifier.util import sign_eat
+
+
+# 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 = {
+  const.INSTANCE_ID: GUID,
+  const.IMPLEMENTATION_ID: ORIGIN,
+  const.CHALLENGE: NONCE,
+  const.CLIENT_ID: 2,
+  const.SECURITY_LIFECYCLE: const.SL_PROVISIONED,
+  const.PROFILE_ID: 'http://example.com',
+  const.BOOT_SEED: BOOT_SEED,
+  const.SW_COMPONENTS: [
+        {
+            # bootloader
+            const.SW_COMPONENT_TYPE: 'BL',
+            const.SIGNER_ID: SIGNER_ID,
+            const.SW_COMPONENT_VERSION: '3.4.2',
+            const.EPOCH: 1,
+            const.MEASUREMENT_VALUE: MEASUREMENT,
+            const.MEASUREMENT_DESCRIPTION: 'TF-M_SHA256MemPreXIP',
+        },
+        {
+            # mod1
+            const.SW_COMPONENT_TYPE: 'M1',
+            const.SIGNER_ID: SIGNER_ID,
+            const.SW_COMPONENT_VERSION: '3.4.2',
+            const.EPOCH: 1,
+            const.MEASUREMENT_VALUE: MEASUREMENT,
+        },
+        {
+            # mod2
+            const.SW_COMPONENT_TYPE: 'M2',
+            const.SIGNER_ID: SIGNER_ID,
+            const.SW_COMPONENT_VERSION: '3.4.2',
+            const.EPOCH: 1,
+            const.MEASUREMENT_VALUE: MEASUREMENT,
+        },
+        {
+            # mod3
+            const.SW_COMPONENT_TYPE: 'M3',
+            const.SIGNER_ID: SIGNER_ID,
+            const.SW_COMPONENT_VERSION: '3.4.2',
+            const.EPOCH: 1,
+            const.MEASUREMENT_VALUE: MEASUREMENT,
+        },
+    ],
+}
+
+
+if __name__ == '__main__':
+    import sys
+    keyfile = sys.argv[1]
+    outfile = sys.argv[2]
+
+    sk = SigningKey.from_pem(open(keyfile, 'rb').read())
+    token = cbor.dumps(token_map)
+    signed_token = sign_eat(token, sk)
+
+    with open(outfile, 'wb') as wfh:
+        wfh.write(signed_token)
diff --git a/tools/iat-verifier/iatverifier/__init__.py b/tools/iat-verifier/iatverifier/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/iat-verifier/iatverifier/__init__.py
diff --git a/tools/iat-verifier/iatverifier/const.py b/tools/iat-verifier/iatverifier/const.py
new file mode 100644
index 0000000..cb7f796
--- /dev/null
+++ b/tools/iat-verifier/iatverifier/const.py
@@ -0,0 +1,102 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+# IAT custom claims
+ARM_RANGE = -75000
+PROFILE_ID = ARM_RANGE
+CLIENT_ID = ARM_RANGE - 1
+SECURITY_LIFECYCLE = ARM_RANGE - 2
+IMPLEMENTATION_ID = ARM_RANGE - 3
+BOOT_SEED = ARM_RANGE - 4
+HARDWARE_ID = ARM_RANGE - 5
+SW_COMPONENTS = ARM_RANGE - 6
+NO_MEASUREMENTS = ARM_RANGE - 7
+CHALLENGE = ARM_RANGE - 8  # nonce
+INSTANCE_ID = ARM_RANGE - 9  # UEID
+ORIGINATOR = ARM_RANGE - 10  # originator
+
+
+# SW component IDs
+SW_COMPONENT_RANGE = 0
+SW_COMPONENT_TYPE = SW_COMPONENT_RANGE + 1
+MEASUREMENT_VALUE = SW_COMPONENT_RANGE + 2
+SW_COMPONENT_VERSION = SW_COMPONENT_RANGE + 4
+SIGNER_ID = SW_COMPONENT_RANGE + 5
+MEASUREMENT_DESCRIPTION = SW_COMPONENT_RANGE + 6
+
+
+NAMES = {
+    INSTANCE_ID: 'INSTANCE_ID',
+    CHALLENGE: 'CHALLENGE',
+    IMPLEMENTATION_ID: 'IMPLEMENTATION_ID',
+    SW_COMPONENTS: 'SW_COMPONENTS',
+    SW_COMPONENT_TYPE: 'SW_COMPONENT_TYPE',
+    SW_COMPONENT_VERSION: 'SW_COMPONENT_VERSION',
+    CLIENT_ID: 'CLIENT_ID',
+    SECURITY_LIFECYCLE: 'SECURITY_LIFECYCLE',
+    BOOT_SEED: 'BOOT_SEED',
+    SIGNER_ID: 'SIGNER_ID',
+    NO_MEASUREMENTS: 'NO_MEASUREMENTS',
+    MEASUREMENT_VALUE: 'MEASUREMENT_VALUE',
+    MEASUREMENT_DESCRIPTION: 'MEASUREMENT_DESCRIPTION',
+    HARDWARE_ID: 'HARDWARE_ID',
+    ORIGINATOR: 'ORIGINATOR',
+    PROFILE_ID: 'PROFILE_ID',
+}
+
+
+IS_UTF_8 = ['SW_COMPONENT_TYPE', 'SW_COMPONENT_VERSION',
+            'MEASUREMENT_DESCRIPTION', 'ORIGNATOR', 'PROFILE_ID']
+
+
+MANDATORY_CLAIMS = [
+    INSTANCE_ID,
+    IMPLEMENTATION_ID,
+    CLIENT_ID,
+    CHALLENGE,
+    SECURITY_LIFECYCLE,
+    PROFILE_ID,
+    BOOT_SEED,
+]
+
+MANDATORY_SW_COMPONENT_CLAIMS = [
+    SW_COMPONENT_TYPE,
+    MEASUREMENT_VALUE,
+]
+
+ALLOWED_SW_COMPONENT_CLAIMS = [
+    SW_COMPONENT_TYPE,
+    SW_COMPONENT_VERSION,
+    MEASUREMENT_VALUE,
+    MEASUREMENT_DESCRIPTION,
+    SIGNER_ID,
+]
+
+
+HASH_SIZES = [32, 48, 64]
+
+
+# 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
+
+
+SL_NAMES = [
+    'SL_UNKNOWN',
+    'SL_PSA_ROT_PROVISIONING',
+    'SL_SECURED',
+    'SL_NON_PSA_ROT_DEBUG',
+    'SL_RECOVERABLE_PSA_ROT_DEBUG',
+    'SL_PSA_LIFECYCLE_DECOMMISSIONED',
+]
+
+SL_SHIFT = 12
+
diff --git a/tools/iat-verifier/iatverifier/util.py b/tools/iat-verifier/iatverifier/util.py
new file mode 100644
index 0000000..f34c8fd
--- /dev/null
+++ b/tools/iat-verifier/iatverifier/util.py
@@ -0,0 +1,139 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+from collections.abc import Iterable
+
+import cbor
+import yaml
+from ecdsa import SigningKey
+from pycose.sign1message import Sign1Message
+
+from iatverifier import const
+
+
+FIELD_NAMES = {v: k for k, v in const.NAMES.items()}
+
+
+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 convert_map_to_token_files(mapfile, keyfile, outfile):
+    token_map = read_token_map(mapfile)
+
+    with open(keyfile) as fh:
+        signing_key = SigningKey.from_pem(fh.read())
+
+    with open(outfile, 'wb') as wfh:
+        convert_map_to_token(token_map, signing_key, wfh)
+
+
+def convert_map_to_token(token_map, signing_key, wfh):
+    token = cbor.dumps(token_map)
+    signed_token = sign_eat(token, signing_key)
+    wfh.write(signed_token)
+
+
+def convert_token_to_map(raw_data):
+    payload = get_cose_payload(raw_data)
+    token_map = cbor.loads(payload)
+    return _relabel_keys(token_map)
+
+
+def read_token_map(f):
+    if hasattr(f, 'read'):
+        raw = yaml.load(fh)
+    else:
+        with open(f) as fh:
+            raw = yaml.load(fh)
+
+    return _parse_raw_token(raw)
+
+
+def extract_iat_from_cose(keyfile, tokenfile, keep_going=False):
+    if keyfile:
+        try:
+            sk = SigningKey.from_pem(open(keyfile, 'rb').read())
+        except Exception as e:
+            msg = 'Bad key file "{}": {}'
+            error(msg.format(keyfile, e), keep_going)
+    else:  # no keyfile
+        sk = None
+
+    try:
+        with open(tokenfile, 'rb') as wfh:
+            return get_cose_payload(wfh.read(), sk)
+    except Exception as e:
+        msg = 'Bad COSE file "{}": {}'
+        raise ValueError(msg.format(tokenfile, e))
+
+
+def get_cose_payload(cose, sk=None):
+    msg = Sign1Message.decode(cose)
+    if sk:
+        msg.key = sk
+        msg.signature = msg.signers
+        try:
+            msg.verify_signature(alg='ES256')
+        except Exception:
+            raise ValueError('Bad signature')
+    return msg.payload
+
+
+def _parse_raw_token(raw):
+    result = {}
+    for raw_key, raw_value in raw.items():
+        if isinstance(raw_key, int):
+            key = raw_key
+        else:
+            field_name = raw_key.upper()
+            try:
+                key = FIELD_NAMES[field_name]
+            except KeyError:
+                mag = 'Unknown field "{}" in token.'.format(field_name)
+                raise ValueError(msg)
+
+        if key == const.SECURITY_LIFECYCLE:
+            value = (const.SL_NAMES.index(raw_value.upper()) + 1) << const.SL_SHIFT
+        elif 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 = raw_value
+
+        result[key] = value
+
+    return result
+
+
+def _relabel_keys(token_map):
+    result = {}
+    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))):
+            # TODO  -- asumes dict elements
+            value = [_relabel_keys(v) for v in value]
+
+        if key == const.SECURITY_LIFECYCLE:
+            value = (const.SL_NAMES[(value >> const.SL_SHIFT) - 1])
+
+        if key in const.NAMES:
+            new_key = const.NAMES[key].lower()
+        else:
+            new_key = key
+        result[new_key] = value
+    return result
diff --git a/tools/iat-verifier/iatverifier/verify.py b/tools/iat-verifier/iatverifier/verify.py
new file mode 100644
index 0000000..5127d55
--- /dev/null
+++ b/tools/iat-verifier/iatverifier/verify.py
@@ -0,0 +1,310 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import argparse
+import base64
+import json
+import logging
+import struct
+import sys
+
+import cbor
+from ecdsa import SigningKey
+from pycose.sign1message import Sign1Message
+
+from iatverifier import const
+from iatverifier.util import extract_iat_from_cose
+
+
+logging.basicConfig(level=logging.INFO, format='%(levelname)8s: %(message)s')
+logger = logging.getLogger('iat-verify')
+
+seen_errors = False
+
+
+def error(message, keep_going=False):
+    global seen_errors
+    seen_errors = True
+    if keep_going:
+        logger.error(message)
+    else:
+        raise ValueError(message)
+
+
+def decode(value, key, keep_going=False):
+    if key in const.IS_UTF_8:
+        try:
+            return value.decode()
+        except UnicodeDecodeError as e:
+            msg = 'Error decodeing value for "{}": {}'
+            error(msg.format(key, e), keep_going)
+            return str(value)[2:-1]
+    else:  # not a UTF-8 value, i.e. a bytestring
+        return str(value)[2:-1]
+
+
+# ----------------------------------------------------------------------------
+# Validation functions
+#
+def validate_instance_id(value, keep_going=False):
+    _validate_bytestring_length(value, 'INSTANCE_ID', 33, keep_going)
+    if value[0] != 0x01:
+        msg = 'Invalid INSTANCE_ID: first byte must be 0x01, found: 0x{}'
+        error(msg.format(value[0]), keep_going)
+
+
+def validate_challege(value, keep_going=False):
+    if not isinstance(value, bytes):
+        msg = 'Invalid CHALLENGE; must be a bytes string.'
+        error(msg, keep_going)
+
+    value_len = len(value)
+    if value_len not in const.HASH_SIZES:
+        msg = 'Invalid CHALLENGE length; must one of {}, found {} bytes'
+        error(msg.format(const.HASH_SIZES, value_len), keep_going)
+
+
+def validate_implementation_id(value, keep_going=False):
+    pass
+
+
+def validate_hardware_id(value, keep_going=False):
+    pass
+
+
+def validate_originator(value, keep_going=False):
+    pass
+
+
+def validate_sw_components(value, keep_going=False):
+    if not isinstance(value, list):
+        msg = 'Invalid SW_COMPONENTS value (must be an array): {}'
+        error(msg.format(value), keep_going)
+        return
+
+    for sw_component in value:
+        if not isinstance(sw_component, dict):
+            msg = 'Invalid SW_COMPONENTS array entry (must be a map): {}'
+            error(msg.format(sw_component), keep_going)
+            return
+
+        for k, v in sw_component.items():
+            if k not in const.ALLOWED_SW_COMPONENT_CLAIMS:
+                msg = 'Uexpected SW_COMPONENT claim: {}'
+                error(msg.format(k), keep_going)
+            try:
+                validation_funcs[k](v, keep_going)
+            except Exception:
+                if not keep_going:
+                    raise
+
+
+def validate_sw_component_type(value, keep_going=False):
+    pass
+
+
+def validate_no_measurements(vlaue, keep_going=False):
+    pass
+
+
+def validate_client_id(value, keep_going=False):
+    if not isinstance(value, int):
+        msg = 'Invalid CLIENT_ID, must be an int: {}'
+        error(msg.format(value), keep_going)
+
+
+def validate_security_lifecycle(value, keep_going=False):
+    if not isinstance(value, int):
+        msg = 'Invalid SECURITY_LIFECYCLE, must be an int: {}'
+        error(msg.format(value), keep_going)
+
+
+def validate_profile_id(value, keep_going=False):
+    if not isinstance(value, str):
+        msg = 'Invalid PROFILE_ID (must be a string): {}'.format(value)
+        error(msg.format(value), keep_going)
+
+
+def validate_boot_seed(value, keep_going=False):
+    _validate_bytestring_length(value, 'BOOT_SEED', 32, keep_going)
+
+
+def validate_signer_id(value, keep_going=False):
+    _validate_bytestring_length(value, 'SIGNER_ID', 32, keep_going)
+
+
+def validate_sw_component_version(value, keep_going=False):
+    pass
+
+
+def validate_epoch(value, keep_going=False):
+    if not (isinstance(value, int) and value >= 0):
+        msg = 'Invalid EPOCH, must be an unsigned integer: {}'
+        error(msg.format(value), keep_going)
+
+
+def validate_measurement_value(value, keep_going=False):
+    _validate_bytestring_length(value, 'MEASUREMENT', 32, keep_going)
+
+
+def validate_measurement_description(value, keep_going=False):
+    pass
+
+
+def validate_challenge(value, keep_going=False):
+    pass
+
+
+validation_funcs = {v: globals().get('validate_{}'.format(n.lower()))
+                    for v, n in const.NAMES.items()}
+
+
+def validate_manadatory_claims(token, keep_going=False):
+    for mand_claim in const.MANDATORY_CLAIMS:
+        if mand_claim not in token:
+            msg = 'Invalid IAT: missing MANDATORY claim "{}"'
+            error(msg.format(const.NAMES[mand_claim]), keep_going)
+
+    if const.SW_COMPONENTS in token:
+        if (not token[const.SW_COMPONENTS] and
+                const.NO_MEASUREMENTS not in token):
+            error('Invalid IAT: no software measurements defined and '
+                  'NO_MEASUREMENTS claim is not present.')
+
+        for entry_number, sw_component_entry in \
+                enumerate(token[const.SW_COMPONENTS]):
+            for mand_claim in const.MANDATORY_SW_COMPONENT_CLAIMS:
+                if mand_claim not in sw_component_entry:
+                    msg = ('Invalid IAT: missing MANDATORY claim "{}" '
+                           'from sw_componentule at index {}')
+                    error(msg.format(const.NAMES[mand_claim],
+                                     entry_number),
+                          keep_going)
+
+    elif const.NO_MEASUREMENTS not in token:
+        error('Invalid IAT: no software measurements defined and '
+              'NO_MEASUREMENTS claim is not present.')
+
+
+def _validate_bytestring_length(value, name, expected_len, keep_going=False):
+    if not isinstance(value, bytes):
+        msg = 'Invalid {}: must be a bytes string: found {}'
+        error(msg.format(name, type(value)), keep_going)
+
+    value_len = len(value)
+    if value_len != expected_len:
+        msg = 'Invalid {} length: must be exactly {} bytes, found {} bytes'
+        error(msg.format(name, expected_len, value_len), keep_going)
+# ----------------------------------------------------------------------------
+
+
+def decode_sw_component(raw_sw_component, keep_going=True):
+    sw_component = {}
+    for k, v in raw_sw_component.items():
+        if isinstance(v, bytes):
+            v = decode(v, k, keep_going)
+        try:
+            sw_component[const.NAMES[k]] = v
+        except KeyError:
+            if not keep_going:
+                raise
+    return sw_component
+
+
+def decode_and_validate_iat(encoded_iat, keep_going=False):
+    try:
+        raw_token = cbor.loads(encoded_iat)
+    except Exception as e:
+        msg = 'Invalid CBOR: {}'
+        raise ValueError(msg.format(e))
+
+    validate_manadatory_claims(raw_token, keep_going)
+
+    token = {}
+    for entry in raw_token.keys():
+        try:
+            entry_name = const.NAMES[entry]
+        except KeyError:
+            error('Invalid IAT identifier: {}'.format(entry), keep_going)
+            if isinstance(value, bytes):
+                value = decode(value, entry, keep_going)
+            token[entry] = value
+            continue
+
+        value = raw_token[entry]
+        validation_funcs[entry](value, keep_going)
+        if entry_name == 'SW_COMPONENTS':
+            try:
+                token[entry_name] = []
+                for raw_sw_component in value:
+                    decoded_component = decode_sw_component(raw_sw_component,
+                                                            keep_going)
+                    token[entry_name].append(decoded_component)
+            except TypeError:
+                error('Invalid SW_COMPONENT value: {}'.format(value),
+                      keep_going)
+        elif entry_name == 'SECURITY_LIFECYCLE':
+            try:
+                token[entry_name] = const.SL_NAMES[(value >> const.SL_SHIFT) - 1]
+            except IndexError:
+                token[entry_name] = 'CUSTOM({})'.format(value)
+        else:  # not SW_COMPONENT or SECURITY_LIFECYCLE
+            if isinstance(value, bytes):
+                value = decode(value, entry_name, keep_going)
+            token[entry_name] = value
+
+    return token
+
+
+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.
+                        ''')
+    args = parser.parse_args()
+
+    logging.basicConfig(level=logging.INFO)
+
+    try:
+        raw_iat = extract_iat_from_cose(args.keyfile, args.tokenfile,
+                                        args.keep_going)
+        if args.keyfile and not seen_errors:
+            print('Signature OK')
+    except ValueError as e:
+        logger.error('Could not extract IAT from COSE:\n\t{}'.format(e))
+        sys.exit(1)
+
+    try:
+        token = decode_and_validate_iat(raw_iat, args.keep_going)
+        if not 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(token, sys.stdout, indent=4)
+        print('')
diff --git a/tools/iat-verifier/sample/badsig.cbor b/tools/iat-verifier/sample/badsig.cbor
new file mode 100644
index 0000000..a38dead
--- /dev/null
+++ b/tools/iat-verifier/sample/badsig.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/iat.cbor b/tools/iat-verifier/sample/iat.cbor
new file mode 100644
index 0000000..7899743
--- /dev/null
+++ b/tools/iat-verifier/sample/iat.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/invalid-profile-id.cbor b/tools/iat-verifier/sample/invalid-profile-id.cbor
new file mode 100644
index 0000000..a8fcbcb
--- /dev/null
+++ b/tools/iat-verifier/sample/invalid-profile-id.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/key.pem b/tools/iat-verifier/sample/key.pem
new file mode 100644
index 0000000..df1e6ec
--- /dev/null
+++ b/tools/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/tools/iat-verifier/sample/malformed.cbor b/tools/iat-verifier/sample/malformed.cbor
new file mode 100644
index 0000000..ac144ed
--- /dev/null
+++ b/tools/iat-verifier/sample/malformed.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/missing-claim.cbor b/tools/iat-verifier/sample/missing-claim.cbor
new file mode 100644
index 0000000..8374469
--- /dev/null
+++ b/tools/iat-verifier/sample/missing-claim.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/missing-sw-comps.cbor b/tools/iat-verifier/sample/missing-sw-comps.cbor
new file mode 100644
index 0000000..67f797b
--- /dev/null
+++ b/tools/iat-verifier/sample/missing-sw-comps.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/no-sw-measurements.cbor b/tools/iat-verifier/sample/no-sw-measurements.cbor
new file mode 100644
index 0000000..b66f4b0
--- /dev/null
+++ b/tools/iat-verifier/sample/no-sw-measurements.cbor
Binary files differ
diff --git a/tools/iat-verifier/sample/submod-missing-claim.cbor b/tools/iat-verifier/sample/submod-missing-claim.cbor
new file mode 100644
index 0000000..6da7dd5
--- /dev/null
+++ b/tools/iat-verifier/sample/submod-missing-claim.cbor
Binary files differ
diff --git a/tools/iat-verifier/scripts/check_iat b/tools/iat-verifier/scripts/check_iat
new file mode 100755
index 0000000..d12275f
--- /dev/null
+++ b/tools/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/tools/iat-verifier/scripts/compile_token b/tools/iat-verifier/scripts/compile_token
new file mode 100755
index 0000000..b7bbf7b
--- /dev/null
+++ b/tools/iat-verifier/scripts/compile_token
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import argparse
+import os
+import sys
+
+import yaml
+from ecdsa import SigningKey
+from iatverifier.util import read_token_map, convert_map_to_token
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('source', help='Token source in YAML format')
+    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.''')
+    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.''')
+    args = parser.parse_args()
+
+    token_map = read_token_map(args.source)
+    if args.keyfile:
+        with open(args.keyfile) as fh:
+            signing_key = SigningKey.from_pem(fh.read())
+    else:
+        signing_key = None
+
+    if args.outfile:
+        with open(args.outfile, 'wb') as wfh:
+            convert_map_to_token(token_map, signing_key, wfh)
+    else:
+        with os.fdopen(sys.stdout.fileno(), 'wb') as wfh:
+            convert_map_to_token(token_map, signing_key, wfh)
+
+
diff --git a/tools/iat-verifier/scripts/decompile_token b/tools/iat-verifier/scripts/decompile_token
new file mode 100755
index 0000000..0d99961
--- /dev/null
+++ b/tools/iat-verifier/scripts/decompile_token
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import argparse
+import os
+import sys
+
+import cbor
+import yaml
+from iatverifier.util import convert_token_to_map
+
+
+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()
+
+    with open(args.source, 'rb') as fh:
+        token_map = convert_token_to_map(fh.read())
+
+    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/tools/iat-verifier/setup.py b/tools/iat-verifier/setup.py
new file mode 100644
index 0000000..30180d1
--- /dev/null
+++ b/tools/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=[
+        'cbor',
+        'cryptography',
+        'ecdsa',
+        'pycose',
+        'pyyaml',
+    ],
+)
diff --git a/tools/iat-verifier/tests/data/invalid-profile-id.yaml b/tools/iat-verifier/tests/data/invalid-profile-id.yaml
new file mode 100644
index 0000000..42fba85
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/key-alt.pem b/tools/iat-verifier/tests/data/key-alt.pem
new file mode 100644
index 0000000..83f86bc
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/key.pem b/tools/iat-verifier/tests/data/key.pem
new file mode 100644
index 0000000..df1e6ec
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/malformed.cbor b/tools/iat-verifier/tests/data/malformed.cbor
new file mode 100644
index 0000000..ac144ed
--- /dev/null
+++ b/tools/iat-verifier/tests/data/malformed.cbor
Binary files differ
diff --git a/tools/iat-verifier/tests/data/missing-claim.yaml b/tools/iat-verifier/tests/data/missing-claim.yaml
new file mode 100644
index 0000000..b148c0b
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/missing-sw-comps.yaml b/tools/iat-verifier/tests/data/missing-sw-comps.yaml
new file mode 100644
index 0000000..1863403
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/submod-missing-claim.yaml b/tools/iat-verifier/tests/data/submod-missing-claim.yaml
new file mode 100644
index 0000000..9fba392
--- /dev/null
+++ b/tools/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/tools/iat-verifier/tests/data/valid-iat.yaml b/tools/iat-verifier/tests/data/valid-iat.yaml
new file mode 100644
index 0000000..365c656
--- /dev/null
+++ b/tools/iat-verifier/tests/data/valid-iat.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/tools/iat-verifier/tests/test_verifier.py b/tools/iat-verifier/tests/test_verifier.py
new file mode 100644
index 0000000..8b930d2
--- /dev/null
+++ b/tools/iat-verifier/tests/test_verifier.py
@@ -0,0 +1,83 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2019, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+import os
+import sys
+import tempfile
+import unittest
+
+from iatverifier.util import convert_map_to_token_files
+from iatverifier.verify import extract_iat_from_cose, decode_and_validate_iat
+
+
+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):
+    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, dest_path)
+    return dest_path
+
+
+def read_iat(filename, keyfile):
+    filepath = os.path.join(DATA_DIR, filename)
+    raw_iat = extract_iat_from_cose(keyfile, filepath)
+    return decode_and_validate_iat(raw_iat)
+
+
+def create_and_read_iat(source_name, keyfile):
+    token_file = create_token(source_name, keyfile)
+    return read_iat(token_file, keyfile)
+
+
+class TestIatVerifier(unittest.TestCase):
+
+    def test_validate_signature(self):
+        good_sig = create_token('valid-iat.yaml', KEYFILE)
+        bad_sig = create_token('valid-iat.yaml', KEYFILE_ALT)
+
+        raw_iat = extract_iat_from_cose(KEYFILE, good_sig)
+
+        with self.assertRaises(ValueError) as cm:
+            raw_iat = extract_iat_from_cose(KEYFILE, bad_sig)
+
+        self.assertIn('Bad signature', cm.exception.args[0])
+
+    def test_validate_iat_structure(self):
+        iat = create_and_read_iat('valid-iat.yaml', KEYFILE)
+
+        with self.assertRaises(ValueError) as cm:
+            iat = create_and_read_iat('invalid-profile-id.yaml', KEYFILE)
+        self.assertIn('Invalid PROFILE_ID', cm.exception.args[0])
+
+        with self.assertRaises(ValueError) as cm:
+            iat = read_iat('malformed.cbor', KEYFILE)
+        self.assertIn('Bad COSE', cm.exception.args[0])
+
+        with self.assertRaises(ValueError) as cm:
+            iat = create_and_read_iat('missing-claim.yaml', KEYFILE)
+        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)
+        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)
+        self.assertIn('NO_MEASUREMENTS claim is not present',
+                      cm.exception.args[0])
+
+    def test_security_lifecycle_decoding(self):
+        iat = create_and_read_iat('valid-iat.yaml', KEYFILE)
+        self.assertEqual(iat['SECURITY_LIFECYCLE'], 'SL_SECURED')
+