diff --git a/Makefile.dep b/Makefile.dep index 867d4615ac..6c07ef3a22 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -953,25 +953,29 @@ ifneq (,$(filter sock_dtls, $(USEMODULE))) USEMODULE += sock_udp endif -ifneq (,$(filter suit_v4_%,$(USEMODULE))) - USEMODULE += suit_v4 -endif - -ifneq (,$(filter suit_v4,$(USEMODULE))) +ifneq (,$(filter suit,$(USEMODULE))) USEPKG += nanocbor USEPKG += libcose USEMODULE += libcose_crypt_c25519 - USEMODULE += suit_conditions + USEMODULE += uuid - # SUIT depends on riotboot support and some extra riotboot modules - FEATURES_REQUIRED += riotboot - USEMODULE += riotboot_slot - USEMODULE += riotboot_flashwrite - USEMODULE += riotboot_flashwrite_verify_sha256 + # tests/suit_manifest has some mock implementations, + # only add the non-mock dependencies if not building that test. + ifeq (,$(filter suit_transport_mock,$(USEMODULE))) + # SUIT depends on riotboot support and some extra riotboot modules + FEATURES_REQUIRED += riotboot + USEMODULE += riotboot_slot + USEMODULE += riotboot_flashwrite + USEMODULE += riotboot_flashwrite_verify_sha256 + endif endif -ifneq (,$(filter suit_conditions,$(USEMODULE))) - USEMODULE += uuid +ifneq (,$(filter suit_transport_%, $(USEMODULE))) + USEMODULE += suit_transport +endif + +ifneq (,$(filter suit_transport_coap, $(USEMODULE))) + USEMODULE += nanocoap endif ifneq (,$(filter suit_%,$(USEMODULE))) diff --git a/Makefile.include b/Makefile.include index 57a430ba6e..3de9e9a98f 100644 --- a/Makefile.include +++ b/Makefile.include @@ -480,6 +480,9 @@ ELFFILE ?= $(BINDIR)/$(APPLICATION).elf HEXFILE ?= $(ELFFILE:.elf=.hex) BINFILE ?= $(ELFFILE:.elf=.bin) +# include basic suit-tool and key handling +include $(RIOTMAKE)/suit.base.inc.mk + # include bootloaders support. It should be included early to allow using # variables defined in `riotboot.mk` for `FLASHFILE` before it is evaluated. # It should be included after defining 'BINFILE' for 'riotboot.bin' handling. diff --git a/dist/tools/flake8/check.sh b/dist/tools/flake8/check.sh index 1595fd7931..8c7cd4db35 100755 --- a/dist/tools/flake8/check.sh +++ b/dist/tools/flake8/check.sh @@ -28,7 +28,7 @@ EXCLUDE="^(.+/vendor/\ |dist/tools/mcuboot\ |dist/tools/uhcpd\ |dist/tools/stm32loader\ -|dist/tools/suit_v4/suit_manifest_encoder_04)\ +|dist/tools/suit_v3/suit-manifest-generator)\ |dist/tools/esptool" FILEREGEX='(\.py$|pyterm$)' FILES=$(FILEREGEX=${FILEREGEX} EXCLUDE=${EXCLUDE} changed_files) 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 diff --git a/dist/tools/suit_v4/gen_key.py b/dist/tools/suit_v4/gen_key.py deleted file mode 100755 index 1d940d3809..0000000000 --- a/dist/tools/suit_v4/gen_key.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/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 sys -import ed25519 - - -def main(): - if len(sys.argv) != 3: - print("usage: gen_key.py ") - sys.exit(1) - - _signing_key, _verifying_key = ed25519.create_keypair() - with open(sys.argv[1], "wb") as f: - f.write(_signing_key.to_bytes()) - - with open(sys.argv[2], "wb") as f: - f.write(_verifying_key.to_bytes()) - - vkey_hex = _verifying_key.to_ascii(encoding="hex") - print("Generated public key: '{}'".format(vkey_hex.decode())) - - -if __name__ == '__main__': - main() diff --git a/dist/tools/suit_v4/gen_manifest.py b/dist/tools/suit_v4/gen_manifest.py deleted file mode 100755 index d41a661c02..0000000000 --- a/dist/tools/suit_v4/gen_manifest.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/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 os -import hashlib -import json -import uuid -import argparse - -from suit_manifest_encoder_04 import compile_to_suit - - -def str2int(x): - if x.startswith("0x"): - return int(x, 16) - else: - return int(x) - - -def sha256_from_file(filepath): - sha256 = hashlib.sha256() - sha256.update(open(filepath, "rb").read()) - return sha256.digest() - - -def parse_arguments(): - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--template', '-t', help='Manifest template file path') - parser.add_argument('--urlroot', '-u', help='') - parser.add_argument('--offsets', '-O', help='') - parser.add_argument('--seqnr', '-s', - help='Sequence number of the manifest') - parser.add_argument('--output', '-o', nargs='?', - help='Manifest output binary file path') - parser.add_argument('--uuid-vendor', '-V', - help='Manifest vendor uuid') - parser.add_argument('--uuid-class', '-C', - help='Manifest class uuid') - parser.add_argument('slotfiles', nargs=2, - 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) - with open(args.template, 'r') as f: - template = json.load(f) - - template["sequence-number"] = int(args.seqnr) - template["conditions"] = [ - {"condition-vendor-id": uuid_vendor.hex}, - {"condition-class-id": uuid_class.hex}, - ] - - offsets = [str2int(offset) for offset in args.offsets.split(",")] - - for slot, slotfile in enumerate(args.slotfiles): - filename = slotfile - size = os.path.getsize(filename) - uri = os.path.join(args.urlroot, os.path.basename(filename)) - offset = offsets[slot] - - _image_slot = template["components"][0]["images"][slot] - _image_slot.update({ - "file": filename, - "uri": uri, - "size": size, - "digest": sha256_from_file(slotfile), - }) - - _image_slot["conditions"][0]["condition-component-offset"] = offset - _image_slot["file"] = filename - - result = compile_to_suit(template) - if args.output is not None: - with open(args.output, 'wb') as f: - f.write(result) - else: - print(result) - - -if __name__ == "__main__": - _args = parse_arguments() - main(_args) diff --git a/dist/tools/suit_v4/sign-04.py b/dist/tools/suit_v4/sign-04.py deleted file mode 100755 index db872b305f..0000000000 --- a/dist/tools/suit_v4/sign-04.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright 2018-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. -# ---------------------------------------------------------------------------- -""" -This is a demo script that is intended to act as a reference for SUIT manifest -signing. - -NOTE: It is expected that C and C++ parser implementations will be written -against this script, so it does not adhere to PEP8 in order to maintain -similarity between the naming in this script and that of C/C++ implementations. -""" - -import sys -import copy - -import cbor -import ed25519 - -from pprint import pprint - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives import serialization - -# Private key in arg 1 -# Public key in arg 2 -# Input file in arg 3 -# Output file in arg 4 - -COSE_Sign_Tag = 98 -APPLICATION_OCTET_STREAM_ID = 42 -ES256 = -7 -EDDSA = -8 - - -def signWrapper(algo, private_key, public_key, encwrapper): - wrapper = cbor.loads(encwrapper) - - pprint(wrapper[1]) - COSE_Sign = copy.deepcopy(wrapper[1]) - if not COSE_Sign: - protected = cbor.dumps({ - 3: APPLICATION_OCTET_STREAM_ID, # Content Type - }) - unprotected = { - } - signatures = [] - - # Create a COSE_Sign_Tagged object - COSE_Sign = [ - protected, - unprotected, - b'', - signatures - ] - - if algo == EDDSA: - public_bytes = public_key.to_bytes() - else: - public_bytes = public_key.public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo) - - # NOTE: Using RFC7093, Method 4 - digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) - digest.update(public_bytes) - kid = digest.finalize() - # Sign the payload - protected = cbor.dumps({ - 1: algo, # alg - }) - # Create the signing object - unprotected = { - 4: kid # kid - } - - Sig_structure = [ - "Signature", # Context - COSE_Sign[0], # Body Protected - protected, # signature protected - b'', # External AAD - wrapper[2] # payload - ] - sig_str = cbor.dumps(Sig_structure, sort_keys=True) - - if algo == EDDSA: - signature = private_key.sign(sig_str) - else: - signature = private_key.sign( - sig_str, - ec.ECDSA(hashes.SHA256()) - ) - - COSE_Signature = [ - protected, - unprotected, - signature - ] - COSE_Sign[3].append(COSE_Signature) - wrapper[1] = cbor.dumps(cbor.Tag(COSE_Sign_Tag, COSE_Sign), sort_keys=True) - return wrapper - - -def main(): - private_key = None - algo = ES256 - with open(sys.argv[1], 'rb') as fd: - priv_key_bytes = fd.read() - try: - private_key = serialization.load_pem_private_key( - priv_key_bytes, password=None, backend=default_backend()) - except ValueError: - algo = EDDSA - private_key = ed25519.SigningKey(priv_key_bytes) - - public_key = None - with open(sys.argv[2], 'rb') as fd: - pub_key_bytes = fd.read() - try: - public_key = serialization.load_pem_public_key( - pub_key_bytes, backend=default_backend()) - except ValueError: - public_key = ed25519.VerifyingKey(pub_key_bytes) - - # Read the input file - doc = None - with open(sys.argv[3], 'rb') as fd: - doc = fd.read() - - outDoc = signWrapper(algo, private_key, public_key, doc) - - with open(sys.argv[4], 'wb') as fd: - fd.write(cbor.dumps(outDoc, sort_keys=True)) - - -if __name__ == '__main__': - main() diff --git a/dist/tools/suit_v4/suit_manifest_encoder_04.py b/dist/tools/suit_v4/suit_manifest_encoder_04.py deleted file mode 100644 index f071f0412b..0000000000 --- a/dist/tools/suit_v4/suit_manifest_encoder_04.py +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env 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 json -import cbor -import binascii -import uuid -import os - -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives import serialization - -COSE_ALG = 1 -COSE_Sign_Tag = 98 -APPLICATION_OCTET_STREAM_ID = 42 -ES256 = -7 -EDDSA = -8 - -SUIT_Authentication_Wrapper = 1 -SUIT_Manifest = 2 -SUIT_Dependency_Resolution = 7 -SUIT_Payload_Fetch = 8 -SUIT_Install = 9 -SUIT_Text = 13 -SUIT_Coswid = 14 - -SUIT_Manifest_Version = 1 -SUIT_Manifest_Sequence_Number = 2 -SUIT_Dependencies = 3 -SUIT_Components = 4 -SUIT_Dependency_Components = 5 -SUIT_Common = 6 -SUIT_Dependency_Resolution = 7 -SUIT_Payload_Fetch = 8 -SUIT_Install = 9 -SUIT_Validate = 10 -SUIT_Load = 11 -SUIT_Run = 12 -SUIT_Text = 13 -SUIT_Coswid = 14 - -SUIT_Dependency_Digest = 1 -SUIT_Dependency_Prefix = 2 - -SUIT_Component_Identifier = 1 -SUIT_Component_Size = 2 -SUIT_Component_Digest = 3 - -SUIT_Component_Dependency_Index = 2 - -SUIT_Condition_Vendor_Identifier = 1 -SUIT_Condition_Class_Identifier = 2 -SUIT_Condition_Device_Identifier = 3 -SUIT_Condition_Image_Match = 4 -SUIT_Condition_Image_Not_Match = 5 -SUIT_Condition_Use_Before = 6 -SUIT_Condition_Minimum_Battery = 7 -SUIT_Condition_Update_Authorised = 8 -SUIT_Condition_Version = 9 -SUIT_Condition_Component_Offset = 10 - -SUIT_Directive_Set_Component_Index = 11 -SUIT_Directive_Set_Manifest_Index = 12 -SUIT_Directive_Run_Sequence = 13 -SUIT_Directive_Run_Sequence_Conditional = 14 -SUIT_Directive_Process_Dependency = 15 -SUIT_Directive_Set_Parameters = 16 -SUIT_Directive_Override_Parameters = 19 -SUIT_Directive_Fetch = 20 -SUIT_Directive_Copy = 21 -SUIT_Directive_Run = 22 -SUIT_Directive_Wait = 23 - -SUIT_Parameter_Strict_Order = 1 -SUIT_Parameter_Coerce_Condition_Failure = 2 -SUIT_Parameter_Vendor_ID = 3 -SUIT_Parameter_Class_ID = 4 -SUIT_Parameter_Device_ID = 5 -SUIT_Parameter_URI_List = 6 -SUIT_Parameter_Encryption_Info = 7 -SUIT_Parameter_Compression_Info = 8 -SUIT_Parameter_Unpack_Info = 9 -SUIT_Parameter_Source_Component = 10 -SUIT_Parameter_Image_Digest = 11 -SUIT_Parameter_Image_Size = 12 - -SUIT_Compression_Algorithm = 1 - -def obj2bytes(o): - if isinstance(o, int): - l = [] - while o: - l.append(o&0xff) - o = o >> 8 - return bytes(l) - if isinstance(o, str): - return o.encode('utf-8') - if isinstance(o, bytes): - return o - return b'' - -def make_SUIT_Components(unused, components): - comps = [] - for component in components: - c = { - SUIT_Component_Identifier : [obj2bytes(x) for x in component["id"]] - } - if "digest" in component: - c[SUIT_Component_Digest] = [1, binascii.a2b_hex(component["digest"])] - if "size" in component: - c[SUIT_Component_Size] = component["size"] - comps.append(c) - return (SUIT_Components, comps) - -def make_SUIT_Compression_Info(info): - algorithms = { - 'gzip' : 1, - 'bzip2' : 2, - 'deflate' : 3, - 'lz4' : 4, - 'lzma' : 7, - } - cinfo = { - SUIT_Compression_Algorithm :algorithms[info['algorithm']] - } - -def make_SUIT_Set_Parameters(parameters): - set_parameters = {} - SUIT_Parameters_Keys = { - # SUIT_Parameter_Strict_Order = 1 - # SUIT_Parameter_Coerce_Condition_Failure = 2 - # SUIT_Parameter_Vendor_ID = 3 - # SUIT_Parameter_Class_ID = 4 - # SUIT_Parameter_Device_ID = 5 - # SUIT_Parameter_URI_List = 6 - 'uris' : lambda x: (SUIT_Parameter_URI_List, cbor.dumps(x)), - # SUIT_Parameter_Encryption_Info = 7 - # SUIT_Parameter_Compression_Info = 8 - 'compression-info': lambda x : ( - SUIT_Parameter_Compression_Info, - cbor.dumps(make_SUIT_Compression_Info(x)) - ), - # SUIT_Parameter_Unpack_Info = 9 - 'source-index' : lambda x :(SUIT_Parameter_Source_Component, int(x)), - 'image-digest' : lambda x :(SUIT_Parameter_Image_Digest, cbor.dumps(x, sort_keys=True)), - 'image-size' : lambda x :(SUIT_Parameter_Image_Size, int(x)), - } - for p in parameters: - if p in SUIT_Parameters_Keys: - k, v = SUIT_Parameters_Keys[p](parameters[p]) - set_parameters[k] = v - else: - raise Exception('ERROR: {} not found!'.format(p)) - - return (SUIT_Directive_Set_Parameters, set_parameters) - -def make_SUIT_Sequence(seq_name, sequence): - seq = [] - SUIT_Sequence_Keys = { - "condition-vendor-id" : lambda x : (SUIT_Condition_Vendor_Identifier, uuid.UUID(x).bytes), - "condition-class-id" : lambda x : (SUIT_Condition_Class_Identifier, uuid.UUID(x).bytes), - "condition-device-id" : lambda x : (SUIT_Condition_Device_Identifier, uuid.UUID(x).bytes), - "condition-image" : lambda x : (SUIT_Condition_Image_Match, None), - "condition-not-image" : lambda x : (SUIT_Condition_Image_Not_Match, None), - # SUIT_Condition_Use_Before = 6 - # SUIT_Condition_Minimum_Battery = 7 - # SUIT_Condition_Update_Authorised = 8 - # SUIT_Condition_Version = 9 - "condition-component-offset" : lambda x: (SUIT_Condition_Component_Offset, int(x)), - # - "directive-set-component" : lambda x : (SUIT_Directive_Set_Component_Index, x), - # SUIT_Directive_Set_Manifest_Index = 12 - # SUIT_Directive_Run_Sequence = 13 - # SUIT_Directive_Run_Sequence_Conditional = 14 - "directive-run-conditional" : lambda x : ( - SUIT_Directive_Run_Sequence_Conditional, - cbor.dumps(make_SUIT_Sequence("conditional-sequence", x), sort_keys = True) - ), - # SUIT_Directive_Process_Dependency = 15 - # SUIT_Directive_Set_Parameters = 16 - "directive-set-var" : make_SUIT_Set_Parameters, - # SUIT_Directive_Override_Parameters = 19 - "directive-fetch" : lambda x : (SUIT_Directive_Fetch, None), - "directive-copy" : lambda x : (SUIT_Directive_Copy, None), - "directive-run" : lambda x : (SUIT_Directive_Run, None), - # SUIT_Directive_Wait = 23 - } - for command in sequence: - com_dict = {} - for c in command: - if c in SUIT_Sequence_Keys: - k, v = SUIT_Sequence_Keys[c](command[c]) - com_dict[k] = v - else: - raise Exception('ERROR: {} not found!'.format(c)) - seq.append(com_dict) - return seq - -def make_SUIT_Manifest(info): - # print(info) - SUIT_Manifest_Keys = { - "structure-version" : lambda y, x: (SUIT_Manifest_Version, x), - "sequence-number" : lambda y, x: (SUIT_Manifest_Sequence_Number, x), - # SUIT_Dependencies = 3 - "components" : make_SUIT_Components, - # SUIT_Dependency_Components = 5 - "common" : lambda y, x: (SUIT_Common, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), - # SUIT_Dependency_Resolution = 7 - # SUIT_Payload_Fetch = 8 - "apply-image" : lambda y, x: (SUIT_Install, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), - "system-verification": lambda y, x: (SUIT_Validate, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), - "load-image" : lambda y, x: (SUIT_Load, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), - "run-image" : lambda y, x: (SUIT_Run, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), - # SUIT_Text = 13 - # SUIT_Coswid = 14 - } - manifest = {} - for field in info: - if field in SUIT_Manifest_Keys: - k, v = SUIT_Manifest_Keys[field](field, info[field]) - manifest[k] = v - else: - raise Exception('ERROR: {} not found!'.format(field)) - - # print ('suit-manifest: {}'.format(manifest)) - return manifest - -def make_SUIT_Outer_Wrapper(info): - Outer_Wrapper = { - SUIT_Authentication_Wrapper : None, - SUIT_Manifest : cbor.dumps(make_SUIT_Manifest(info), sort_keys = True) - } - # print('Outer_Wrapper: {}'.format(Outer_Wrapper)) - return Outer_Wrapper - - -def make_SUIT_Components(unused, components): - comps = [] - for component in components: - c = { - SUIT_Component_Identifier : [obj2bytes(x) for x in component["id"]] - } - if "digest" in component: - c[SUIT_Component_Digest] = [1, binascii.a2b_hex(component["digest"])] - if "size" in component: - c[SUIT_Component_Size] = component["size"] - comps.append(c) - return (SUIT_Components, comps) - -# Expected input format: -# { -# "digest-type" : "str", -# "structure-version" : 1, -# "sequence-number" : 2, -# "components": [ -# { -# "component-id":[bytes()], -# "bootable" : bool(), -# "images" : [ -# { -# "conditions" : [ -# {"current-digest": bytes()}, -# {"target-offset" : int()}, -# {"target-id": [bytes()]}, -# ], -# "digest": bytes(), -# "size" : int(), -# "uri" : str(), -# } -# ] -# "conditions" : [ -# {"current-digest": bytes()}, -# {"vendor-id" : bytes()}, -# {"class-id" : bytes()}, -# {"device-id" : bytes()}, -# ] -# } -# ], -# "conditions" : [ -# {"vendor-id" : bytes()}, -# {"class-id" : bytes()}, -# {"device-id" : bytes()}, -# ] -# } -def digest_str_to_id(s): - return { - 'sha-256' : 1, - 'sha-256-128' : 2, - 'sha-256-120' : 3, - 'sha-256-96' : 4, - 'sha-256-64' : 5, - 'sha-256-32' : 6, - 'sha-384' : 7, - 'sha-512' : 8, - 'sha3-224' : 9, - 'sha3-256' : 10, - 'sha3-384' : 11, - 'sha3-512' : 12, - }.get(s, 1) - -def compile_to_suit(suit_info): - digest_id = digest_str_to_id(suit_info.get('digest-type', 'sha-256')) - suit_manifest_desc = { - 'structure-version':int(suit_info.get('structure-version', 1)), - 'sequence-number':int(suit_info['sequence-number']), - } - #TODO: Dependencies - # Components - components = [] - #TODO: dependency components - common = [] - dependency_fetch = None - #TODO: Image Fetch when not in streaming mode - fetch_image = None - apply_image = [] - # System Verification - #TODO: Dependencies - system_verification = [ - {"directive-set-component": True}, - {"condition-image": None}, - ] - #TODO: Load Image - load_image = None - run_image = [] - - for con in suit_info.get('conditions', []): - common.append(con) - # for each component - for i, comp in enumerate(suit_info['components']): - comp_info = { - 'id' : comp['id'] - } - components.append(comp_info) - if len(comp['images']) == 1: - set_comp = {"directive-set-component": i} - set_params = { - "directive-set-var" : { - "image-size" : int(comp['images'][0]['size']), - "image-digest" : [digest_id, bytes(comp['images'][0]['digest'])] - } - } - common.append(set_comp) - common.append(set_params) - set_params = { - "directive-set-var" : { - "uris" : [[0, str(comp['images'][0]['uri'])]] - } - } - apply_image.append(set_comp) - apply_image.append(set_params) - else: - for image in comp['images']: - set_comp = {"directive-set-component": i} - set_params = { - "directive-set-var" : { - "image-size" : int(image['size']), - "image-digest" : [digest_id, bytes(image['digest'])] - } - } - conditional_seq = [set_comp] + image.get('conditions',[])[:] + [set_params] - conditional_set_params = { - 'directive-run-conditional': conditional_seq - } - common.append(conditional_set_params) - set_params = { - "directive-set-var" : { - "uris" : [[0, str(image['uri'])]] - } - } - conditional_seq = [set_comp] + image.get('conditions',[])[:] + [set_params] - conditional_set_params = { - 'directive-run-conditional': conditional_seq - } - - apply_image.append(conditional_set_params) - if comp.get('bootable', False): - run_image.append({'directive-set-component' : i}) - run_image.append({'directive-run':None}) - - apply_image.append({"directive-set-component": True}) - apply_image.append({"directive-fetch": None}) - - suit_manifest_desc.update({ - "components" : components, - "common" : common, - "apply-image" : apply_image, - "system-verification": system_verification, - "run-image" : run_image, - }) - - print(suit_manifest_desc) - - return cbor.dumps(make_SUIT_Outer_Wrapper(suit_manifest_desc), sort_keys=True) diff --git a/dist/tools/suit_v4/test-2img.json b/dist/tools/suit_v4/test-2img.json deleted file mode 100644 index fc18289b3d..0000000000 --- a/dist/tools/suit_v4/test-2img.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "digest-type" : "sha-256", - "structure-version" : 1, - "sequence-number" : 2, - "components": [ - { - "id":["00"], - "bootable" : true, - "images" : [ - { - "file" : "encode-04.py", - "uri" : "http://example.com/file.bin", - "conditions" : [ - {"condition-component-offset":4096} - ] - - }, - { - "file" : "suit_manifest_encoder_04.py", - "uri" : "http://example.com/file1.bin", - "conditions" : [ - {"condition-component-offset":8192} - ] - - } - ] - } - ], - "conditions" : [ - {"condition-vendor-id" : "fa6b4a53-d5ad-5fdf-be9d-e663e4d41ffe"}, - {"condition-class-id" : "1492af14-2569-5e48-bf42-9b2d51f2ab45"} - ] -} diff --git a/examples/suit_update/Makefile b/examples/suit_update/Makefile index ac2d2f0757..a7e9dc67bb 100644 --- a/examples/suit_update/Makefile +++ b/examples/suit_update/Makefile @@ -44,10 +44,7 @@ QUIET ?= 1 # USEMODULE += nanocoap_sock sock_util -USEMODULE += suit suit_coap - -# SUIT draft v4 support: -USEMODULE += suit_v4 +USEMODULE += suit suit_transport_coap # Display a progress bar during firmware download USEMODULE += progress_bar @@ -97,6 +94,10 @@ TEST_EXTRA_FILES += $(SLOT_RIOT_ELFS) $(SUIT_SEC) $(SUIT_PUB) # a terminal is opened to synchronize. TESTRUNNER_RESET_AFTER_TERM ?= 1 +# This can be removed as soon as the Pi-fleet runners support an Openssl version +# with ed25519 support. +TEST_ON_CI_BLACKLIST = all + include $(RIOTBASE)/Makefile.include .PHONY: host-tools diff --git a/examples/suit_update/README.md b/examples/suit_update/README.md index 56c5204b0c..b648b55f6c 100644 --- a/examples/suit_update/README.md +++ b/examples/suit_update/README.md @@ -127,7 +127,7 @@ In another terminal, run: [setup-wireless]: #Setup-a-wireless-device-behind-a-border-router If the workflow for updating using ethos is successful, you can try doing the -same over "real" network interfaces, by updating a node that is connected +same over wireless network interfaces, by updating a node that is connected wirelessly with a border router in between. Depending on your device you can use BLE or 802.15.4. @@ -369,22 +369,59 @@ see 6 pairs of messages indicating where (filepath) the file was published and the corresponding coap resource URI ... - published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv4_signed.1557135946.bin" - as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv4_signed.1557135946.bin" - published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv4_signed.latest.bin" - as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv4_signed.latest.bin" + published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" + published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" ... ### Notify an update to the device [update-notify]: #Norify-an-update-to-the-device If the network has been started with a standalone node, the RIOT node should be -reachable via link-local `fe80::2%riot0` on the ethos interface. If it was setup as a -wireless device it will be reachable via its global address, something like `2001:db8::7b7e:3255:1313:8d96` +reachable via link-local EUI64 address on the ethos interface, e.g: + + + Iface 5 HWaddr: 02:BE:74:C0:2F:B9 + L2-PDU:1500 MTU:1500 HL:64 RTR + RTR_ADV + Source address length: 6 + Link type: wired + inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL + inet6 addr: fe80::2 scope: link VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ffc0:2fb9 + inet6 group: ff02::1:ff00:2 + +the EUI64 link local address is `fe80::7b7e:3255:1313:8d96` and +SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot0]. + +If it was setup as a wireless device it will be reachable via its global +address, e.g: + + + Iface 6 HWaddr: 0D:96 Channel: 26 Page: 0 NID: 0x23 + Long HWaddr: 79:7E:32:55:13:13:8D:96 + TX-Power: 0dBm State: IDLE max. Retrans.: 3 CSMA Retries: 4 + AUTOACK ACK_REQ CSMA L2-PDU:102 MTU:1280 HL:64 RTR + RTR_ADV 6LO IPHC + Source address length: 8 + Link type: wireless + inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL + inet6 addr: 2001:db8::7b7e:3255:1313:8d96 scope: global VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ff17:dd59 + inet6 group: ff02::1:ff00:2 + + +the global address is `2001:db8::7b7e:3255:1313:8d96` and +SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96]. - In wired mode: - $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::2%riot0] BOARD=samr21-xpro make -C examples/suit_update suit/notify + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot] BOARD=samr21-xpro make -C examples/suit_update suit/notify - In wireless mode: @@ -394,16 +431,12 @@ wireless device it will be reachable via its global address, something like `200 This notifies the node of a new available manifest. Once the notification is received by the device, it fetches it. -If using `suit-v4` the node hangs for a couple of seconds when verifying the +If using `suit-v3` the node hangs for a couple of seconds when verifying the signature: .... - INFO # suit_coap: got manifest with size 545 - INFO # jumping into map - INFO # )got key val=1 - INFO # handler res=0 - INFO # got key val=2 - INFO # suit: verifying manifest signature... + suit_coap: got manifest with size 470 + suit: verifying manifest signature .... Once the signature is validated it continues validating other parts of the @@ -412,12 +445,12 @@ Among these validations it checks some condition like firmware offset position in regards to the running slot to see witch firmware image to fetch. .... - INFO # Handling handler with key 10 at 0x2b981 - INFO # Comparing manifest offset 4096 with other slot offset 4096 - .... - INFO # Handling handler with key 10 at 0x2b981 - INFO # Comparing manifest offset 133120 with other slot offset 4096 - INFO # Sequence handler error + suit: validated manifest version + )suit: validated sequence number + )validating vendor ID + Comparing 547d0d74-6d3a-5a92-9662-4881afd9407b to 547d0d74-6d3a-5a92-9662-4881afd9407b from manifest + validating vendor ID: OK + validating class id .... Once the manifest validation is complete, the application fetches the image @@ -432,27 +465,22 @@ displayed during this step: Once the new image is written, a final validation is performed and, in case of success, the application reboots on the new slot: - 2019-04-05 16:19:26,363 - INFO # riotboot: verifying digest at 0x20003f37 (img at: 0x20800 size: 80212) - 2019-04-05 16:19:26,704 - INFO # handler res=0 - 2019-04-05 16:19:26,705 - INFO # got key val=10 - 2019-04-05 16:19:26,707 - INFO # no handler found - 2019-04-05 16:19:26,708 - INFO # got key val=12 - 2019-04-05 16:19:26,709 - INFO # no handler found - 2019-04-05 16:19:26,711 - INFO # handler res=0 - 2019-04-05 16:19:26,713 - INFO # suit_v4_parse() success - 2019-04-05 16:19:26,715 - INFO # SUIT policy check OK. - 2019-04-05 16:19:26,718 - INFO # suit_coap: finalizing image flash - 2019-04-05 16:19:26,725 - INFO # riotboot_flashwrite: riotboot flashing completed successfully - 2019-04-05 16:19:26,728 - INFO # Image magic_number: 0x544f4952 - 2019-04-05 16:19:26,730 - INFO # Image Version: 0x5ca76390 - 2019-04-05 16:19:26,733 - INFO # Image start address: 0x00020900 - 2019-04-05 16:19:26,738 - INFO # Header chksum: 0x13b466db + Verifying image digest + riotboot: verifying digest at 0x1fffbd15 (img at: 0x1000 size: 77448) + Verifying image digest + riotboot: verifying digest at 0x1fffbd15 (img at: 0x1000 size: 77448) + suit_parse() success + SUIT policy check OK. + suit_coap: finalizing image flash + riotboot_flashwrite: riotboot flashing completed successfully + Image magic_number: 0x544f4952 + Image Version: 0x5e71f662 + Image start address: 0x00001100 + Header chksum: 0x745a0376 - - main(): This is RIOT! (Version: 2019.04-devel-606-gaa7b-ota_suit_v2) + main(): This is RIOT! (Version: 2020.04) RIOT SUIT update example application running from slot 1 - Waiting for address autoconfiguration... The slot number should have changed from after the application reboots. You can do the publish-notify sequence several times to verify this. @@ -466,11 +494,9 @@ For the suit_update to work there are important modules that aren't normally bui in a RIOT application: * riotboot - * riotboot_hdr -* riotboot_slot + * riotboot_flashwrite * suit - * suit_coap - * suit_v4 + * suit_transport_coap #### riotboot @@ -492,21 +518,21 @@ The flash memory will be divided in the following way: The riotboot part of the flash will not be changed during suit_updates but be flashed a first time with at least one slot with suit_capable fw. - $ BOARD=samr21-xpro make -C examples/suit_update clean riotboot/flash + $ BOARD=samr21-xpro make -C examples/suit_update clean flash -When calling make with the riotboot/flash argument it will flash the bootloader +When calling make with the `flash` argument it will flash the bootloader and then to slot0 a copy of the firmware you intend to build. New images must be of course written to the inactive slot, the device mist be able to boot from the previous image in case the update had some kind of error, eg: the image corresponds to the wrong slot. -The active/inactive coap resources is used so the publisher can send a manifest -built for the inactive slot. - -On boot the bootloader will check the riotboot_hdr and boot on the newest +On boot the bootloader will check the `riotboot_hdr` and boot on the newest image. +`riotboot_flashwrite` module is needed to be able to write the new firmware to +the inactive slot. + riotboot is not supported by all boards. The default board is `samr21-xpro`, but any board supporting `riotboot`, `flashpage` and with 256kB of flash should be able to run the demo. @@ -516,7 +542,7 @@ be able to run the demo. The suit module encloses all the other suit_related module. Formally this only includes the `sys/suit` directory into the build system dirs. -- **suit_coap** +- **suit_transport_coap** To enable support for suit_updates over coap a new thread is created. This thread will expose 4 suit related resources: @@ -533,9 +559,9 @@ When a new manifest url is received on the trigger resource a message is resent to the coap thread with the manifest's url. The thread will then fetch the manifest by a block coap request to the specified url. -- **support for v4** +- **support for v3** -This includes v4 manifest support. When a url is received in the /suit/trigger +This includes v3 manifest support. When a url is received in the /suit/trigger coap resource it will trigger a coap blockwise fetch of the manifest. When this manifest is received it will be parsed. The signature of the manifest will be verified and then the rest of the manifest content. If the received manifest is valid it @@ -610,20 +636,19 @@ The following variables are defined in makefiles/suit.inc.mk: The following convention is used when naming a manifest - SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv4.$(APP_VER).bin - SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv4.latest.bin - SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv4_signed.$(APP_VER).bin - SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv4_signed.latest.bin + SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv3.$(APP_VER).bin + SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv3.latest.bin + SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv3_signed.$(APP_VER).bin + SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv3_signed.latest.bin The following default values are using for generating the manifest: - SUIT_VENDOR ?= RIOT - SUIT_VERSION ?= $(APP_VER) + SUIT_VENDOR ?= "riot-os.org" + SUIT_SEQNR ?= $(APP_VER) SUIT_CLASS ?= $(BOARD) SUIT_KEY ?= default SUIT_KEY_DIR ?= $(RIOTBASE)/keys - SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY) - SUIT_PUB ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pub + SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pem All files (both slot binaries, both manifests, copies of manifests with "latest" instead of `$APP_VER` in riotboot build) are copied into the folder @@ -640,7 +665,7 @@ The following recipes are defined in makefiles/suit.inc.mk: suit/manifest: creates a non signed and signed manifest, and also a latest tag for these. It uses following parameters: - - $(SUIT_KEY): name of keypair to sign the manifest + - $(SUIT_KEY): name of key to sign the manifest - $(SUIT_COAP_ROOT): coap root address - $(SUIT_CLASS) - $(SUIT_VERSION) diff --git a/examples/suit_update/coap_handler.c b/examples/suit_update/coap_handler.c index b3c6aedc5c..69a12d6f80 100644 --- a/examples/suit_update/coap_handler.c +++ b/examples/suit_update/coap_handler.c @@ -11,7 +11,7 @@ #include #include "net/nanocoap.h" -#include "suit/coap.h" +#include "suit/transport/coap.h" static ssize_t _riot_board_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, void *context) { diff --git a/examples/suit_update/main.c b/examples/suit_update/main.c index fcc03d770a..d95c5852d7 100644 --- a/examples/suit_update/main.c +++ b/examples/suit_update/main.c @@ -26,7 +26,7 @@ #include "shell.h" -#include "suit/coap.h" +#include "suit/transport/coap.h" #include "riotboot/slot.h" #ifdef MODULE_PERIPH_GPIO diff --git a/examples/suit_update/tests/01-run.py b/examples/suit_update/tests/01-run.py index f2c1d030d3..d10f300ac0 100755 --- a/examples/suit_update/tests/01-run.py +++ b/examples/suit_update/tests/01-run.py @@ -138,7 +138,7 @@ def _test_invalid_version(child, client, app_ver): publish(TMPDIR.name, COAP_HOST, app_ver - 1) notify(COAP_HOST, client, app_ver - 1) child.expect_exact("suit_coap: trigger received") - child.expect_exact("suit: verifying manifest signature...") + child.expect_exact("suit: verifying manifest signature") child.expect_exact("seq_nr <= running image") @@ -146,7 +146,7 @@ def _test_invalid_signature(child, client, app_ver): publish(TMPDIR.name, COAP_HOST, app_ver + 1, 'invalid_keys') notify(COAP_HOST, client, app_ver + 1) child.expect_exact("suit_coap: trigger received") - child.expect_exact("suit: verifying manifest signature...") + child.expect_exact("suit: verifying manifest signature") child.expect_exact("Unable to validate signature") @@ -156,7 +156,7 @@ def _test_successful_update(child, client, app_ver): publish(TMPDIR.name, COAP_HOST, version) notify(COAP_HOST, client, version) child.expect_exact("suit_coap: trigger received") - child.expect_exact("suit: verifying manifest signature...") + child.expect_exact("suit: verifying manifest signature") child.expect( r"riotboot_flashwrite: initializing update to target slot (\d+)\r\n", timeout=MANIFEST_TIMEOUT, diff --git a/makefiles/boot/riotboot.mk b/makefiles/boot/riotboot.mk index 1cdbba05e7..3e5dc8f760 100644 --- a/makefiles/boot/riotboot.mk +++ b/makefiles/boot/riotboot.mk @@ -146,8 +146,8 @@ riotboot/flash: riotboot/flash-slot0 riotboot/flash-bootloader FLASHFILE = $(RIOTBOOT_EXTENDED_BIN) # include suit targets -ifneq (,$(filter suit_v4, $(USEMODULE))) - include $(RIOTMAKE)/suit.v4.inc.mk +ifneq (,$(filter suit, $(USEMODULE))) + include $(RIOTMAKE)/suit.inc.mk endif else diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 0840b4f1de..42638e67fe 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -96,7 +96,7 @@ PSEUDOMODULES += stdin PSEUDOMODULES += stdio_ethos PSEUDOMODULES += stdio_cdc_acm PSEUDOMODULES += stdio_uart_rx -PSEUDOMODULES += suit_% +PSEUDOMODULES += suit_transport_% PSEUDOMODULES += wakaama_objects_% PSEUDOMODULES += zptr PSEUDOMODULES += ztimer% @@ -104,9 +104,6 @@ PSEUDOMODULES += ztimer% # ztimer's main module is called "ztimer_core" NO_PSEUDOMODULES += ztimer_core -# handle suit_v4 being a distinct module -NO_PSEUDOMODULES += suit_v4 - # print ascii representation in function od_hex_dump() PSEUDOMODULES += od_string diff --git a/makefiles/suit.base.inc.mk b/makefiles/suit.base.inc.mk new file mode 100644 index 0000000000..ab0d84c103 --- /dev/null +++ b/makefiles/suit.base.inc.mk @@ -0,0 +1,40 @@ +# +# path to suit-tool +SUIT_TOOL ?= $(RIOTBASE)/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool + +# +# SUIT encryption keys +# + +# Specify key to use. +# Will use $(SUIT_KEY_DIR)/$(SUIT_KEY).pem as combined private/public key +# files. +SUIT_KEY ?= default + +ifeq (1, $(RIOT_CI_BUILD)) + SUIT_KEY_DIR ?= $(BINDIR) +else + SUIT_KEY_DIR ?= $(RIOTBASE)/keys +endif + +SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pem + +SUIT_PUB_HDR = $(BINDIR)/riotbuild/public_key.h +SUIT_PUB_HDR_DIR = $(dir $(SUIT_PUB_HDR)) +CFLAGS += -I$(SUIT_PUB_HDR_DIR) +BUILDDEPS += $(SUIT_PUB_HDR) + +$(SUIT_SEC): $(CLEAN) + @echo suit: generating key in $(SUIT_KEY_DIR) + @mkdir -p $(SUIT_KEY_DIR) + @$(RIOTBASE)/dist/tools/suit_v3/gen_key.py $(SUIT_SEC) + +# set FORCE so switching between keys using "SUIT_KEY=foo make ..." +# triggers a rebuild even if the new key would otherwise not (because the other +# key's mtime is too far back). +$(SUIT_PUB_HDR): $(SUIT_SEC) FORCE | $(CLEAN) + @mkdir -p $(SUIT_PUB_HDR_DIR) + @$(SUIT_TOOL) pubkey -k $(SUIT_SEC) \ + | '$(LAZYSPONGE)' $(LAZYSPONGE_FLAGS) '$@' + +suit/genkey: $(SUIT_SEC) diff --git a/makefiles/suit.inc.mk b/makefiles/suit.inc.mk new file mode 100644 index 0000000000..e343b2bac1 --- /dev/null +++ b/makefiles/suit.inc.mk @@ -0,0 +1,72 @@ +# +# This file contains stuff related to SUIT manifest generation. +# It depends on SUIT key generation, which can be found in +# makefiles/suit.base.inc.mk +# +# +SUIT_COAP_BASEPATH ?= fw/$(BOARD) +SUIT_COAP_SERVER ?= localhost +SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) +SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot + +# +SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv3.$(APP_VER).bin +SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv3.latest.bin +SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv3_signed.$(APP_VER).bin +SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv3_signed.latest.bin + +SUIT_NOTIFY_VERSION ?= latest +SUIT_NOTIFY_MANIFEST ?= $(APPLICATION)-riot.suitv3_signed.$(SUIT_NOTIFY_VERSION).bin + +# Long manifest names require more buffer space when parsing +export CFLAGS += -DCONFIG_SOCK_URLPATH_MAXLEN=128 + +SUIT_VENDOR ?= "riot-os.org" +SUIT_SEQNR ?= $(APP_VER) +SUIT_CLASS ?= $(BOARD) + +# +$(SUIT_MANIFEST): $(SLOT0_RIOT_BIN) $(SLOT1_RIOT_BIN) + $(RIOTBASE)/dist/tools/suit_v3/gen_manifest.py \ + --urlroot $(SUIT_COAP_ROOT) \ + --seqnr $(SUIT_SEQNR) \ + --uuid-vendor $(SUIT_VENDOR) \ + --uuid-class $(SUIT_CLASS) \ + -o $@.tmp \ + $(SLOT0_RIOT_BIN):$(SLOT0_OFFSET) \ + $(SLOT1_RIOT_BIN):$(SLOT1_OFFSET) + + $(SUIT_TOOL) create -f suit -i $@.tmp -o $@ + + rm -f $@.tmp + + +$(SUIT_MANIFEST_SIGNED): $(SUIT_MANIFEST) $(SUIT_SEC) + $(SUIT_TOOL) sign -k $(SUIT_SEC) -m $(SUIT_MANIFEST) -o $@ + +$(SUIT_MANIFEST_LATEST): $(SUIT_MANIFEST) + @ln -f -s $< $@ + +$(SUIT_MANIFEST_SIGNED_LATEST): $(SUIT_MANIFEST_SIGNED) + @ln -f -s $< $@ + +SUIT_MANIFESTS := $(SUIT_MANIFEST) \ + $(SUIT_MANIFEST_LATEST) \ + $(SUIT_MANIFEST_SIGNED) \ + $(SUIT_MANIFEST_SIGNED_LATEST) + +suit/manifest: $(SUIT_MANIFESTS) + +suit/publish: $(SUIT_MANIFESTS) $(SLOT0_RIOT_BIN) $(SLOT1_RIOT_BIN) + @mkdir -p $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) + @cp $^ $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) + @for file in $^; do \ + echo "published \"$$file\""; \ + echo " as \"$(SUIT_COAP_ROOT)/$$(basename $$file)\""; \ + done + +suit/notify: | $(filter suit/publish, $(MAKECMDGOALS)) + @test -n "$(SUIT_CLIENT)" || { echo "error: SUIT_CLIENT unset!"; false; } + aiocoap-client -m POST "coap://$(SUIT_CLIENT)/suit/trigger" \ + --payload "$(SUIT_COAP_ROOT)/$(SUIT_NOTIFY_MANIFEST)" && \ + echo "Triggered $(SUIT_CLIENT) to update." diff --git a/makefiles/suit.v4.inc.mk b/makefiles/suit.v4.inc.mk deleted file mode 100644 index 3d8dbf82c8..0000000000 --- a/makefiles/suit.v4.inc.mk +++ /dev/null @@ -1,103 +0,0 @@ -# -SUIT_COAP_BASEPATH ?= fw/$(BOARD) -SUIT_COAP_SERVER ?= localhost -SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) -SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot - -# -SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv4.$(APP_VER).bin -SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv4.latest.bin -SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv4_signed.$(APP_VER).bin -SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv4_signed.latest.bin - -SUIT_NOTIFY_VERSION ?= latest -SUIT_NOTIFY_MANIFEST ?= $(APPLICATION)-riot.suitv4_signed.$(SUIT_NOTIFY_VERSION).bin - -# Long manifest names require more buffer space when parsing -export CFLAGS += -DCONFIG_SOCK_URLPATH_MAXLEN=128 - -SUIT_VENDOR ?= "riot-os.org" -SUIT_SEQNR ?= $(APP_VER) -SUIT_CLASS ?= $(BOARD) - -# -# SUIT encryption keys -# - -# Specify key to use. -# Will use $(SUIT_KEY_DIR)/$(SUIT_KEY) $(SUIT_KEY_DIR)/$(SUIT_KEY).pub as -# private/public key files, similar to how ssh names its key files. -SUIT_KEY ?= default - -ifeq (1, $(RIOT_CI_BUILD)) - SUIT_KEY_DIR ?= $(BINDIR) -else - SUIT_KEY_DIR ?= $(RIOTBASE)/keys -endif - -SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY) -SUIT_PUB ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pub - -SUIT_PUB_HDR = $(BINDIR)/riotbuild/public_key.h -SUIT_PUB_HDR_DIR = $(dir $(SUIT_PUB_HDR)) -CFLAGS += -I$(SUIT_PUB_HDR_DIR) -BUILDDEPS += $(SUIT_PUB_HDR) - -$(SUIT_SEC) $(SUIT_PUB): $(CLEAN) - @echo suit: generating key pair in $(SUIT_KEY_DIR) - @mkdir -p $(SUIT_KEY_DIR) - @$(RIOTBASE)/dist/tools/suit_v4/gen_key.py $(SUIT_SEC) $(SUIT_PUB) - -# set FORCE so switching between keys using "SUIT_KEY=foo make ..." -# triggers a rebuild even if the new key would otherwise not (because the other -# key's mtime is too far back). -$(SUIT_PUB_HDR): $(SUIT_PUB) FORCE | $(CLEAN) - @mkdir -p $(SUIT_PUB_HDR_DIR) - @cp $(SUIT_PUB) $(SUIT_PUB_HDR_DIR)/public.key - @cd $(SUIT_PUB_HDR_DIR) && xxd -i public.key \ - | '$(LAZYSPONGE)' $(LAZYSPONGE_FLAGS) '$@' - -suit/genkey: $(SUIT_SEC) $(SUIT_PUB) - -# -$(SUIT_MANIFEST): $(SLOT0_RIOT_BIN) $(SLOT1_RIOT_BIN) - $(RIOTBASE)/dist/tools/suit_v4/gen_manifest.py \ - --template $(RIOTBASE)/dist/tools/suit_v4/test-2img.json \ - --urlroot $(SUIT_COAP_ROOT) \ - --seqnr $(SUIT_SEQNR) \ - --uuid-vendor $(SUIT_VENDOR) \ - --uuid-class $(SUIT_CLASS) \ - --offsets $(SLOT0_OFFSET),$(SLOT1_OFFSET) \ - -o $@ \ - $^ - -$(SUIT_MANIFEST_SIGNED): $(SUIT_MANIFEST) $(SUIT_SEC) $(SUIT_PUB) - $(RIOTBASE)/dist/tools/suit_v4/sign-04.py \ - $(SUIT_SEC) $(SUIT_PUB) $< $@ - -$(SUIT_MANIFEST_LATEST): $(SUIT_MANIFEST) - @ln -f -s $< $@ - -$(SUIT_MANIFEST_SIGNED_LATEST): $(SUIT_MANIFEST_SIGNED) - @ln -f -s $< $@ - -SUIT_MANIFESTS := $(SUIT_MANIFEST) \ - $(SUIT_MANIFEST_LATEST) \ - $(SUIT_MANIFEST_SIGNED) \ - $(SUIT_MANIFEST_SIGNED_LATEST) - -suit/manifest: $(SUIT_MANIFESTS) - -suit/publish: $(SUIT_MANIFESTS) $(SLOT0_RIOT_BIN) $(SLOT1_RIOT_BIN) - @mkdir -p $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) - @cp $^ $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) - @for file in $^; do \ - echo "published \"$$file\""; \ - echo " as \"$(SUIT_COAP_ROOT)/$$(basename $$file)\""; \ - done - -suit/notify: | $(filter suit/publish, $(MAKECMDGOALS)) - @test -n "$(SUIT_CLIENT)" || { echo "error: SUIT_CLIENT unset!"; false; } - aiocoap-client -m POST "coap://$(SUIT_CLIENT)/suit/trigger" \ - --payload "$(SUIT_COAP_ROOT)/$(SUIT_NOTIFY_MANIFEST)" && \ - echo "Triggered $(SUIT_CLIENT) to update." diff --git a/sys/include/suit/v4/suit.h b/sys/include/suit.h similarity index 63% rename from sys/include/suit/v4/suit.h rename to sys/include/suit.h index 0f2defc0a4..f352abe86f 100644 --- a/sys/include/suit/v4/suit.h +++ b/sys/include/suit.h @@ -7,10 +7,18 @@ * directory for more details. */ /** - * @defgroup sys_suit_v4 SUIT draft v4 - * @ingroup sys_suit + * @defgroup sys_suit SUIT secure firmware OTA upgrade infrastructure + * @ingroup sys * @brief SUIT manifest handling * + * @experimental + * + * @note The current implementation of this specification is based on the + * IETF-SUIT-v3 draft. The module is still experimental and will change to + * match future draft specifications + * + * @see https://tools.ietf.org/html/draft-ietf-suit-manifest-03 + * * @{ * * @brief Handler functions for SUIT manifests @@ -19,8 +27,8 @@ * */ -#ifndef SUIT_V4_SUIT_H -#define SUIT_V4_SUIT_H +#ifndef SUIT_H +#define SUIT_H #include #include @@ -38,40 +46,46 @@ extern "C" { * @brief Buffer size used for Cose */ #ifndef SUIT_COSE_BUF_SIZE -#define SUIT_COSE_BUF_SIZE (512U) +#define SUIT_COSE_BUF_SIZE (180U) #endif /** - * @brief Maximum number of components used for SUIT v4 + * @brief Maximum number of components supported in a SUIT manifest */ -#define SUIT_V4_COMPONENT_MAX (1U) - -/** - * @brief Supported SUIT manifest version - */ -#define SUIT_MANIFEST_VERSION (4) +#define SUIT_COMPONENT_MAX (1U) /** * @brief Current SUIT serialization format version * - * see https://tools.ietf.org/html/draft-moran-suit-manifest-04#section-8.2 for + * see https://tools.ietf.org/html/draft-ietf-suit-manifest-03#section-7 for * details */ -#define SUIT_VERSION (1) +#define SUIT_VERSION (1) + +/** + * @brief COSE signature OK + */ +#define SUIT_STATE_COSE_AUTHENTICATED (1 << 1) + +/** + * @brief COSE payload matches SUIT manifest digest + */ +#define SUIT_STATE_FULLY_AUTHENTICATED (1 << 2) /** * @brief SUIT error codes */ typedef enum { - SUIT_OK = 0, /**< Manifest parsed and validated */ - SUIT_ERR_INVALID_MANIFEST = -1, /**< Unexpected CBOR structure detected */ - SUIT_ERR_UNSUPPORTED = -2, /**< Unsupported SUIT feature detected */ - SUIT_ERR_NOT_SUPPORTED = -3, /**< Unsupported manifest features detected */ - SUIT_ERR_COND = -4, /**< Conditionals evaluate to false */ - SUIT_ERR_SEQUENCE_NUMBER = -5, /**< Sequence number less or equal to - current sequence number */ - SUIT_ERR_SIGNATURE = -6, /**< Unable to verify signature */ -} suit_v4_error_t; + SUIT_OK = 0, /**< Manifest parsed and validated */ + SUIT_ERR_INVALID_MANIFEST = -1, /**< Unexpected CBOR structure detected */ + SUIT_ERR_UNSUPPORTED = -2, /**< Unsupported SUIT feature detected */ + SUIT_ERR_NOT_SUPPORTED = -3, /**< Unsupported features detected */ + SUIT_ERR_COND = -4, /**< Conditionals evaluate to false */ + SUIT_ERR_SEQUENCE_NUMBER = -5, /**< Sequence number less or equal to + current sequence number */ + SUIT_ERR_SIGNATURE = -6, /**< Unable to verify signature */ + SUIT_ERR_DIGEST_MISMATCH = -7, /**< Digest mismatch with COSE and SUIT */ +} suit_error_t; /** * @brief SUIT payload digest algorithms @@ -84,7 +98,7 @@ typedef enum { SUIT_DIGEST_SHA256 = 1, /**< SHA256 */ SUIT_DIGEST_SHA384 = 2, /**< SHA384 */ SUIT_DIGEST_SHA512 = 3, /**< SHA512 */ -} suit_v4_digest_t; +} suit_digest_t; /** * @brief SUIT payload digest types @@ -97,7 +111,7 @@ typedef enum { SUIT_DIGEST_TYPE_INSTALLED = 2, /**< Installed firmware digest */ SUIT_DIGEST_TYPE_CIPHERTEXT = 3, /**< Ciphertext digest */ SUIT_DIGEST_TYPE_PREIMAGE = 4 /**< Pre-image digest */ -} suit_v4_digest_type_t; +} suit_digest_type_t; /** * @brief SUIT component types @@ -112,36 +126,35 @@ enum { }; /** - * @brief SUIT v4 component struct + * @brief SUIT component struct */ typedef struct { uint32_t size; /**< Size */ - nanocbor_value_t identifier; /**< Identifier*/ + nanocbor_value_t identifier; /**< Identifier */ nanocbor_value_t url; /**< Url */ nanocbor_value_t digest; /**< Digest */ -} suit_v4_component_t; +} suit_component_t; /** * @brief SUIT manifest struct */ typedef struct { - cose_sign_dec_t verify; /**< COSE signature validation struct */ const uint8_t *buf; /**< ptr to the buffer of the manifest */ size_t len; /**< length of the manifest */ + const uint8_t *cose_payload; /**< ptr to the payload of the COSE sign */ + size_t cose_payload_len; /**< length of the COSE payload */ uint32_t validated; /**< bitfield of validated policies */ uint32_t state; /**< bitfield holding state information */ - /** List of components in the manifest */ - suit_v4_component_t components[SUIT_V4_COMPONENT_MAX]; + suit_component_t components[SUIT_COMPONENT_MAX]; unsigned components_len; /**< Current number of components */ - int32_t component_current; /**< Current component index */ + uint32_t component_current; /**< Current component index */ riotboot_flashwrite_t *writer; /**< Pointer to the riotboot flash writer */ /** Manifest validation buffer */ uint8_t validation_buf[SUIT_COSE_BUF_SIZE]; - cose_key_t *key; /**< Ptr to the public key for validation */ char *urlbuf; /**< Buffer containing the manifest url */ size_t urlbuf_len; /**< Length of the manifest url */ -} suit_v4_manifest_t; +} suit_manifest_t; /** * @brief Bit flags used to determine if SUIT manifest contains components @@ -163,9 +176,9 @@ typedef struct { * @param[in] len length of the manifest data in the buffer * * @return SUIT_OK on parseable manifest - * @return negative @ref suit_v4_error_t code on error + * @return negative @ref suit_error_t code on error */ -int suit_v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, size_t len); +int suit_parse(suit_manifest_t *manifest, const uint8_t *buf, size_t len); /** * @brief Check a manifest policy @@ -175,31 +188,7 @@ int suit_v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, size_t len); * @return 0 on valid manifest policy * @return -1 on invalid manifest policy */ -int suit_v4_policy_check(suit_v4_manifest_t *manifest); - -/** - * @brief Iterate over a cbor map container - * - * @param[in] it cbor container iterator - * @param[out] key the returned key - * @param[out] value the returned value - * - * @return 0 when the iterator is already at the end of the container - * @return the number of returned (key, value) pair, e.g. 1 - */ -int suit_cbor_map_iterate(nanocbor_value_t *it, nanocbor_value_t *key, nanocbor_value_t *value); - -/** - * @brief Parser a cbor subsequence - * - * @param[in] bseq subsequence value - * @param[out] it cbor iterator - * - * @return 0 on success - * @return -1 if bseq is not a cbor string - * @return CborError code on other cbor parser errors - */ -int suit_cbor_subparse(nanocbor_value_t *bseq, nanocbor_value_t *it); +int suit_policy_check(suit_manifest_t *manifest); /** * @brief Helper function for writing bytes on flash a specified offset @@ -220,5 +209,5 @@ int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, } #endif -#endif /* SUIT_V4_SUIT_H */ +#endif /* SUIT_H */ /** @} */ diff --git a/sys/include/suit/handlers.h b/sys/include/suit/handlers.h new file mode 100644 index 0000000000..538bf7e124 --- /dev/null +++ b/sys/include/suit/handlers.h @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Kaspar Schleiser + * 2019 Inria + * 2019 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. + */ +/** + * @ingroup sys_suit + * @brief SUIT draft-ietf-suit-manifest-03 manifest handlers + * + * @experimental + * + * @{ + * + * @brief Handler functions for SUIT manifests + * @author Koen Zandberg + * @author Kaspar Schleiser + */ + +#ifndef SUIT_HANDLERS_H +#define SUIT_HANDLERS_H + +#include +#include + +#include "suit.h" +#include "uuid.h" +#include "nanocbor/nanocbor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name SUIT outer wrapper identifiers + * @{ + */ +#define SUIT_WRAPPER_AUTHENTICATION (2) +#define SUIT_WRAPPER_MANIFEST (3) +/** @} */ + +/** + * @name SUIT container identifiers + * @{ + */ +#define SUIT_CONTAINER_VERSION (1) +#define SUIT_CONTAINER_SEQ_NO (2) +#define SUIT_CONTAINER_COMMON (3) +#define SUIT_CONTAINER_DEPS_RESOLUTION (7) +#define SUIT_CONTAINER_PAYLOAD_FETCH (8) +#define SUIT_CONTAINER_INSTALL (9) +#define SUIT_CONTAINER_VALIDATE (10) +#define SUIT_CONTAINER_LOAD (11) +#define SUIT_CONTAINER_RUN (12) +#define SUIT_CONTAINER_TEXT (13) +/** @} */ + +/** + * @name SUIT common section identifiers + * @{ + */ +#define SUIT_COMMON_DEPENDENCIES (1) +#define SUIT_COMMON_COMPONENTS (2) +#define SUIT_COMMON_DEP_COMPONENTS (3) +#define SUIT_COMMON_COMMAND_SEQUENCE (4) +/** @} */ + +/** + * @name SUIT condition identifiers + * @{ + */ +#define SUIT_COND_VENDOR_ID (1) +#define SUIT_COND_CLASS_ID (2) +#define SUIT_COND_IMAGE_MATCH (3) +#define SUIT_COND_USE_BEFORE (4) +#define SUIT_COND_COMPONENT_OFFSET (5) +#define SUIT_COND_DEVICE_ID (24) +#define SUIT_COND_IMAGE_NOT_MATCH (25) +#define SUIT_COND_MIN_BATTERY (26) +#define SUIT_COND_UPDATE_AUTHZ (27) +#define SUIT_COND_VERSION (28) +/** @} */ + +/** + * @name SUIT directive identifiers + * @{ + */ +#define SUIT_DIR_SET_COMPONENT_IDX (12) +#define SUIT_DIR_SET_DEPENDENCY_IDX (13) +#define SUIT_DIR_ABORT (14) +#define SUIT_DIR_TRY_EACH (15) +#define SUIT_DIR_PROCESS_DEPS (18) +#define SUIT_DIR_SET_PARAM (19) +#define SUIT_DIR_OVERRIDE_PARAM (20) +#define SUIT_DIR_FETCH (21) +#define SUIT_DIR_COPY (22) +#define SUIT_DIR_RUN (23) +#define SUIT_DIR_WAIT (29) +#define SUIT_DIR_RUN_SEQUENCE (30) +#define SUIT_DIR_RUN_WITH_ARGS (31) +#define SUIT_DIR_SWAP (32) +/** @} */ + +/** + * @brief suit handler prototype + * + * @param manifest SUIT manifest context + * @param key SUIT map index of this content + * @param it nanocbor_value_t iterator to the content + * + * @return SUIT_OK on success + * @return negative on error + */ +typedef int (*suit_manifest_handler_t)(suit_manifest_t *manifest, int key, + nanocbor_value_t *it); + +/** + * @brief global handler reference + */ +extern const suit_manifest_handler_t suit_global_handlers[]; +extern const size_t suit_global_handlers_len; + +/** + * @brief SUIT sequence handler reference + */ +extern const suit_manifest_handler_t suit_command_sequence_handlers[]; + +/** + * @brief SUIT sequence handler length + */ +extern const size_t suit_command_sequence_handlers_len; + +/** + * @brief SUIT container handlers reference + */ +extern const suit_manifest_handler_t suit_container_handlers[]; + +/** + * @brief length of the SUIT container handlers + */ +extern const size_t suit_container_handlers_len; + +/** + * @brief SUIT common handlers reference + */ +extern const suit_manifest_handler_t suit_common_handlers[]; + +/** + * @brief length of the SUIT common handlers + */ +extern const size_t suit_common_handlers_len; + +/** + * @brief Manifest structure handler function + * + * Iterates over the supplied nanocbor map or array and calls the manifest + * handler function for every key. + * + * @param manifest SUIT manifest context + * @param it Nanocbor map/array element + * @param handlers Array of SUIT manifest handlers to use + * @param handlers_len Length of the SUIT manifest handlers + * + * @returns SUIT_OK if all handlers executed successfully + * @returns negative on error, see @ref suit_error_t + */ +int suit_handle_manifest_structure(suit_manifest_t *manifest, + nanocbor_value_t *it, + const suit_manifest_handler_t *handlers, + size_t handlers_len); + +/** + * @brief Byte string wrapped manifest structure handler function + * + * Extracts the nanocbor byte string and Iterates over the CBOR map or array + * contained in the bytestring and calls the manifest handler function for + * every key. + * + * @param manifest SUIT manifest context + * @param bseq Nanocbor byte string + * @param handlers Array of SUIT manifest handlers to use + * @param handlers_len Length of the SUIT manifest handlers + * + * @returns SUIT_OK if all handlers executed successfully + * @returns negative on error, see @ref suit_v3_error_t + */ +int suit_handle_manifest_structure_bstr(suit_manifest_t *manifest, + nanocbor_value_t *bseq, + const suit_manifest_handler_t *handlers, + size_t handlers_len); + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_HANDLERS_H */ +/** @} */ diff --git a/sys/include/suit/v4/policy.h b/sys/include/suit/policy.h similarity index 85% rename from sys/include/suit/v4/policy.h rename to sys/include/suit/policy.h index 223113123e..b1b3c96d07 100644 --- a/sys/include/suit/v4/policy.h +++ b/sys/include/suit/policy.h @@ -9,7 +9,7 @@ * directory for more details. */ /** - * @ingroup sys_suit_v4 + * @ingroup sys_suit * @brief SUIT policy definitions * * @{ @@ -20,8 +20,8 @@ * */ -#ifndef SUIT_V4_POLICY_H -#define SUIT_V4_POLICY_H +#ifndef SUIT_POLICY_H +#define SUIT_POLICY_H #include #include @@ -48,11 +48,12 @@ extern "C" { * @brief SUIT default policy */ #define SUIT_DEFAULT_POLICY \ - (SUIT_VALIDATED_VERSION | SUIT_VALIDATED_SEQ_NR | SUIT_VALIDATED_VENDOR | SUIT_VALIDATED_CLASS) + (SUIT_VALIDATED_VERSION | SUIT_VALIDATED_SEQ_NR | SUIT_VALIDATED_VENDOR | \ + SUIT_VALIDATED_CLASS) #ifdef __cplusplus } #endif -#endif /* SUIT_V4_POLICY_H */ +#endif /* SUIT_POLICY_H */ /** @} */ diff --git a/sys/include/suit/coap.h b/sys/include/suit/transport/coap.h similarity index 94% rename from sys/include/suit/coap.h rename to sys/include/suit/transport/coap.h index 4c1ebbe929..be5fbbbdea 100644 --- a/sys/include/suit/coap.h +++ b/sys/include/suit/transport/coap.h @@ -9,11 +9,9 @@ */ /** - * @defgroup sys_suit SUIT secure firmware updates - * @ingroup sys - * @brief SUIT secure firmware updates - * - * @experimental + * @ingroup sys_suit + * @defgroup sys_suit_transport_coap SUIT firmware CoAP transport + * @brief SUIT secure firmware updates over CoAP * * @{ * @@ -24,8 +22,8 @@ * */ -#ifndef SUIT_COAP_H -#define SUIT_COAP_H +#ifndef SUIT_TRANSPORT_COAP_H +#define SUIT_TRANSPORT_COAP_H #include "net/nanocoap.h" @@ -157,5 +155,5 @@ void suit_coap_trigger(const uint8_t *url, size_t len); } #endif -#endif /* SUIT_COAP_H */ +#endif /* SUIT_TRANSPORT_COAP_H */ /** @} */ diff --git a/sys/include/suit/v4/handlers.h b/sys/include/suit/v4/handlers.h deleted file mode 100644 index 9012dd8171..0000000000 --- a/sys/include/suit/v4/handlers.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2019 Koen Zandberg - * 2019 Kaspar Schleiser - * 2019 Inria - * 2019 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. - */ -/** - * @ingroup sys_suit_v4 - * @brief SUIT v4 manifest handlers - * - * @experimental - * - * @{ - * - * @brief Handler functions for SUIT manifests - * @author Koen Zandberg - * @author Kaspar Schleiser - */ - -#ifndef SUIT_V4_HANDLERS_H -#define SUIT_V4_HANDLERS_H - -#include -#include - -#include "suit/v4/suit.h" -#include "uuid.h" -#include "nanocbor/nanocbor.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief suit handler prototype - * - * @param manifest SUIT v4 manifest context - * @param it nanocbor_value_t iterator to the content the handler must handle - * - * @return 1 on success - * @return negative on error - */ -typedef int (*suit_manifest_handler_t)(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it); - -/** - * @brief Get suit manifest handler for given integer key - * - * @param[in] key: integer key - * - * @return ptr to handler function - * @return NULL (if handler unavailable or key out of range) - */ -suit_manifest_handler_t suit_manifest_get_manifest_handler(int key); - -#ifdef __cplusplus -} -#endif - -#endif /* SUIT_V4_HANDLERS_H */ -/** @} */ diff --git a/sys/shell/commands/sc_suit.c b/sys/shell/commands/sc_suit.c index 29ae844e03..89e0320f09 100644 --- a/sys/shell/commands/sc_suit.c +++ b/sys/shell/commands/sc_suit.c @@ -22,7 +22,7 @@ #include #include -#include "suit/coap.h" +#include "suit/transport/coap.h" int _suit_handler(int argc, char **argv) diff --git a/sys/suit/Makefile b/sys/suit/Makefile index 5d9edcc723..c763337223 100644 --- a/sys/suit/Makefile +++ b/sys/suit/Makefile @@ -1,9 +1,5 @@ -SUBMODULES := 1 - -# don't complain about missing submodule .c file. -# necessary to not fail for suit_v*_*. -SUBMODULES_NOFORCE := 1 - -DIRS += v4 +ifneq (,$(filter suit_transport_%,$(USEMODULE))) + DIRS += transport +endif include $(RIOTBASE)/Makefile.base diff --git a/sys/suit/conditions.c b/sys/suit/conditions.c index 53690d4dda..aa66866db9 100644 --- a/sys/suit/conditions.c +++ b/sys/suit/conditions.c @@ -13,7 +13,7 @@ * @{ * * @file - * @brief SUIT conditions + * @brief SUIT condition initializers * * @author Koen Zandberg * @author Kaspar Schleiser diff --git a/sys/suit/handlers.c b/sys/suit/handlers.c new file mode 100644 index 0000000000..5d98c4ce38 --- /dev/null +++ b/sys/suit/handlers.c @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * + * 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. + */ + +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT content handler helper functions + * + * @author Koen Zandberg + * + * @} + */ + +#include +#include + +#include "suit/handlers.h" +#include "suit.h" + +#include "log.h" + +static suit_manifest_handler_t _get_handler(int key, + const suit_manifest_handler_t *map, + size_t len) +{ + if (key < 0 || (size_t)key >= len) { + return NULL; + } + return map[key]; +} + +int suit_handle_manifest_structure(suit_manifest_t *manifest, + nanocbor_value_t *it, + const suit_manifest_handler_t *handlers, + size_t handlers_len) +{ + LOG_DEBUG("Handling command sequence\n"); + nanocbor_value_t container; + + if ((nanocbor_enter_array(it, &container) < 0) && + (nanocbor_enter_map(it, &container) < 0)) { + LOG_DEBUG("Neither array nor map: %d\n", nanocbor_get_type(it)); + return SUIT_ERR_INVALID_MANIFEST; + } + + while (!nanocbor_at_end(&container)) { + int32_t key; + if (nanocbor_get_int32(&container, &key) < 0) { + LOG_DEBUG("No key found: %d\n", nanocbor_get_type(&container)); + return SUIT_ERR_INVALID_MANIFEST; + } + nanocbor_value_t value = container; + LOG_DEBUG("Executing handler with key %" PRIi32 "\n", key); + suit_manifest_handler_t handler = _get_handler(key, handlers, + handlers_len); + if (!handler) { + return SUIT_ERR_UNSUPPORTED; + } + int res = handler(manifest, key, &value); + if (res < 0) { + LOG_DEBUG("Sequence handler error\n"); + return res; + } + nanocbor_skip(&container); + } + nanocbor_leave_container(it, &container); + LOG_DEBUG("Leaving sequence handler\n"); + + return 0; +} + +int suit_handle_manifest_structure_bstr(suit_manifest_t *manifest, + nanocbor_value_t *bseq, + const suit_manifest_handler_t *handlers, + size_t handlers_len) +{ + const uint8_t *buf; + size_t len; + + LOG_DEBUG("Handling command sequence starting with CBOR type %d\n", + nanocbor_get_type(bseq)); + if (nanocbor_get_bstr(bseq, &buf, &len) < 0) { + return SUIT_ERR_INVALID_MANIFEST; + } + + nanocbor_value_t it; + nanocbor_decoder_init(&it, buf, len); + return suit_handle_manifest_structure(manifest, &it, handlers, + handlers_len); +} diff --git a/sys/suit/handlers_command_seq.c b/sys/suit/handlers_command_seq.c new file mode 100644 index 0000000000..3a5bb3cc43 --- /dev/null +++ b/sys/suit/handlers_command_seq.c @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2020 Inria + * + * 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. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT Handlers for the command sequences in the common section of + * a SUIT manifest. + * + * This file contains the functions to handle command sequences from a SUIT + * manifest. This includes both directives and conditions. + * + * @author Koen Zandberg + * + * @} + */ + +#include +#include + +#include "kernel_defines.h" +#include "suit/conditions.h" +#include "suit/handlers.h" +#include "suit/policy.h" +#include "suit.h" +#include "riotboot/hdr.h" +#include "riotboot/slot.h" + +#ifdef MODULE_SUIT_TRANSPORT_COAP +#include "suit/transport/coap.h" +#endif + +#include "log.h" + +static int _validate_uuid(suit_manifest_t *manifest, + nanocbor_value_t *it, + uuid_t *uuid) +{ + (void)manifest; + const uint8_t *uuid_manifest_ptr; + size_t len = sizeof(uuid_t); + char uuid_str[UUID_STR_LEN + 1]; + char uuid_str2[UUID_STR_LEN + 1]; + if (nanocbor_get_bstr(it, &uuid_manifest_ptr, &len) < 0) { + return SUIT_ERR_INVALID_MANIFEST; + } + + uuid_to_string((uuid_t *)uuid_manifest_ptr, uuid_str); + uuid_to_string(uuid, uuid_str2); + LOG_INFO("Comparing %s to %s from manifest\n", uuid_str2, uuid_str); + return uuid_equal(uuid, (uuid_t *)uuid_manifest_ptr) + ? SUIT_OK + : SUIT_ERR_COND; +} + +static int _cond_vendor_handler(suit_manifest_t *manifest, + int key, + nanocbor_value_t *it) +{ + (void)key; + LOG_INFO("validating vendor ID\n"); + int rc = _validate_uuid(manifest, it, suit_get_vendor_id()); + if (rc == SUIT_OK) { + LOG_INFO("validating vendor ID: OK\n"); + manifest->validated |= SUIT_VALIDATED_VENDOR; + } + return rc; +} + +static int _cond_class_handler(suit_manifest_t *manifest, + int key, + nanocbor_value_t *it) +{ + (void)key; + LOG_INFO("validating class id\n"); + int rc = _validate_uuid(manifest, it, suit_get_class_id()); + if (rc == SUIT_OK) { + LOG_INFO("validating class id: OK\n"); + manifest->validated |= SUIT_VALIDATED_CLASS; + } + return rc; +} + +static int _cond_comp_offset(suit_manifest_t *manifest, + int key, + nanocbor_value_t *it) +{ + (void)manifest; + (void)key; + uint32_t offset; + + int rc = nanocbor_get_uint32(it, &offset); + if (rc < 0) { + LOG_WARNING("_cond_comp_offset(): expected int, got rc=%i type=%i\n", + rc, nanocbor_get_type(it)); + return SUIT_ERR_INVALID_MANIFEST; + } + uint32_t other_offset = (uint32_t)riotboot_slot_offset( + riotboot_slot_other()); + + LOG_INFO("Comparing manifest offset %u with other slot offset %u\n", + (unsigned)offset, (unsigned)other_offset); + return other_offset == offset ? SUIT_OK : SUIT_ERR_COND; +} + +static int _dtv_set_comp_idx(suit_manifest_t *manifest, + int key, + nanocbor_value_t *it) +{ + (void)key; + if (nanocbor_get_type(it) == NANOCBOR_TYPE_FLOAT) { + LOG_DEBUG("_dtv_set_comp_idx() ignoring boolean and floats\n)"); + nanocbor_skip(it); + } + else if (nanocbor_get_uint32(it, &manifest->component_current) < 0) { + return SUIT_ERR_INVALID_MANIFEST; + } + if (manifest->component_current >= SUIT_COMPONENT_MAX) { + return SUIT_ERR_INVALID_MANIFEST; + } + LOG_DEBUG("Setting component index to %d\n", + (int)manifest->component_current); + return 0; +} + +static int _dtv_run_seq_cond(suit_manifest_t *manifest, + int key, + nanocbor_value_t *it) +{ + (void)key; + LOG_DEBUG("Starting conditional sequence handler\n"); + return suit_handle_manifest_structure_bstr(manifest, it, + suit_command_sequence_handlers, + suit_command_sequence_handlers_len); +} + +static int _dtv_try_each(suit_manifest_t *manifest, + int key, nanocbor_value_t *it) +{ + (void)key; + LOG_DEBUG("Starting suit-directive-try-each handler\n"); + nanocbor_value_t container; + + if ((nanocbor_enter_array(it, &container) < 0) && + (nanocbor_enter_map(it, &container) < 0)) { + return SUIT_ERR_INVALID_MANIFEST; + } + + int res = SUIT_ERR_COND; + while (!nanocbor_at_end(&container)) { + nanocbor_value_t _container = container; + /* `_container` should be CBOR _bstr wrapped according to the spec, but + * it is not */ + res = suit_handle_manifest_structure(manifest, &_container, + suit_command_sequence_handlers, + suit_command_sequence_handlers_len); + + nanocbor_skip(&container); + + if (res != SUIT_ERR_COND) { + break; + } + } + + return res; +} + +static int _param_get_uri_list(suit_manifest_t *manifest, + nanocbor_value_t *it) +{ + LOG_DEBUG("got url list\n"); + manifest->components[manifest->component_current].url = *it; + return 0; +} +static int _param_get_digest(suit_manifest_t *manifest, nanocbor_value_t *it) +{ + LOG_DEBUG("got digest\n"); + manifest->components[manifest->component_current].digest = *it; + return 0; +} + +static int _param_get_img_size(suit_manifest_t *manifest, + nanocbor_value_t *it) +{ + int res = nanocbor_get_uint32(it, &manifest->components[0].size); + + if (res < 0) { + LOG_DEBUG("error getting image size\n"); + return res; + } + return res; +} + +static int _dtv_set_param(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + /* `it` points to the entry of the map containing the type and value */ + nanocbor_value_t map; + + nanocbor_enter_map(it, &map); + + while (!nanocbor_at_end(&map)) { + /* map points to the key of the param */ + int32_t param_key; + nanocbor_get_int32(&map, ¶m_key); + LOG_DEBUG("Current component index: %" PRIi32 "\n", + manifest->component_current); + LOG_DEBUG("param_key=%" PRIi32 "\n", param_key); + int res; + switch (param_key) { + case 6: /* SUIT URI LIST */ + res = _param_get_uri_list(manifest, &map); + break; + case 11: /* SUIT DIGEST */ + res = _param_get_digest(manifest, &map); + break; + case 12: /* SUIT IMAGE SIZE */ + res = _param_get_img_size(manifest, &map); + break; + default: + LOG_DEBUG("Unsupported parameter %" PRIi32 "\n", param_key); + res = SUIT_ERR_UNSUPPORTED; + } + + nanocbor_skip(&map); + + if (res) { + return res; + } + } + return SUIT_OK; +} + +static int _dtv_fetch(suit_manifest_t *manifest, int key, + nanocbor_value_t *_it) +{ + (void)key; (void)_it; + LOG_DEBUG("_dtv_fetch() key=%i\n", key); + + const uint8_t *url; + size_t url_len; + + int err = nanocbor_get_tstr(&manifest->components[0].url, &url, &url_len); + if (err < 0) { + LOG_DEBUG("URL parsing failed\n)"); + return err; + } + memcpy(manifest->urlbuf, url, url_len); + manifest->urlbuf[url_len] = '\0'; + + LOG_DEBUG("_dtv_fetch() fetching \"%s\" (url_len=%u)\n", manifest->urlbuf, + (unsigned)url_len); + + int target_slot = riotboot_slot_other(); + riotboot_flashwrite_init(manifest->writer, target_slot); + + int res = -1; + + if (0) {} +#ifdef MODULE_SUIT_TRANSPORT_COAP + else if (strncmp(manifest->urlbuf, "coap://", 7) == 0) { + res = suit_coap_get_blockwise_url(manifest->urlbuf, COAP_BLOCKSIZE_64, + suit_flashwrite_helper, + manifest); + } +#endif +#ifdef MODULE_SUIT_TRANSPORT_MOCK + else if (strncmp(manifest->urlbuf, "test://", 7) == 0) { + res = SUIT_OK; + } +#endif + else { + LOG_WARNING("suit: unsupported URL scheme!\n)"); + return res; + } + + if (res) { + LOG_INFO("image download failed\n)"); + return res; + } + + manifest->state |= SUIT_MANIFEST_HAVE_IMAGE; + + LOG_DEBUG("Update OK\n"); + return SUIT_OK; +} + +static int _dtv_verify_image_match(suit_manifest_t *manifest, int key, + nanocbor_value_t *_it) +{ + (void)key; (void)_it; + LOG_DEBUG("dtv_image_match\n"); + const uint8_t *digest; + size_t digest_len; + int target_slot = riotboot_slot_other(); + + LOG_INFO("Verifying image digest\n"); + nanocbor_value_t _v = manifest->components[0].digest; + int res = nanocbor_get_subcbor(&_v, &digest, &digest_len); + if (res < 0) { + LOG_DEBUG("Unable to parse digest structure\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + /* "digest" points to a 36 byte string that includes the digest type. + * riotboot_flashwrite_verify_sha256() is only interested in the 32b digest, + * so shift the pointer accordingly. + */ + res = riotboot_flashwrite_verify_sha256(digest + 4, + manifest->components[0].size, + target_slot); + if (res != 0) { + return SUIT_ERR_COND; + } + return SUIT_OK; +} + +/* begin{code-style-ignore} */ +const suit_manifest_handler_t suit_command_sequence_handlers[] = { + [SUIT_COND_VENDOR_ID] = _cond_vendor_handler, + [SUIT_COND_CLASS_ID] = _cond_class_handler, + [SUIT_COND_IMAGE_MATCH] = _dtv_verify_image_match, + [SUIT_COND_COMPONENT_OFFSET] = _cond_comp_offset, + [SUIT_DIR_SET_COMPONENT_IDX] = _dtv_set_comp_idx, + [SUIT_DIR_TRY_EACH] = _dtv_try_each, + [SUIT_DIR_SET_PARAM] = _dtv_set_param, + [SUIT_DIR_OVERRIDE_PARAM] = _dtv_set_param, + [SUIT_DIR_FETCH] = _dtv_fetch, + [SUIT_DIR_RUN_SEQUENCE] = _dtv_run_seq_cond, +}; +/* end{code-style-ignore} */ + +const size_t suit_command_sequence_handlers_len = ARRAY_SIZE(suit_command_sequence_handlers); diff --git a/sys/suit/handlers_common.c b/sys/suit/handlers_common.c new file mode 100644 index 0000000000..6bfac92bfb --- /dev/null +++ b/sys/suit/handlers_common.c @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2020 Inria + * + * 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. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT handler implementations for the manifest Common sections + * + * This file contains functions to handle the common info sections of a manifest. + * This includes components, dependencies and command sequences. + * + * @author Koen Zandberg + * + * @} + */ + +#include +#include + +#include "kernel_defines.h" +#include "suit/handlers.h" +#include "suit.h" + +#include "log.h" + +static int _component_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)manifest; + (void)key; + const uint8_t *subcbor; + size_t sub_size; + nanocbor_value_t _it; + nanocbor_get_bstr(it, &subcbor, &sub_size); + nanocbor_decoder_init(&_it, subcbor, sub_size); + + /* This is a list of lists, something like: + * [ + * [ "sda" "firmwareA" ], + * [ "sda" "firmwareB" ] + * ] + * */ + nanocbor_value_t arr; + if (nanocbor_enter_array(&_it, &arr) < 0) { + LOG_DEBUG("components field not an array %d\n", nanocbor_get_type(it)); + return SUIT_ERR_INVALID_MANIFEST; + } + unsigned n = 0; + while (!nanocbor_at_end(&arr)) { + nanocbor_value_t comp; + if (nanocbor_enter_array(&arr, &comp) < 0) { + LOG_DEBUG("component elements field not an array %d\n", + nanocbor_get_type(it)); + return SUIT_ERR_INVALID_MANIFEST; + } + while (!nanocbor_at_end(&comp)) { + const uint8_t *identifier; + size_t id_len; + if (nanocbor_get_bstr(&comp, &identifier, &id_len) < 0) { + LOG_DEBUG("Component name not a byte string\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + } + nanocbor_leave_container(&arr, &comp); + n++; + } + if (n > 1) { + LOG_INFO("More than 1 component found, exiting\n"); + return SUIT_ERR_UNSUPPORTED; + } + manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; + return 0; +} + +static int _dependencies_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)manifest; + (void)key; + (void)it; + /* No dependency support */ + return SUIT_ERR_UNSUPPORTED; +} + +int _common_sequence_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + LOG_DEBUG("Starting conditional sequence handler\n"); + return suit_handle_manifest_structure_bstr(manifest, it, + suit_command_sequence_handlers, + suit_command_sequence_handlers_len); +} + +/* begin{code-style-ignore} */ +const suit_manifest_handler_t suit_common_handlers[] = { + [SUIT_COMMON_DEPENDENCIES] = _dependencies_handler, + [SUIT_COMMON_COMPONENTS] = _component_handler, + [SUIT_COMMON_COMMAND_SEQUENCE] = _common_sequence_handler, +}; +/* end{code-style-ignore} */ + +const size_t suit_common_handlers_len = ARRAY_SIZE(suit_common_handlers); diff --git a/sys/suit/handlers_container.c b/sys/suit/handlers_container.c new file mode 100644 index 0000000000..a3beba5dd7 --- /dev/null +++ b/sys/suit/handlers_container.c @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2020 Inria + * + * 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. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT handlers for the SUIT outer wrapper + * + * This file contains the handlers for the content of the SUIT outer wrapper. + * This includes the authentication wrapper and the manifest itself. + * + * @author Koen Zandberg + * + * @} + */ + +#include +#include + +#include "hashes/sha256.h" +#include "kernel_defines.h" +#include "log.h" +#include "public_key.h" +#include "suit/conditions.h" +#include "suit/handlers.h" +#include "suit.h" + +static int _auth_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + cose_sign_dec_t verify; + const uint8_t *cose_buf; + const uint8_t *cose_container; + size_t container_len; + size_t cose_len = 0; + /* It is a list of cose signatures */ + int res = nanocbor_get_bstr(it, &cose_container, &container_len); + if (res < 0) { + LOG_INFO("Unable to get COSE signature\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + nanocbor_value_t _cont, arr; + nanocbor_decoder_init(&_cont, cose_container, container_len); + + int rc = nanocbor_enter_array(&_cont, &arr); + if (rc < 0) { + LOG_INFO("Unable to enter COSE signatures\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + uint32_t tag; + nanocbor_get_tag(&arr, &tag); + arr.remaining++; + res = nanocbor_get_subcbor(&arr, &cose_buf, &cose_len); + if (res < 0) { + LOG_INFO("Unable to get subcbor: %d\n", res); + } + + res = cose_sign_decode(&verify, cose_buf, cose_len); + if (res < 0) { + LOG_INFO("Unable to parse COSE signature\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + /* Iterate over signatures, should only be a single signature */ + cose_signature_dec_t signature; + + cose_sign_signature_iter_init(&signature); + if (!cose_sign_signature_iter(&verify, &signature)) { + LOG_INFO("Unable to get signature iteration\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + /* Initialize key from hardcoded public key */ + cose_key_t pkey; + cose_key_init(&pkey); + cose_key_set_keys(&pkey, COSE_EC_CURVE_ED25519, COSE_ALGO_EDDSA, + (uint8_t *)public_key, NULL, NULL); + + LOG_INFO("suit: verifying manifest signature\n"); + int verification = cose_sign_verify(&verify, &signature, + &pkey, manifest->validation_buf, + SUIT_COSE_BUF_SIZE); + if (verification != 0) { + LOG_INFO("Unable to validate signature: %d\n", verification); + return SUIT_ERR_SIGNATURE; + } + + manifest->cose_payload = verify.payload; + manifest->cose_payload_len = verify.payload_len; + manifest->state |= SUIT_STATE_COSE_AUTHENTICATED; + + return 0; +} + +static int _manifest_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + const uint8_t *manifest_buf; + size_t manifest_len; + + if (!(manifest->state & SUIT_STATE_COSE_AUTHENTICATED)) { + return SUIT_ERR_SIGNATURE; + } + + nanocbor_value_t cbor_buf = *it; + + nanocbor_get_subcbor(&cbor_buf, &manifest_buf, &manifest_len); + + uint8_t digest_struct[4 + SHA256_DIGEST_LENGTH] = + /* CBOR array of length 2, sha256 digest and a bytestring of SHA256 + * length + */ + { 0x82, 0x02, 0x58, SHA256_DIGEST_LENGTH }; + sha256(manifest_buf, manifest_len, digest_struct + 4); + + /* The COSE payload and the sha256 of the manifest itself is public info and + * verification does not depend on secret info. No need for cryptographic + * memcmp here */ + if (memcmp(digest_struct, manifest->cose_payload, + sizeof(digest_struct)) != 0) { + LOG_ERROR("SUIT manifest digest and COSE digest mismatch\n"); + return SUIT_ERR_DIGEST_MISMATCH; + } + + manifest->state |= SUIT_STATE_FULLY_AUTHENTICATED; + + LOG_DEBUG("Starting global sequence handler\n"); + return suit_handle_manifest_structure_bstr(manifest, it, + suit_global_handlers, + suit_global_handlers_len); +} + +/* begin{code-style-ignore} */ +const suit_manifest_handler_t suit_container_handlers[] = { + [SUIT_WRAPPER_AUTHENTICATION] = _auth_handler, + [SUIT_WRAPPER_MANIFEST] = _manifest_handler, +}; +/* end{code-style-ignore} */ + +const size_t suit_container_handlers_len = ARRAY_SIZE(suit_container_handlers); diff --git a/sys/suit/handlers_global.c b/sys/suit/handlers_global.c new file mode 100644 index 0000000000..d962f2033c --- /dev/null +++ b/sys/suit/handlers_global.c @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2020 Inria + * + * 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. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT handlers for the global SUIT manifest content. + * + * This file includes the implementation of the SUIT manifest content. + * + * @author Koen Zandberg + * + * @} + */ + +#include +#include + +#include "kernel_defines.h" +#include "log.h" +#include "suit/conditions.h" +#include "suit/handlers.h" +#include "suit/policy.h" +#include "suit.h" + +extern int _common_sequence_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it); + +static int _version_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)manifest; + (void)key; + /* Validate manifest version */ + int32_t version = -1; + if (nanocbor_get_int32(it, &version) >= 0) { + if (version == SUIT_VERSION) { + manifest->validated |= SUIT_VALIDATED_VERSION; + LOG_INFO("suit: validated manifest version\n)"); + return SUIT_OK; + } + } + return SUIT_ERR_SEQUENCE_NUMBER; +} + +static int _seq_no_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + + int32_t seq_nr; + + if (nanocbor_get_int32(it, &seq_nr) < 0) { + LOG_INFO("Unable to get sequence number\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_current()); + if (seq_nr <= (int32_t)hdr->version) { + LOG_INFO("%" PRId32 " <= %" PRId32 "\n", seq_nr, hdr->version); + LOG_INFO("seq_nr <= running image\n)"); + return SUIT_ERR_SEQUENCE_NUMBER; + } + + hdr = riotboot_slot_get_hdr(riotboot_slot_other()); + if (riotboot_hdr_validate(hdr) == 0) { + if (seq_nr <= (int32_t)hdr->version) { + LOG_INFO("%" PRIu32 " <= %" PRIu32 "\n", seq_nr, hdr->version); + LOG_INFO("seq_nr <= other image\n)"); + return SUIT_ERR_SEQUENCE_NUMBER; + } + } + LOG_INFO("suit: validated sequence number\n)"); + manifest->validated |= SUIT_VALIDATED_SEQ_NR; + return SUIT_OK; + +} + +static int _common_handler(suit_manifest_t *manifest, int key, + nanocbor_value_t *it) +{ + (void)key; + LOG_DEBUG("Starting common section handler\n"); + return suit_handle_manifest_structure_bstr(manifest, it, + suit_common_handlers, + suit_common_handlers_len); +} + +/* begin{code-style-ignore} */ +const suit_manifest_handler_t suit_global_handlers[] = { + [ 0] = NULL, + [SUIT_CONTAINER_VERSION] = _version_handler, + [SUIT_CONTAINER_SEQ_NO] = _seq_no_handler, + [SUIT_CONTAINER_COMMON] = _common_handler, + /* Install and validate both consist of a command sequence */ + [SUIT_CONTAINER_INSTALL] = _common_sequence_handler, + [SUIT_CONTAINER_VALIDATE] = _common_sequence_handler, +}; +/* end{code-style-ignore} */ + +const size_t suit_global_handlers_len = ARRAY_SIZE(suit_global_handlers); diff --git a/sys/suit/v4/policy.c b/sys/suit/policy.c similarity index 77% rename from sys/suit/v4/policy.c rename to sys/suit/policy.c index 35fa7551bb..67b86051ed 100644 --- a/sys/suit/v4/policy.c +++ b/sys/suit/policy.c @@ -8,23 +8,22 @@ * directory for more details. */ /** - * @ingroup sys_suit_v4 + * @ingroup sys_suit * @{ * * @file - * @brief SUIT v4 policy checking code + * @brief SUIT update policy checking code * * @author Kaspar Schleiser * * @} */ -#include "suit/v4/suit.h" -#include "suit/v4/policy.h" - #include "log.h" +#include "suit/policy.h" +#include "suit.h" -int suit_v4_policy_check(suit_v4_manifest_t *manifest) +int suit_policy_check(suit_manifest_t *manifest) { if (SUIT_DEFAULT_POLICY & ~(manifest->validated)) { LOG_INFO("SUIT policy check failed!\n"); diff --git a/sys/suit/suit.c b/sys/suit/suit.c new file mode 100644 index 0000000000..7ca96bc333 --- /dev/null +++ b/sys/suit/suit.c @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 Freie Universität Berlin + * Copyright (C) 2019 Kaspar Schleiser + * 2020 Inria + * + * 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. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT secure OTA firmware upgrade implementation for + * CBOR based manifests + * + * @author Koen Zandberg + * @author Kaspar Schleiser + * + * @} + */ + +#include +#include +#include +#include + +#include "log.h" +#include "suit/handlers.h" +#include "suit/policy.h" +#include "suit.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +int suit_parse(suit_manifest_t *manifest, const uint8_t *buf, + size_t len) +{ + nanocbor_value_t it; + + manifest->buf = buf; + manifest->len = len; + nanocbor_decoder_init(&it, buf, len); + LOG_DEBUG("Starting container sequence handler\n"); + return suit_handle_manifest_structure(manifest, &it, + suit_container_handlers, + suit_container_handlers_len); +} diff --git a/sys/suit/transport/Makefile b/sys/suit/transport/Makefile new file mode 100644 index 0000000000..fbda59b2f9 --- /dev/null +++ b/sys/suit/transport/Makefile @@ -0,0 +1,5 @@ +MODULE := suit_transport +SUBMODULES := 1 +BASE_MODULE := suit_transport + +include $(RIOTBASE)/Makefile.base diff --git a/sys/suit/coap.c b/sys/suit/transport/coap.c similarity index 86% rename from sys/suit/coap.c rename to sys/suit/transport/coap.c index 7873b5f9a7..f666615baf 100644 --- a/sys/suit/coap.c +++ b/sys/suit/transport/coap.c @@ -32,7 +32,7 @@ #include "thread.h" #include "periph/pm.h" -#include "suit/coap.h" +#include "suit/transport/coap.h" #include "net/sock/util.h" #ifdef MODULE_RIOTBOOT_SLOT @@ -40,8 +40,8 @@ #include "riotboot/flashwrite.h" #endif -#ifdef MODULE_SUIT_V4 -#include "suit/v4/suit.h" +#ifdef MODULE_SUIT +#include "suit.h" #endif #if defined(MODULE_PROGRESS_BAR) @@ -53,7 +53,7 @@ #ifndef SUIT_COAP_STACKSIZE /* allocate stack needed to keep a page buffer and do manifest validation */ -#define SUIT_COAP_STACKSIZE (3*THREAD_STACKSIZE_LARGE + FLASHPAGE_SIZE) +#define SUIT_COAP_STACKSIZE (3 * THREAD_STACKSIZE_LARGE + FLASHPAGE_SIZE) #endif #ifndef SUIT_COAP_PRIO @@ -74,8 +74,9 @@ static char _stack[SUIT_COAP_STACKSIZE]; static char _url[SUIT_URL_MAX]; static uint8_t _manifest_buf[SUIT_MANIFEST_BUFSIZE]; -#ifdef MODULE_SUIT_V4 -static inline void _print_download_progress(size_t offset, size_t len, uint32_t image_size) +#ifdef MODULE_SUIT +static inline void _print_download_progress(size_t offset, size_t len, + uint32_t image_size) { (void)offset; (void)len; @@ -145,6 +146,7 @@ static inline uint32_t deadline_from_interval(int32_t interval) static inline uint32_t deadline_left(uint32_t deadline) { int32_t left = (int32_t)(deadline - _now()); + if (left < 0) { left = 0; } @@ -155,7 +157,7 @@ static ssize_t _nanocoap_request(sock_udp_t *sock, coap_pkt_t *pkt, size_t len) { ssize_t res = -EAGAIN; size_t pdu_len = (pkt->payload - (uint8_t *)pkt->hdr) + pkt->payload_len; - uint8_t *buf = (uint8_t*)pkt->hdr; + uint8_t *buf = (uint8_t *)pkt->hdr; uint32_t id = coap_get_id(pkt); /* TODO: timeout random between between ACK_TIMEOUT and (ACK_TIMEOUT * @@ -163,7 +165,9 @@ static ssize_t _nanocoap_request(sock_udp_t *sock, coap_pkt_t *pkt, size_t len) uint32_t timeout = COAP_ACK_TIMEOUT * US_PER_SEC; uint32_t deadline = deadline_from_interval(timeout); - unsigned tries_left = COAP_MAX_RETRANSMIT + 1; /* add 1 for initial transmit */ + /* add 1 for initial transmit */ + unsigned tries_left = COAP_MAX_RETRANSMIT + 1; + while (tries_left) { if (res == -EAGAIN) { res = sock_udp_send(sock, buf, pdu_len, NULL); @@ -210,14 +214,19 @@ static ssize_t _nanocoap_request(sock_udp_t *sock, coap_pkt_t *pkt, size_t len) return res; } -static int _fetch_block(coap_pkt_t *pkt, uint8_t *buf, sock_udp_t *sock, const char *path, coap_blksize_t blksize, size_t num) +static int _fetch_block(coap_pkt_t *pkt, uint8_t *buf, sock_udp_t *sock, + const char *path, coap_blksize_t blksize, size_t num) { uint8_t *pktpos = buf; + pkt->hdr = (coap_hdr_t *)buf; - pktpos += coap_build_hdr(pkt->hdr, COAP_TYPE_CON, NULL, 0, COAP_METHOD_GET, num); + pktpos += coap_build_hdr(pkt->hdr, COAP_TYPE_CON, NULL, 0, COAP_METHOD_GET, + num); pktpos += coap_opt_put_uri_path(pktpos, 0, path); - pktpos += coap_opt_put_uint(pktpos, COAP_OPT_URI_PATH, COAP_OPT_BLOCK2, (num << 4) | blksize); + pktpos += + coap_opt_put_uint(pktpos, COAP_OPT_URI_PATH, COAP_OPT_BLOCK2, + (num << 4) | blksize); pkt->payload = pktpos; pkt->payload_len = 0; @@ -237,8 +246,8 @@ static int _fetch_block(coap_pkt_t *pkt, uint8_t *buf, sock_udp_t *sock, const c } int suit_coap_get_blockwise(sock_udp_ep_t *remote, const char *path, - coap_blksize_t blksize, - coap_blockwise_cb_t callback, void *arg) + coap_blksize_t blksize, + coap_blockwise_cb_t callback, void *arg) { /* mmmmh dynamically sized array */ uint8_t buf[64 + (0x1 << (blksize + 4))]; @@ -248,14 +257,12 @@ int suit_coap_get_blockwise(sock_udp_ep_t *remote, const char *path, /* HACK: use random local port */ local.port = 0x8000 + (xtimer_now_usec() % 0XFFF); - sock_udp_t sock; int res = sock_udp_create(&sock, &local, remote, 0); if (res < 0) { return res; } - int more = 1; size_t num = 0; res = -1; @@ -269,7 +276,8 @@ int suit_coap_get_blockwise(sock_udp_ep_t *remote, const char *path, coap_get_block2(&pkt, &block2); more = block2.more; - if (callback(arg, block2.offset, pkt.payload, pkt.payload_len, more)) { + if (callback(arg, block2.offset, pkt.payload, pkt.payload_len, + more)) { DEBUG("callback res != 0, aborting.\n"); res = -1; goto out; @@ -290,8 +298,8 @@ out: } int suit_coap_get_blockwise_url(const char *url, - coap_blksize_t blksize, - coap_blockwise_cb_t callback, void *arg) + coap_blksize_t blksize, + coap_blockwise_cb_t callback, void *arg) { char hostport[CONFIG_SOCK_HOSTPORT_MAXLEN]; char urlpath[CONFIG_SOCK_URLPATH_MAXLEN]; @@ -346,25 +354,27 @@ static int _2buf(void *arg, size_t offset, uint8_t *buf, size_t len, int more) } ssize_t suit_coap_get_blockwise_url_buf(const char *url, - coap_blksize_t blksize, - uint8_t *buf, size_t len) + coap_blksize_t blksize, + uint8_t *buf, size_t len) { - _buf_t _buf = { .ptr=buf, .len=len }; + _buf_t _buf = { .ptr = buf, .len = len }; int res = suit_coap_get_blockwise_url(url, blksize, _2buf, &_buf); + return (res < 0) ? (ssize_t)res : (ssize_t)_buf.offset; } static void _suit_handle_url(const char *url) { LOG_INFO("suit_coap: downloading \"%s\"\n", url); - ssize_t size = suit_coap_get_blockwise_url_buf(url, COAP_BLOCKSIZE_64, _manifest_buf, - SUIT_MANIFEST_BUFSIZE); + ssize_t size = suit_coap_get_blockwise_url_buf(url, COAP_BLOCKSIZE_64, + _manifest_buf, + SUIT_MANIFEST_BUFSIZE); if (size >= 0) { LOG_INFO("suit_coap: got manifest with size %u\n", (unsigned)size); riotboot_flashwrite_t writer; -#ifdef MODULE_SUIT_V4 - suit_v4_manifest_t manifest; +#ifdef MODULE_SUIT + suit_manifest_t manifest; memset(&manifest, 0, sizeof(manifest)); manifest.writer = &writer; @@ -372,18 +382,18 @@ static void _suit_handle_url(const char *url) manifest.urlbuf_len = SUIT_URL_MAX; int res; - if ((res = suit_v4_parse(&manifest, _manifest_buf, size)) != SUIT_OK) { - LOG_INFO("suit_v4_parse() failed. res=%i\n", res); + if ((res = suit_parse(&manifest, _manifest_buf, size)) != SUIT_OK) { + LOG_INFO("suit_parse() failed. res=%i\n", res); return; } - LOG_INFO("suit_v4_parse() success\n"); + LOG_INFO("suit_parse() success\n"); if (!(manifest.state & SUIT_MANIFEST_HAVE_IMAGE)) { LOG_INFO("manifest parsed, but no image fetched\n"); return; } - res = suit_v4_policy_check(&manifest); + res = suit_policy_check(&manifest); if (res) { return; } @@ -393,7 +403,8 @@ static void _suit_handle_url(const char *url) LOG_INFO("suit_coap: finalizing image flash\n"); riotboot_flashwrite_finish(&writer); - const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_other()); + const riotboot_hdr_t *hdr = riotboot_slot_get_hdr( + riotboot_slot_other()); riotboot_hdr_print(hdr); xtimer_sleep(1); @@ -414,7 +425,7 @@ static void _suit_handle_url(const char *url) int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, int more) { - suit_v4_manifest_t *manifest = (suit_v4_manifest_t *)arg; + suit_manifest_t *manifest = (suit_manifest_t *)arg; riotboot_flashwrite_t *writer = manifest->writer; if (offset == 0) { @@ -428,8 +439,9 @@ int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, } if (writer->offset != offset) { - LOG_WARNING("_suit_flashwrite(): writer->offset=%u, offset==%u, aborting\n", - (unsigned)writer->offset, (unsigned)offset); + LOG_WARNING( + "_suit_flashwrite(): writer->offset=%u, offset==%u, aborting\n", + (unsigned)writer->offset, (unsigned)offset); return -1; } @@ -481,10 +493,11 @@ static ssize_t _version_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, #ifdef MODULE_RIOTBOOT_SLOT static ssize_t _slot_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, - void *context) + void *context) { /* context is passed either as NULL or 0x1 for /active or /inactive */ char c = '0'; + if (context) { c += riotboot_slot_other(); } @@ -509,7 +522,7 @@ static ssize_t _trigger_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, } else { code = COAP_CODE_CREATED; - LOG_INFO("suit: received URL: \"%s\"\n", (char*)pkt->payload); + LOG_INFO("suit: received URL: \"%s\"\n", (char *)pkt->payload); suit_coap_trigger(pkt->payload, payload_len); } } @@ -532,9 +545,10 @@ void suit_coap_trigger(const uint8_t *url, size_t len) static const coap_resource_t _subtree[] = { #ifdef MODULE_RIOTBOOT_SLOT { "/suit/slot/active", COAP_METHOD_GET, _slot_handler, NULL }, - { "/suit/slot/inactive", COAP_METHOD_GET, _slot_handler, (void*)0x1 }, + { "/suit/slot/inactive", COAP_METHOD_GET, _slot_handler, (void *)0x1 }, #endif - { "/suit/trigger", COAP_METHOD_PUT | COAP_METHOD_POST, _trigger_handler, NULL }, + { "/suit/trigger", COAP_METHOD_PUT | COAP_METHOD_POST, _trigger_handler, + NULL }, { "/suit/version", COAP_METHOD_GET, _version_handler, NULL }, }; diff --git a/sys/suit/transport/mock.c b/sys/suit/transport/mock.c new file mode 100644 index 0000000000..3489247f75 --- /dev/null +++ b/sys/suit/transport/mock.c @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Kaspar Schleiser + * + * 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. + */ + +#include +#include +#include + +#include "kernel_defines.h" +#include "cpu_conf.h" + +#include "riotboot/flashwrite.h" +#include "riotboot/hdr.h" + +#define SLOT0_OFFSET 0x1000 +#define SLOT1_OFFSET 0x2000 + +static riotboot_hdr_t _riotboot_slots[] = { + { .magic_number = RIOTBOOT_MAGIC, + .version = 1, + .start_addr=0x100000, + }, + { .magic_number = RIOTBOOT_MAGIC, + .version = 2, + .start_addr=0x200000, + }, +}; + +const riotboot_hdr_t * const riotboot_slots[] = { + &_riotboot_slots[0], + &_riotboot_slots[1], +}; + +const unsigned riotboot_slot_numof = ARRAY_SIZE(riotboot_slots); + +static int _current_slot; + +int riotboot_slot_current(void) +{ + return _current_slot; +} + +int riotboot_slot_other(void) +{ + return (_current_slot == 0) ? 1 : 0; +} + +const riotboot_hdr_t *riotboot_slot_get_hdr(unsigned slot) +{ + assert(slot < riotboot_slot_numof); + + return riotboot_slots[slot]; +} + +size_t riotboot_slot_offset(unsigned slot) +{ + return (slot == 0) ? SLOT0_OFFSET : SLOT1_OFFSET; +} + +int riotboot_flashwrite_init_raw(riotboot_flashwrite_t *state, int target_slot, + size_t offset) +{ + (void)state; + (void)target_slot; + (void)offset; + puts("riotboot_flashwrite_init_raw() empty mock"); + return 0; +} + +int riotboot_flashwrite_verify_sha256(const uint8_t *sha256_digest, + size_t img_size, int target_slot) +{ + (void)sha256_digest; + (void)img_size; + (void)target_slot; + return 0; +} diff --git a/sys/suit/v4/cbor.c b/sys/suit/v4/cbor.c deleted file mode 100644 index 3ec824ad40..0000000000 --- a/sys/suit/v4/cbor.c +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (C) 2018 Freie Universität Berlin - * Copyright (C) 2018 Inria - * 2019 Kaspar Schleiser - * - * 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. - */ -/** - * @ingroup sys_suit - * @{ - * - * @file - * @brief SUIT manifest parser library for CBOR based manifests - * - * @author Koen Zandberg - * @author Kaspar Schleiser - * - * @} - */ -#include -#include -#include - -#include "suit/v4/handlers.h" -#include "suit/v4/suit.h" -#include "suit/v4/policy.h" -#include "nanocbor/nanocbor.h" -#include "cose/sign.h" - -#include "public_key.h" - -#include "log.h" - -#define ENABLE_DEBUG (0) -#include "debug.h" - -static suit_manifest_handler_t _manifest_get_auth_wrapper_handler(int key); -typedef suit_manifest_handler_t (*suit_manifest_handler_getter_t)(int key); - -int suit_cbor_subparse(nanocbor_value_t *bseq, nanocbor_value_t *it) -{ - const uint8_t *bytes; - size_t bytes_len = 0; - int res = nanocbor_get_bstr(bseq, &bytes, &bytes_len); - if (res < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - nanocbor_decoder_init(it, bytes, bytes_len); - return SUIT_OK; -} - -int _v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, - size_t len, suit_manifest_handler_getter_t getter) -{ - nanocbor_value_t it, map; - - nanocbor_decoder_init(&it, buf, len); - - map = it; - - if (nanocbor_enter_map(&map, &it) < 0) { - LOG_DEBUG("suit _v4_parse(): manifest not a map!\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - - while (!nanocbor_at_end(&it)) { - int32_t integer_key; - - if (nanocbor_get_int32(&it, &integer_key) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - LOG_DEBUG("got key val=%" PRIi32 "\n", integer_key); - suit_manifest_handler_t handler = getter(integer_key); - - nanocbor_value_t value = it; - nanocbor_skip(&it); - - if (handler) { - int res = handler(manifest, integer_key, &value); - if (res < 0) { - LOG_INFO("handler returned <0\n)"); - return SUIT_ERR_INVALID_MANIFEST; - } - } - else { - LOG_DEBUG("no handler found\n"); - } - } - - nanocbor_leave_container(&map, &it); - - return SUIT_OK; -} - -int suit_v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, - size_t len) -{ - manifest->buf = buf; - manifest->len = len; - return _v4_parse(manifest, buf, len, _manifest_get_auth_wrapper_handler); -} - -static int _auth_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - (void)key; - const uint8_t *cose_buf; - size_t cose_len = 0; - int res = nanocbor_get_bstr(it, &cose_buf, &cose_len); - - if (res < 0) { - LOG_INFO("Unable to get COSE signature\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - res = cose_sign_decode(&manifest->verify, cose_buf, cose_len); - if (res < 0) { - LOG_INFO("Unable to parse COSE signature\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - return 0; -} - -static int _manifest_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - (void)key; - const uint8_t *manifest_buf; - size_t manifest_len; - - nanocbor_get_bstr(it, &manifest_buf, &manifest_len); - - /* Validate the COSE struct first now that we have the payload */ - cose_sign_decode_set_payload(&manifest->verify, manifest_buf, manifest_len); - - /* Iterate over signatures, should only be a single signature */ - cose_signature_dec_t signature; - - cose_sign_signature_iter_init(&signature); - if (!cose_sign_signature_iter(&manifest->verify, &signature)) { - return SUIT_ERR_INVALID_MANIFEST; - } - - /* Initialize key from hardcoded public key */ - cose_key_t pkey; - cose_key_init(&pkey); - cose_key_set_keys(&pkey, COSE_EC_CURVE_ED25519, COSE_ALGO_EDDSA, - public_key, NULL, NULL); - - LOG_INFO("suit: verifying manifest signature...\n"); - int verification = cose_sign_verify(&manifest->verify, &signature, - &pkey, manifest->validation_buf, - SUIT_COSE_BUF_SIZE); - if (verification != 0) { - LOG_INFO("Unable to validate signature\n"); - return SUIT_ERR_SIGNATURE; - } - - return _v4_parse(manifest, manifest_buf, - manifest_len, suit_manifest_get_manifest_handler); -} - -static suit_manifest_handler_t _suit_manifest_get_handler(int key, - const suit_manifest_handler_t *handlers, - size_t len) -{ - if (key < 0 || (size_t)key >= len) { - return NULL; - } - return handlers[key]; -} - -/* begin{code-style-ignore} */ -static const suit_manifest_handler_t _auth_handlers[] = { - [ 0] = NULL, - [ 1] = _auth_handler, - [ 2] = _manifest_handler, -}; -/* end{code-style-ignore} */ - -static const unsigned _auth_handlers_len = ARRAY_SIZE(_auth_handlers); - -static suit_manifest_handler_t _manifest_get_auth_wrapper_handler(int key) -{ - return _suit_manifest_get_handler(key, _auth_handlers, - _auth_handlers_len); -} diff --git a/sys/suit/v4/handlers.c b/sys/suit/v4/handlers.c deleted file mode 100644 index 017d9dce9c..0000000000 --- a/sys/suit/v4/handlers.c +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright (C) 2019 Koen Zandberg - * - * 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. - */ -/** - * @ingroup sys_suit_v4 - * @{ - * - * @file - * @brief SUIT v4 - * - * @author Koen Zandberg - * - * @} - */ - -#include - -#include "suit/coap.h" -#include "suit/conditions.h" -#include "suit/v4/suit.h" -#include "suit/v4/handlers.h" -#include "suit/v4/policy.h" -#include "suit/v4/suit.h" -#include "riotboot/hdr.h" -#include "riotboot/slot.h" -#include - -#include "log.h" - -#define HELLO_HANDLER_MAX_STRLEN 32 - -static int _handle_command_sequence(suit_v4_manifest_t *manifest, nanocbor_value_t *it, - suit_manifest_handler_t handler); -static int _common_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it); -static int _common_sequence_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it); - -static int _validate_uuid(suit_v4_manifest_t *manifest, nanocbor_value_t *it, uuid_t *uuid) -{ - (void)manifest; - const uint8_t *uuid_manifest_ptr; - size_t len = sizeof(uuid_t); - char uuid_str[UUID_STR_LEN + 1]; - char uuid_str2[UUID_STR_LEN + 1]; - if (nanocbor_get_bstr(it, &uuid_manifest_ptr, &len) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - - uuid_to_string((uuid_t *)uuid_manifest_ptr, uuid_str); - uuid_to_string(uuid, uuid_str2); - LOG_INFO("Comparing %s to %s from manifest\n", uuid_str2, uuid_str); - return uuid_equal(uuid, (uuid_t *)uuid_manifest_ptr) ? 0 : -1; -} - -static int _cond_vendor_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - LOG_INFO("validating vendor ID\n"); - int rc = _validate_uuid(manifest, it, suit_get_vendor_id()); - if (rc == SUIT_OK) { - LOG_INFO("validating vendor ID: OK\n"); - manifest->validated |= SUIT_VALIDATED_VENDOR; - } - return rc; -} - -static int _cond_class_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - LOG_INFO("validating class id\n"); - int rc = _validate_uuid(manifest, it, suit_get_class_id()); - if (rc == SUIT_OK) { - LOG_INFO("validating class id: OK\n"); - manifest->validated |= SUIT_VALIDATED_CLASS; - } - return rc; -} - -static int _cond_comp_offset(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)manifest; - (void)key; - uint32_t offset; - nanocbor_get_uint32(it, &offset); - uint32_t other_offset = (uint32_t)riotboot_slot_offset(riotboot_slot_other()); - LOG_INFO("Comparing manifest offset %u with other slot offset %u\n", - (unsigned)offset, (unsigned)other_offset); - return other_offset == offset ? 0 : -1; -} - -static int _dtv_set_comp_idx(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - if (nanocbor_get_type(it) == NANOCBOR_TYPE_FLOAT) { - LOG_DEBUG("_dtv_set_comp_idx() ignoring boolean and floats\n)"); - nanocbor_skip(it); - } - else if (nanocbor_get_int32(it, &manifest->component_current) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - LOG_DEBUG("Setting component index to %d\n", (int)manifest->component_current); - return 0; -} - -static int _dtv_run_seq_cond(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - LOG_DEBUG("Starting conditional sequence handler\n"); - _handle_command_sequence(manifest, it, _common_sequence_handler); - return 0; -} - -static int _param_get_uri_list(suit_v4_manifest_t *manifest, nanocbor_value_t *it) -{ - LOG_DEBUG("got url list\n"); - manifest->components[manifest->component_current].url = *it; - return 0; -} -static int _param_get_digest(suit_v4_manifest_t *manifest, nanocbor_value_t *it) -{ - LOG_DEBUG("got digest\n"); - manifest->components[manifest->component_current].digest = *it; - return 0; -} - -static int _param_get_img_size(suit_v4_manifest_t *manifest, nanocbor_value_t *it) -{ - int res = nanocbor_get_uint32(it, &manifest->components[0].size); - if (res < 0) { - LOG_DEBUG("error getting image size\n"); - return res; - } - return res; -} - -static int _dtv_set_param(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - /* `it` points to the entry of the map containing the type and value */ - nanocbor_value_t map; - - nanocbor_enter_map(it, &map); - - while (!nanocbor_at_end(&map)) { - /* map points to the key of the param */ - int32_t param_key; - nanocbor_get_int32(&map, ¶m_key); - LOG_DEBUG("Setting component index to %" PRIi32 "\n", manifest->component_current); - LOG_DEBUG("param_key=%" PRIi32 "\n", param_key); - int res; - switch (param_key) { - case 6: /* SUIT URI LIST */ - res = _param_get_uri_list(manifest, &map); - break; - case 11: /* SUIT DIGEST */ - res = _param_get_digest(manifest, &map); - break; - case 12: /* SUIT IMAGE SIZE */ - res = _param_get_img_size(manifest, &map); - break; - default: - res = -1; - } - - nanocbor_skip(&map); - - if (res) { - return res; - } - } - return SUIT_OK; -} - -static int _dtv_fetch(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *_it) -{ - (void)key; (void)_it; (void)manifest; - LOG_DEBUG("_dtv_fetch() key=%i\n", key); - - const uint8_t *url; - size_t url_len; - - /* TODO: there must be a simpler way */ - { - /* the url list is a binary sequence containing a cbor array of - * (priority, url) tuples (represented as array with length two) - * */ - - nanocbor_value_t it; - - /* open sequence with cbor parser */ - int err = suit_cbor_subparse(&manifest->components[0].url, &it); - if (err < 0) { - LOG_DEBUG("subparse failed\n)"); - return err; - } - - nanocbor_value_t url_it; - /* enter container, confirm it is an array, too */ - if (nanocbor_enter_array(&it, &url_it) < 0) { - LOG_DEBUG("url list no array\n)"); - return SUIT_ERR_INVALID_MANIFEST; - } - - nanocbor_value_t url_value_it; - if (nanocbor_enter_array(&url_it, &url_value_it) < 0) { - LOG_DEBUG("url entry no array\n)"); - return SUIT_ERR_INVALID_MANIFEST; - } - - /* expect two entries: priority as int, url as byte string. bail out if not. */ - uint32_t prio; - /* check that first array entry is an int (the priority of the url) */ - if (nanocbor_get_uint32(&url_value_it, &prio) < 0) { - LOG_DEBUG("expected URL priority (int), got %d\n", - nanocbor_get_type(&url_value_it)); - return -1; - } - LOG_DEBUG("URL priority %"PRIu32"\n", prio); - - int res = nanocbor_get_tstr(&url_value_it, &url, &url_len); - if (res < 0) { - LOG_DEBUG("error parsing URL\n)"); - return SUIT_ERR_INVALID_MANIFEST; - } - if (url_len >= manifest->urlbuf_len) { - LOG_INFO("url too large: %u>%u\n)", (unsigned)url_len, - (unsigned)manifest->urlbuf_len); - return SUIT_ERR_UNSUPPORTED; - } - memcpy(manifest->urlbuf, url, url_len); - manifest->urlbuf[url_len] = '\0'; - - nanocbor_leave_container(&url_it, &url_value_it); - nanocbor_leave_container(&it, &url_it); - } - - LOG_DEBUG("_dtv_fetch() fetching \"%s\" (url_len=%u)\n", manifest->urlbuf, - (unsigned)url_len); - - int target_slot = riotboot_slot_other(); - riotboot_flashwrite_init(manifest->writer, target_slot); - int res = suit_coap_get_blockwise_url(manifest->urlbuf, COAP_BLOCKSIZE_64, - suit_flashwrite_helper, manifest); - - if (res) { - LOG_INFO("image download failed\n)"); - return res; - } - - const uint8_t *digest; - size_t digest_len; - - res = nanocbor_get_bstr(&manifest->components[0].digest, &digest, &digest_len); - if (res < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - - /* "digest" points to a 36 byte string that includes the digest type. - * riotboot_flashwrite_verify_sha256() is only interested in the 32b digest, - * so shift the pointer accordingly. - */ - res = riotboot_flashwrite_verify_sha256(digest + 4, manifest->components[0].size, - target_slot); - if (res) { - LOG_INFO("image verification failed\n"); - return res; - } - - manifest->state |= SUIT_MANIFEST_HAVE_IMAGE; - - return SUIT_OK; -} - -static int _version_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - (void)manifest; - (void)key; - /* Validate manifest version */ - int32_t version = -1; - if (nanocbor_get_int32(it, &version) >= 0) { - if (version == SUIT_VERSION) { - manifest->validated |= SUIT_VALIDATED_VERSION; - LOG_INFO("suit: validated manifest version\n)"); - return 0; - } - else { - return -1; - } - } - return -1; -} - -static int _seq_no_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - - int32_t seq_nr; - - if (nanocbor_get_int32(it, &seq_nr) < 0) { - LOG_INFO("Unable to get sequence number\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_current()); - if (seq_nr <= (int32_t)hdr->version) { - LOG_INFO("%"PRId32" <= %"PRId32"\n", seq_nr, hdr->version); - LOG_INFO("seq_nr <= running image\n)"); - return -1; - } - - hdr = riotboot_slot_get_hdr(riotboot_slot_other()); - if (riotboot_hdr_validate(hdr) == 0) { - if (seq_nr<= (int32_t)hdr->version) { - LOG_INFO("%"PRIu32" <= %"PRIu32"\n", seq_nr, hdr->version); - LOG_INFO("seq_nr <= other image\n)"); - return -1; - } - } - LOG_INFO("suit: validated sequence number\n)"); - manifest->validated |= SUIT_VALIDATED_SEQ_NR; - return 0; - -} - -static int _dependencies_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - (void)manifest; - (void)key; - (void)it; - /* No dependency support */ - return 0; -} - -static int _component_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - (void)manifest; - (void)key; - - nanocbor_value_t arr; - - LOG_DEBUG("storing components\n)"); - if (nanocbor_enter_array(it, &arr) < 0) { - LOG_DEBUG("components field not an array\n"); - return -1; - } - unsigned n = 0; - while (!nanocbor_at_end(&arr)) { - nanocbor_value_t map; - if (n < SUIT_V4_COMPONENT_MAX) { - manifest->components_len += 1; - } - else { - LOG_DEBUG("too many components\n)"); - return SUIT_ERR_INVALID_MANIFEST; - } - - if (nanocbor_enter_map(&arr, &map) < 0) { - LOG_DEBUG("suit _v4_parse(): manifest not a map!\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - - suit_v4_component_t *current = &manifest->components[n]; - - while (!nanocbor_at_end(&map)) { - - /* handle key, value */ - int32_t integer_key; - if (nanocbor_get_int32(&map, &integer_key) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - - switch (integer_key) { - case SUIT_COMPONENT_IDENTIFIER: - current->identifier = map; - break; - case SUIT_COMPONENT_SIZE: - LOG_DEBUG("skipping SUIT_COMPONENT_SIZE"); - break; - case SUIT_COMPONENT_DIGEST: - current->digest = map; - break; - default: - LOG_DEBUG("ignoring unexpected component data (nr. %" PRIi32 ")\n", - integer_key); - } - nanocbor_skip(&map); - - LOG_DEBUG("component %u parsed\n", n); - } - nanocbor_leave_container(&arr, &map); - n++; - } - - manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; - - nanocbor_leave_container(it, &arr); - - LOG_DEBUG("storing components done\n)"); - - return 0; -} - -/* begin{code-style-ignore} */ -static const suit_manifest_handler_t global_handlers[] = { - [ 0] = NULL, - [ 1] = _version_handler, - [ 2] = _seq_no_handler, - [ 3] = _dependencies_handler, - [ 4] = _component_handler, - [ 5] = NULL, - [ 6] = _common_handler, - [ 9] = _common_handler, -}; -/* end{code-style-ignore} */ - -static const unsigned global_handlers_len = ARRAY_SIZE(global_handlers); - -/* begin{code-style-ignore} */ -static const suit_manifest_handler_t _sequence_handlers[] = { - [ 0] = NULL, - [ 1] = _cond_vendor_handler, - [ 2] = _cond_class_handler, - [10] = _cond_comp_offset, - /* Directives */ - [11] = _dtv_set_comp_idx, - /* [12] = _dtv_set_man_idx, */ - /* [13] = _dtv_run_seq, */ - [14] = _dtv_run_seq_cond, - [16] = _dtv_set_param, - [20] = _dtv_fetch, - /* [22] = _dtv_run, */ -}; -/* end{code-style-ignore} */ - -static const unsigned _sequence_handlers_len = ARRAY_SIZE(_sequence_handlers); - -static suit_manifest_handler_t _suit_manifest_get_handler(int key, - const suit_manifest_handler_t *handlers, - size_t len) -{ - if (key < 0 || (size_t)key >= len) { - return NULL; - } - return handlers[key]; -} - -suit_manifest_handler_t suit_manifest_get_manifest_handler(int key) -{ - return _suit_manifest_get_handler(key, global_handlers, - global_handlers_len); -} - -static int _common_sequence_handler(suit_v4_manifest_t *manifest, int key, - nanocbor_value_t *it) -{ - - suit_manifest_handler_t handler = _suit_manifest_get_handler(key, - _sequence_handlers, - _sequence_handlers_len); - LOG_DEBUG("Handling handler with key %d at %p\n", key, handler); - if (handler) { - return handler(manifest, key, it); - } - else { - LOG_DEBUG("Sequence handler not implemented, ID: %d\n", key); - return -1; - } -} - -static int _common_handler(suit_v4_manifest_t *manifest, int key, nanocbor_value_t *it) -{ - (void)key; - return _handle_command_sequence(manifest, it, _common_sequence_handler); -} - -int _handle_command_sequence(suit_v4_manifest_t *manifest, nanocbor_value_t *bseq, - suit_manifest_handler_t handler) -{ - - LOG_DEBUG("Handling command sequence\n"); - nanocbor_value_t it, arr; - - int err = suit_cbor_subparse(bseq, &it); - if (err < 0) { - return err; - } - - if (nanocbor_enter_array(&it, &arr) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - - while (!nanocbor_at_end(&arr)) { - nanocbor_value_t map; - if (nanocbor_enter_map(&arr, &map) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - int32_t integer_key; - if (nanocbor_get_int32(&map, &integer_key) < 0) { - return SUIT_ERR_INVALID_MANIFEST; - } - int res = handler(manifest, integer_key, &map); - if (res < 0) { - LOG_DEBUG("Sequence handler error\n"); - return res; - } - nanocbor_leave_container(&arr, &map); - } - nanocbor_leave_container(&it, &arr); - - return 0; -} diff --git a/tests/suit_manifest/Makefile b/tests/suit_manifest/Makefile new file mode 100644 index 0000000000..4ef7c23808 --- /dev/null +++ b/tests/suit_manifest/Makefile @@ -0,0 +1,40 @@ +include ../Makefile.tests_common + +USEMODULE += suit +USEMODULE += riotboot_hdr +USEMODULE += embunit + +# Lots of structs on the stack and crypto verification +CFLAGS += -DTHREAD_STACKSIZE_MAIN=\(8*THREAD_STACKSIZE_DEFAULT\) + +# Add a macro for the board name without quotes to use in the include file +# generator macro +CFLAGS += -DBOARD_NAME_UNQ=$(BOARD) + +# BINDIR is not included until Makefile.include is parsed +MANIFEST_DIR ?= bin/$(BOARD)/manifests +BLOBS += $(MANIFEST_DIR)/manifest0.bin +BLOBS += $(MANIFEST_DIR)/manifest1.bin +BLOBS += $(MANIFEST_DIR)/manifest2.bin +BLOBS += $(MANIFEST_DIR)/manifest3.bin + +USEMODULE += suit_transport_mock + +# Use a version of 'native' that includes flash page support +ifeq (native, $(BOARD)) + BOARDSDIR = $(CURDIR)/native_flashpage +endif + +FEATURES_REQUIRED += periph_flashpage + +TEST_DATA = $(MANIFEST_DIR)/created +BUILDDEPS += $(TEST_DATA) + +include $(RIOTBASE)/Makefile.include + +$(call target-export-variables,all,SUIT_TOOL SUIT_SEC MANIFEST_DIR) + +$(TEST_DATA): $(SUIT_SEC) $(SUIT_PUB_HDR) + @mkdir -p $(MANIFEST_DIR) + sh create_test_data.sh + @touch $@ diff --git a/tests/suit_manifest/Makefile.ci b/tests/suit_manifest/Makefile.ci new file mode 100644 index 0000000000..29fd972f24 --- /dev/null +++ b/tests/suit_manifest/Makefile.ci @@ -0,0 +1,17 @@ +BOARD_INSUFFICIENT_MEMORY := \ + chronos \ + i-nucleo-lrwan1 \ + msb-430 \ + msb-430h \ + nucleo-f030r8 \ + nucleo-f042k6 \ + nucleo-l031k6 \ + nucleo-l053r8 \ + stm32f030f4-demo \ + stm32l0538-disco \ + stm32f0discovery \ + telosb \ + wsn430-v1_3b \ + wsn430-v1_4 \ + z1 \ + # diff --git a/tests/suit_manifest/create_test_data.sh b/tests/suit_manifest/create_test_data.sh new file mode 100644 index 0000000000..79a3af71f5 --- /dev/null +++ b/tests/suit_manifest/create_test_data.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -e + +gen_manifest() { + local out="$1" + shift + local seqnr="$1" + shift + + + "${RIOTBASE}/dist/tools/suit_v3/gen_manifest.py" \ + --urlroot "test://test" \ + --seqnr "$seqnr" \ + --uuid-vendor "riot-os.org" \ + --uuid-class "${BOARD}" \ + -o "$out.tmp" \ + "${1}:$((0x1000))" "${2}:$((0x2000))" + + ${SUIT_TOOL} create -f suit -i "$out.tmp" -o "$out" + + rm -f "$out.tmp" +} + +sign_manifest() { + local in="$1" + local out="$2" + + "${SUIT_TOOL}" sign -k "${SUIT_SEC}" -m "$in" -o "$out" +} + +# random invalid data files +echo foo > "${MANIFEST_DIR}/file1.bin" +echo bar > "${MANIFEST_DIR}/file2.bin" + +# random valid cbor (manifest but not signed, missing cose auth) +gen_manifest "${MANIFEST_DIR}/manifest0.bin" 1 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin" + +# manifest with invalid seqnr +sign_manifest "${MANIFEST_DIR}/manifest0.bin" "${MANIFEST_DIR}/manifest1.bin" + +(BOARD=invalid gen_manifest "${MANIFEST_DIR}/manifest2.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin") +sign_manifest "${MANIFEST_DIR}/manifest2.bin".unsigned "${MANIFEST_DIR}/manifest2.bin" + +# valid manifest, valid seqnr, signed +gen_manifest "${MANIFEST_DIR}/manifest3.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin" +sign_manifest "${MANIFEST_DIR}/manifest3.bin".unsigned "${MANIFEST_DIR}/manifest3.bin" diff --git a/tests/suit_manifest/include/riotboot/flashwrite.h b/tests/suit_manifest/include/riotboot/flashwrite.h new file mode 100644 index 0000000000..cd7b50e31d --- /dev/null +++ b/tests/suit_manifest/include/riotboot/flashwrite.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 Kaspar Schleiser + * + * 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. + */ + +#ifndef RIOTBOOT_FLASHWRITE_H +#define RIOTBOOT_FLASHWRITE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief mockup flashwrite context + */ +typedef struct { + unsigned target_slot; /**< Mockup slot */ +} riotboot_flashwrite_t; + +/** + * @brief Mockup flashwrite initialization function + * + * @param state flashwrite state + * @param target_slot Target slot + * + * @return Always returns 0 + */ +static inline int riotboot_flashwrite_init(riotboot_flashwrite_t *state, + int target_slot) +{ + (void)state; + (void)target_slot; + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif /* RIOTBOOT_FLASHWRITE_H */ diff --git a/tests/suit_manifest/main.c b/tests/suit_manifest/main.c new file mode 100644 index 0000000000..dfb89c1fd1 --- /dev/null +++ b/tests/suit_manifest/main.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 Inria + * + * 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. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Tests for suit manifest parser module + * + * @author Kaspar Schleiser + */ + +#include + +#include "kernel_defines.h" +#include "log.h" + +#include "suit.h" +#include "embUnit.h" + +#define TEST_MANIFEST_INCLUDE(file) + +/* cppcheck-suppress preprocessorErrorDirective + * (reason: board-dependent include paths) */ +#include TEST_MANIFEST_INCLUDE(manifest0.bin.h) +#include TEST_MANIFEST_INCLUDE(manifest1.bin.h) +#include TEST_MANIFEST_INCLUDE(manifest2.bin.h) +#include TEST_MANIFEST_INCLUDE(manifest3.bin.h) + +#define SUIT_URL_MAX 128 + +typedef struct { + const unsigned char *data; + size_t len; + int expected; +} manifest_blob_t; + +const manifest_blob_t manifest_blobs[] = { + /* Older GCC can't handle manifestx_bin_len here */ + { manifest0_bin, sizeof(manifest0_bin), SUIT_ERR_SIGNATURE }, + { manifest1_bin, sizeof(manifest1_bin), SUIT_ERR_SEQUENCE_NUMBER }, + { manifest2_bin, sizeof(manifest2_bin), SUIT_ERR_COND }, + { manifest3_bin, sizeof(manifest3_bin), SUIT_OK }, +}; + +const unsigned manifest_blobs_numof = ARRAY_SIZE(manifest_blobs); + +static int test_suit_manifest(const unsigned char *manifest_bin, + size_t manifest_bin_len) +{ + char _url[SUIT_URL_MAX]; + suit_manifest_t manifest; + riotboot_flashwrite_t writer; + + memset(&manifest, 0, sizeof(manifest)); + memset(&writer, 0, sizeof(writer)); + + manifest.writer = &writer; + manifest.urlbuf = _url; + manifest.urlbuf_len = SUIT_URL_MAX; + + int res; + if ((res = + suit_parse(&manifest, manifest_bin, + manifest_bin_len)) != SUIT_OK) { + return res; + } + + return res; +} + +static void test_suit_manifest_01_manifests(void) +{ + for (unsigned i = 0; i < manifest_blobs_numof; i++) { + printf("\n--- testing manifest %u\n", i); + int res = \ + test_suit_manifest(manifest_blobs[i].data, manifest_blobs[i].len); + printf("---- res=%i (expected=%i)\n", res, manifest_blobs[i].expected); + TEST_ASSERT_EQUAL_INT(manifest_blobs[i].expected, res); + } +} + +Test *tests_suit_manifest(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_suit_manifest_01_manifests), + }; + + EMB_UNIT_TESTCALLER(suit_manifest_tests, NULL, NULL, fixtures); + + return (Test *)&suit_manifest_tests; +} + +int main(void) +{ + TESTS_START(); + TESTS_RUN(tests_suit_manifest()); + TESTS_END(); + return 0; +} diff --git a/sys/suit/v4/Makefile b/tests/suit_manifest/native_flashpage/native/Makefile similarity index 53% rename from sys/suit/v4/Makefile rename to tests/suit_manifest/native_flashpage/native/Makefile index 6919f3a3e9..3c5636a97e 100644 --- a/sys/suit/v4/Makefile +++ b/tests/suit_manifest/native_flashpage/native/Makefile @@ -1,2 +1,3 @@ -MODULE := suit_v4 +DIRS += $(RIOTBOARD)/native + include $(RIOTBASE)/Makefile.base diff --git a/tests/suit_manifest/native_flashpage/native/Makefile.dep b/tests/suit_manifest/native_flashpage/native/Makefile.dep new file mode 100644 index 0000000000..2494d8582b --- /dev/null +++ b/tests/suit_manifest/native_flashpage/native/Makefile.dep @@ -0,0 +1 @@ +include $(RIOTBOARD)/native/Makefile.dep diff --git a/tests/suit_manifest/native_flashpage/native/Makefile.features b/tests/suit_manifest/native_flashpage/native/Makefile.features new file mode 100644 index 0000000000..17ba6f7b21 --- /dev/null +++ b/tests/suit_manifest/native_flashpage/native/Makefile.features @@ -0,0 +1,3 @@ +FEATURES_PROVIDED += periph_flashpage + +include $(RIOTBOARD)/native/Makefile.features diff --git a/tests/suit_manifest/native_flashpage/native/Makefile.include b/tests/suit_manifest/native_flashpage/native/Makefile.include new file mode 100644 index 0000000000..4a771cbc41 --- /dev/null +++ b/tests/suit_manifest/native_flashpage/native/Makefile.include @@ -0,0 +1,8 @@ +CFLAGS += -DFLASHPAGE_SIZE=256 +CFLAGS += -DFLASHPAGE_NUMOF=16 + +# We must duplicate the include done by $(RIOTBASE)/Makefile.include +# to also include the main board header +INCLUDES += $(addprefix -I,$(wildcard $(RIOTBOARD)/native/include)) + +include $(RIOTBOARD)/native/Makefile.include diff --git a/tests/suit_manifest/tests/01-run.py b/tests/suit_manifest/tests/01-run.py new file mode 100755 index 0000000000..1eee4e986c --- /dev/null +++ b/tests/suit_manifest/tests/01-run.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2018 Francisco Acosta +# +# 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 os +import sys +from testrunner import run + + +def testfunc(child): + board = os.environ['BOARD'] + # Increase timeout on "real" hardware + # 16 seconds on `samr21-xpro` + timeout = 30 if board != 'native' else -1 + child.expect(r"OK \(\d+ tests\)", timeout=timeout) + + +if __name__ == "__main__": + sys.exit(run(testfunc))