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:
commit
5ab8d92702
3
.gitignore
vendored
3
.gitignore
vendored
@ -68,3 +68,6 @@ results/
|
||||
# Clangd compile flags (language server)
|
||||
compile_commands.json
|
||||
compile_flags.txt
|
||||
|
||||
# suit manifest keys
|
||||
keys/
|
||||
|
25
Makefile.dep
25
Makefile.dep
@ -932,6 +932,31 @@ ifneq (,$(filter sock_dtls, $(USEMODULE)))
|
||||
USEMODULE += sock_udp
|
||||
endif
|
||||
|
||||
ifneq (,$(filter suit_v4_%,$(USEMODULE)))
|
||||
USEMODULE += suit_v4
|
||||
endif
|
||||
|
||||
ifneq (,$(filter suit_v4,$(USEMODULE)))
|
||||
USEPKG += tinycbor
|
||||
USEPKG += libcose
|
||||
USEMODULE += libcose_crypt_hacl
|
||||
USEMODULE += suit_conditions
|
||||
|
||||
# SUIT depends on riotboot support and some extra riotboot modules
|
||||
FEATURES_REQUIRED += riotboot
|
||||
USEMODULE += riotboot_slot
|
||||
USEMODULE += riotboot_flashwrite
|
||||
USEMODULE += riotboot_flashwrite_verify_sha256
|
||||
endif
|
||||
|
||||
ifneq (,$(filter suit_conditions,$(USEMODULE)))
|
||||
USEMODULE += uuid
|
||||
endif
|
||||
|
||||
ifneq (,$(filter suit_%,$(USEMODULE)))
|
||||
USEMODULE += suit
|
||||
endif
|
||||
|
||||
# Enable periph_gpio when periph_gpio_irq is enabled
|
||||
ifneq (,$(filter periph_gpio_irq,$(USEMODULE)))
|
||||
FEATURES_REQUIRED += periph_gpio
|
||||
|
7
dist/tools/flake8/check.sh
vendored
7
dist/tools/flake8/check.sh
vendored
@ -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
33
dist/tools/suit_v4/gen_key.py
vendored
Executable 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
94
dist/tools/suit_v4/gen_manifest.py
vendored
Executable 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
154
dist/tools/suit_v4/sign-04.py
vendored
Executable 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()
|
411
dist/tools/suit_v4/suit_manifest_encoder_04.py
vendored
Normal file
411
dist/tools/suit_v4/suit_manifest_encoder_04.py
vendored
Normal 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
33
dist/tools/suit_v4/test-2img.json
vendored
Normal 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"}
|
||||
]
|
||||
}
|
102
examples/suit_update/Makefile
Normal file
102
examples/suit_update/Makefile
Normal 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
|
559
examples/suit_update/README.md
Normal file
559
examples/suit_update/README.md
Normal 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
|
||||
```
|
32
examples/suit_update/coap_handler.c
Normal file
32
examples/suit_update/coap_handler.c
Normal 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);
|
77
examples/suit_update/main.c
Normal file
77
examples/suit_update/main.c
Normal 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;
|
||||
}
|
165
examples/suit_update/tests/01-run.py
Executable file
165
examples/suit_update/tests/01-run.py
Executable 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)
|
@ -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)"
|
||||
|
@ -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
103
makefiles/suit.v4.inc.mk
Normal 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."
|
@ -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))))
|
||||
|
||||
|
@ -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
151
sys/include/suit/coap.h
Normal 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 */
|
||||
/** @} */
|
109
sys/include/suit/conditions.h
Normal file
109
sys/include/suit/conditions.h
Normal 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 */
|
||||
/** @} */
|
64
sys/include/suit/v4/handlers.h
Normal file
64
sys/include/suit/v4/handlers.h
Normal 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 */
|
||||
/** @} */
|
58
sys/include/suit/v4/policy.h
Normal file
58
sys/include/suit/v4/policy.h
Normal 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
288
sys/include/suit/v4/suit.h
Normal 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
9
sys/suit/Makefile
Normal 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
513
sys/suit/coap.c
Normal 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
67
sys/suit/conditions.c
Normal 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
2
sys/suit/v4/Makefile
Normal file
@ -0,0 +1,2 @@
|
||||
MODULE := suit_v4
|
||||
include $(RIOTBASE)/Makefile.base
|
275
sys/suit/v4/cbor.c
Normal file
275
sys/suit/v4/cbor.c
Normal 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
535
sys/suit/v4/handlers.c
Normal 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, ¶m_key);
|
||||
cbor_value_advance(&map);
|
||||
LOG_DEBUG("Setting component index to %d\n", manifest->component_current);
|
||||
LOG_DEBUG("param_key=%i\n", param_key);
|
||||
int res;
|
||||
switch (param_key) {
|
||||
case 6: /* SUIT URI LIST */
|
||||
res = _param_get_uri_list(manifest, &map);
|
||||
break;
|
||||
case 11: /* SUIT DIGEST */
|
||||
res = _param_get_digest(manifest, &map);
|
||||
break;
|
||||
case 12: /* SUIT IMAGE SIZE */
|
||||
res = _param_get_img_size(manifest, &map);
|
||||
break;
|
||||
default:
|
||||
res = -1;
|
||||
}
|
||||
|
||||
cbor_value_advance(&map);
|
||||
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return SUIT_OK;
|
||||
}
|
||||
|
||||
static int _dtv_fetch(suit_v4_manifest_t *manifest, int key, CborValue *_it)
|
||||
{
|
||||
(void)key; (void)_it; (void)manifest;
|
||||
LOG_DEBUG("_dtv_fetch() key=%i\n", key);
|
||||
|
||||
const uint8_t *url;
|
||||
size_t url_len;
|
||||
|
||||
/* TODO: there must be a simpler way */
|
||||
{
|
||||
/* the url list is a binary sequence containing a cbor array of
|
||||
* (priority, url) tuples (represented as array with length two)
|
||||
* */
|
||||
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
|
||||
/* open sequence with cbor parser */
|
||||
int err = suit_cbor_subparse(&parser, &manifest->components[0].url, &it);
|
||||
if (err < 0) {
|
||||
LOG_DEBUG("subparse failed\n)");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* confirm the document contains an array */
|
||||
if (!cbor_value_is_array(&it)) {
|
||||
LOG_DEBUG("url list no array\n)");
|
||||
LOG_DEBUG("type: %u\n", cbor_value_get_type(&it));
|
||||
}
|
||||
|
||||
/* enter container, confirm it is an array, too */
|
||||
CborValue url_it;
|
||||
cbor_value_enter_container(&it, &url_it);
|
||||
if (!cbor_value_is_array(&url_it)) {
|
||||
LOG_DEBUG("url entry no array\n)");
|
||||
}
|
||||
|
||||
/* expect two entries: priority as int, url as byte string. bail out if not. */
|
||||
CborValue url_value_it;
|
||||
cbor_value_enter_container(&url_it, &url_value_it);
|
||||
|
||||
/* check that first array entry is an int (the priority of the url) */
|
||||
if (cbor_value_get_type(&url_value_it) != CborIntegerType) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cbor_value_advance(&url_value_it);
|
||||
|
||||
int res = suit_cbor_get_string(&url_value_it, &url, &url_len);
|
||||
if (res) {
|
||||
LOG_DEBUG("error parsing URL\n)");
|
||||
return -1;
|
||||
}
|
||||
if (url_len >= manifest->urlbuf_len) {
|
||||
LOG_INFO("url too large: %u>%u\n)", (unsigned)url_len, (unsigned)manifest->urlbuf_len);
|
||||
return -1;
|
||||
}
|
||||
memcpy(manifest->urlbuf, url, url_len);
|
||||
manifest->urlbuf[url_len] = '\0';
|
||||
|
||||
cbor_value_leave_container(&url_it, &url_value_it);
|
||||
cbor_value_leave_container(&it, &url_it);
|
||||
}
|
||||
|
||||
LOG_DEBUG("_dtv_fetch() fetching \"%s\" (url_len=%u)\n", manifest->urlbuf, (unsigned)url_len);
|
||||
|
||||
int target_slot = riotboot_slot_other();
|
||||
riotboot_flashwrite_init(manifest->writer, target_slot);
|
||||
int res = suit_coap_get_blockwise_url(manifest->urlbuf, COAP_BLOCKSIZE_64, suit_flashwrite_helper,
|
||||
manifest->writer);
|
||||
|
||||
if (res) {
|
||||
LOG_INFO("image download failed\n)");
|
||||
return res;
|
||||
}
|
||||
|
||||
const uint8_t *digest;
|
||||
size_t digest_len;
|
||||
|
||||
res = suit_cbor_get_string(&manifest->components[0].digest, &digest, &digest_len);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
|
||||
/* "digest" points to a 36 byte string that includes the digest type.
|
||||
* riotboot_flashwrite_verify_sha256() is only interested in the 32b digest,
|
||||
* so shift the pointer accordingly.
|
||||
*/
|
||||
res = riotboot_flashwrite_verify_sha256(digest + 4, manifest->components[0].size, target_slot);
|
||||
if (res) {
|
||||
LOG_INFO("image verification failed\n");
|
||||
return res;
|
||||
}
|
||||
|
||||
manifest->state |= SUIT_MANIFEST_HAVE_IMAGE;
|
||||
|
||||
return SUIT_OK;
|
||||
}
|
||||
|
||||
static int _version_handler(suit_v4_manifest_t *manifest, int key,
|
||||
CborValue *it)
|
||||
{
|
||||
(void)manifest;
|
||||
(void)key;
|
||||
/* Validate manifest version */
|
||||
int version = -1;
|
||||
if (cbor_value_is_integer(it) &&
|
||||
(cbor_value_get_int(it, &version) == CborNoError)) {
|
||||
if (version == SUIT_VERSION) {
|
||||
manifest->validated |= SUIT_VALIDATED_VERSION;
|
||||
LOG_INFO("suit: validated manifest version\n)");
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int _seq_no_handler(suit_v4_manifest_t *manifest, int key, CborValue *it)
|
||||
{
|
||||
(void)manifest;
|
||||
(void)key;
|
||||
(void)it;
|
||||
|
||||
int64_t seq_nr;
|
||||
if (cbor_value_is_unsigned_integer(it) &&
|
||||
(cbor_value_get_int64_checked(it, &seq_nr) == CborNoError)) {
|
||||
|
||||
const riotboot_hdr_t *hdr = riotboot_slot_get_hdr(riotboot_slot_current());
|
||||
if (seq_nr <= (int64_t)hdr->version) {
|
||||
LOG_INFO("%"PRIu64" <= %"PRIu32"\n", seq_nr, hdr->version);
|
||||
LOG_INFO("seq_nr <= running image\n)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
hdr = riotboot_slot_get_hdr(riotboot_slot_other());
|
||||
if (riotboot_hdr_validate(hdr) == 0) {
|
||||
if (seq_nr <= (int64_t)hdr->version) {
|
||||
LOG_INFO("%"PRIu64" <= %"PRIu32"\n", seq_nr, hdr->version);
|
||||
LOG_INFO("seq_nr <= other image\n)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("suit: validated sequence number\n)");
|
||||
manifest->validated |= SUIT_VALIDATED_SEQ_NR;
|
||||
return 0;
|
||||
}
|
||||
LOG_INFO("Unable to get sequence number\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int _dependencies_handler(suit_v4_manifest_t *manifest, int key,
|
||||
CborValue *it)
|
||||
{
|
||||
(void)manifest;
|
||||
(void)key;
|
||||
(void)it;
|
||||
/* No dependency support */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _component_handler(suit_v4_manifest_t *manifest, int key,
|
||||
CborValue *it)
|
||||
{
|
||||
(void)manifest;
|
||||
(void)key;
|
||||
|
||||
CborValue arr;
|
||||
|
||||
LOG_DEBUG("storing components\n)");
|
||||
if (!cbor_value_is_array(it)) {
|
||||
LOG_DEBUG("components field not an array\n");
|
||||
return -1;
|
||||
}
|
||||
cbor_value_enter_container(it, &arr);
|
||||
|
||||
unsigned n = 0;
|
||||
while (!cbor_value_at_end(&arr)) {
|
||||
CborValue map, key, value;
|
||||
if (n < SUIT_V4_COMPONENT_MAX) {
|
||||
manifest->components_len += 1;
|
||||
}
|
||||
else {
|
||||
LOG_DEBUG("too many components\n)");
|
||||
return SUIT_ERR_INVALID_MANIFEST;
|
||||
}
|
||||
|
||||
suit_cbor_map_iterate_init(&arr, &map);
|
||||
|
||||
suit_v4_component_t *current = &manifest->components[n];
|
||||
|
||||
while (suit_cbor_map_iterate(&map, &key, &value)) {
|
||||
/* handle key, value */
|
||||
int integer_key;
|
||||
if (suit_cbor_get_int(&key, &integer_key)) {
|
||||
return SUIT_ERR_INVALID_MANIFEST;
|
||||
}
|
||||
|
||||
switch (integer_key) {
|
||||
case SUIT_COMPONENT_IDENTIFIER:
|
||||
current->identifier = value;
|
||||
break;
|
||||
case SUIT_COMPONENT_SIZE:
|
||||
LOG_DEBUG("skipping SUIT_COMPONENT_SIZE");
|
||||
break;
|
||||
case SUIT_COMPONENT_DIGEST:
|
||||
current->digest = value;
|
||||
break;
|
||||
default:
|
||||
LOG_DEBUG("ignoring unexpected component data (nr. %i)\n", integer_key);
|
||||
}
|
||||
|
||||
LOG_DEBUG("component %u parsed\n", n);
|
||||
}
|
||||
|
||||
cbor_value_advance(&arr);
|
||||
n++;
|
||||
}
|
||||
|
||||
manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS;
|
||||
cbor_value_leave_container(it, &arr);
|
||||
|
||||
LOG_DEBUG("storing components done\n)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* begin{code-style-ignore} */
|
||||
static suit_manifest_handler_t global_handlers[] = {
|
||||
[ 0] = _hello_handler,
|
||||
[ 1] = _version_handler,
|
||||
[ 2] = _seq_no_handler,
|
||||
[ 3] = _dependencies_handler,
|
||||
[ 4] = _component_handler,
|
||||
[ 5] = NULL,
|
||||
[ 6] = _common_handler,
|
||||
[ 9] = _common_handler,
|
||||
};
|
||||
/* end{code-style-ignore} */
|
||||
|
||||
static const unsigned global_handlers_len = ARRAY_SIZE(global_handlers);
|
||||
|
||||
/* begin{code-style-ignore} */
|
||||
static suit_manifest_handler_t _sequence_handlers[] = {
|
||||
[ 0] = NULL,
|
||||
[ 1] = _cond_vendor_handler,
|
||||
[ 2] = _cond_class_handler,
|
||||
[10] = _cond_comp_offset,
|
||||
/* Directives */
|
||||
[11] = _dtv_set_comp_idx,
|
||||
/* [12] = _dtv_set_man_idx, */
|
||||
/* [13] = _dtv_run_seq, */
|
||||
[14] = _dtv_run_seq_cond,
|
||||
[16] = _dtv_set_param,
|
||||
[20] = _dtv_fetch,
|
||||
/* [22] = _dtv_run, */
|
||||
};
|
||||
/* end{code-style-ignore} */
|
||||
|
||||
static const unsigned _sequence_handlers_len = ARRAY_SIZE(_sequence_handlers);
|
||||
|
||||
static suit_manifest_handler_t _suit_manifest_get_handler(int key,
|
||||
const suit_manifest_handler_t *handlers,
|
||||
size_t len)
|
||||
{
|
||||
if (key < 0 || (size_t)key >= len) {
|
||||
return NULL;
|
||||
}
|
||||
return handlers[key];
|
||||
}
|
||||
|
||||
suit_manifest_handler_t suit_manifest_get_manifest_handler(int key)
|
||||
{
|
||||
return _suit_manifest_get_handler(key, global_handlers,
|
||||
global_handlers_len);
|
||||
}
|
||||
|
||||
static int _common_sequence_handler(suit_v4_manifest_t *manifest, int key, CborValue *it)
|
||||
{
|
||||
|
||||
suit_manifest_handler_t handler = _suit_manifest_get_handler(key, _sequence_handlers, _sequence_handlers_len);
|
||||
LOG_DEBUG("Handling handler with key %d at %p\n", key, handler);
|
||||
if (handler) {
|
||||
return handler(manifest, key, it);
|
||||
}
|
||||
else {
|
||||
LOG_DEBUG("Sequence handler not implemented, ID: %d\n", key);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int _common_handler(suit_v4_manifest_t *manifest, int key, CborValue *it)
|
||||
{
|
||||
(void)key;
|
||||
return _handle_command_sequence(manifest, it, _common_sequence_handler);
|
||||
}
|
||||
|
||||
int _handle_command_sequence(suit_v4_manifest_t *manifest, CborValue *bseq,
|
||||
suit_manifest_handler_t handler)
|
||||
{
|
||||
|
||||
LOG_DEBUG("Handling command sequence\n");
|
||||
CborParser parser;
|
||||
CborValue it, arr;
|
||||
|
||||
int err = suit_cbor_subparse(&parser, bseq, &it);
|
||||
if (err < 0) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!cbor_value_is_array(&it)) {
|
||||
LOG_DEBUG("Not a byte array\n");
|
||||
return -1;
|
||||
}
|
||||
cbor_value_enter_container(&it, &arr);
|
||||
|
||||
while (!cbor_value_at_end(&arr)) {
|
||||
CborValue map;
|
||||
if (!cbor_value_is_map(&arr)) {
|
||||
return SUIT_ERR_INVALID_MANIFEST;
|
||||
}
|
||||
cbor_value_enter_container(&arr, &map);
|
||||
int integer_key;
|
||||
if (suit_cbor_get_int(&map, &integer_key)) {
|
||||
return SUIT_ERR_INVALID_MANIFEST;
|
||||
}
|
||||
|
||||
cbor_value_advance(&map);
|
||||
int res = handler(manifest, integer_key, &map);
|
||||
if (res < 0) {
|
||||
LOG_DEBUG("Sequence handler error\n");
|
||||
return res;
|
||||
}
|
||||
cbor_value_advance(&map);
|
||||
cbor_value_leave_container(&arr, &map);
|
||||
}
|
||||
cbor_value_leave_container(&it, &arr);
|
||||
|
||||
return 0;
|
||||
}
|
37
sys/suit/v4/policy.c
Normal file
37
sys/suit/v4/policy.c
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user