diff --git a/dist/tools/suit_v3/gen_key.py b/dist/tools/suit_v3/gen_key.py new file mode 100755 index 0000000000..3a5bbf148e --- /dev/null +++ b/dist/tools/suit_v3/gen_key.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2020 Kaspar Schleiser +# 2020 Inria +# 2020 Freie Universität Berlin + +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +import sys + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.serialization import PrivateFormat +from cryptography.hazmat.primitives.serialization import NoEncryption + + +def main(): + if len(sys.argv) != 2: + print("usage: gen_key.py ") + sys.exit(1) + + pk = Ed25519PrivateKey.generate() + pem = pk.private_bytes(encoding=Encoding.PEM, + format=PrivateFormat.PKCS8, + encryption_algorithm=NoEncryption() + ) + + with open(sys.argv[1], "wb") as f: + f.write(pem) + + +if __name__ == '__main__': + main() diff --git a/dist/tools/suit_v3/gen_manifest.py b/dist/tools/suit_v3/gen_manifest.py new file mode 100755 index 0000000000..cbd88c4285 --- /dev/null +++ b/dist/tools/suit_v3/gen_manifest.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2019 Inria +# 2019 FU Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +import argparse +import json +import os +import uuid + + +def str2int(x): + if x.startswith("0x"): + return int(x, 16) + else: + return int(x) + + +def parse_arguments(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--urlroot', '-u', help='', + default="coap://example.org") + parser.add_argument('--seqnr', '-s', default=0, + help='Sequence number of the manifest') + parser.add_argument('--output', '-o', default="out.json", + help='Manifest output binary file path') + parser.add_argument('--uuid-vendor', '-V', default="riot-os.org", + help='Manifest vendor uuid') + parser.add_argument('--uuid-class', '-C', default="native", + help='Manifest class uuid') + parser.add_argument('slotfiles', nargs="+", + help='The list of slot file paths') + return parser.parse_args() + + +def main(args): + uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, args.uuid_vendor) + uuid_class = uuid.uuid5(uuid_vendor, args.uuid_class) + + template = {} + + template["manifest-version"] = int(1) + template["manifest-sequence-number"] = int(args.seqnr) + + images = [] + for filename_offset in args.slotfiles: + split = filename_offset.split(":") + if len(split) == 1: + filename, offset = split[0], 0 + else: + filename, offset = split[0], str2int(split[1]) + + images.append((filename, offset)) + + template["components"] = [] + + for slot, image in enumerate(images): + filename, offset = image + + uri = os.path.join(args.urlroot, os.path.basename(filename)) + + component = { + "install-id": ["00"], + "vendor-id": uuid_vendor.hex, + "class-id": uuid_class.hex, + "file": filename, + "uri": uri, + "bootable": True, + } + + if offset: + component.update({"offset": offset}) + + template["components"].append(component) + + with open(args.output, 'w') as f: + json.dump(template, f, indent=4) + + +if __name__ == "__main__": + _args = parse_arguments() + main(_args) diff --git a/dist/tools/suit_v3/suit-manifest-generator/.gitignore b/dist/tools/suit_v3/suit-manifest-generator/.gitignore new file mode 100644 index 0000000000..8b2f59d6b8 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/.gitignore @@ -0,0 +1,5 @@ +*__pycache__ +*.pyc +*.DS_Store +*.hex +examples/*.cbor diff --git a/dist/tools/suit_v3/suit-manifest-generator/LICENSE b/dist/tools/suit_v3/suit-manifest-generator/LICENSE new file mode 100644 index 0000000000..59cd3f8a32 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/LICENSE @@ -0,0 +1,165 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. diff --git a/dist/tools/suit_v3/suit-manifest-generator/README.md b/dist/tools/suit_v3/suit-manifest-generator/README.md new file mode 100644 index 0000000000..221593c16e --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/README.md @@ -0,0 +1,209 @@ +# Manifest Generator + +This repository contains a tool to generate manifests following the specification in https://tools.ietf.org/html/draft-ietf-suit-manifest-03. + +# Installing + +First clone this repo: + +``` +$ git clone https://github.com/ARMmbed/suit-manifest-generator.git +``` + +Next, use pip to install the repo: + +``` +$ cd suit-manifest-generator +$ python3 -m pip install --user --upgrade . +``` + +# Input File Description + +The input file is organised into four high-level elements: + +* `manifest-version` (a positive integer), the version of the manifest specification +* `manifest-sequence-number` (a positive integer), the anti-rollback counter of the manifest +* `components`, a list of components that are described by the manifest + + +Each component is a JSON map that may contain the following elements. Some elements are required for the target to be able to install the component. + +Required elements: + +* `install-id` (a Component ID), the identifier of the location to install the described component. +* `install-digest` (a SUIT Digest), the digest of the component after installation. +* `install-size` (a positive integer), the size of the component after installation. +* `vendor-id` (a RFC 4122 UUID), the UUID for the component vendor. This must match the UUID that the manifest processor expects for the specified `install-id`. The suit-tool expects at least one component to have a `vendor-id` +* `class-id` (a RFC 4122 UUID), the UUID for the component. This must match the UUID that the manifest processor expects for the specified `install-id`. The `suit-tool` expects at least one component with a `vendor-id` to also have a `class-id` +* `file` (a string), the path to a payload file. The `install-digest` and `install-size` will be calculated from this file. + +Some elements are not required by the tool, but are necessary in order to accomplish one or more use-cases. + +Optional elements: + +* `bootable` (a boolean, default: `false`), when set to true, the `suit-tool` will generate commands to execute the component, either from `install-id` or from `load-id` (see below) +* `uri` (a text string), the location at which to find the payload. This element is required in order to generate the `payload-fetch` and `install` sections. +* `loadable` (a boolean, default: `false`), when set to true, the `suit-tool` loads this component in the `load` section. +* `compression-info` (a choice of string values), indicates how a payload is compressed. When specified, payload is decompressed before installation. The `install-size` must match the decompressed size of the payload and the install-digest must match the decompressed payload. N.B. The suit-tool does not perform compression. Supported values are: + + * `gzip` + * `bzip2` + * `deflate` + * `lz4` + * `lzma` + +* `download-digest` (a SUIT Digest), a digest of the component after download. Only required if `compression-info` is present and `decompress-on-load` is `false`. +* `decompress-on-load` (a boolean, default: `false`), when set to true, payload is not decompressed during installation. Instead, the payload is decompressed during loading. This element has no effect if `loadable` is `false`. +* `load-digest` (a SUIT Digest), a digest of the component after loading. Only required if `decompress-on-load` is `true`. +* `install-on-download` (boolean, default: true), If true, payload is written to `install-id` during download, otherwise, payload is written to `download-id`. +* `download-id` (a component id), the location where a downloaded payload should be stored before installation--only used when `install-on-download` is `false`. + +## Component ID + +The `suit-tool` expects component IDs to be a JSON list of strings. The `suit-tool` converts the strings to bytes by: + +1. Attempting to convert from hex +2. Attempting to convert from base64 +3. Encoding the string to UTF-8 + +For example, + +* `["00"]` will encode to `814100` (`[h'00']`) +* `["0"]` will encode to `814130` (`[h'30']`) +* `["MTIzNA=="]` will encode to `814431323334` (`[h'31323334']`) +* `["example"]` will encode to `81476578616D706C65` (`[h'6578616d706c65']`) + +N.B. Be careful that certain strings can appear to be hex or base64 and will be treated as such. Any characters outside the set `[0-9a-fA-F]` ensure that the string is not treated as hex. Any characters outside the set `[0-9A-Za-z+/]` or a number of characters not divisible by 4 will ensure that the string is not treated as base64. + +## SUIT Digest + +The format of a digest is a JSON map: + +```JSON +{ + "algorithm-id" : "sha256", + "digest-bytes" : "base64-or-hex" +} +``` + +The `algorithm-id` must be one of: + +* `sha224` +* `sha256` +* `sha384` +* `sha512` + +The `digest-bytes` is a string of either hex- or base64-encoded bytes. The same decoding rules as those in Component ID are applied. + +## Example Input File + +```JSON +{ + "components" : [ + { + "download-id" : ["01"], + "install-id" : ["00"], + "install-digest": { + "algorithm-id": "sha256", + "digest-bytes": "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210" + }, + "install-size" : 34768, + "uri": "http://example.com/file.bin", + "vendor-id" : "fa6b4a53-d5ad-5fdf-be9d-e663e4d41ffe", + "class-id" : "1492af14-2569-5e48-bf42-9b2d51f2ab45", + "bootable" : true, + "install-on-download" : false, + "loadable" : true, + "decompress-on-load" : true, + "load-id" : ["02"], + "compression-info" : "gzip", + "load-digest" : { + "algorithm-id": "sha256", + "digest-bytes": "0011223344556677889901234567899876543210aabbccddeeffabcdeffedcba" + }, + }, + { + "install-id" : ["03", "01"], + "install-digest": { + "algorithm-id": "sha256", + "digest-bytes": "0123456789abcdeffedcba987654321000112233445566778899aabbccddeeff" + }, + "install-size" : 76834, + "uri": "http://example.com/file2.bin" + } + ], + "manifest-version": 1, + "manifest-sequence-number": 7 +} +``` + +# Invoking the suit-tool + +The `suit-tool` supports three sub-commands: + +* `create` generates a new manifest. +* `sign` signs a manifest. +* `parse` parses an existing manifest into cbor-debug or a json representation. + +The `suit-tool` has a configurable log level, specified with `-l`: + +* `suit-tool -l debug` verbose output +* `suit-tool -l info` normal output +* `suit-tool -l warning` suppress informational messages +* `suit-tool -l exception` suppress warning and informational messages + +## Create + +To create a manifest, invoke the `suit-tool` with: + +```sh +suit-tool create -i IFILE -o OFILE +``` + +The format of `IFILE` is as described above. `OFILE` defaults to a CBOR-encoded SUIT manifest. + +`-f` specifies the output format: + +* `suit`: CBOR-encoded SUIT manifest +* `suit-debug`: CBOR-debug SUIT manifest +* `json`: JSON-representation of a SUIT manifest + +The `suit-tool` can generate a manifest with severable fields. To enable this mode, add the `-s` flag. + +To add a component to the manifest from the command-line, use the following syntax: + +``` +-c 'FIELD1=VALUE1,FIELD2=VALUE2' +``` + +The supported fields are: + +* `file` the path to a file to use as a payload file. +* `inst` the `install-id`. +* `uri` the URI where the file will be found. + +## Sign + +To sign an existing manifest, invoke the `suit-tool` with: + +```sh +suit-tool sign -m MANIFEST -k PRIVKEY -o OFILE +``` + +`PRIVKEY` must be a secp256r1 ECC private key in PEM format. + +If the COSE Signature needs to indicate the key ID, add a key id with: + +``` +-i KEYID +``` + +## Parse + +To parse an existing manifest, invoke the `suit-tool` with: + +```sh +suit-tool parse -m MANIFEST +``` + +If a json-representation is needed, add the '-j' flag. diff --git a/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool b/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool new file mode 100755 index 0000000000..11960296ff --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2016-2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- + +import sys +import os +suittoolPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..') +sys.path.insert(0,suittoolPath) +from suit_tool import clidriver + +def main(): + return clidriver.main() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dist/tools/suit_v3/suit-manifest-generator/setup.py b/dist/tools/suit_v3/suit-manifest-generator/setup.py new file mode 100644 index 0000000000..beb15ae719 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/setup.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import setuptools +import os +import suit_tool + +with open('README.md', 'r') as fd: + long_description = fd.read() + +if os.name == 'nt': + entry_points={ + "console_scripts": [ + "suit-tool=suit_tool.clidriver:main", + ], + } + scripts = [] +else: + platform_deps = [] + # entry points are nice, but add ~100ms to startup time with all the + # pkg_resources infrastructure, so we use scripts= instead on unix-y + # platforms: + scripts = ['bin/suit-tool', ] + entry_points = {} + +setuptools.setup ( + name = 'ietf-suit-tool', + version = suit_tool.__version__, + author = 'Brendan Moran', + author_email = 'brendan.moran@arm.com', + description = 'A tool for constructing SUIT manifests', + long_description = long_description, + url = 'https://github.com/ARMmbed/suit-manifest-generator', + packages = setuptools.find_packages(exclude=['examples*', 'parser_examples*', '.git*']), + python_requires ='>=3.6', + scripts = scripts, + entry_points = entry_points, + zip_safe = False, + install_requires = [ + 'cbor>=1.0.0', + 'colorama>=0.4.0', + 'cryptography>=2.8' + ], + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Development Status :: 3 - Alpha", + "Operating System :: OS Independent" + ], + long_description_content_type = 'text/markdown' +) diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py new file mode 100644 index 0000000000..126457d2ce --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2016-2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +__version__ = '0.0.1' diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py new file mode 100644 index 0000000000..84b90cfb55 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019-2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import sys +import argparse +from suit_tool import __version__ +import re + + +def str_to_component(s): + types = { + 'file' : ('file', lambda x : str(x.strip('"'))), + 'inst' : ('install-id', lambda x : [ str(y) for y in eval(x) ]), + 'uri' : ('uri', lambda x : str(x.strip('"'))) + } + d = {types[k][0]:types[k][1](v) for k,v in [ re.split(r'=',e, maxsplit=1) for e in re.split(r''',\s*(?=["']?[a-zA-Z0-9_-]+["']?=)''', s)]} + return d + + +class MainArgumentParser(object): + + def __init__(self): + self.parser = self._make_parser() + + def _make_parser(self): + parser = argparse.ArgumentParser(description = 'Create or transform a manifest.' + ' Use {} [command] -h for help on each command.'.format(sys.argv[0])) + + # Add all top-level commands + parser.add_argument('-l', '--log-level', choices=['debug','info','warning','exception'], default='info', + help='Set the verbosity level of console output.') + parser.add_argument('--version', action='version', version=__version__, + help='display the version' + ) + subparsers = parser.add_subparsers(dest="action") + subparsers.required = True + create_parser = subparsers.add_parser('create', help='Create a new manifest') + + # create_parser.add_argument('-v', '--manifest-version', choices=['1'], default='1') + create_parser.add_argument('-i', '--input-file', metavar='FILE', type=argparse.FileType('r'), + help='An input file describing the update. The file must be formatted as JSON. The overal structure is described in README.') + create_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) + create_parser.add_argument('-f', '--format', metavar='FMT', choices=['suit', 'suit-debug', 'json'], default='suit') + create_parser.add_argument('-s', '--severable', action='store_true', help='Convert large elements to severable fields.') + create_parser.add_argument('-c', '--add-component', action='append', type=str_to_component, dest='components', default=[]) + + sign_parser = subparsers.add_parser('sign', help='Sign a manifest') + + sign_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) + sign_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True) + sign_parser.add_argument('-i', '--key-id', metavar='ID', type=str) + sign_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) + + parse_parser = subparsers.add_parser('parse', help='Parse a manifest') + + parse_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) + parse_parser.add_argument('-j', '--json-output', default=False, action='store_true', dest='json') + + get_uecc_pubkey_parser = subparsers.add_parser('pubkey', help='Get the public key for a supplied private key in uECC-compatible C definition.') + + get_uecc_pubkey_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True) + + return parser + + + def parse_args(self, args=None): + self.options = self.parser.parse_args(args) + + return self diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py new file mode 100644 index 0000000000..5b38813929 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2018-2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import logging +import sys + +from suit_tool.argparser import MainArgumentParser +from suit_tool import create, sign, parse, get_uecc_pubkey + +LOG = logging.getLogger(__name__) +LOG_FORMAT = '[%(levelname)s] %(asctime)s - %(name)s - %(message)s' + + +def main(): + driver = CLIDriver() + return driver.main() + + +class CLIDriver(object): + + def __init__(self): + self.options = MainArgumentParser().parse_args().options + + log_level = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'exception': logging.CRITICAL + }[self.options.log_level] + logging.basicConfig(level=log_level, + format=LOG_FORMAT, + datefmt='%Y-%m-%d %H:%M:%S') + LOG.debug('CLIDriver created. Arguments parsed and logging setup.') + + def main(self): + rc = { + "create": create.main, + "parse": parse.main, + # "verify": verify.main, + # "cert": cert.main, + # "init": init.main, + # "update" : update.main, + "pubkey": get_uecc_pubkey.main, + "sign": sign.main + }[self.options.action](self.options) or 0 + + sys.exit(rc) diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py new file mode 100644 index 0000000000..15101f027c --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py @@ -0,0 +1,289 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import binascii +import copy + +import logging + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes + + +from suit_tool.manifest import SUITComponentId, SUITCommon, SUITSequence, \ + SUITCommand, \ + SUITWrapper, SUITTryEach + +LOG = logging.getLogger(__name__) + +def runable_id(c): + id = c['install-id'] + if c.get('loadable'): + id = c['load-id'] + return id + +def hash_file(fname, alg): + imgsize = 0 + digest = hashes.Hash(alg, backend=default_backend()) + with open(fname, 'rb') as fd: + def read_in_chunks(): + while True: + data = fd.read(1024) + if not data: + break + yield data + for chunk in read_in_chunks(): + imgsize += len(chunk) + digest.update(chunk) + return digest, imgsize + + +def mkCommand(cid, name, arg): + if hasattr(arg, 'to_json'): + jarg = arg.to_json() + else: + jarg = arg + return SUITCommand().from_json({ + 'component-id' : cid.to_json(), + 'command-id' : name, + 'command-arg' : jarg + }) + +def check_eq(ids, choices): + eq = {} + neq = {} + + check = lambda x: x[:-1]==x[1:] + get = lambda k, l: [d.get(k) for d in l] + eq = { k: ids[k] for k in ids if any([k in c for c in choices]) and check(get(k, choices)) } + check = lambda x: not x[:-1]==x[1:] + neq = { k: ids[k] for k in ids if any([k in c for c in choices]) and check(get(k, choices)) } + return eq, neq + + +def make_sequence(cid, choices, seq, params, cmds, pcid_key=None, param_drctv='directive-set-parameters'): + eqcmds, neqcmds = check_eq(cmds, choices) + eqparams, neqparams = check_eq(params, choices) + if not pcid_key: + pcid = cid + else: + pcid = SUITComponentId().from_json(choices[0][pcid_key]) + params = {} + for param, pcmd in eqparams.items(): + k,v = pcmd(pcid, choices[0]) + params[k] = v + if len(params): + seq.append(mkCommand(pcid, param_drctv, params)) + TryEachCmd = SUITTryEach() + for c in choices: + TECseq = SUITSequence() + for item, cmd in neqcmds.items(): + TECseq.append(cmd(cid, c)) + params = {} + for param, pcmd in neqparams.items(): + k,v = pcmd(cid, c) + params[k] = v + if len(params): + TECseq.append(mkCommand(pcid, param_drctv, params)) + if len(TECseq.items): + TryEachCmd.append(TECseq) + if len(TryEachCmd.items): + seq.append(mkCommand(cid, 'directive-try-each', TryEachCmd)) + # Finally, and equal commands + for item, cmd in eqcmds.items(): + seq.append(cmd(cid, choices[0])) + return seq + +def compile_manifest(options, m): + m = copy.deepcopy(m) + m['components'] += options.components + # Compile list of All Component IDs + ids = set([ + SUITComponentId().from_json(id) for comp_ids in [ + [c[f] for f in [ + 'install-id', 'download-id', 'load-id' + ] if f in c] for c in m['components'] + ] for id in comp_ids + ]) + cid_data = {} + for c in m['components']: + if not 'install-id' in c: + LOG.critical('install-id required for all components') + raise Exception('No install-id') + + cid = SUITComponentId().from_json(c['install-id']) + if not cid in cid_data: + cid_data[cid] = [c] + else: + cid_data[cid].append(c) + + for id, choices in cid_data.items(): + for c in choices: + if 'file' in c: + digest, imgsize = hash_file(c['file'], hashes.SHA256()) + c['install-digest'] = { + 'algorithm-id' : 'sha256', + 'digest-bytes' : binascii.b2a_hex(digest.finalize()) + } + c['install-size'] = imgsize + + if not any(c.get('vendor-id', None) for c in m['components']): + LOG.critical('A vendor-id is required for at least one component') + raise Exception('No Vendor ID') + + if not any(c.get('class-id', None) for c in m['components'] if 'vendor-id' in c): + LOG.critical('A class-id is required for at least one component that also has a vendor-id') + raise Exception('No Class ID') + + # Construct common sequence + CommonCmds = { + 'offset': lambda cid, data: mkCommand(cid, 'condition-component-offset', data['offset']) + } + CommonParams = { + 'install-digest': lambda cid, data: ('image-digest', data['install-digest']), + 'install-size': lambda cid, data: ('image-size', data['install-size']), + } + CommonSeq = SUITSequence() + for cid, choices in cid_data.items(): + if any(['vendor-id' in c for c in choices]): + CommonSeq.append(mkCommand(cid, 'condition-vendor-identifier', + [c['vendor-id'] for c in choices if 'vendor-id' in c][0])) + if any(['vendor-id' in c for c in choices]): + CommonSeq.append(mkCommand(cid, 'condition-class-identifier', + [c['class-id'] for c in choices if 'class-id' in c][0])) + CommonSeq = make_sequence(cid, choices, CommonSeq, CommonParams, + CommonCmds, param_drctv='directive-override-parameters') + + InstSeq = SUITSequence() + FetchSeq = SUITSequence() + for cid, choices in cid_data.items(): + if any([c.get('install-on-download', True) and 'uri' in c for c in choices]): + InstParams = { + 'uri' : lambda cid, data: ('uri', data['uri']), + } + if any(['compression-info' in c and not c.get('decompress-on-load', False) for c in choices]): + InstParams['compression-info'] = lambda cid, data: data.get('compression-info') + InstCmds = { + 'offset': lambda cid, data: mkCommand( + cid, 'condition-component-offset', data['offset']) + } + InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds) + InstSeq.append(mkCommand(cid, 'directive-fetch', None)) + InstSeq.append(mkCommand(cid, 'condition-image-match', None)) + + elif any(['uri' in c for c in choices]): + FetchParams = { + 'uri' : lambda cid, data: ('uri', data['uri']), + 'download-digest' : lambda cid, data : ( + 'image-digest', data.get('download-digest', data['install-digest'])) + } + if any(['compression-info' in c and not c.get('decompress-on-load', False) for c in choices]): + FetchParams['compression-info'] = lambda cid, data: data.get('compression-info') + + FetchCmds = { + 'offset': lambda cid, data: mkCommand( + cid, 'condition-component-offset', data['offset']), + 'fetch' : lambda cid, data: mkCommand( + data.get('download-id', cid.to_json()), 'directive-fetch', None), + 'match' : lambda cid, data: mkCommand( + data.get('download-id', cid.to_json()), 'condition-image-match', None) + } + FetchSeq = make_sequence(cid, choices, FetchSeq, FetchParams, FetchCmds, 'download-id') + + InstParams = { + 'download-id' : lambda cid, data : ('source-component', data['download-id']) + } + InstCmds = { + } + InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds) + InstSeq.append(mkCommand(cid, 'directive-copy', None)) + InstSeq.append(mkCommand(cid, 'condition-image-match', None)) + + # TODO: Dependencies + # If there are dependencies + # Construct dependency resolution step + + ValidateSeq = SUITSequence() + RunSeq = SUITSequence() + LoadSeq = SUITSequence() + # If any component is marked bootable + for cid, choices in cid_data.items(): + if any([c.get('bootable', False) for c in choices]): + # TODO: Dependencies + # If there are dependencies + # Verify dependencies + # Process dependencies + ValidateSeq.append(mkCommand(cid, 'condition-image-match', None)) + + if any(['loadable' in c for c in choices]): + # Generate image load section + LoadParams = { + 'install-id' : lambda cid, data : ('source-component', c['install-id']), + 'load-digest' : ('image-digest', c.get('load-digest', c['install-digest'])), + 'load-size' : ('image-size', c.get('load-size', c['install-size'])) + } + if 'compression-info' in c and c.get('decompress-on-load', False): + LoadParams['compression-info'] = lambda cid, data: ('compression-info', c['compression-info']) + LoadCmds = { + # Move each loadable component + } + load_id = SUITComponentId().from_json(choices[0]['load-id']) + LoadSeq = make_sequence(load_id, choices, ValidateSeq, LoadParams, LoadCmds) + LoadSeq.append(mkCommand(load_id, 'directive-copy', None)) + LoadSeq.append(mkCommand(load_id, 'condition-image-match', None)) + + # Generate image invocation section + bootable_components = [x for x in m['components'] if x.get('bootable')] + if len(bootable_components) == 1: + c = bootable_components[0] + RunSeq.append(SUITCommand().from_json({ + 'component-id' : runable_id(c), + 'command-id' : 'directive-run', + 'command-arg' : None + })) + else: + t = [] + for c in bootable_components: + pass + # TODO: conditions + # t.append( + # + # ) + #TODO: Text + common = SUITCommon().from_json({ + 'components': [id.to_json() for id in ids], + 'common-sequence': CommonSeq.to_json(), + }) + + jmanifest = { + 'manifest-version' : m['manifest-version'], + 'manifest-sequence-number' : m['manifest-sequence-number'], + 'common' : common.to_json() + } + + jmanifest.update({k:v for k,v in { + 'payload-fetch' : FetchSeq.to_json(), + 'install' : InstSeq.to_json(), + 'validate' : ValidateSeq.to_json(), + 'run' : RunSeq.to_json(), + 'load' : LoadSeq.to_json() + }.items() if v}) + + wrapped_manifest = SUITWrapper().from_json({'manifest' : jmanifest}) + return wrapped_manifest diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py new file mode 100644 index 0000000000..a930a3c914 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +from suit_tool.compile import compile_manifest +import json +import cbor +import itertools +import textwrap + +def main(options): + m = json.loads(options.input_file.read()) + + nm = compile_manifest(options, m) + if hasattr(options, 'severable') and options.severable: + nm = nm.to_severable() + output = { + 'suit' : lambda x: cbor.dumps(x.to_suit(), sort_keys=True), + 'suit-debug' : lambda x: '\n'.join(itertools.chain.from_iterable( + map(textwrap.wrap, x.to_debug('').split('\n')) + )).encode('utf-8'), + 'json' : lambda x : json.dumps(x.to_json(), indent=2).encode('utf-8') + }.get(options.format)(nm) + options.output_file.write(output) + + return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py new file mode 100644 index 0000000000..6993044a68 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import textwrap + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization as ks + +def main(options): + private_key = ks.load_pem_private_key( + options.private_key.read(), + password=None, + backend=default_backend() + ) + #public_numbers = private_key.public_key().public_numbers() + #x = public_numbers.x + #y = public_numbers.y + #uecc_bytes = x.to_bytes( + # (x.bit_length() + 7) // 8, byteorder='big' + #) + y.to_bytes( + # (y.bit_length() + 7) // 8, byteorder='big' + #) + #uecc_c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( + # ', '.join(['{:0=#4x}'.format(x) for x in uecc_bytes]), + # 76 + #) + public_bytes = private_key.public_key().public_bytes( + encoding=ks.Encoding.Raw, + format=ks.PublicFormat.Raw + ) + + c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( + ', '.join(['{:0=#4x}'.format(x) for x in public_bytes]), + 76 + ) + print('\n '.join(c_def) + '\n};') + return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py new file mode 100644 index 0000000000..57760f5057 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py @@ -0,0 +1,688 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import collections +import binascii +import cbor +import copy +import uuid +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes + +ManifestKey = collections.namedtuple( + 'ManifestKey', + [ + 'json_key', + 'suit_key', + 'obj' + ] +) +def to_bytes(s): + try: + return binascii.a2b_hex(s) + except: + try: + return binascii.a2b_base64(s) + except: + if isinstance(s,str): + return s.encode('utf-8') + elif isinstance(s,bytes): + return s + else: + return str(s).encode('utf-8') + +class SUITCommonInformation: + def __init__(self): + self.component_ids = [] + self.current_index = 0 + self.indent_size = 4 + def component_id_to_index(self, cid): + id = -1 + for i, c in enumerate(self.component_ids): + if c == cid and i >= 0: + id = i + return id + +suitCommonInfo = SUITCommonInformation() +one_indent = ' ' + +class SUITInt: + def from_json(self, v): + self.v = int(v) + return self + def to_json(self): + return self.v + def from_suit(self, v): + self.v = int(v) + return self + def to_suit(self): + return self.v + def to_debug(self, indent): + return str(self.v) + +class SUITPosInt(SUITInt): + def from_json(self, v): + _v = int(v) + if _v < 0: + raise Exception('Positive Integers must be >= 0') + self.v = _v + return self + def from_suit(self, v): + return self.from_json(v) + +class SUITManifestDict: + def mkfields(d): + # rd = {} + return {k: ManifestKey(*v) for k,v in d.items()} + + def __init__(self): + pass + def from_json(self, data): + for k, f in self.fields.items(): + v = data.get(f.json_key, None) + setattr(self, k, f.obj().from_json(v) if v is not None else None) + return self + + def to_json(self): + j = {} + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + j[f.json_key] = v.to_json() + return j + + def from_suit(self, data): + for k, f in self.fields.items(): + v = data.get(f.suit_key, None) + d = f.obj().from_suit(v) if v is not None else None + setattr(self, k, d) + return self + + def to_suit(self): + sd = {} + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + sd[f.suit_key] = v.to_suit() + return sd + def to_debug(self, indent): + s = '{' + newindent = indent + one_indent + + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + s += '\n{ind}/ {jk} / {sk}:'.format(ind=newindent, jk=f.json_key, sk=f.suit_key) + s += v.to_debug(newindent) + ',' + s += '\n' + indent + '}' + return s + + +class SUITManifestNamedList(SUITManifestDict): + def from_suit(self, data): + for k, f in self.fields.items(): + setattr(self, k, f.obj().from_suit(data[f.suit_key])) + return self + + def to_suit(self): + sd = [None] * (max([f.suit_key for k, f in self.fields.items()]) + 1) + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + sd[f.suit_key] = v.to_suit() + return sd + def to_debug(self, indent): + newindent = indent + one_indent + items = [] + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + items.append('/ ' + f.json_key + ' / ' + v.to_debug(newindent)) + s = '[\n{newindent}{items}\n{indent}]'.format( + newindent=newindent, + indent=indent, + items=',\n{newindent}'.format(newindent=newindent).join(items) + ) + return s + +class SUITKeyMap: + def mkKeyMaps(m): + return {v:k for k,v in m.items()}, m + def to_json(self): + return self.rkeymap[self.v] + def from_json(self, d): + self.v = self.keymap[d] + return self + def to_suit(self): + return self.v + def from_suit(self, d): + self.v = self.keymap[self.rkeymap[d]] + return self + def to_debug(self, indent): + s = str(self.v) + ' / ' + self.to_json() + ' /' + return s + +def SUITBWrapField(c): + class SUITBWrapper: + def to_suit(self): + return cbor.dumps(self.v.to_suit(), sort_keys=True) + def from_suit(self, d): + self.v = c().from_suit(cbor.loads(d)) + return self + def to_json(self): + return self.v.to_json() + def from_json(self, d): + self.v = c().from_json(d) + return self + def to_debug(self, indent): + s = 'h\'' + s += binascii.b2a_hex(self.to_suit()).decode('utf-8') + s += '\' / ' + s += self.v.to_debug(indent) + s += ' /' + return s + + return SUITBWrapper + +class SUITManifestArray: + def __init__(self): + self.items=[] + def __eq__(self, rhs): + if len(self.items) != len(rhs.items): + return False + for a,b in zip(self.items, rhs.items): + if not a == b: + return False + return True + + def from_json(self, data): + self.items = [] + for d in data: + self.items.append(self.field.obj().from_json(d)) + return self + + def to_json(self): + j = [] + for i in self.items: + j.append(i.to_json()) + return j + + def from_suit(self, data): + self.items = [] + for d in data: + self.items.append(self.field.obj().from_suit(d)) + return self + + def to_suit(self): + l = [] + for i in self.items: + l.append(i.to_suit()) + return l + + def append(self, element): + if not isinstance(element, self.field.obj): + raise Exception('element {} is not a {}'.format(element, self.field.obj)) + self.items.append(element) + + def to_debug(self, indent): + newindent = indent + one_indent + s = '[\n' + s += ' ,\n'.join([newindent + v.to_debug(newindent) for v in self.items]) + s += '\n' + indent + ']' + return s +class SUITBytes: + def to_json(self): + return binascii.b2a_hex(self.v).decode('utf-8') + def from_json(self, d): + self.v = to_bytes(d) + return self + def from_suit(self, d): + self.v = bytes(d) + return self + def to_suit(self): + return self.v + def to_debug(self, indent): + return 'h\'' + self.to_json() + '\'' + def __eq__(self, rhs): + return self.v == rhs.v + +class SUITUUID(SUITBytes): + def from_json(self, d): + self.v = uuid.UUID(d).bytes + return self + def from_suit(self, d): + self.v = uuid.UUID(bytes=d).bytes + return self + def to_debug(self, indent): + return 'h\'' + self.to_json() + '\' / ' + str(uuid.UUID(bytes=self.v)) + ' /' + + +class SUITRaw: + def to_json(self): + return self.v + def from_json(self, d): + self.v = d + return self + def to_suit(self): + return self.v + def from_suit(self, d): + self.v = d + return self + def to_debug(self, indent): + return str(self.v) + +class SUITNil: + def to_json(self): + return None + def from_json(self, d): + if d is not None: + raise Exception('Expected Nil') + return self + def to_suit(self): + return None + def from_suit(self, d): + if d is not None: + raise Exception('Expected Nil') + return self + def to_debug(self, indent): + return 'F6 / nil /' + +class SUITTStr(SUITRaw): + def from_json(self, d): + self.v = str(d) + return self + def from_suit(self, d): + self.v = str(d) + return self + def to_debug(self, indent): + return '\''+ str(self.v) + '\'' + +class SUITComponentId(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITBytes) + def to_debug(self, indent): + newindent = indent + one_indent + s = '[' + ''.join([v.to_debug(newindent) for v in self.items]) + ']' + return s + def __hash__(self): + return hash(tuple([i.v for i in self.items])) + +class SUITComponentIndex(SUITComponentId): + def to_suit(self): + return suitCommonInfo.component_id_to_index(self) + def from_suit(self, d): + return super(SUITComponentIndex, self).from_suit( + suitCommonInfo.component_ids[d].to_suit() + ) + def to_debug(self, indent): + newindent = indent + one_indent + s = '{suit} / [{dbg}] /'.format( + suit=self.to_suit(), + dbg=''.join([v.to_debug(newindent) for v in self.items]) + ) + return s + + +class SUITComponents(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITComponentId) + + def from_suit(self, data): + super(SUITComponents, self).from_suit(data) + suitCommonInfo.component_ids = self.items + return self + + def from_json(self, j): + super(SUITComponents, self).from_json(j) + suitCommonInfo.component_ids = self.items + return self + +class SUITDigestAlgo(SUITKeyMap): + rkeymap, keymap = SUITKeyMap.mkKeyMaps({ + 'sha224' : 1, + 'sha256' : 2, + 'sha384' : 3, + 'sha512' : 4 + }) + +class SUITDigest(SUITManifestNamedList): + fields = SUITManifestNamedList.mkfields({ + 'algo' : ('algorithm-id', 0, SUITDigestAlgo), + 'digest' : ('digest-bytes', 1, SUITBytes) + }) + +class SUITCompressionInfo(SUITKeyMap): + rkeymap, keymap = SUITKeyMap.mkKeyMaps({ + 'gzip' : 1, + 'bzip2' : 2, + 'deflate' : 3, + 'lz4' : 4, + 'lzma' : 7 + }) + +class SUITParameters(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'digest' : ('image-digest', 11, SUITDigest), + 'size' : ('image-size', 12, SUITPosInt), + 'uri' : ('uri', 6, SUITTStr), + 'src' : ('source-component', 10, SUITComponentIndex), + 'compress' : ('compression-info', 8, SUITCompressionInfo) + }) + def from_json(self, j): + return super(SUITParameters, self).from_json(j) + +class SUITTryEach(SUITManifestArray): + pass + +def SUITCommandContainer(jkey, skey, argtype): + class SUITCmd(SUITCommand): + json_key = jkey + suit_key = skey + def __init__(self): + pass + def to_suit(self): + return [self.suit_key, self.arg.to_suit()] + def to_json(self): + if self.json_key == 'directive-set-component-index': + return {} + else: + return { + 'command-id' : self.json_key, + 'command-arg' : self.arg.to_json(), + 'component-id' : self.cid.to_json() + } + def from_json(self, j): + if j['command-id'] != self.json_key: + raise Except('JSON Key mismatch error') + if self.json_key != 'directive-set-component-index': + self.cid = SUITComponentId().from_json(j['component-id']) + self.arg = argtype().from_json(j['command-arg']) + return self + def from_suit(self, s): + if s[0] != self.suit_key: + raise Except('SUIT Key mismatch error') + if self.json_key == 'directive-set-component-index': + suitCommonInfo.current_index = s[1] + else: + self.cid = suitCommonInfo.component_ids[suitCommonInfo.current_index] + self.arg = argtype().from_suit(s[1]) + return self + def to_debug(self, indent): + s = '/ {} / {},'.format(self.json_key, self.suit_key) + s += self.arg.to_debug(indent) + return s + return SUITCmd + + +class SUITCommand: + def from_json(self, j): + return self.jcommands[j['command-id']]().from_json(j) + def from_suit(self, s): + return self.scommands[s[0]]().from_suit(s) + +SUITCommand.commands = [ + SUITCommandContainer('condition-vendor-identifier', 1, SUITUUID), + SUITCommandContainer('condition-class-identifier', 2, SUITUUID), + SUITCommandContainer('condition-image-match', 3, SUITNil), + SUITCommandContainer('condition-use-before', 4, SUITRaw), + SUITCommandContainer('condition-component-offset', 5, SUITRaw), + SUITCommandContainer('condition-custom', 6, SUITRaw), + SUITCommandContainer('condition-device-identifier', 24, SUITRaw), + SUITCommandContainer('condition-image-not-match', 25, SUITRaw), + SUITCommandContainer('condition-minimum-battery', 26, SUITRaw), + SUITCommandContainer('condition-update-authorised', 27, SUITRaw), + SUITCommandContainer('condition-version', 28, SUITRaw), + SUITCommandContainer('directive-set-component-index', 12, SUITPosInt), + SUITCommandContainer('directive-set-dependency-index', 13, SUITRaw), + SUITCommandContainer('directive-abort', 14, SUITRaw), + SUITCommandContainer('directive-try-each', 15, SUITTryEach), + SUITCommandContainer('directive-process-dependency', 18, SUITRaw), + SUITCommandContainer('directive-set-parameters', 19, SUITParameters), + SUITCommandContainer('directive-override-parameters', 20, SUITParameters), + SUITCommandContainer('directive-fetch', 21, SUITNil), + SUITCommandContainer('directive-copy', 22, SUITRaw), + SUITCommandContainer('directive-run', 23, SUITRaw), + SUITCommandContainer('directive-wait', 29, SUITRaw), + SUITCommandContainer('directive-run-sequence', 30, SUITRaw), + SUITCommandContainer('directive-run-with-arguments', 31, SUITRaw), + SUITCommandContainer('directive-swap', 32, SUITRaw), +] +SUITCommand.jcommands = { c.json_key : c for c in SUITCommand.commands} +SUITCommand.scommands = { c.suit_key : c for c in SUITCommand.commands} + + +class SUITSequence(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITCommand) + def to_suit(self): + suit_l = [] + suitCommonInfo.current_index = 0 if len(suitCommonInfo.component_ids) == 1 else None + for i in self.items: + if i.json_key == 'directive-set-component-index': + suitCommonInfo.current_index = i.arg.v + else: + cidx = suitCommonInfo.component_id_to_index(i.cid) + if cidx != suitCommonInfo.current_index: + # Change component + cswitch = SUITCommand().from_json({ + 'command-id' : 'directive-set-component-index', + 'command-arg' : cidx + }) + suitCommonInfo.current_index = cidx + suit_l += cswitch.to_suit() + suit_l += i.to_suit() + return suit_l + def to_debug(self, indent): + return super(SUITSequence, SUITSequence().from_suit(self.to_suit())).to_debug(indent) + def from_suit(self, s): + self.items = [SUITCommand().from_suit(i) for i in zip(*[iter(s)]*2)] + return self + +SUITTryEach.field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITSequence) + +class SUITSequenceComponentReset(SUITSequence): + def to_suit(self): + suitCommonInfo.current_index = None + return super(SUITSequenceComponentReset, self).to_suit() + +def SUITMakeSeverableField(c): + class SUITSeverableField: + objtype = SUITBWrapField(c) + def from_json(self, data): + if 'algorithm-id' in data: + self.v = SUITDigest().from_json(data) + else: + self.v = self.objtype().from_json(data) + return self + def from_suit(self, data): + if isinstance(data, list): + self.v = SUITDigest().from_suit(data) + else: + self.v = self.objtype().from_suit(data) + return self + def to_json(self): + return self.v.to_json() + def to_suit(self): + return self.v.to_suit() + def to_debug(self, indent): + return self.v.to_debug(indent) + return SUITSeverableField +# class SUITSequenceOrDigest() + +class SUITCommon(SUITManifestDict): + fields = SUITManifestNamedList.mkfields({ + # 'dependencies' : ('dependencies', 1, SUITBWrapField(SUITDependencies)), + 'components' : ('components', 2, SUITBWrapField(SUITComponents)), + # 'dependency_components' : ('dependency-components', 3, SUITBWrapField(SUITDependencies)), + 'common_sequence' : ('common-sequence', 4, SUITBWrapField(SUITSequenceComponentReset)), + }) + + +class SUITManifest(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'version' : ('manifest-version', 1, SUITPosInt), + 'sequence' : ('manifest-sequence-number', 2, SUITPosInt), + 'common' : ('common', 3, SUITBWrapField(SUITCommon)), + 'deres' : ('dependency-resolution', 7, SUITMakeSeverableField(SUITSequenceComponentReset)), + 'fetch' : ('payload-fetch', 8, SUITMakeSeverableField(SUITSequenceComponentReset)), + 'install' : ('install', 9, SUITMakeSeverableField(SUITSequenceComponentReset)), + 'validate' : ('validate', 10, SUITBWrapField(SUITSequenceComponentReset)), + 'load' : ('load', 11, SUITBWrapField(SUITSequenceComponentReset)), + 'run' : ('run', 12, SUITBWrapField(SUITSequenceComponentReset)), + }) + +class COSE_Algorithms(SUITKeyMap): + rkeymap, keymap = SUITKeyMap.mkKeyMaps({ + 'ES256' : -7, + 'ES384' : -35, + 'ES512' : -36, + 'EdDSA' : -8, + 'HSS-LMS' : -46, + }) + +class COSE_CritList(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITInt) + +class COSE_header_map(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + # 1: algorithm Identifier + 'alg' : ('alg', 1, COSE_Algorithms), + # 2: list of critical headers (criticality) + # 3: content type + # 4: key id + 'kid' : ('kid', 4, SUITBytes), + # 5: IV + # 6: partial IV + # 7: counter signature(s) + }) + +class COSE_Sign: + pass +class COSE_Sign1(SUITManifestNamedList): + fields = SUITManifestDict.mkfields({ + 'protected' : ('protected', 0, SUITBWrapField(COSE_header_map)), + 'unprotected' : ('unprotected', 1, COSE_header_map), + 'payload' : ('payload', 2, SUITBWrapField(SUITDigest)), + 'signature' : ('signature', 3, SUITBytes) + }) +class COSE_Mac: + pass +class COSE_Mac0: + pass + +class COSETagChoice(SUITManifestDict): + def to_suit(self): + for k, f in self.fields.items(): + v = getattr(self, k, None) + if v: + return cbor.Tag(tag=f.suit_key, value=v.to_suit()) + return None + + def from_suit(self, data): + for k, f in self.fields.items(): + if data.tag == f.suit_key: + v = data.value + d = f.obj().from_suit(v) if v is not None else None + setattr(self, k, d) + return self + + + def to_debug(self, indent): + s = '' + for k, f in self.fields.items(): + if hasattr(self, k): + v = getattr(self, k) + newindent = indent + one_indent + s = '{tag}({value})'.format(tag=f.suit_key, value=v.to_debug(newindent)) + return s + +class COSETaggedAuth(COSETagChoice): + fields = SUITManifestDict.mkfields({ + 'cose_sign' : ('COSE_Sign_Tagged', 98, COSE_Sign), + 'cose_sign1' : ('COSE_Sign1_Tagged', 18, COSE_Sign1), + 'cose_mac' : ('COSE_Mac_Tagged', 97, COSE_Mac), + 'cose_mac0' : ('COSE_Mac0_Tagged', 17, COSE_Mac0) + }) + +class COSEList(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=COSETaggedAuth) + def from_suit(self, data): + return super(COSEList, self).from_suit(data) + +class SUITWrapper(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'auth' : ('authentication-wrapper', 2, SUITBWrapField(COSEList)), + 'manifest' : ('manifest', 3, SUITBWrapField(SUITManifest)), + 'deres': ('dependency-resolution', 7, SUITBWrapField(SUITSequence)), + 'fetch': ('payload-fetch', 8, SUITBWrapField(SUITSequence)), + 'install': ('install', 9, SUITBWrapField(SUITSequence)), + 'validate': ('validate', 10, SUITBWrapField(SUITSequence)), + 'load': ('load', 11, SUITBWrapField(SUITSequence)), + 'run': ('run', 12, SUITBWrapField(SUITSequence)), + # 'text': ('text', 13, SUITBWrapField(SUITSequence)), + }) + severable_fields = {'deres', 'fetch', 'install'} #, 'text'} + digest_algorithms = { + 'sha224' : hashes.SHA224, + 'sha256' : hashes.SHA256, + 'sha384' : hashes.SHA384, + 'sha512' : hashes.SHA512 + } + def to_severable(self, digest_alg): + sev = copy.deepcopy(self) + for k in sev.severable_fields: + f = sev.manifest.v.fields[k] + if not hasattr(sev.manifest.v, k): + continue + v = getattr(sev.manifest.v, k) + if v is None: + continue + cbor_field = cbor.dumps(v.to_suit(), sort_keys=True) + digest = hashes.Hash(digest_algorithms.get(digest_alg)(), backend=default_backend()) + digest.update(cbor_field) + field_digest = SUITDigest().from_json({ + 'algorithm-id' : digest_alg, + 'digest-bytes' : digest.finalize() + }) + cbor_digest = cbor.dumps(field_digest.to_suit(), sort_keys=True) + if len(cbor_digest) < len(cbor_field): + setattr(sev.manifest.v, k, field_digest) + setattr(sev,k,v) + return sev + def from_severable(self): + raise Exception('From Severable unimplemented') + nsev = copy.deepcopy(self) + for k in nsev.severable_fields: + f = nsev.fields[k] + if not hasattr(nsev, k): + continue + v = getattr(nsev, k) + if v is None: + continue + # Verify digest + cbor_field = cbor.dumps(v.to_suit(), sort_keys=True) + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(cbor_field) + actual_digest = digest.finalize() + field_digest = getattr(sev.nsev.v, k) + expected_digest = field_digest.to_suit()[1] + if digest != expected_digest: + raise Exception('Field Digest mismatch: For {}, expected: {}, got {}'.format( + f.json_key, expected_digest, actual_digest + )) + setattr(nsev.manifest.v, k, v) + delattr(nsev, k) + return nsev diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py new file mode 100644 index 0000000000..9544778d64 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import cbor +import json +import itertools +import textwrap + +from suit_tool.manifest import SUITWrapper + +def main(options): + # Read the manifest wrapper + decoded_cbor_wrapper = cbor.loads(options.manifest.read()) + wrapper = SUITWrapper().from_suit(decoded_cbor_wrapper) + if options.json: + print (json.dumps(wrapper.to_json(),indent=2)) + else: + print ('\n'.join(itertools.chain.from_iterable( + [textwrap.wrap(t, 70) for t in wrapper.to_debug('').split('\n')] + ))) + return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py new file mode 100644 index 0000000000..f573331374 --- /dev/null +++ b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py @@ -0,0 +1,111 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import cbor + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.asymmetric import utils as asymmetric_utils +from cryptography.hazmat.primitives import serialization as ks + + +from suit_tool.manifest import COSE_Sign1, COSEList, \ + SUITWrapper, SUITBytes, SUITBWrapField +import logging +import binascii +LOG = logging.getLogger(__name__) + +def get_cose_es_bytes(private_key, sig_val): + ASN1_signature = private_key.sign(sig_val, ec.ECDSA(hashes.SHA256())) + r,s = asymmetric_utils.decode_dss_signature(ASN1_signature) + ssize = private_key.key_size + signature_bytes = r.to_bytes(ssize//8, byteorder='big') + s.to_bytes(ssize//8, byteorder='big') + return signature_bytes + +def get_cose_ed25519_bytes(private_key, sig_val): + return private_key.sign(sig_val) + +def main(options): + # Read the manifest wrapper + wrapper = cbor.loads(options.manifest.read()) + + private_key = None + digest = None + try: + private_key = ks.load_pem_private_key(options.private_key.read(), password=None, backend=default_backend()) + if isinstance(private_key, ec.EllipticCurvePrivateKey): + options.key_type = 'ES{}'.format(private_key.key_size) + elif isinstance(private_key, ed25519.Ed25519PrivateKey): + options.key_type = 'EdDSA' + else: + LOG.critical('Unrecognized key: {}'.format(type(private_key).__name__)) + return 1 + digest = { + 'ES256' : hashes.Hash(hashes.SHA256(), backend=default_backend()), + 'ES384' : hashes.Hash(hashes.SHA384(), backend=default_backend()), + 'ES512' : hashes.Hash(hashes.SHA512(), backend=default_backend()), + 'EdDSA' : hashes.Hash(hashes.SHA256(), backend=default_backend()), + }.get(options.key_type) + except: + digest= hashes.Hash(hashes.SHA256(), backend=default_backend()) + # private_key = None + # TODO: Implement loading of DSA keys not supported by python cryptography + LOG.critical('Non-library key type not implemented') + # return 1 + + digest.update(cbor.dumps(wrapper[SUITWrapper.fields['manifest'].suit_key])) + + cose_signature = COSE_Sign1().from_json({ + 'protected' : { + 'alg' : options.key_type + }, + 'unprotected' : {}, + 'payload' : { + 'algorithm-id' : 'sha256', + 'digest-bytes' : binascii.b2a_hex(digest.finalize()) + } + }) + + Sig_structure = cbor.dumps([ + "Signature1", + cose_signature.protected.to_suit(), + b'', + cose_signature.payload.to_suit(), + ], sort_keys = True) + sig_val = Sig_structure + + signature_bytes = { + 'ES256' : get_cose_es_bytes, + 'ES384' : get_cose_es_bytes, + 'ES512' : get_cose_es_bytes, + 'EdDSA' : get_cose_ed25519_bytes, + }.get(options.key_type)(private_key, sig_val) + + cose_signature.signature = SUITBytes().from_suit(signature_bytes) + + auth = SUITBWrapField(COSEList)().from_json([{ + 'COSE_Sign1_Tagged' : cose_signature.to_json() + }]) + + wrapper[SUITWrapper.fields['auth'].suit_key] = auth.to_suit() + + options.output_file.write(cbor.dumps(wrapper, sort_keys=True)) + return 0