1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

Merge pull request #13486 from bergzand/pr/suit/ietf_v3

SUIT: Update to draft-ietf-v3
This commit is contained in:
Kaspar Schleiser 2020-03-20 14:44:29 +01:00 committed by GitHub
commit 7d0c475113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 3779 additions and 1814 deletions

View File

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

View File

@ -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.

View File

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

38
dist/tools/suit_v3/gen_key.py vendored Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020 Kaspar Schleiser <kaspar@schleiser.de>
# 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 <secret filename>")
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()

89
dist/tools/suit_v3/gen_manifest.py vendored Executable file
View File

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

View File

@ -0,0 +1,5 @@
*__pycache__
*.pyc
*.DS_Store
*.hex
examples/*.cbor

View File

@ -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.

View File

@ -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.

View File

@ -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())

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <secret filename> <public filename>")
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()

View File

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

View File

@ -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()

View File

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

View File

@ -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"}
]
}

View File

@ -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

View File

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

View File

@ -11,7 +11,7 @@
#include <string.h>
#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)
{

View File

@ -26,7 +26,7 @@
#include "shell.h"
#include "suit/coap.h"
#include "suit/transport/coap.h"
#include "riotboot/slot.h"
#ifdef MODULE_PERIPH_GPIO

View File

@ -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,

View File

@ -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

View File

@ -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

View File

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

72
makefiles/suit.inc.mk Normal file
View File

@ -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."

View File

@ -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."

View File

@ -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 <stddef.h>
#include <stdint.h>
@ -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 */
/** @} */

201
sys/include/suit/handlers.h Normal file
View File

@ -0,0 +1,201 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 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 <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*/
#ifndef SUIT_HANDLERS_H
#define SUIT_HANDLERS_H
#include <stddef.h>
#include <stdint.h>
#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 */
/** @} */

View File

@ -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 <stddef.h>
#include <stdint.h>
@ -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 */
/** @} */

View File

@ -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 */
/** @} */

View File

@ -1,64 +0,0 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 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 <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*/
#ifndef SUIT_V4_HANDLERS_H
#define SUIT_V4_HANDLERS_H
#include <stddef.h>
#include <stdint.h>
#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 */
/** @} */

View File

@ -22,7 +22,7 @@
#include <string.h>
#include <inttypes.h>
#include "suit/coap.h"
#include "suit/transport/coap.h"
int _suit_handler(int argc, char **argv)

View File

@ -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

View File

@ -13,7 +13,7 @@
* @{
*
* @file
* @brief SUIT conditions
* @brief SUIT condition initializers
*
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>

97
sys/suit/handlers.c Normal file
View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#include <nanocbor/nanocbor.h>
#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);
}

View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#include <nanocbor/nanocbor.h>
#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, &param_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);

110
sys/suit/handlers_common.c Normal file
View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#include <nanocbor/nanocbor.h>
#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);

View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <cose/sign.h>
#include <nanocbor/nanocbor.h>
#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);

107
sys/suit/handlers_global.c Normal file
View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#include <nanocbor/nanocbor.h>
#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);

View File

@ -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 <kaspar@schleiser.de>
*
* @}
*/
#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");

49
sys/suit/suit.c Normal file
View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 Freie Universität Berlin
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 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 <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
* @}
*/
#include <nanocbor/nanocbor.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#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);
}

View File

@ -0,0 +1,5 @@
MODULE := suit_transport
SUBMODULES := 1
BASE_MODULE := suit_transport
include $(RIOTBASE)/Makefile.base

View File

@ -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 },
};

81
sys/suit/transport/mock.c Normal file
View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2020 Kaspar Schleiser <kaspar@schleiser.de>
*
* 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 <assert.h>
#include <stdio.h>
#include <stdbool.h>
#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;
}

View File

@ -1,188 +0,0 @@
/*
* Copyright (C) 2018 Freie Universität Berlin
* Copyright (C) 2018 Inria
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* 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 <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
* @}
*/
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#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);
}

View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#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 <nanocbor/nanocbor.h>
#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, &param_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;
}

View File

@ -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 $@

View File

@ -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 \
#

View File

@ -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"

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 Kaspar Schleiser <kaspar@schleiser.de>
*
* 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 */

106
tests/suit_manifest/main.c Normal file
View File

@ -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 <kaspar@schleiser.de>
*/
#include <stdio.h>
#include "kernel_defines.h"
#include "log.h"
#include "suit.h"
#include "embUnit.h"
#define TEST_MANIFEST_INCLUDE(file) <blob/bin/BOARD_NAME_UNQ/manifests/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;
}

View File

@ -1,2 +1,3 @@
MODULE := suit_v4
DIRS += $(RIOTBOARD)/native
include $(RIOTBASE)/Makefile.base

View File

@ -0,0 +1 @@
include $(RIOTBOARD)/native/Makefile.dep

View File

@ -0,0 +1,3 @@
FEATURES_PROVIDED += periph_flashpage
include $(RIOTBOARD)/native/Makefile.features

View File

@ -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

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
# Copyright (C) 2018 Francisco Acosta <francisco.acosta@inria.fr>
#
# 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))