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

Merge pull request #11818 from kaspar030/suit-pr

sys/suit: initial support for SUIT firmware updates
This commit is contained in:
Kaspar Schleiser 2019-10-10 10:38:01 +02:00 committed by GitHub
commit 5ab8d92702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3922 additions and 2 deletions

3
.gitignore vendored
View File

@ -68,3 +68,6 @@ results/
# Clangd compile flags (language server)
compile_commands.json
compile_flags.txt
# suit manifest keys
keys/

View File

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

View File

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

33
dist/tools/suit_v4/gen_key.py vendored Executable file
View File

@ -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 <secret filename> <public filename>")
sys.exit(1)
_signing_key, _verifying_key = ed25519.create_keypair()
with open(sys.argv[1], "wb") as f:
f.write(_signing_key.to_bytes())
with open(sys.argv[2], "wb") as f:
f.write(_verifying_key.to_bytes())
vkey_hex = _verifying_key.to_ascii(encoding="hex")
print("Generated public key: '{}'".format(vkey_hex.decode()))
if __name__ == '__main__':
main()

94
dist/tools/suit_v4/gen_manifest.py vendored Executable file
View File

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

154
dist/tools/suit_v4/sign-04.py vendored Executable file
View File

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

View File

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

33
dist/tools/suit_v4/test-2img.json vendored Normal file
View File

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

View File

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

View File

@ -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=<BR 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
```

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#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);

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup examples
* @{
*
* @file
* @brief SUIT updates over CoAP example server application (using nanocoap)
*
* @author Kaspar Schleiser <kaspar@schleiser.de>
* @}
*/
#include <stdio.h>
#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;
}

View File

@ -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<gladdr>[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<lladdr>[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: (?P<app_ver>0x[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)

View File

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

View File

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

103
makefiles/suit.v4.inc.mk Normal file
View File

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

View File

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

View File

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

151
sys/include/suit/coap.h Normal file
View File

@ -0,0 +1,151 @@
/*
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 2019 Inria
* 2019 Freie Universität Berlin
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup sys_suit SUIT secure firmware updates
* @ingroup sys
* @brief SUIT secure firmware updates
*
* @experimental
*
* @{
*
* @brief SUIT CoAP helper API
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
*/
#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 */
/** @} */

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit
* @brief SUIT conditions
*
* @{
*
* @brief SUIT conditions API
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
*/
#ifndef SUIT_CONDITIONS_H
#define SUIT_CONDITIONS_H
#include <stddef.h>
#include <stdint.h>
#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 */
/** @} */

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 2019 Inria
* 2019 Freie Universität Berlin
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit_v4
* @brief SUIT v4 manifest handlers
*
* @experimental
*
* @{
*
* @brief Handler functions for SUIT manifests
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*/
#ifndef SUIT_V4_HANDLERS_H
#define SUIT_V4_HANDLERS_H
#include <stddef.h>
#include <stdint.h>
#include "suit/v4/suit.h"
#include "uuid.h"
#include "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 */
/** @} */

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 2019 Inria
* 2019 Freie Universität Berlin
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit_v4
* @brief SUIT policy definitions
*
* @{
*
* @brief SUIT policy definitions
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
*/
#ifndef SUIT_V4_POLICY_H
#define SUIT_V4_POLICY_H
#include <stddef.h>
#include <stdint.h>
#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 */
/** @} */

288
sys/include/suit/v4/suit.h Normal file
View File

@ -0,0 +1,288 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup sys_suit_v4 SUIT draft v4
* @ingroup sys_suit
* @brief SUIT manifest handling
*
* @{
*
* @brief Handler functions for SUIT manifests
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
*/
#ifndef SUIT_V4_SUIT_H
#define SUIT_V4_SUIT_H
#include <stddef.h>
#include <stdint.h>
#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 */
/** @} */

9
sys/suit/Makefile Normal file
View File

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

513
sys/suit/coap.c Normal file
View File

@ -0,0 +1,513 @@
/*
* Copyright (C) 2019 Freie Universität Berlin
* 2019 Inria
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit
* @{
*
* @file
* @brief SUIT coap
*
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
* @}
*/
#include <inttypes.h>
#include <string.h>
#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)
};

67
sys/suit/conditions.c Normal file
View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2019 Koen Zandberg
* 2019 Inria
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit
* @{
*
* @file
* @brief SUIT conditions
*
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
* @}
*/
#include <string.h>
#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;
}

2
sys/suit/v4/Makefile Normal file
View File

@ -0,0 +1,2 @@
MODULE := suit_v4
include $(RIOTBASE)/Makefile.base

275
sys/suit/v4/cbor.c Normal file
View File

@ -0,0 +1,275 @@
/*
* Copyright (C) 2018 Freie Universität Berlin
* Copyright (C) 2018 Inria
* 2019 Kaspar Schleiser <kaspar@schleiser.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit
* @{
*
* @file
* @brief SUIT manifest parser library for CBOR based manifests
*
* @author Koen Zandberg <koen@bergzand.net>
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
* @}
*/
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "suit/v4/handlers.h"
#include "suit/v4/suit.h"
#include "suit/v4/policy.h"
#include "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);
}

535
sys/suit/v4/handlers.c Normal file
View File

@ -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 <koen@bergzand.net>
*
* @}
*/
#include <inttypes.h>
#include "suit/coap.h"
#include "suit/conditions.h"
#include "suit/v4/suit.h"
#include "suit/v4/handlers.h"
#include "suit/v4/policy.h"
#include "suit/v4/suit.h"
#include "riotboot/hdr.h"
#include "riotboot/slot.h"
#include "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, &param_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;
}

37
sys/suit/v4/policy.c Normal file
View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
* 2019 Inria
* 2019 Freie Universität Berlin
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_suit_v4
* @{
*
* @file
* @brief SUIT v4 policy checking code
*
* @author Kaspar Schleiser <kaspar@schleiser.de>
*
* @}
*/
#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;
}
}