diff --git a/.gitignore b/.gitignore index afd467804e62..40bdef62cb17 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ results/ # Clangd compile flags (language server) compile_commands.json compile_flags.txt + +# suit manifest keys +keys/ diff --git a/Makefile.dep b/Makefile.dep index 1339dbca5c22..9511a1e61d5a 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -932,6 +932,31 @@ ifneq (,$(filter sock_dtls, $(USEMODULE))) USEMODULE += sock_udp endif +ifneq (,$(filter suit_v4_%,$(USEMODULE))) + USEMODULE += suit_v4 +endif + +ifneq (,$(filter suit_v4,$(USEMODULE))) + USEPKG += tinycbor + USEPKG += libcose + USEMODULE += libcose_crypt_hacl + USEMODULE += suit_conditions + + # 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 + +ifneq (,$(filter suit_conditions,$(USEMODULE))) + USEMODULE += uuid +endif + +ifneq (,$(filter suit_%,$(USEMODULE))) + USEMODULE += suit +endif + # Enable periph_gpio when periph_gpio_irq is enabled ifneq (,$(filter periph_gpio_irq,$(USEMODULE))) FEATURES_REQUIRED += periph_gpio diff --git a/dist/tools/flake8/check.sh b/dist/tools/flake8/check.sh index 08e2168cf033..f8705ffe1551 100755 --- a/dist/tools/flake8/check.sh +++ b/dist/tools/flake8/check.sh @@ -23,7 +23,12 @@ cd $RIOTBASE : "${RIOTTOOLS:=${RIOTBASE}/dist/tools}" . "${RIOTTOOLS}"/ci/changed_files.sh -EXCLUDE='^(.+/vendor/|dist/tools/cc2538-bsl|dist/tools/mcuboot|dist/tools/uhcpd|dist/tools/stm32loader)' +EXCLUDE="^(.+/vendor/\ +|dist/tools/cc2538-bsl\ +|dist/tools/mcuboot\ +|dist/tools/uhcpd\ +|dist/tools/stm32loader\ +|dist/tools/suit_v4/suit_manifest_encoder_04)" FILEREGEX='(\.py$|pyterm$)' FILES=$(FILEREGEX=${FILEREGEX} EXCLUDE=${EXCLUDE} changed_files) diff --git a/dist/tools/suit_v4/gen_key.py b/dist/tools/suit_v4/gen_key.py new file mode 100755 index 000000000000..1d940d3809b7 --- /dev/null +++ b/dist/tools/suit_v4/gen_key.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2019 Inria +# 2019 FU Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +import sys +import ed25519 + + +def main(): + if len(sys.argv) != 3: + print("usage: gen_key.py ") + sys.exit(1) + + _signing_key, _verifying_key = ed25519.create_keypair() + with open(sys.argv[1], "wb") as f: + f.write(_signing_key.to_bytes()) + + with open(sys.argv[2], "wb") as f: + f.write(_verifying_key.to_bytes()) + + vkey_hex = _verifying_key.to_ascii(encoding="hex") + print("Generated public key: '{}'".format(vkey_hex.decode())) + + +if __name__ == '__main__': + main() diff --git a/dist/tools/suit_v4/gen_manifest.py b/dist/tools/suit_v4/gen_manifest.py new file mode 100755 index 000000000000..d41a661c029a --- /dev/null +++ b/dist/tools/suit_v4/gen_manifest.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2019 Inria +# 2019 FU Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +import os +import hashlib +import json +import uuid +import argparse + +from suit_manifest_encoder_04 import compile_to_suit + + +def str2int(x): + if x.startswith("0x"): + return int(x, 16) + else: + return int(x) + + +def sha256_from_file(filepath): + sha256 = hashlib.sha256() + sha256.update(open(filepath, "rb").read()) + return sha256.digest() + + +def parse_arguments(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--template', '-t', help='Manifest template file path') + parser.add_argument('--urlroot', '-u', help='') + parser.add_argument('--offsets', '-O', help='') + parser.add_argument('--seqnr', '-s', + help='Sequence number of the manifest') + parser.add_argument('--output', '-o', nargs='?', + help='Manifest output binary file path') + parser.add_argument('--uuid-vendor', '-V', + help='Manifest vendor uuid') + parser.add_argument('--uuid-class', '-C', + help='Manifest class uuid') + parser.add_argument('slotfiles', nargs=2, + help='The list of slot file paths') + return parser.parse_args() + + +def main(args): + uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, args.uuid_vendor) + uuid_class = uuid.uuid5(uuid_vendor, args.uuid_class) + with open(args.template, 'r') as f: + template = json.load(f) + + template["sequence-number"] = int(args.seqnr) + template["conditions"] = [ + {"condition-vendor-id": uuid_vendor.hex}, + {"condition-class-id": uuid_class.hex}, + ] + + offsets = [str2int(offset) for offset in args.offsets.split(",")] + + for slot, slotfile in enumerate(args.slotfiles): + filename = slotfile + size = os.path.getsize(filename) + uri = os.path.join(args.urlroot, os.path.basename(filename)) + offset = offsets[slot] + + _image_slot = template["components"][0]["images"][slot] + _image_slot.update({ + "file": filename, + "uri": uri, + "size": size, + "digest": sha256_from_file(slotfile), + }) + + _image_slot["conditions"][0]["condition-component-offset"] = offset + _image_slot["file"] = filename + + result = compile_to_suit(template) + if args.output is not None: + with open(args.output, 'wb') as f: + f.write(result) + else: + print(result) + + +if __name__ == "__main__": + _args = parse_arguments() + main(_args) diff --git a/dist/tools/suit_v4/sign-04.py b/dist/tools/suit_v4/sign-04.py new file mode 100755 index 000000000000..db872b305fa4 --- /dev/null +++ b/dist/tools/suit_v4/sign-04.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2018-2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +""" +This is a demo script that is intended to act as a reference for SUIT manifest +signing. + +NOTE: It is expected that C and C++ parser implementations will be written +against this script, so it does not adhere to PEP8 in order to maintain +similarity between the naming in this script and that of C/C++ implementations. +""" + +import sys +import copy + +import cbor +import ed25519 + +from pprint import pprint + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization + +# Private key in arg 1 +# Public key in arg 2 +# Input file in arg 3 +# Output file in arg 4 + +COSE_Sign_Tag = 98 +APPLICATION_OCTET_STREAM_ID = 42 +ES256 = -7 +EDDSA = -8 + + +def signWrapper(algo, private_key, public_key, encwrapper): + wrapper = cbor.loads(encwrapper) + + pprint(wrapper[1]) + COSE_Sign = copy.deepcopy(wrapper[1]) + if not COSE_Sign: + protected = cbor.dumps({ + 3: APPLICATION_OCTET_STREAM_ID, # Content Type + }) + unprotected = { + } + signatures = [] + + # Create a COSE_Sign_Tagged object + COSE_Sign = [ + protected, + unprotected, + b'', + signatures + ] + + if algo == EDDSA: + public_bytes = public_key.to_bytes() + else: + public_bytes = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo) + + # NOTE: Using RFC7093, Method 4 + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(public_bytes) + kid = digest.finalize() + # Sign the payload + protected = cbor.dumps({ + 1: algo, # alg + }) + # Create the signing object + unprotected = { + 4: kid # kid + } + + Sig_structure = [ + "Signature", # Context + COSE_Sign[0], # Body Protected + protected, # signature protected + b'', # External AAD + wrapper[2] # payload + ] + sig_str = cbor.dumps(Sig_structure, sort_keys=True) + + if algo == EDDSA: + signature = private_key.sign(sig_str) + else: + signature = private_key.sign( + sig_str, + ec.ECDSA(hashes.SHA256()) + ) + + COSE_Signature = [ + protected, + unprotected, + signature + ] + COSE_Sign[3].append(COSE_Signature) + wrapper[1] = cbor.dumps(cbor.Tag(COSE_Sign_Tag, COSE_Sign), sort_keys=True) + return wrapper + + +def main(): + private_key = None + algo = ES256 + with open(sys.argv[1], 'rb') as fd: + priv_key_bytes = fd.read() + try: + private_key = serialization.load_pem_private_key( + priv_key_bytes, password=None, backend=default_backend()) + except ValueError: + algo = EDDSA + private_key = ed25519.SigningKey(priv_key_bytes) + + public_key = None + with open(sys.argv[2], 'rb') as fd: + pub_key_bytes = fd.read() + try: + public_key = serialization.load_pem_public_key( + pub_key_bytes, backend=default_backend()) + except ValueError: + public_key = ed25519.VerifyingKey(pub_key_bytes) + + # Read the input file + doc = None + with open(sys.argv[3], 'rb') as fd: + doc = fd.read() + + outDoc = signWrapper(algo, private_key, public_key, doc) + + with open(sys.argv[4], 'wb') as fd: + fd.write(cbor.dumps(outDoc, sort_keys=True)) + + +if __name__ == '__main__': + main() diff --git a/dist/tools/suit_v4/suit_manifest_encoder_04.py b/dist/tools/suit_v4/suit_manifest_encoder_04.py new file mode 100644 index 000000000000..f071f0412b17 --- /dev/null +++ b/dist/tools/suit_v4/suit_manifest_encoder_04.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import json +import cbor +import binascii +import uuid +import os + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization + +COSE_ALG = 1 +COSE_Sign_Tag = 98 +APPLICATION_OCTET_STREAM_ID = 42 +ES256 = -7 +EDDSA = -8 + +SUIT_Authentication_Wrapper = 1 +SUIT_Manifest = 2 +SUIT_Dependency_Resolution = 7 +SUIT_Payload_Fetch = 8 +SUIT_Install = 9 +SUIT_Text = 13 +SUIT_Coswid = 14 + +SUIT_Manifest_Version = 1 +SUIT_Manifest_Sequence_Number = 2 +SUIT_Dependencies = 3 +SUIT_Components = 4 +SUIT_Dependency_Components = 5 +SUIT_Common = 6 +SUIT_Dependency_Resolution = 7 +SUIT_Payload_Fetch = 8 +SUIT_Install = 9 +SUIT_Validate = 10 +SUIT_Load = 11 +SUIT_Run = 12 +SUIT_Text = 13 +SUIT_Coswid = 14 + +SUIT_Dependency_Digest = 1 +SUIT_Dependency_Prefix = 2 + +SUIT_Component_Identifier = 1 +SUIT_Component_Size = 2 +SUIT_Component_Digest = 3 + +SUIT_Component_Dependency_Index = 2 + +SUIT_Condition_Vendor_Identifier = 1 +SUIT_Condition_Class_Identifier = 2 +SUIT_Condition_Device_Identifier = 3 +SUIT_Condition_Image_Match = 4 +SUIT_Condition_Image_Not_Match = 5 +SUIT_Condition_Use_Before = 6 +SUIT_Condition_Minimum_Battery = 7 +SUIT_Condition_Update_Authorised = 8 +SUIT_Condition_Version = 9 +SUIT_Condition_Component_Offset = 10 + +SUIT_Directive_Set_Component_Index = 11 +SUIT_Directive_Set_Manifest_Index = 12 +SUIT_Directive_Run_Sequence = 13 +SUIT_Directive_Run_Sequence_Conditional = 14 +SUIT_Directive_Process_Dependency = 15 +SUIT_Directive_Set_Parameters = 16 +SUIT_Directive_Override_Parameters = 19 +SUIT_Directive_Fetch = 20 +SUIT_Directive_Copy = 21 +SUIT_Directive_Run = 22 +SUIT_Directive_Wait = 23 + +SUIT_Parameter_Strict_Order = 1 +SUIT_Parameter_Coerce_Condition_Failure = 2 +SUIT_Parameter_Vendor_ID = 3 +SUIT_Parameter_Class_ID = 4 +SUIT_Parameter_Device_ID = 5 +SUIT_Parameter_URI_List = 6 +SUIT_Parameter_Encryption_Info = 7 +SUIT_Parameter_Compression_Info = 8 +SUIT_Parameter_Unpack_Info = 9 +SUIT_Parameter_Source_Component = 10 +SUIT_Parameter_Image_Digest = 11 +SUIT_Parameter_Image_Size = 12 + +SUIT_Compression_Algorithm = 1 + +def obj2bytes(o): + if isinstance(o, int): + l = [] + while o: + l.append(o&0xff) + o = o >> 8 + return bytes(l) + if isinstance(o, str): + return o.encode('utf-8') + if isinstance(o, bytes): + return o + return b'' + +def make_SUIT_Components(unused, components): + comps = [] + for component in components: + c = { + SUIT_Component_Identifier : [obj2bytes(x) for x in component["id"]] + } + if "digest" in component: + c[SUIT_Component_Digest] = [1, binascii.a2b_hex(component["digest"])] + if "size" in component: + c[SUIT_Component_Size] = component["size"] + comps.append(c) + return (SUIT_Components, comps) + +def make_SUIT_Compression_Info(info): + algorithms = { + 'gzip' : 1, + 'bzip2' : 2, + 'deflate' : 3, + 'lz4' : 4, + 'lzma' : 7, + } + cinfo = { + SUIT_Compression_Algorithm :algorithms[info['algorithm']] + } + +def make_SUIT_Set_Parameters(parameters): + set_parameters = {} + SUIT_Parameters_Keys = { + # SUIT_Parameter_Strict_Order = 1 + # SUIT_Parameter_Coerce_Condition_Failure = 2 + # SUIT_Parameter_Vendor_ID = 3 + # SUIT_Parameter_Class_ID = 4 + # SUIT_Parameter_Device_ID = 5 + # SUIT_Parameter_URI_List = 6 + 'uris' : lambda x: (SUIT_Parameter_URI_List, cbor.dumps(x)), + # SUIT_Parameter_Encryption_Info = 7 + # SUIT_Parameter_Compression_Info = 8 + 'compression-info': lambda x : ( + SUIT_Parameter_Compression_Info, + cbor.dumps(make_SUIT_Compression_Info(x)) + ), + # SUIT_Parameter_Unpack_Info = 9 + 'source-index' : lambda x :(SUIT_Parameter_Source_Component, int(x)), + 'image-digest' : lambda x :(SUIT_Parameter_Image_Digest, cbor.dumps(x, sort_keys=True)), + 'image-size' : lambda x :(SUIT_Parameter_Image_Size, int(x)), + } + for p in parameters: + if p in SUIT_Parameters_Keys: + k, v = SUIT_Parameters_Keys[p](parameters[p]) + set_parameters[k] = v + else: + raise Exception('ERROR: {} not found!'.format(p)) + + return (SUIT_Directive_Set_Parameters, set_parameters) + +def make_SUIT_Sequence(seq_name, sequence): + seq = [] + SUIT_Sequence_Keys = { + "condition-vendor-id" : lambda x : (SUIT_Condition_Vendor_Identifier, uuid.UUID(x).bytes), + "condition-class-id" : lambda x : (SUIT_Condition_Class_Identifier, uuid.UUID(x).bytes), + "condition-device-id" : lambda x : (SUIT_Condition_Device_Identifier, uuid.UUID(x).bytes), + "condition-image" : lambda x : (SUIT_Condition_Image_Match, None), + "condition-not-image" : lambda x : (SUIT_Condition_Image_Not_Match, None), + # SUIT_Condition_Use_Before = 6 + # SUIT_Condition_Minimum_Battery = 7 + # SUIT_Condition_Update_Authorised = 8 + # SUIT_Condition_Version = 9 + "condition-component-offset" : lambda x: (SUIT_Condition_Component_Offset, int(x)), + # + "directive-set-component" : lambda x : (SUIT_Directive_Set_Component_Index, x), + # SUIT_Directive_Set_Manifest_Index = 12 + # SUIT_Directive_Run_Sequence = 13 + # SUIT_Directive_Run_Sequence_Conditional = 14 + "directive-run-conditional" : lambda x : ( + SUIT_Directive_Run_Sequence_Conditional, + cbor.dumps(make_SUIT_Sequence("conditional-sequence", x), sort_keys = True) + ), + # SUIT_Directive_Process_Dependency = 15 + # SUIT_Directive_Set_Parameters = 16 + "directive-set-var" : make_SUIT_Set_Parameters, + # SUIT_Directive_Override_Parameters = 19 + "directive-fetch" : lambda x : (SUIT_Directive_Fetch, None), + "directive-copy" : lambda x : (SUIT_Directive_Copy, None), + "directive-run" : lambda x : (SUIT_Directive_Run, None), + # SUIT_Directive_Wait = 23 + } + for command in sequence: + com_dict = {} + for c in command: + if c in SUIT_Sequence_Keys: + k, v = SUIT_Sequence_Keys[c](command[c]) + com_dict[k] = v + else: + raise Exception('ERROR: {} not found!'.format(c)) + seq.append(com_dict) + return seq + +def make_SUIT_Manifest(info): + # print(info) + SUIT_Manifest_Keys = { + "structure-version" : lambda y, x: (SUIT_Manifest_Version, x), + "sequence-number" : lambda y, x: (SUIT_Manifest_Sequence_Number, x), + # SUIT_Dependencies = 3 + "components" : make_SUIT_Components, + # SUIT_Dependency_Components = 5 + "common" : lambda y, x: (SUIT_Common, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), + # SUIT_Dependency_Resolution = 7 + # SUIT_Payload_Fetch = 8 + "apply-image" : lambda y, x: (SUIT_Install, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), + "system-verification": lambda y, x: (SUIT_Validate, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), + "load-image" : lambda y, x: (SUIT_Load, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), + "run-image" : lambda y, x: (SUIT_Run, cbor.dumps(make_SUIT_Sequence(y, x), sort_keys=True)), + # SUIT_Text = 13 + # SUIT_Coswid = 14 + } + manifest = {} + for field in info: + if field in SUIT_Manifest_Keys: + k, v = SUIT_Manifest_Keys[field](field, info[field]) + manifest[k] = v + else: + raise Exception('ERROR: {} not found!'.format(field)) + + # print ('suit-manifest: {}'.format(manifest)) + return manifest + +def make_SUIT_Outer_Wrapper(info): + Outer_Wrapper = { + SUIT_Authentication_Wrapper : None, + SUIT_Manifest : cbor.dumps(make_SUIT_Manifest(info), sort_keys = True) + } + # print('Outer_Wrapper: {}'.format(Outer_Wrapper)) + return Outer_Wrapper + + +def make_SUIT_Components(unused, components): + comps = [] + for component in components: + c = { + SUIT_Component_Identifier : [obj2bytes(x) for x in component["id"]] + } + if "digest" in component: + c[SUIT_Component_Digest] = [1, binascii.a2b_hex(component["digest"])] + if "size" in component: + c[SUIT_Component_Size] = component["size"] + comps.append(c) + return (SUIT_Components, comps) + +# Expected input format: +# { +# "digest-type" : "str", +# "structure-version" : 1, +# "sequence-number" : 2, +# "components": [ +# { +# "component-id":[bytes()], +# "bootable" : bool(), +# "images" : [ +# { +# "conditions" : [ +# {"current-digest": bytes()}, +# {"target-offset" : int()}, +# {"target-id": [bytes()]}, +# ], +# "digest": bytes(), +# "size" : int(), +# "uri" : str(), +# } +# ] +# "conditions" : [ +# {"current-digest": bytes()}, +# {"vendor-id" : bytes()}, +# {"class-id" : bytes()}, +# {"device-id" : bytes()}, +# ] +# } +# ], +# "conditions" : [ +# {"vendor-id" : bytes()}, +# {"class-id" : bytes()}, +# {"device-id" : bytes()}, +# ] +# } +def digest_str_to_id(s): + return { + 'sha-256' : 1, + 'sha-256-128' : 2, + 'sha-256-120' : 3, + 'sha-256-96' : 4, + 'sha-256-64' : 5, + 'sha-256-32' : 6, + 'sha-384' : 7, + 'sha-512' : 8, + 'sha3-224' : 9, + 'sha3-256' : 10, + 'sha3-384' : 11, + 'sha3-512' : 12, + }.get(s, 1) + +def compile_to_suit(suit_info): + digest_id = digest_str_to_id(suit_info.get('digest-type', 'sha-256')) + suit_manifest_desc = { + 'structure-version':int(suit_info.get('structure-version', 1)), + 'sequence-number':int(suit_info['sequence-number']), + } + #TODO: Dependencies + # Components + components = [] + #TODO: dependency components + common = [] + dependency_fetch = None + #TODO: Image Fetch when not in streaming mode + fetch_image = None + apply_image = [] + # System Verification + #TODO: Dependencies + system_verification = [ + {"directive-set-component": True}, + {"condition-image": None}, + ] + #TODO: Load Image + load_image = None + run_image = [] + + for con in suit_info.get('conditions', []): + common.append(con) + # for each component + for i, comp in enumerate(suit_info['components']): + comp_info = { + 'id' : comp['id'] + } + components.append(comp_info) + if len(comp['images']) == 1: + set_comp = {"directive-set-component": i} + set_params = { + "directive-set-var" : { + "image-size" : int(comp['images'][0]['size']), + "image-digest" : [digest_id, bytes(comp['images'][0]['digest'])] + } + } + common.append(set_comp) + common.append(set_params) + set_params = { + "directive-set-var" : { + "uris" : [[0, str(comp['images'][0]['uri'])]] + } + } + apply_image.append(set_comp) + apply_image.append(set_params) + else: + for image in comp['images']: + set_comp = {"directive-set-component": i} + set_params = { + "directive-set-var" : { + "image-size" : int(image['size']), + "image-digest" : [digest_id, bytes(image['digest'])] + } + } + conditional_seq = [set_comp] + image.get('conditions',[])[:] + [set_params] + conditional_set_params = { + 'directive-run-conditional': conditional_seq + } + common.append(conditional_set_params) + set_params = { + "directive-set-var" : { + "uris" : [[0, str(image['uri'])]] + } + } + conditional_seq = [set_comp] + image.get('conditions',[])[:] + [set_params] + conditional_set_params = { + 'directive-run-conditional': conditional_seq + } + + apply_image.append(conditional_set_params) + if comp.get('bootable', False): + run_image.append({'directive-set-component' : i}) + run_image.append({'directive-run':None}) + + apply_image.append({"directive-set-component": True}) + apply_image.append({"directive-fetch": None}) + + suit_manifest_desc.update({ + "components" : components, + "common" : common, + "apply-image" : apply_image, + "system-verification": system_verification, + "run-image" : run_image, + }) + + print(suit_manifest_desc) + + return cbor.dumps(make_SUIT_Outer_Wrapper(suit_manifest_desc), sort_keys=True) diff --git a/dist/tools/suit_v4/test-2img.json b/dist/tools/suit_v4/test-2img.json new file mode 100644 index 000000000000..fc18289b3d29 --- /dev/null +++ b/dist/tools/suit_v4/test-2img.json @@ -0,0 +1,33 @@ +{ + "digest-type" : "sha-256", + "structure-version" : 1, + "sequence-number" : 2, + "components": [ + { + "id":["00"], + "bootable" : true, + "images" : [ + { + "file" : "encode-04.py", + "uri" : "http://example.com/file.bin", + "conditions" : [ + {"condition-component-offset":4096} + ] + + }, + { + "file" : "suit_manifest_encoder_04.py", + "uri" : "http://example.com/file1.bin", + "conditions" : [ + {"condition-component-offset":8192} + ] + + } + ] + } + ], + "conditions" : [ + {"condition-vendor-id" : "fa6b4a53-d5ad-5fdf-be9d-e663e4d41ffe"}, + {"condition-class-id" : "1492af14-2569-5e48-bf42-9b2d51f2ab45"} + ] +} diff --git a/examples/suit_update/Makefile b/examples/suit_update/Makefile new file mode 100644 index 000000000000..93fddffc13e0 --- /dev/null +++ b/examples/suit_update/Makefile @@ -0,0 +1,102 @@ +# name of your application +APPLICATION = suit_update + +# If no BOARD is found in the environment, use this default: +BOARD ?= samr21-xpro + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +BOARD_INSUFFICIENT_MEMORY := arduino-duemilanove arduino-mega2560 arduino-nano \ + arduino-uno b-l072z-lrwan1 chronos lsn50 msb-430 \ + msb-430h nucleo-f031k6 nucleo-f042k6 nucleo-l031k6 \ + nucleo-f030r8 nucleo-f302r8 nucleo-f303k8 \ + nucleo-f334r8 nucleo-l053r8 nucleo-l073rz ruuvitag \ + saml10-xpro saml11-xpro stm32f0discovery thingy52 \ + telosb waspmote-pro wsn430-v1_3b wsn430-v1_4 z1 + +# lower pktbuf size to something sufficient for this application +CFLAGS += -DGNRC_PKTBUF_SIZE=2000 + +# +# Networking +# +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present + +# uncomment this to compile in support for a possibly available radio +#USEMODULE += gnrc_netdev_default + +USEMODULE += auto_init_gnrc_netif +# Specify the mandatory networking modules for IPv6 and UDP +USEMODULE += gnrc_ipv6_router_default +USEMODULE += gnrc_udp +USEMODULE += gnrc_sock_udp +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo + +# include this for printing IP addresses +USEMODULE += shell_commands + +# Set this to 1 to enable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +DEVELHELP ?= 0 + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +# +# SUIT update specific stuff +# + +USEMODULE += nanocoap_sock sock_util +USEMODULE += suit suit_coap + +# SUIT draft v4 support: +USEMODULE += suit_v4 + +# Change this to 0 to not use ethos +USE_ETHOS ?= 1 + +ifeq (1,$(USE_ETHOS)) + GNRC_NETIF_NUMOF := 2 + USEMODULE += stdio_ethos + USEMODULE += gnrc_uhcpc + + # ethos baudrate can be configured from make command + ETHOS_BAUDRATE ?= 115200 + CFLAGS += -DETHOS_BAUDRATE=$(ETHOS_BAUDRATE) + + # make sure ethos and uhcpd are built + TERMDEPS += host-tools + + # For local testing, run + # + # $ cd dist/tools/ethos; sudo ./setup_network.sh riot0 2001:db8::0/64 + # + #... in another shell and keep it running. + export TAP ?= riot0 + TERMPROG = $(RIOTTOOLS)/ethos/ethos + TERMFLAGS = $(TAP) $(PORT) +endif + +# The test needs the linked slot binaries without header in order to be able to +# create final binaries with specific APP_VER values. The CI RasPi test workers +# don't compile themselves and re-create signed images, thus add the required +# files here so they will be submitted along with the test jobs. +TEST_EXTRA_FILES += $(SLOT_RIOT_ELFS) $(SUIT_SEC) $(SUIT_PUB) + +# Due to issues with source address selection, this test currently might not +# run reliably on CI. +# See #12404, #12408. +TEST_ON_CI_BLACKLIST += all + +include $(RIOTBASE)/Makefile.include + +.PHONY: host-tools + +host-tools: + $(Q)env -u CC -u CFLAGS make -C $(RIOTTOOLS) + +include $(RIOTMAKE)/default-channel.inc.mk diff --git a/examples/suit_update/README.md b/examples/suit_update/README.md new file mode 100644 index 000000000000..4dbc70e2a1c3 --- /dev/null +++ b/examples/suit_update/README.md @@ -0,0 +1,559 @@ +# Overview + +This example shows how to integrate SUIT-compliant firmware updates into a +RIOT application. It implements basic support of the SUIT architecture using +the manifest format specified in +[draft-moran-suit-manifest-04](https://datatracker.ietf.org/doc/draft-moran-suit-manifest/04/). + +**WARNING**: This code should not be considered production ready for the time being. + It has not seen much exposure or security auditing. + +Table of contents: + +- [Prerequisites][prerequisites] +- [Setup][setup] + - [Signing key management][key-management] + - [Setup a wired device using ethos][setup-wired] + - [Provision the device][setup-wired-provision] + - [Configure the network][setup-wired-network] + - [Alternative: Setup a wireless device behind a border router][setup-wireless] + - [Provision the wireless device][setup-wireless-provision] + - [Configure the wireless network][setup-wireless-network] + - [Start aiocoap fileserver][start-aiocoap-fileserver] +- [Perform an update][update] + - [Build and publish the firmware update][update-build-publish] + - [Notify an update to the device][update-notify] +- [Detailed explanation][detailed-explanation] +- [Automatic test][test] + +## Prerequisites +[prerequisites]: #Prerequisites + +- Install python dependencies (only Python3.6 and later is supported): + + $ pip3 install --user ed25519 pyasn1 cbor + +- Install aiocoap from the source + + $ pip3 install --user --upgrade "git+https://github.com/chrysn/aiocoap#egg=aiocoap[all]" + + See the [aiocoap installation instructions](https://aiocoap.readthedocs.io/en/latest/installation.html) + for more details. + +- add `~/.local/bin` to PATH + + The aiocoap tools are installed to `~/.local/bin`. Either add + "export `PATH=$PATH:~/.local/bin"` to your `~/.profile` and re-login, or execute + that command *in every shell you use for this tutorial*. + +- Clone this repository: + + $ git clone https://github.com/RIOT-OS/RIOT + $ cd RIOT + +- In all setup below, `ethos` (EThernet Over Serial) is used to provide an IP + link between the host computer and a board. + + Just build `ethos` and `uhcpd` with the following commands: + + $ make -C dist/tools/ethos clean all + $ make -C dist/tools/uhcpd clean all + + It is possible to interact with the device over it's serial terminal as usual + using `make term`, but that requires an already set up tap interface. + See [update] for more information. + +## Setup +[setup]: #Setup + +### Key Management +[key-management]: #Key-management + +SUIT keys consist of a private and a public key file, stored in `$(SUIT_KEY_DIR)`. +Similar to how ssh names its keyfiles, the public key filename equals the +private key file, but has an extra `.pub` appended. + +`SUIT_KEY_DIR` defaults to the `keys/` folder at the top of a RIOT checkout. + +If the chosen key doesn't exist, it will be generated automatically. +That step can be done manually using the `suit/genkey` target. + +### Setup a wired device using ethos +[setup-wired]: #Setup-a-wired-device-using-ethos + +#### Configure the network +[setup-wired-network]: #Configure-the-network + +In one terminal, start: + + $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 + +This will create a tap interface called `riot0`, owned by the user. It will +also run an instance of uhcpcd, which starts serving the prefix +`2001:db8::/64`. Keep the shell open as long as you need the network. +Make sure to exit the "make term" instance from the next section *before* +exiting this, as otherwise the "riot0" interface doesn't get cleaned up +properly. + +#### Provision the device +[setup-wired-provision]: #Provision-the-device + +In order to get a SUIT capable firmware onto the node, run + + $ BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 + +This command also generates the cryptographic keys (private/public) used to +sign and verify the manifest and images. See the "Key generation" section in +[SUIT detailed explanation][detailed-explanation] for details. + +From another terminal on the host, add a routable address on the host `riot0` +interface: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +In another terminal, run: + + $ BOARD=samr21-xpro make -C examples/suit_update/ term + +### Alternative: Setup a wireless device behind a border router +[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 +wirelessly with a border router in between. + +#### Configure the wireless network +[setup-wireless-network]: #Configure-the-wireless-network + +A wireless node has no direct connection to the Internet so a border router (BR) +between 802.15.4 and Ethernet must be configured. +Any board providing a 802.15.4 radio can be used as BR. + +Plug the BR board on the computer and flash the +[gnrc_border_router](https://github.com/RIOT-OS/RIOT/tree/master/examples/gnrc_border_router) +application on it: + + $ make BOARD=
-C examples/gnrc_border_router flash + +In on terminal, start the network (assuming on the host the virtual port of the +board is `/dev/ttyACM0`): + + $ sudo ./dist/tools/ethos/start_network.sh /dev/ttyACM0 riot0 2001:db8::/64 + +Keep this terminal open. + +From another terminal on the host, add a routable address on the host `riot0` +interface: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +#### Provision the wireless device +[setup-wireless-provision]: #Provision-the-wireless-device +First un-comment L28 in the application [Makefile](Makefile) so `gnrc_netdev_default` is included in the build. +In this scenario the node will be connected through a border router. Ethos must +be disabled in the firmware when building and flashing the firmware: + + $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 + +Open a serial terminal on the device to get its global address: + + $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update term + +If the Border Router is already set up when opening the terminal you should get + + ... + + 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: local 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 + + suit_coap: started. + +Here the global IPv6 is `2001:db8::7b7e:3255:1313:8d96`. +**The address will be different according to your device and the chosen prefix**. +In this case the RIOT node can be reached from the host using its global address: + + $ ping6 2001:db8::7b7e:3255:1313:8d96 + +### Start aiocoap-fileserver +[Start-aiocoap-fileserver]: #start-aiocoap-fileserver + +`aiocoap-fileserver` is used for hosting firmwares available for updates. +Devices retrieve the new firmware using the CoAP protocol. + +Start `aiocoap-fileserver`: + + $ mkdir -p coaproot + $ aiocoap-fileserver coaproot + +Keep the server running in the terminal. + +## Perform an update +[update]: #Perform-an-update + +### Build and publish the firmware update +[update-build-publish]: #Build-and-publish-the-firmware-update + +Currently, the build system assumes that it can publish files by simply copying +them to a configurable folder. + +For this example, aiocoap-fileserver serves the files via CoAP. + +- To publish an update for a node in wired mode (behind ethos): + + $ BOARD=samr21-xpro SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish + +- To publish an update for a node in wireless mode (behind a border router): + + $ BOARD=samr21-xpro USE_ETHOS=0 SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish + +This publishes into the server a new firmware for a samr21-xpro board. You should +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" + ... + +### 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` + +- In wired mode: + + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::2%riot0] BOARD=samr21-xpro make -C examples/suit_update suit/notify + +- In wireless mode: + + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96] BOARD=samr21-xpro make -C examples/suit_update suit/notify + + +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 +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... + .... + +Once the signature is validated it continues validating other parts of the +manifest. +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 + .... + +Once the manifest validation is complete, the application fetches the image +and starts flashing. +This step takes some time to fetch and write to flash, a series of messages like +the following are printed to the terminal: + + .... + riotboot_flashwrite: processing bytes 1344-1407 + riotboot_flashwrite: processing bytes 1408-1471 + riotboot_flashwrite: processing bytes 1472-1535 + ... + +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 + + + main(): This is RIOT! (Version: 2019.04-devel-606-gaa7b-ota_suit_v2) + 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. + +## Detailed explanation +[detailed-explanation]: #Detailed-explanation + +### Node + +For the suit_update to work there are important modules that aren't normally built +in a RIOT application: + +* riotboot + * riotboot_hdr +* riotboot_slot +* suit + * suit_coap + * suit_v4 + +#### riotboot + +To be able to receive updates, the firmware on the device needs a bootloader +that can decide from witch of the firmware images (new one and olds ones) to boot. + +For suit updates you need at least two slots in the current conception on riotboot. +The flash memory will be divided in the following way: + +``` +|------------------------------- FLASH ------------------------------------------------------------| +|-RIOTBOOT_LEN-|------ RIOTBOOT_SLOT_SIZE (slot 0) ------|------ RIOTBOOT_SLOT_SIZE (slot 1) ------| + |----- RIOTBOOT_HDR_LEN ------| |----- RIOTBOOT_HDR_LEN ------| + --------------------------------------------------------------------------------------------------| +| riotboot | riotboot_hdr_1 + filler (0) | slot_0_fw | riotboot_hdr_2 + filler (0) | slot_1_fw | + --------------------------------------------------------------------------------------------------| +``` + +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 + +When calling make with the riotboot/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 +image. + +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. + +#### suit + +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** + +To enable support for suit_updates over coap a new thread is created. +This thread will expose 4 suit related resources: + +* /suit/slot/active: a resource that returns the number of their active slot +* /suit/slot/inactive: a resource that returns the number of their inactive slot +* /suit/trigger: this resource allows POST/PUT where the payload is assumed +tu be a url with the location of a manifest for a new firmware update on the +inactive slot. +* /suit/version: this resource is currently not implemented and return "NONE", +it should return the version of the application running on the device. + +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** + +This includes v4 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 +will extract the url for the firmware location from the manifest. + +It will then fetch the firmware, write it to the inactive slot and reboot the device. +Digest validation is done once all the firmware is written to flash. +From there the bootloader takes over, verifying the slot riotboot_hdr and boots +from the newest image. + +#### Key Generation + +To sign the manifest and for the device to verify the manifest a pair of keys +must be generated. Note that this is done automatically when building an +updatable RIOT image with `riotboot` or `suit/publish` make targets. + +This is simply done using the `suit/genkey` make target: + + $ BOARD=samr21-xpro make -C examples/suit_update suit/genkey + +You will get this message in the terminal: + + Generated public key: 'a0fc7fe714d0c81edccc50c9e3d9e6f9c72cc68c28990f235ede38e4553b4724' + +### Network + +For connecting the device with the internet we are using ethos (a simple +ethernet over serial driver). + +When executing $RIOTBASE/dist/tools/ethos: + + $ sudo ./start_network.sh /dev/ttyACM0 riot0 2001:db8::1/64 + +A tap interface named `riot0` is setup. `fe80::1/64` is set up as it's +link local address and `fd00:dead:beef::1/128` as the "lo" unique link local address. + +Also `2001:db8::1/64` is configured- as a prefix for the network. It also sets-up +a route to the `2001:db8::1/64` subnet through `fe80::2`. Where `fe80::2` is the default +link local address of the UHCP interface. + +Finally when: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +We are adding a routable address to the riot0 tap interface. The device can +now send messages to the the coap server through the riot0 tap interface. You could +use a different address for the coap server as long as you also add a routable +address, so: + + $ sudo ip address add $(SUIT_COAP_SERVER) dev riot0 + +When using a border router the same thing is happening although the node is no +longer reachable through its link local address but routed through to border router +so we can reach it with its global address. + +NOTE: if we weren't using a local server you would need to have ipv6 support +on your network or use tunneling. + +NOTE: using `fd00:dead:beef::1` as an address for the coap server would also +work and you wouldn't need to add a routable address to the tap interface since +a route to the loopback interface (`lo`) is already configured. + +### Server and file system variables + +The following variables are defined in makefiles/suit.inc.mk: + + SUIT_COAP_BASEPATH ?= firmware/$(APPLICATION)/$(BOARD) + SUIT_COAP_SERVER ?= localhost + SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) + SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot + SUIT_PUB_HDR ?= $(BINDIR)/riotbuild/public_key.h + +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 + +The following default values are using for generating the manifest: + + SUIT_VENDOR ?= RIOT + SUIT_VERSION ?= $(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 + +All files (both slot binaries, both manifests, copies of manifests with +"latest" instead of `$APP_VER` in riotboot build) are copied into the folder +`$(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH)`. The manifests contain URLs to +`$(SUIT_COAP_ROOT)/*` and are signed that way. + +The whole tree under `$(SUIT_COAP_FSROOT)` is expected to be served via CoAP +under `$(SUIT_COAP_ROOT)`. This can be done by e.g., `aiocoap-fileserver $(SUIT_COAP_FSROOT)`. + +### Makefile recipes + +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_COAP_ROOT): coap root address + - $(SUIT_CLASS) + - $(SUIT_VERSION) + - $(SUIT_VENDOR) + +suit/publish: makes the suit manifest, `slot*` bin and publishes it to the + aiocoap-fileserver + + 1.- builds slot0 and slot1 bin's + 2.- builds manifest + 3.- creates $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) directory + 4.- copy's binaries to $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) + - $(SUIT_COAP_ROOT): root url for the coap resources + +suit/notify: triggers a device update, it sends two requests: + + 1.- COAP get to check which slot is inactive on the device + 2.- COAP POST with the url where to fetch the latest manifest for + the inactive slot + + - $(SUIT_CLIENT): define the client ipv6 address + - $(SUIT_COAP_ROOT): root url for the coap resources + - $(SUIT_NOTIFY_MANIFEST): name of the manifest to notify, `latest` by + default. + +suit/genkey: this recipe generates a ed25519 key to sign the manifest + +**NOTE**: to plugin a new server you would only have to change the suit/publish +recipe, respecting or adjusting to the naming conventions.** + +## Automatic test +[Automatic test]: #test + +This applications ships with an automatic test. The test script itself expects +the application and bootloader to be flashed. It will then create two more +manifests with increasing version numbers and update twice, confirming after +each update that the newly flashed image is actually running. + +To run the test, + +- ensure the [prerequisites] are installed + +- make sure aiocoap-fileserver is in $PATH + +- compile and flash the application and bootloader: + +``` + $ make -C examples/suit_update clean all flash -j4 +``` + +- [set up the network][setup-wired-network] (in another shell): + +``` + $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 +``` + +- run the test: + +``` + $ make -C examples/suit_update test +``` diff --git a/examples/suit_update/coap_handler.c b/examples/suit_update/coap_handler.c new file mode 100644 index 000000000000..b3c6aedc5c10 --- /dev/null +++ b/examples/suit_update/coap_handler.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#include +#include +#include + +#include "net/nanocoap.h" +#include "suit/coap.h" + +static ssize_t _riot_board_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, void *context) +{ + (void)context; + return coap_reply_simple(pkt, COAP_CODE_205, buf, len, + COAP_FORMAT_TEXT, (uint8_t*)RIOT_BOARD, strlen(RIOT_BOARD)); +} + +/* must be sorted by path (ASCII order) */ +const coap_resource_t coap_resources[] = { + COAP_WELL_KNOWN_CORE_DEFAULT_HANDLER, + { "/riot/board", COAP_GET, _riot_board_handler, NULL }, + + /* this line adds the whole "/suit"-subtree */ + SUIT_COAP_SUBTREE, +}; + +const unsigned coap_resources_numof = ARRAY_SIZE(coap_resources); diff --git a/examples/suit_update/main.c b/examples/suit_update/main.c new file mode 100644 index 000000000000..10bb23b92585 --- /dev/null +++ b/examples/suit_update/main.c @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup examples + * @{ + * + * @file + * @brief SUIT updates over CoAP example server application (using nanocoap) + * + * @author Kaspar Schleiser + * @} + */ + +#include + +#include "irq.h" +#include "net/nanocoap_sock.h" +#include "xtimer.h" + +#include "suit/coap.h" +#include "riotboot/slot.h" + +#define COAP_INBUF_SIZE (256U) + +#define MAIN_QUEUE_SIZE (8) +static msg_t _main_msg_queue[MAIN_QUEUE_SIZE]; + +/* import "ifconfig" shell command, used for printing addresses */ +extern int _gnrc_netif_config(int argc, char **argv); + +int main(void) +{ + puts("RIOT SUIT update example application"); + + int current_slot = riotboot_slot_current(); + if (current_slot != -1) { + /* Sometimes, udhcp output messes up the following printfs. That + * confuses the test script. As a workaround, just disable interrupts + * for a while. + */ + irq_disable(); + printf("running from slot %d\n", current_slot); + printf("slot start addr = %p\n", (void *)riotboot_slot_get_hdr(current_slot)); + riotboot_slot_print_hdr(current_slot); + irq_enable(); + } + else { + printf("[FAILED] You're not running riotboot\n"); + } + + /* nanocoap_server uses gnrc sock which uses gnrc which needs a msg queue */ + msg_init_queue(_main_msg_queue, MAIN_QUEUE_SIZE); + + puts("Waiting for address autoconfiguration..."); + xtimer_sleep(3); + + /* print network addresses */ + puts("Configured network interfaces:"); + _gnrc_netif_config(0, NULL); + + /* start suit coap updater thread */ + suit_coap_run(); + + /* initialize nanocoap server instance */ + uint8_t buf[COAP_INBUF_SIZE]; + sock_udp_ep_t local = { .port=COAP_PORT, .family=AF_INET6 }; + nanocoap_server(&local, buf, sizeof(buf)); + + /* should be never reached */ + return 0; +} diff --git a/examples/suit_update/tests/01-run.py b/examples/suit_update/tests/01-run.py new file mode 100755 index 000000000000..e2114f7283ab --- /dev/null +++ b/examples/suit_update/tests/01-run.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + +# 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. + +import os +import subprocess +import sys +import tempfile + +from testrunner import run + +# Default test over loopback interface +COAP_HOST = "[fd00:dead:beef::1]" + +UPDATING_TIMEOUT = 10 +MANIFEST_TIMEOUT = 15 + +USE_ETHOS = int(os.getenv("USE_ETHOS", "1")) +TAP = os.getenv("TAP", "riot0") +TMPDIR = tempfile.TemporaryDirectory() + + +def start_aiocoap_fileserver(): + aiocoap_process = subprocess.Popen( + "exec aiocoap-fileserver %s" % TMPDIR.name, shell=True + ) + + return aiocoap_process + + +def cleanup(aiocoap_process): + aiocoap_process.kill() + TMPDIR.cleanup() + + +def notify(coap_server, client_url, version=None): + cmd = [ + "make", + "suit/notify", + "SUIT_COAP_SERVER={}".format(coap_server), + "SUIT_CLIENT={}".format(client_url), + ] + if version is not None: + cmd.append("SUIT_NOTIFY_VERSION={}".format(version)) + assert not subprocess.call(cmd) + + +def publish(server_dir, server_url, app_ver, latest_name=None): + cmd = [ + "make", + "suit/publish", + "SUIT_COAP_FSROOT={}".format(server_dir), + "SUIT_COAP_SERVER={}".format(server_url), + "APP_VER={}".format(app_ver), + "RIOTBOOT_SKIP_COMPILE=1", + ] + if latest_name is not None: + cmd.append("SUIT_MANIFEST_SIGNED_LATEST={}".format(latest_name)) + + assert not subprocess.call(cmd) + + +def wait_for_update(child): + return child.expect([r"riotboot_flashwrite: processing bytes (\d+)-(\d+)", + "riotboot_flashwrite: riotboot flashing " + "completed successfully"], + timeout=UPDATING_TIMEOUT) + + +def get_ipv6_addr(child): + if USE_ETHOS == 0: + # Get device global address + child.expect( + r"inet6 addr: (?P[0-9a-fA-F:]+:[A-Fa-f:0-9]+)" + " scope: global VAL" + ) + addr = child.match.group("gladdr").lower() + else: + # Get device local address + child.expect_exact("Link type: wired") + child.expect( + r"inet6 addr: (?P[0-9a-fA-F:]+:[A-Fa-f:0-9]+)" + " scope: local VAL" + ) + addr = "{}%{}".format(child.match.group("lladdr").lower(), TAP) + return addr + + +def ping6(client): + print("pinging node...") + ping_ok = False + for _i in range(10): + try: + subprocess.check_call(["ping", "-q", "-c1", "-w1", client]) + ping_ok = True + break + except subprocess.CalledProcessError: + pass + + if not ping_ok: + print("pinging node failed. aborting test.") + sys.exit(1) + else: + print("pinging node succeeded.") + return ping_ok + + +def testfunc(child): + """For one board test if specified application is updatable""" + + # Initial Setup and wait for address configuration + child.expect_exact("main(): This is RIOT!") + + # get version of currently running image + # "Image Version: 0x00000000" + child.expect(r"Image Version: (?P0x[0-9a-fA-F:]+)") + current_app_ver = int(child.match.group("app_ver"), 16) + + for version in [current_app_ver + 1, current_app_ver + 2]: + # Get address, if using ethos it will change on each reboot + client_addr = get_ipv6_addr(child) + client = "[{}]".format(client_addr) + # Wait for suit_coap thread to start + # Ping6 + ping6(client_addr) + child.expect_exact("suit_coap: started.") + # Trigger update process, verify it validates manifest correctly + 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( + r"riotboot_flashwrite: initializing update to target slot (\d+)", + timeout=MANIFEST_TIMEOUT, + ) + target_slot = int(child.match.group(1)) + # Wait for update to complete + while wait_for_update(child) == 0: + pass + + # Verify running slot + child.expect(r"running from slot (\d+)") + assert target_slot == int(child.match.group(1)), "BOOTED FROM SAME SLOT" + + print("TEST PASSED") + + +if __name__ == "__main__": + try: + res = 1 + aiocoap_process = start_aiocoap_fileserver() + # TODO: wait for coap port to be available + + res = run(testfunc, echo=True) + + except Exception as e: + print(e) + finally: + cleanup(aiocoap_process) + + sys.exit(res) diff --git a/makefiles/boot/riotboot.mk b/makefiles/boot/riotboot.mk index a1281422d020..1d984f6a8a0b 100644 --- a/makefiles/boot/riotboot.mk +++ b/makefiles/boot/riotboot.mk @@ -148,6 +148,11 @@ riotboot/flash: riotboot/flash-slot0 riotboot/flash-bootloader # It also makes 'flash' and 'flash-only' work without specific command. FLASHFILE = $(RIOTBOOT_EXTENDED_BIN) +# include suit targets +ifneq (,$(filter suit_v4, $(USEMODULE))) + include $(RIOTMAKE)/suit.v4.inc.mk +endif + else riotboot: $(Q)echo "error: riotboot feature not selected! (try FEATURES_REQUIRED += riotboot)" diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 55bad5757bbf..c7beabb713a2 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -84,7 +84,10 @@ PSEUDOMODULES += stdin PSEUDOMODULES += stdio_ethos PSEUDOMODULES += stdio_cdc_acm PSEUDOMODULES += stdio_uart_rx -PSEUDOMODULES += sock_dtls +PSEUDOMODULES += suit_% + +# handle suit_v4 being a distinct module +NO_PSEUDOMODULES += suit_v4 # print ascii representation in function od_hex_dump() PSEUDOMODULES += od_string diff --git a/makefiles/suit.v4.inc.mk b/makefiles/suit.v4.inc.mk new file mode 100644 index 000000000000..3a023a2c5b0c --- /dev/null +++ b/makefiles/suit.v4.inc.mk @@ -0,0 +1,103 @@ +# +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 ?= $(BINDIR_APP)-riot.suitv4_signed.$(SUIT_NOTIFY_VERSION).bin + +# Long manifest names require more buffer space when parsing +export CFLAGS += -DSOCK_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 -t $(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)/$$(basename $(SUIT_NOTIFY_MANIFEST))" && \ + echo "Triggered $(SUIT_CLIENT) to update." diff --git a/sys/Makefile b/sys/Makefile index 1926dabe36a8..3cb305621271 100644 --- a/sys/Makefile +++ b/sys/Makefile @@ -154,6 +154,9 @@ endif ifneq (,$(filter credman,$(USEMODULE))) DIRS += net/credman endif +ifneq (,$(filter suit%,$(USEMODULE))) + DIRS += suit +endif DIRS += $(dir $(wildcard $(addsuffix /Makefile, $(USEMODULE)))) diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index 8f8bed199506..872faf6fced6 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -580,4 +580,9 @@ void auto_init(void) auto_init_candev(); #endif /* MODULE_AUTO_INIT_CAN */ + +#ifdef MODULE_SUIT + extern void suit_init_conditions(void); + suit_init_conditions(); +#endif /* MODULE_SUIT */ } diff --git a/sys/include/suit/coap.h b/sys/include/suit/coap.h new file mode 100644 index 000000000000..e04cdea08dc8 --- /dev/null +++ b/sys/include/suit/coap.h @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * 2019 Inria + * 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_suit SUIT secure firmware updates + * @ingroup sys + * @brief SUIT secure firmware updates + * + * @experimental + * + * @{ + * + * @brief SUIT CoAP helper API + * @author Kaspar Schleiser + * + */ + +#ifndef SUIT_COAP_H +#define SUIT_COAP_H + +#include "net/nanocoap.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Start SUIT CoAP thread + */ +void suit_coap_run(void); + +/** + * @brief SUIT CoAP endpoint entry. + * + * In order to use, include this header, then add SUIT_COAP_SUBTREE to the nanocoap endpoint array. + * Mind the alphanumerical sorting! + * + * See examples/suit_update for an example. + */ +#define SUIT_COAP_SUBTREE \ + { \ + .path="/suit/", \ + .methods=COAP_MATCH_SUBTREE | COAP_METHOD_GET | COAP_METHOD_POST | COAP_METHOD_PUT, \ + .handler=coap_subtree_handler, \ + .context=(void*)&coap_resource_subtree_suit \ + } + +/* + * Dear Reviewer, + * + * At the time of PR'ing this code, there was a pile of CoAP PR's waiting for + * reviews. Some of that functionality is needed in one way or another for + * SUIT. In order to not block software updates with CoAP refactoring, some of + * the work-in-progress code has been copied here. We expect this to be + * removed as soon as CoAP in master provides similar functionality. + * + * As this is internal code that will go soon, I exclude this from Doxygen. + * + * Kaspar (July 2019) + */ +#ifndef DOXYGEN + +/** + * @brief Coap subtree handler + * + * @param[in,out] pkt Packet struct containing the request. Is reused for + * the response + * @param[in] buf Buffer to write reply to + * @param[in] len Total length of the buffer associated with the + * request + * @param[in] buf Buffer to write reply to + * + * @returns ssize_t Size of the reply + */ +ssize_t coap_subtree_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, + void *context); + +/** + * @brief Type for CoAP resource subtrees + */ +typedef const struct { + const coap_resource_t *resources; /**< ptr to resource array */ + const size_t resources_numof; /**< nr of entries in array */ +} coap_resource_subtree_t; + +/** + * @brief Coap blockwise request callback descriptor + * + * @param[in] arg Pointer to be passed as arguments to the callback + * @param[in] offset Offset of received data + * @param[in] buf Pointer to the received data + * @param[in] len Length of the received data + * @param[in] more -1 for no option, 0 for last block, 1 for more blocks + * + * @returns 0 on success + * @returns -1 on error + */ +typedef int (*coap_blockwise_cb_t)(void *arg, size_t offset, uint8_t *buf, size_t len, int more); + +/** + * @brief Reference to the coap resource subtree + */ +extern const coap_resource_subtree_t coap_resource_subtree_suit; + +/** + * @brief Coap block-wise-transfer size SZX + */ +typedef enum { + COAP_BLOCKSIZE_32 = 1, + COAP_BLOCKSIZE_64, + COAP_BLOCKSIZE_128, + COAP_BLOCKSIZE_256, + COAP_BLOCKSIZE_512, + COAP_BLOCKSIZE_1024, +} coap_blksize_t; + +/** + * @brief Performs a blockwise coap get request to the specified url. + * + * This function will fetch the content of the specified resource path via + * block-wise-transfer. A coap_blockwise_cb_t will be called on each received + * block. + * + * @param[in] url url pointer to source path + * @param[in] blksize sender suggested SZX for the COAP block request + * @param[in] callback callback to be executed on each received block + * @param[in] arg optional function arguments + * + * @returns -EINVAL if an invalid url is provided + * @returns -1 if failed to fetch the url content + * @returns 0 on success + */ +int suit_coap_get_blockwise_url(const char *url, + coap_blksize_t blksize, + coap_blockwise_cb_t callback, void *arg); + +#endif /* DOXYGEN */ + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_COAP_H */ +/** @} */ diff --git a/sys/include/suit/conditions.h b/sys/include/suit/conditions.h new file mode 100644 index 000000000000..573ca7acf6a5 --- /dev/null +++ b/sys/include/suit/conditions.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit + * @brief SUIT conditions + * + * @{ + * + * @brief SUIT conditions API + * @author Koen Zandberg + * @author Kaspar Schleiser + * + */ + +#ifndef SUIT_CONDITIONS_H +#define SUIT_CONDITIONS_H + +#include +#include + +#include "uuid.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The SUIT vendor ID source + * + * The basis of the UUID must be the vendor domain, please change this when + * using this module in a product + */ +#ifndef SUIT_VENDOR_DOMAIN +#define SUIT_VENDOR_DOMAIN "riot-os.org" /**< Device vendor domain */ +#endif + +/** + * @brief The SUIT class ID source + * + * By default the RIOT_VERSION define is used for this + */ +#ifndef SUIT_CLASS_ID +#define SUIT_CLASS_ID RIOT_BOARD +#endif + +/** + * @brief SUIT conditionals + */ +enum { + SUIT_COND_VENDOR_ID = 1, /**< Vendor ID match conditional */ + SUIT_COND_CLASS_ID = 2, /**< Class ID match conditional */ + SUIT_COND_DEV_ID = 3, /**< Device ID match conditional */ + SUIT_COND_BEST_BEFORE = 4, /**< Best before conditional */ +}; + +/** + * @brief SUIT condition parameters + */ +typedef struct { + uuid_t vendor; /**< Vendor url as UUID */ + uuid_t class; /**< Device class UUID */ + uuid_t device; /**< Device specific information as UUID */ +} suit_condition_params_t; + +/** + * @brief Initialize boot-time conditions for SUIT manifests + * + * This initializes the device-based conditions for validating manifest + * preconditions + * + * Vendor url as UUID: UUID5(DNS_PREFIX, SUIT_VENDOR_DOMAIN) + * Device class UUID: UUID5(vendor, SUIT_CLASS_ID) + * Device specific UUID: UUID5(vendor, Device ID) + */ +void suit_init_conditions(void); + +/** + * @brief Retrieve the generated vendor ID + * + * @returns The vendor ID as UUID + */ +uuid_t *suit_get_vendor_id(void); + +/** + * @brief Retrieve the generated class ID + * + * @returns The class ID as UUID + */ +uuid_t *suit_get_class_id(void); + +/** + * @brief Retrieve the generated device ID + * + * @returns The device ID as UUID + */ +uuid_t *suit_get_device_id(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_CONDITIONS_H */ +/** @} */ diff --git a/sys/include/suit/v4/handlers.h b/sys/include/suit/v4/handlers.h new file mode 100644 index 000000000000..c12b8bba607e --- /dev/null +++ b/sys/include/suit/v4/handlers.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Kaspar Schleiser + * 2019 Inria + * 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit_v4 + * @brief SUIT v4 manifest handlers + * + * @experimental + * + * @{ + * + * @brief Handler functions for SUIT manifests + * @author Koen Zandberg + * @author Kaspar Schleiser + */ + +#ifndef SUIT_V4_HANDLERS_H +#define SUIT_V4_HANDLERS_H + +#include +#include + +#include "suit/v4/suit.h" +#include "uuid.h" +#include "cbor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief suit handler prototype + * + * @param manifest SUIT v4 manifest context + * @param it CborValue 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, CborValue *it); + +/** + * @brief Get suit manifest handler for given integer key + * + * @param[in] key: integer key + * + * @return ptr to handler function + * @return NULL (if handler unavailable or key out of range) + */ +suit_manifest_handler_t suit_manifest_get_manifest_handler(int key); + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_V4_HANDLERS_H */ +/** @} */ diff --git a/sys/include/suit/v4/policy.h b/sys/include/suit/v4/policy.h new file mode 100644 index 000000000000..223113123e4b --- /dev/null +++ b/sys/include/suit/v4/policy.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Kaspar Schleiser + * 2019 Inria + * 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit_v4 + * @brief SUIT policy definitions + * + * @{ + * + * @brief SUIT policy definitions + * @author Koen Zandberg + * @author Kaspar Schleiser + * + */ + +#ifndef SUIT_V4_POLICY_H +#define SUIT_V4_POLICY_H + +#include +#include + +#include "uuid.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name bitfield of required policies + * @{ + */ +#define SUIT_VALIDATED_AUTH 0x1 /**< currently unused */ +#define SUIT_VALIDATED_VERSION 0x2 /**< SUIT format version */ +#define SUIT_VALIDATED_SEQ_NR 0x4 /**< new seq nr > old seq nr */ +#define SUIT_VALIDATED_VENDOR 0x8 /**< vendor UUID matches */ +#define SUIT_VALIDATED_CLASS 0x10 /**< class UUID matches */ +#define SUIT_VALIDATED_DEVICE 0x20 /**< device UUID matches */ +/** @} */ + +/** + * @brief SUIT default policy + */ +#define SUIT_DEFAULT_POLICY \ + (SUIT_VALIDATED_VERSION | SUIT_VALIDATED_SEQ_NR | SUIT_VALIDATED_VENDOR | SUIT_VALIDATED_CLASS) + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_V4_POLICY_H */ +/** @} */ diff --git a/sys/include/suit/v4/suit.h b/sys/include/suit/v4/suit.h new file mode 100644 index 000000000000..5b3d3227bd07 --- /dev/null +++ b/sys/include/suit/v4/suit.h @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @defgroup sys_suit_v4 SUIT draft v4 + * @ingroup sys_suit + * @brief SUIT manifest handling + * + * @{ + * + * @brief Handler functions for SUIT manifests + * @author Koen Zandberg + * @author Kaspar Schleiser + * + */ + +#ifndef SUIT_V4_SUIT_H +#define SUIT_V4_SUIT_H + +#include +#include + +#include "cose/sign.h" +#include "cbor.h" +#include "uuid.h" +#include "riotboot/flashwrite.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Buffer size used for Cose + */ +#ifndef SUIT_COSE_BUF_SIZE +#define SUIT_COSE_BUF_SIZE (512U) +#endif + +/** + * @brief Maximum number of components used for SUIT v4 + */ +#define SUIT_V4_COMPONENT_MAX (1U) + +/** + * @brief Supported SUIT manifest version + */ +#define SUIT_MANIFEST_VERSION (4) + +/** + * @brief Current SUIT serialization format version + * + * see https://tools.ietf.org/html/draft-moran-suit-manifest-04#section-8.2 for + * details + */ +#define SUIT_VERSION (1) + +/** + * @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; + +/** + * @brief TinyCBOR validation mode to use + */ +#define SUIT_TINYCBOR_VALIDATION_MODE CborValidateStrictMode + +/** + * @brief SUIT payload digest algorithms + * + * Unofficial list from + * [suit-manifest-generator](https://github.com/ARMmbed/suit-manifest-generator) + */ +typedef enum { + SUIT_DIGEST_NONE = 0, /**< No digest algo supplied */ + SUIT_DIGEST_SHA256 = 1, /**< SHA256 */ + SUIT_DIGEST_SHA384 = 2, /**< SHA384 */ + SUIT_DIGEST_SHA512 = 3, /**< SHA512 */ +} suit_v4_digest_t; + +/** + * @brief SUIT payload digest types + * + * Unofficial list from + * [suit-manifest-generator](https://github.com/ARMmbed/suit-manifest-generator) + */ +typedef enum { + SUIT_DIGEST_TYPE_RAW = 1, /**< Raw payload digest */ + 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; + +/** + * @brief SUIT component types + * + * Unofficial list from + * [suit-manifest-generator](https://github.com/ARMmbed/suit-manifest-generator) + */ +enum { + SUIT_COMPONENT_IDENTIFIER = 1, /**< Identifier component */ + SUIT_COMPONENT_SIZE = 2, /**< Size component */ + SUIT_COMPONENT_DIGEST = 3, /**< Digest component */ +}; + +/** + * @brief SUIT v4 component struct + */ +typedef struct { + uint32_t size; /**< Size */ + CborValue identifier; /**< Identifier*/ + CborValue url; /**< Url */ + CborValue digest; /**< Digest */ +} suit_v4_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 */ + 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]; + unsigned components_len; /**< Current number of components */ + int 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; + +/** + * @brief Bit flags used to determine if SUIT manifest contains components + */ +#define SUIT_MANIFEST_HAVE_COMPONENTS (0x1) +/** + * @brief Bit flags used to determine if SUIT manifest contains an image + */ +#define SUIT_MANIFEST_HAVE_IMAGE (0x2) + +/** + * @brief Parse a manifest + * + * @note The buffer is still required after parsing, please don't reuse the + * buffer while the @p manifest is used + * + * @param[in] manifest manifest context to store information in + * @param[in] buf buffer to parse the manifest from + * @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 + */ +int suit_v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, size_t len); + +/** + * @brief Check a manifest policy + * + * @param[in] manifest manifest context to check the policy for + * + * @return 0 on valid manifest policy + * @return -1 on invalid manifest policy + */ +int suit_v4_policy_check(suit_v4_manifest_t *manifest); + +/** + * @brief Initialize a cbor iterator for SUIT cbor map container parsing + * + * @param[in] map the cbor container + * @param[in] it the cbor iterator + * + * @return SUIT_OK when initialization is successful + * @return SUIT_ERR_INVALID_MANIFEST if the manifest is not a cbor container + */ +int suit_cbor_map_iterate_init(CborValue *map, CborValue *it); + +/** + * @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(CborValue *it, CborValue *key, CborValue *value); + +/** + * @brief Get cbor value as int + * + * @param[in] it cbor container iterator + * @param[out] out address of the returned integer + * + * @return SUIT_OK on success + * @return SUIT_ERR_INVALID_MANIFEST if value doesn't fit in an int + */ +int suit_cbor_get_int(const CborValue *it, int *out); + +/** + * @brief Get cbor value as unsigned + * + * @param[in] it cbor container iterator + * @param[out] out address of the returned unsigned + * + * @return SUIT_OK on success + * @return SUIT_ERR_INVALID_MANIFEST if value doesn't fit or cannot + * be converted to unsigned + */ +int suit_cbor_get_uint(const CborValue *it, unsigned *out); + +/** + * @brief Get cbor value as unsigned long + * + * @param[in] it cbor container iterator + * @param[out] out address of the returned unsigned long + * + * @return SUIT_OK on success + * @return SUIT_ERR_INVALID_MANIFEST if value doesn't fit or cannot + * be converted to unsigned long + */ +int suit_cbor_get_uint32(const CborValue *it, uint32_t *out); + +/** + * @brief Get cbor value as string + * + * @param[in] it cbor container iterator + * @param[out] buf address of the string buffer + * @param[out] len address of the len of the string + * + * @return SUIT_OK on success + * @return SUIT_ERR_INVALID_MANIFEST if value is not a valid string + */ +int suit_cbor_get_string(const CborValue *it, const uint8_t **buf, size_t *len); + +/** + * @brief Parser a cbor subsequence + * + * @param[in] parser ptr to cbor subparser + * @param[out] 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(CborParser *parser, CborValue *bseq, CborValue *it); + +/** + * @brief Helper function for writing bytes on flash a specified offset + * + * @param[in] arg ptr to flash writer + * @param[in] offset offset to write to on flash + * @param[in] buf bytes to write + * @param[in] len length of bytes to write + * @param[in] more whether more data is comming + * + * @return 0 on success + * @return <0 on error + */ +int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, + int more); + +#ifdef __cplusplus +} +#endif + +#endif /* SUIT_V4_SUIT_H */ +/** @} */ diff --git a/sys/suit/Makefile b/sys/suit/Makefile new file mode 100644 index 000000000000..5d9edcc7239f --- /dev/null +++ b/sys/suit/Makefile @@ -0,0 +1,9 @@ +SUBMODULES := 1 + +# don't complain about missing submodule .c file. +# necessary to not fail for suit_v*_*. +SUBMODULES_NOFORCE := 1 + +DIRS += v4 + +include $(RIOTBASE)/Makefile.base diff --git a/sys/suit/coap.c b/sys/suit/coap.c new file mode 100644 index 000000000000..e62771f1e161 --- /dev/null +++ b/sys/suit/coap.c @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2019 Freie Universität Berlin + * 2019 Inria + * 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT coap + * + * @author Koen Zandberg + * @author Kaspar Schleiser + * @} + */ + +#include +#include + +#include "msg.h" +#include "log.h" +#include "net/nanocoap.h" +#include "net/nanocoap_sock.h" +#include "thread.h" +#include "periph/pm.h" + +#include "suit/coap.h" +#include "net/sock/util.h" + +#ifdef MODULE_RIOTBOOT_SLOT +#include "riotboot/slot.h" +#include "riotboot/flashwrite.h" +#endif + +#ifdef MODULE_SUIT_V4 +#include "suit/v4/suit.h" +#endif + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#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) +#endif + +#ifndef SUIT_COAP_PRIO +#define SUIT_COAP_PRIO THREAD_PRIORITY_MAIN - 1 +#endif + +#ifndef SUIT_URL_MAX +#define SUIT_URL_MAX 128 +#endif + +#ifndef SUIT_MANIFEST_BUFSIZE +#define SUIT_MANIFEST_BUFSIZE 640 +#endif + +#define SUIT_MSG_TRIGGER 0x12345 + +static char _stack[SUIT_COAP_STACKSIZE]; +static char _url[SUIT_URL_MAX]; +static uint8_t _manifest_buf[SUIT_MANIFEST_BUFSIZE]; + +static kernel_pid_t _suit_coap_pid; + +ssize_t coap_subtree_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, + void *context) +{ + uint8_t uri[NANOCOAP_URI_MAX]; + + unsigned method_flag = coap_method2flag(coap_get_code_detail(pkt)); + + if (coap_get_uri_path(pkt, uri) > 0) { + coap_resource_subtree_t *subtree = context; + + for (unsigned i = 0; i < subtree->resources_numof; i++) { + const coap_resource_t *resource = &subtree->resources[i]; + + if (!(resource->methods & method_flag)) { + continue; + } + + int res = coap_match_path(resource, uri); + if (res > 0) { + continue; + } + else if (res < 0) { + break; + } + else { + return resource->handler(pkt, buf, len, resource->context); + } + } + } + + return coap_reply_simple(pkt, COAP_CODE_INTERNAL_SERVER_ERROR, buf, + len, COAP_FORMAT_TEXT, NULL, 0); +} + +static inline uint32_t _now(void) +{ + return xtimer_now_usec(); +} + +static inline uint32_t deadline_from_interval(int32_t interval) +{ + assert(interval >= 0); + return _now() + (uint32_t)interval; +} + +static inline uint32_t deadline_left(uint32_t deadline) +{ + int32_t left = (int32_t)(deadline - _now()); + if (left < 0) { + left = 0; + } + return left; +} + +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; + uint32_t id = coap_get_id(pkt); + + /* TODO: timeout random between between ACK_TIMEOUT and (ACK_TIMEOUT * + * ACK_RANDOM_FACTOR) */ + 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 */ + while (tries_left) { + if (res == -EAGAIN) { + res = sock_udp_send(sock, buf, pdu_len, NULL); + if (res <= 0) { + DEBUG("nanocoap: error sending coap request, %d\n", (int)res); + break; + } + } + + res = sock_udp_recv(sock, buf, len, deadline_left(deadline), NULL); + if (res <= 0) { + if (res == -ETIMEDOUT) { + DEBUG("nanocoap: timeout\n"); + + tries_left--; + if (!tries_left) { + DEBUG("nanocoap: maximum retries reached\n"); + break; + } + else { + timeout *= 2; + deadline = deadline_from_interval(timeout); + res = -EAGAIN; + continue; + } + } + DEBUG("nanocoap: error receiving coap response, %d\n", (int)res); + break; + } + else { + if (coap_parse(pkt, (uint8_t *)buf, res) < 0) { + DEBUG("nanocoap: error parsing packet\n"); + res = -EBADMSG; + } + else if (coap_get_id(pkt) != id) { + res = -EBADMSG; + continue; + } + + break; + } + } + + 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) +{ + 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_opt_put_uri_path(pktpos, 0, path); + pktpos += coap_opt_put_uint(pktpos, COAP_OPT_URI_PATH, COAP_OPT_BLOCK2, (num << 4) | blksize); + + pkt->payload = pktpos; + pkt->payload_len = 0; + + int res = _nanocoap_request(sock, pkt, 64 + (0x1 << (blksize + 4))); + if (res < 0) { + return res; + } + + res = coap_get_code(pkt); + DEBUG("code=%i\n", res); + if (res != 205) { + return -res; + } + + return 0; +} + +int suit_coap_get_blockwise(sock_udp_ep_t *remote, const char *path, + coap_blksize_t blksize, + coap_blockwise_cb_t callback, void *arg) +{ + /* mmmmh dynamically sized array */ + uint8_t buf[64 + (0x1 << (blksize + 4))]; + sock_udp_ep_t local = SOCK_IPV6_EP_ANY; + coap_pkt_t pkt; + + /* 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; + while (more == 1) { + DEBUG("fetching block %u\n", (unsigned)num); + res = _fetch_block(&pkt, buf, &sock, path, blksize, num); + DEBUG("res=%i\n", res); + + if (!res) { + coap_block1_t block2; + coap_get_block2(&pkt, &block2); + more = block2.more; + + if (callback(arg, block2.offset, pkt.payload, pkt.payload_len, more)) { + DEBUG("callback res != 0, aborting.\n"); + res = -1; + goto out; + } + } + else { + DEBUG("error fetching block\n"); + res = -1; + goto out; + } + + num += 1; + } + +out: + sock_udp_close(&sock); + return res; +} + +int suit_coap_get_blockwise_url(const char *url, + coap_blksize_t blksize, + coap_blockwise_cb_t callback, void *arg) +{ + char hostport[SOCK_HOSTPORT_MAXLEN]; + char urlpath[SOCK_URLPATH_MAXLEN]; + sock_udp_ep_t remote; + + if (strncmp(url, "coap://", 7)) { + LOG_INFO("suit: URL doesn't start with \"coap://\"\n"); + return -EINVAL; + } + + if (sock_urlsplit(url, hostport, urlpath) < 0) { + LOG_INFO("suit: invalid URL\n"); + return -EINVAL; + } + + if (sock_udp_str2ep(&remote, hostport) < 0) { + LOG_INFO("suit: invalid URL\n"); + return -EINVAL; + } + + if (!remote.port) { + remote.port = COAP_PORT; + } + + return suit_coap_get_blockwise(&remote, urlpath, blksize, callback, arg); +} + +typedef struct { + size_t offset; + uint8_t *ptr; + size_t len; +} _buf_t; + +static int _2buf(void *arg, size_t offset, uint8_t *buf, size_t len, int more) +{ + (void)more; + + _buf_t *_buf = arg; + if (_buf->offset != offset) { + return 0; + } + if (len > _buf->len) { + return -1; + } + else { + memcpy(_buf->ptr, buf, len); + _buf->offset += len; + _buf->ptr += len; + _buf->len -= len; + return 0; + } +} + +ssize_t suit_coap_get_blockwise_url_buf(const char *url, + coap_blksize_t blksize, + uint8_t *buf, size_t 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); + 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; + memset(&manifest, 0, sizeof(manifest)); + + manifest.writer = &writer; + manifest.urlbuf = _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); + return; + } + + LOG_INFO("suit_v4_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); + if (res) { + return; + } + +#endif + if (res == 0) { + LOG_INFO("suit_coap: finalizing image flash\n"); + riotboot_flashwrite_finish(&writer); + + const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_other()); + riotboot_hdr_print(hdr); + xtimer_sleep(1); + + if (riotboot_hdr_validate(hdr) == 0) { + LOG_INFO("suit_coap: rebooting..."); + pm_reboot(); + } + else { + LOG_INFO("suit_coap: update failed, hdr invalid"); + } + } + } + else { + LOG_INFO("suit_coap: error getting manifest\n"); + } +} + +int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, + int more) +{ + riotboot_flashwrite_t *writer = arg; + + if (offset == 0) { + if (len < RIOTBOOT_FLASHWRITE_SKIPLEN) { + LOG_WARNING("_suit_flashwrite(): offset==0, len<4. aborting\n"); + return -1; + } + offset = RIOTBOOT_FLASHWRITE_SKIPLEN; + buf += RIOTBOOT_FLASHWRITE_SKIPLEN; + len -= RIOTBOOT_FLASHWRITE_SKIPLEN; + } + + if (writer->offset != offset) { + LOG_WARNING("_suit_flashwrite(): writer->offset=%u, offset==%u, aborting\n", + (unsigned)writer->offset, (unsigned)offset); + return -1; + } + + DEBUG("_suit_flashwrite(): writing %u bytes at pos %u\n", len, offset); + + return riotboot_flashwrite_putbytes(writer, buf, len, more); +} + +static void *_suit_coap_thread(void *arg) +{ + (void)arg; + + LOG_INFO("suit_coap: started.\n"); + msg_t msg_queue[4]; + msg_init_queue(msg_queue, 4); + + _suit_coap_pid = thread_getpid(); + + msg_t m; + while (true) { + msg_receive(&m); + DEBUG("suit_coap: got msg with type %" PRIu32 "\n", m.content.value); + switch (m.content.value) { + case SUIT_MSG_TRIGGER: + LOG_INFO("suit_coap: trigger received\n"); + _suit_handle_url(_url); + break; + default: + LOG_WARNING("suit_coap: warning: unhandled msg\n"); + } + } + return NULL; +} + +void suit_coap_run(void) +{ + thread_create(_stack, SUIT_COAP_STACKSIZE, SUIT_COAP_PRIO, + THREAD_CREATE_STACKTEST, + _suit_coap_thread, NULL, "suit_coap"); +} + +static ssize_t _version_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, + void *context) +{ + (void)context; + return coap_reply_simple(pkt, COAP_CODE_205, buf, len, + COAP_FORMAT_TEXT, (uint8_t *)"NONE", 4); +} + +#ifdef MODULE_RIOTBOOT_SLOT +static ssize_t _slot_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, + void *context) +{ + /* context is passed either as NULL or 0x1 for /active or /inactive */ + char c = '0'; + if (context) { + c += riotboot_slot_other(); + } + else { + c += riotboot_slot_current(); + } + + return coap_reply_simple(pkt, COAP_CODE_205, buf, len, + COAP_FORMAT_TEXT, (uint8_t *)&c, 1); +} +#endif + +static ssize_t _trigger_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, + void *context) +{ + (void)context; + unsigned code; + size_t payload_len = pkt->payload_len; + if (payload_len) { + if (payload_len >= SUIT_URL_MAX) { + code = COAP_CODE_REQUEST_ENTITY_TOO_LARGE; + } + else { + memcpy(_url, pkt->payload, payload_len); + _url[payload_len] = '\0'; + + code = COAP_CODE_CREATED; + LOG_INFO("suit: received URL: \"%s\"\n", _url); + msg_t m = { .content.value = SUIT_MSG_TRIGGER }; + msg_send(&m, _suit_coap_pid); + } + } + else { + code = COAP_CODE_REQUEST_ENTITY_INCOMPLETE; + } + + return coap_reply_simple(pkt, code, buf, len, + COAP_FORMAT_NONE, NULL, 0); +} + +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 }, +#endif + { "/suit/trigger", COAP_METHOD_PUT | COAP_METHOD_POST, _trigger_handler, NULL }, + { "/suit/version", COAP_METHOD_GET, _version_handler, NULL }, +}; + +const coap_resource_subtree_t coap_resource_subtree_suit = +{ + .resources = &_subtree[0], + .resources_numof = ARRAY_SIZE(_subtree) +}; diff --git a/sys/suit/conditions.c b/sys/suit/conditions.c new file mode 100644 index 000000000000..53690d4dda99 --- /dev/null +++ b/sys/suit/conditions.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * 2019 Inria + * 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT conditions + * + * @author Koen Zandberg + * @author Kaspar Schleiser + * @} + */ + +#include + +#include "suit/conditions.h" +#include "uuid.h" +#include "luid.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#define SUIT_DEVID_BYTES 32 + +static suit_condition_params_t _conditions; + +void suit_init_conditions(void) +{ + /* Generate UUID's following the instructions from + * https://tools.ietf.org/html/draft-moran-suit-manifest-03#section-7.7.1 + */ + uuid_v5(&_conditions.vendor, &uuid_namespace_dns, + (uint8_t *)SUIT_VENDOR_DOMAIN, strlen(SUIT_VENDOR_DOMAIN)); + + uuid_v5(&_conditions.class, &_conditions.vendor, (uint8_t *)SUIT_CLASS_ID, + strlen(SUIT_CLASS_ID)); + + uint8_t devid[SUIT_DEVID_BYTES]; + /* Use luid_base to ensure an identical ID independent of previous luid + * calls */ + luid_base(devid, SUIT_DEVID_BYTES); + uuid_v5(&_conditions.device, &_conditions.vendor, devid, SUIT_DEVID_BYTES); +} + +uuid_t *suit_get_vendor_id(void) +{ + return &_conditions.vendor; +} + +uuid_t *suit_get_class_id(void) +{ + return &_conditions.class; +} + +uuid_t *suit_get_device_id(void) +{ + return &_conditions.device; +} diff --git a/sys/suit/v4/Makefile b/sys/suit/v4/Makefile new file mode 100644 index 000000000000..6919f3a3e902 --- /dev/null +++ b/sys/suit/v4/Makefile @@ -0,0 +1,2 @@ +MODULE := suit_v4 +include $(RIOTBASE)/Makefile.base diff --git a/sys/suit/v4/cbor.c b/sys/suit/v4/cbor.c new file mode 100644 index 000000000000..2bc00851e65d --- /dev/null +++ b/sys/suit/v4/cbor.c @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2018 Freie Universität Berlin + * Copyright (C) 2018 Inria + * 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit + * @{ + * + * @file + * @brief SUIT manifest parser library for CBOR based manifests + * + * @author Koen Zandberg + * @author Kaspar Schleiser + * + * @} + */ +#include +#include +#include + +#include "suit/v4/handlers.h" +#include "suit/v4/suit.h" +#include "suit/v4/policy.h" +#include "cbor.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_map_iterate_init(CborValue *map, CborValue *it) +{ + if (!cbor_value_is_map(map)) { + LOG_INFO("suit_v4_parse(): manifest not a map\n)"); + return SUIT_ERR_INVALID_MANIFEST; + } + + cbor_value_enter_container(map, it); + + return SUIT_OK; +} + +int suit_cbor_map_iterate(CborValue *it, CborValue *key, CborValue *value) +{ + if (cbor_value_at_end(it)) { + return 0; + } + + *key = *it; + cbor_value_advance(it); + + *value = *it; + cbor_value_advance(it); + + return 1; +} + +int suit_cbor_get_int(const CborValue *it, int *out) +{ + if (!cbor_value_is_integer(it)) { + LOG_DEBUG("expected integer type, got %u\n", cbor_value_get_type(it)); + return SUIT_ERR_INVALID_MANIFEST; + } + + /* This check tests whether the integer fits into "int", thus the check + * is platform dependent. This is for lack of specification of actually + * allowed values, to be made explicit at some point. */ + if (cbor_value_get_int_checked(it, out) == CborErrorDataTooLarge) { + LOG_DEBUG("integer doesn't fit into int type\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + return SUIT_OK; +} + +int suit_cbor_get_string(const CborValue *it, const uint8_t **buf, size_t *len) +{ + if (!(cbor_value_is_text_string(it) || cbor_value_is_byte_string(it) || cbor_value_is_length_known(it))) { + return SUIT_ERR_INVALID_MANIFEST; + } + CborValue next = *it; + cbor_value_get_string_length(it, len); + cbor_value_advance(&next); + *buf = next.ptr - *len; + return SUIT_OK; +} + +int suit_cbor_get_uint32(const CborValue *it, uint32_t *out) +{ + int res; + int64_t val; + if (!cbor_value_is_unsigned_integer(it)) { + return CborErrorIllegalType; + } + if ((res = cbor_value_get_int64_checked(it, &val))) { + return res; + } + if (val > 0xFFFFFFFF) { + return CborErrorDataTooLarge; + } + *out = (val & 0xFFFFFFFF); + + return CborNoError; +} + +int suit_cbor_get_uint(const CborValue *it, unsigned *out) +{ + return suit_cbor_get_uint32(it, (uint32_t *)out); +} + +int suit_cbor_subparse(CborParser *parser, CborValue *bseq, CborValue *it) +{ + const uint8_t *bytes; + size_t bytes_len = 0; + + if (!cbor_value_is_byte_string(bseq)) { + LOG_DEBUG("suit_cbor_subparse(): bseq not a byte string\n"); + return -1; + } + + suit_cbor_get_string(bseq, &bytes, &bytes_len); + + return cbor_parser_init(bytes, bytes_len, SUIT_TINYCBOR_VALIDATION_MODE, parser, + it); +} + +static int _v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, + size_t len, suit_manifest_handler_getter_t getter) +{ + + CborParser parser; + CborValue it, map, key, value; + CborError err = cbor_parser_init(buf, len, SUIT_TINYCBOR_VALIDATION_MODE, + &parser, &it); + + if (err != 0) { + return SUIT_ERR_INVALID_MANIFEST; + } + + map = it; + + if (suit_cbor_map_iterate_init(&map, &it) != SUIT_OK) { + LOG_DEBUG("manifest not a map!\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + + LOG_DEBUG("jumping into map\n)"); + + while (suit_cbor_map_iterate(&it, &key, &value)) { + int integer_key; + if (suit_cbor_get_int(&key, &integer_key) != SUIT_OK){ + return SUIT_ERR_INVALID_MANIFEST; + } + LOG_DEBUG("got key val=%i\n", integer_key); + suit_manifest_handler_t handler = getter(integer_key); + + if (handler) { + int res = handler(manifest, integer_key, &value); + LOG_DEBUG("handler res=%i\n", res); + if (res < 0) { + LOG_INFO("handler returned <0\n)"); + return SUIT_ERR_INVALID_MANIFEST; + } + } + else { + LOG_DEBUG("no handler found\n"); + } + } + + cbor_value_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, CborValue *it) +{ + (void)key; + const uint8_t *cose_buf; + size_t cose_len = 0; + int res = suit_cbor_get_string(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, CborValue *it) +{ + (void)key; + const uint8_t *manifest_buf; + size_t manifest_len; + + suit_cbor_get_string(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 suit_manifest_handler_t _auth_handlers[] = { + [ 0] = NULL, + [ 1] = _auth_handler, + [ 2] = _manifest_handler, +}; +/* end{code-style-ignore} */ + +static const unsigned _auth_handlers_len = ARRAY_SIZE(_auth_handlers); + +static suit_manifest_handler_t _manifest_get_auth_wrapper_handler(int key) +{ + return _suit_manifest_get_handler(key, _auth_handlers, + _auth_handlers_len); +} diff --git a/sys/suit/v4/handlers.c b/sys/suit/v4/handlers.c new file mode 100644 index 000000000000..30dde7a3f6d4 --- /dev/null +++ b/sys/suit/v4/handlers.c @@ -0,0 +1,535 @@ +/* + * Copyright (C) 2019 Koen Zandberg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit_v4 + * @{ + * + * @file + * @brief SUIT v4 + * + * @author Koen Zandberg + * + * @} + */ + +#include + +#include "suit/coap.h" +#include "suit/conditions.h" +#include "suit/v4/suit.h" +#include "suit/v4/handlers.h" +#include "suit/v4/policy.h" +#include "suit/v4/suit.h" +#include "riotboot/hdr.h" +#include "riotboot/slot.h" +#include "cbor.h" + +#include "log.h" + +#define HELLO_HANDLER_MAX_STRLEN 32 + +static int _handle_command_sequence(suit_v4_manifest_t *manifest, CborValue *it, + suit_manifest_handler_t handler); +static int _common_handler(suit_v4_manifest_t *manifest, int key, CborValue *it); +static int _common_sequence_handler(suit_v4_manifest_t *manifest, int key, CborValue *it); + +static int _hello_handler(suit_v4_manifest_t *manifest, int key, CborValue *it) +{ + (void)manifest; + (void)key; + + if (cbor_value_is_text_string(it)) { + size_t len = HELLO_HANDLER_MAX_STRLEN; + char buf[HELLO_HANDLER_MAX_STRLEN]; + cbor_value_copy_text_string(it, buf, &len, NULL); + return SUIT_OK; + } + else { + LOG_DEBUG("_hello_handler(): unexpected value type: %u\n", cbor_value_get_type( + it)); + return -1; + } +} + +static int _validate_uuid(suit_v4_manifest_t *manifest, CborValue *it, uuid_t *uuid) +{ + (void)manifest; + uuid_t uuid_manifest; + char uuid_str[UUID_STR_LEN + 1]; + char uuid_str2[UUID_STR_LEN + 1]; + size_t len = sizeof(uuid_t); + cbor_value_copy_byte_string(it, (uint8_t*)&uuid_manifest, &len, NULL); + uuid_to_string(&uuid_manifest, 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_manifest) ? 0 : -1; +} + +static int _cond_vendor_handler(suit_v4_manifest_t *manifest, int key, CborValue *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, CborValue *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, CborValue *it) +{ + (void)manifest; + (void)key; + uint32_t offset; + suit_cbor_get_uint32(it, &offset); + uint32_t other_offset = (uint32_t)riotboot_slot_get_hdr(riotboot_slot_other()) \ + - CPU_FLASH_BASE; + 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, CborValue *it) +{ + (void)key; + if (cbor_value_is_boolean(it)) { + LOG_DEBUG("_dtv_set_comp_idx() ignoring boolean\n)"); + return 0; + } + int res = suit_cbor_get_int(it, &manifest->component_current); + if (!res) { + LOG_DEBUG("Setting component index to %d\n", manifest->component_current); + } + return res; +} + +static int _dtv_run_seq_cond(suit_v4_manifest_t *manifest, int key, CborValue *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, CborValue *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, CborValue *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, CborValue *it) +{ + int res = suit_cbor_get_uint32(it, &manifest->components[0].size); + if (res) { + LOG_DEBUG("error getting image size\n"); + return res; + } + return res; +} + +static int _dtv_set_param(suit_v4_manifest_t *manifest, int key, CborValue *it) +{ + (void)key; + /* `it` points to the entry of the map containing the type and value */ + CborValue map; + + cbor_value_enter_container(it, &map); + + while (!cbor_value_at_end(&map)) { + /* map points to the key of the param */ + int param_key; + suit_cbor_get_int(&map, ¶m_key); + cbor_value_advance(&map); + LOG_DEBUG("Setting component index to %d\n", manifest->component_current); + LOG_DEBUG("param_key=%i\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; + } + + cbor_value_advance(&map); + + if (res) { + return res; + } + } + return SUIT_OK; +} + +static int _dtv_fetch(suit_v4_manifest_t *manifest, int key, CborValue *_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) + * */ + + CborParser parser; + CborValue it; + + /* open sequence with cbor parser */ + int err = suit_cbor_subparse(&parser, &manifest->components[0].url, &it); + if (err < 0) { + LOG_DEBUG("subparse failed\n)"); + return err; + } + + /* confirm the document contains an array */ + if (!cbor_value_is_array(&it)) { + LOG_DEBUG("url list no array\n)"); + LOG_DEBUG("type: %u\n", cbor_value_get_type(&it)); + } + + /* enter container, confirm it is an array, too */ + CborValue url_it; + cbor_value_enter_container(&it, &url_it); + if (!cbor_value_is_array(&url_it)) { + LOG_DEBUG("url entry no array\n)"); + } + + /* expect two entries: priority as int, url as byte string. bail out if not. */ + CborValue url_value_it; + cbor_value_enter_container(&url_it, &url_value_it); + + /* check that first array entry is an int (the priority of the url) */ + if (cbor_value_get_type(&url_value_it) != CborIntegerType) { + return -1; + } + + cbor_value_advance(&url_value_it); + + int res = suit_cbor_get_string(&url_value_it, &url, &url_len); + if (res) { + LOG_DEBUG("error parsing URL\n)"); + return -1; + } + if (url_len >= manifest->urlbuf_len) { + LOG_INFO("url too large: %u>%u\n)", (unsigned)url_len, (unsigned)manifest->urlbuf_len); + return -1; + } + memcpy(manifest->urlbuf, url, url_len); + manifest->urlbuf[url_len] = '\0'; + + cbor_value_leave_container(&url_it, &url_value_it); + cbor_value_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->writer); + + if (res) { + LOG_INFO("image download failed\n)"); + return res; + } + + const uint8_t *digest; + size_t digest_len; + + res = suit_cbor_get_string(&manifest->components[0].digest, &digest, &digest_len); + if (res) { + return res; + } + + /* "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, + CborValue *it) +{ + (void)manifest; + (void)key; + /* Validate manifest version */ + int version = -1; + if (cbor_value_is_integer(it) && + (cbor_value_get_int(it, &version) == CborNoError)) { + 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, CborValue *it) +{ + (void)manifest; + (void)key; + (void)it; + + int64_t seq_nr; + if (cbor_value_is_unsigned_integer(it) && + (cbor_value_get_int64_checked(it, &seq_nr) == CborNoError)) { + + const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_current()); + if (seq_nr <= (int64_t)hdr->version) { + LOG_INFO("%"PRIu64" <= %"PRIu32"\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 <= (int64_t)hdr->version) { + LOG_INFO("%"PRIu64" <= %"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; + } + LOG_INFO("Unable to get sequence number\n"); + return -1; +} + +static int _dependencies_handler(suit_v4_manifest_t *manifest, int key, + CborValue *it) +{ + (void)manifest; + (void)key; + (void)it; + /* No dependency support */ + return 0; +} + +static int _component_handler(suit_v4_manifest_t *manifest, int key, + CborValue *it) +{ + (void)manifest; + (void)key; + + CborValue arr; + + LOG_DEBUG("storing components\n)"); + if (!cbor_value_is_array(it)) { + LOG_DEBUG("components field not an array\n"); + return -1; + } + cbor_value_enter_container(it, &arr); + + unsigned n = 0; + while (!cbor_value_at_end(&arr)) { + CborValue map, key, value; + if (n < SUIT_V4_COMPONENT_MAX) { + manifest->components_len += 1; + } + else { + LOG_DEBUG("too many components\n)"); + return SUIT_ERR_INVALID_MANIFEST; + } + + suit_cbor_map_iterate_init(&arr, &map); + + suit_v4_component_t *current = &manifest->components[n]; + + while (suit_cbor_map_iterate(&map, &key, &value)) { + /* handle key, value */ + int integer_key; + if (suit_cbor_get_int(&key, &integer_key)) { + return SUIT_ERR_INVALID_MANIFEST; + } + + switch (integer_key) { + case SUIT_COMPONENT_IDENTIFIER: + current->identifier = value; + break; + case SUIT_COMPONENT_SIZE: + LOG_DEBUG("skipping SUIT_COMPONENT_SIZE"); + break; + case SUIT_COMPONENT_DIGEST: + current->digest = value; + break; + default: + LOG_DEBUG("ignoring unexpected component data (nr. %i)\n", integer_key); + } + + LOG_DEBUG("component %u parsed\n", n); + } + + cbor_value_advance(&arr); + n++; + } + + manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; + cbor_value_leave_container(it, &arr); + + LOG_DEBUG("storing components done\n)"); + return 0; +} + +/* begin{code-style-ignore} */ +static suit_manifest_handler_t global_handlers[] = { + [ 0] = _hello_handler, + [ 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 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, CborValue *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, CborValue *it) +{ + (void)key; + return _handle_command_sequence(manifest, it, _common_sequence_handler); +} + +int _handle_command_sequence(suit_v4_manifest_t *manifest, CborValue *bseq, + suit_manifest_handler_t handler) +{ + + LOG_DEBUG("Handling command sequence\n"); + CborParser parser; + CborValue it, arr; + + int err = suit_cbor_subparse(&parser, bseq, &it); + if (err < 0) { + return err; + } + + if (!cbor_value_is_array(&it)) { + LOG_DEBUG("Not a byte array\n"); + return -1; + } + cbor_value_enter_container(&it, &arr); + + while (!cbor_value_at_end(&arr)) { + CborValue map; + if (!cbor_value_is_map(&arr)) { + return SUIT_ERR_INVALID_MANIFEST; + } + cbor_value_enter_container(&arr, &map); + int integer_key; + if (suit_cbor_get_int(&map, &integer_key)) { + return SUIT_ERR_INVALID_MANIFEST; + } + + cbor_value_advance(&map); + int res = handler(manifest, integer_key, &map); + if (res < 0) { + LOG_DEBUG("Sequence handler error\n"); + return res; + } + cbor_value_advance(&map); + cbor_value_leave_container(&arr, &map); + } + cbor_value_leave_container(&it, &arr); + + return 0; +} diff --git a/sys/suit/v4/policy.c b/sys/suit/v4/policy.c new file mode 100644 index 000000000000..35fa7551bbcf --- /dev/null +++ b/sys/suit/v4/policy.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * 2019 Inria + * 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys_suit_v4 + * @{ + * + * @file + * @brief SUIT v4 policy checking code + * + * @author Kaspar Schleiser + * + * @} + */ + +#include "suit/v4/suit.h" +#include "suit/v4/policy.h" + +#include "log.h" + +int suit_v4_policy_check(suit_v4_manifest_t *manifest) +{ + if (SUIT_DEFAULT_POLICY & ~(manifest->validated)) { + LOG_INFO("SUIT policy check failed!\n"); + return -1; + } + else { + LOG_INFO("SUIT policy check OK.\n"); + return 0; + } +}