diff --git a/dist/tools/flake8/check.sh b/dist/tools/flake8/check.sh index 8c7cd4db35ac..ac69ed50a1fc 100755 --- a/dist/tools/flake8/check.sh +++ b/dist/tools/flake8/check.sh @@ -28,7 +28,7 @@ EXCLUDE="^(.+/vendor/\ |dist/tools/mcuboot\ |dist/tools/uhcpd\ |dist/tools/stm32loader\ -|dist/tools/suit_v3/suit-manifest-generator)\ +|dist/tools/suit/suit-manifest-generator)\ |dist/tools/esptool" FILEREGEX='(\.py$|pyterm$)' FILES=$(FILEREGEX=${FILEREGEX} EXCLUDE=${EXCLUDE} changed_files) diff --git a/dist/tools/suit_v3/gen_key.py b/dist/tools/suit/gen_key.py similarity index 100% rename from dist/tools/suit_v3/gen_key.py rename to dist/tools/suit/gen_key.py diff --git a/dist/tools/suit_v3/gen_manifest.py b/dist/tools/suit/gen_manifest.py similarity index 100% rename from dist/tools/suit_v3/gen_manifest.py rename to dist/tools/suit/gen_manifest.py diff --git a/dist/tools/suit_v3/suit-manifest-generator/LICENSE b/dist/tools/suit/suit-manifest-generator/LICENSE similarity index 100% rename from dist/tools/suit_v3/suit-manifest-generator/LICENSE rename to dist/tools/suit/suit-manifest-generator/LICENSE diff --git a/dist/tools/suit_v3/suit-manifest-generator/README.md b/dist/tools/suit/suit-manifest-generator/README.md similarity index 90% rename from dist/tools/suit_v3/suit-manifest-generator/README.md rename to dist/tools/suit/suit-manifest-generator/README.md index 221593c16ef6..6a0fb78cfc0f 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/README.md +++ b/dist/tools/suit/suit-manifest-generator/README.md @@ -144,6 +144,8 @@ The `suit-tool` supports three sub-commands: * `create` generates a new manifest. * `sign` signs a manifest. * `parse` parses an existing manifest into cbor-debug or a json representation. +* `keygen` Create a signing key. Not for production use. +* `pubkey` Get the public key for a supplied private key in uECC-compatible C definition. The `suit-tool` has a configurable log level, specified with `-l`: @@ -178,7 +180,7 @@ To add a component to the manifest from the command-line, use the following synt The supported fields are: -* `file` the path to a file to use as a payload file. +* `file` the path fo a file to use as a payload file. * `inst` the `install-id`. * `uri` the URI where the file will be found. @@ -207,3 +209,28 @@ suit-tool parse -m MANIFEST ``` If a json-representation is needed, add the '-j' flag. + +## Keygen + +Create an asymmetric keypair for non-production use. Production systems should use closely guarded keys, such as keys stored in an HSM. + +```sh + suit-tool keygen [-t TYPE] -o KEYFILE + ``` + +`suit-tool keygen` defaults to creating SECP256r1 keys. To create another type of key, use `-t`followed by one of: + +* `secp256r1` +* `secp384r1` +* `secp521r1` +* `ed25519` + +## UECC public key + +Derive a public key in the format used by micro ECC. The input is a PEM private key. + +```sh +suit-tool pubkey -k FILE +``` + +The tool will then print the public key in micro ECC format. \ No newline at end of file diff --git a/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool b/dist/tools/suit/suit-manifest-generator/bin/suit-tool similarity index 95% rename from dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool rename to dist/tools/suit/suit-manifest-generator/bin/suit-tool index 11960296ff15..e36f2cfa03cd 100755 --- a/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool +++ b/dist/tools/suit/suit-manifest-generator/bin/suit-tool @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- -# Copyright 2016-2019 ARM Limited or its affiliates +# Copyright 2016-2020 ARM Limited or its affiliates # # SPDX-License-Identifier: Apache-2.0 # diff --git a/dist/tools/suit_v3/suit-manifest-generator/setup.py b/dist/tools/suit/suit-manifest-generator/setup.py similarity index 97% rename from dist/tools/suit_v3/suit-manifest-generator/setup.py rename to dist/tools/suit/suit-manifest-generator/setup.py index beb15ae7193f..bdf7d3e7af89 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/setup.py +++ b/dist/tools/suit/suit-manifest-generator/setup.py @@ -55,7 +55,8 @@ install_requires = [ 'cbor>=1.0.0', 'colorama>=0.4.0', - 'cryptography>=2.8' + 'cryptography>=2.8', + 'pyhsslms>=1.0.0', ], classifiers = [ "Programming Language :: Python :: 3", diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py b/dist/tools/suit/suit-manifest-generator/suit_tool/__init__.py similarity index 94% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/__init__.py index 126457d2cebd..dd088ece5db2 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- # Copyright 2016-2019 ARM Limited or its affiliates @@ -17,4 +16,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = '0.0.1' +__version__ = '0.0.2' diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py b/dist/tools/suit/suit-manifest-generator/suit_tool/argparser.py similarity index 66% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/argparser.py index 84b90cfb5573..c56329f8b8c3 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/argparser.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- # Copyright 2019-2020 ARM Limited or its affiliates @@ -17,22 +16,23 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------- -import sys -import argparse +import sys, argparse, os from suit_tool import __version__ +from suit_tool import keygen +from suit_tool import get_pubkey +import json import re - def str_to_component(s): types = { 'file' : ('file', lambda x : str(x.strip('"'))), + # 'desc' : ('component-description', lambda x : str(x.strip('"'))), 'inst' : ('install-id', lambda x : [ str(y) for y in eval(x) ]), 'uri' : ('uri', lambda x : str(x.strip('"'))) } d = {types[k][0]:types[k][1](v) for k,v in [ re.split(r'=',e, maxsplit=1) for e in re.split(r''',\s*(?=["']?[a-zA-Z0-9_-]+["']?=)''', s)]} return d - class MainArgumentParser(object): def __init__(self): @@ -54,7 +54,7 @@ def _make_parser(self): # create_parser.add_argument('-v', '--manifest-version', choices=['1'], default='1') create_parser.add_argument('-i', '--input-file', metavar='FILE', type=argparse.FileType('r'), - help='An input file describing the update. The file must be formatted as JSON. The overal structure is described in README.') + help='An input file describing the update. The file must be formated as JSON. The overal structure is described in README.') create_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) create_parser.add_argument('-f', '--format', metavar='FMT', choices=['suit', 'suit-debug', 'json'], default='suit') create_parser.add_argument('-s', '--severable', action='store_true', help='Convert large elements to severable fields.') @@ -72,9 +72,25 @@ def _make_parser(self): parse_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) parse_parser.add_argument('-j', '--json-output', default=False, action='store_true', dest='json') - get_uecc_pubkey_parser = subparsers.add_parser('pubkey', help='Get the public key for a supplied private key in uECC-compatible C definition.') + get_pubkey_parser = subparsers.add_parser('pubkey', help='Get the public key for a supplied private key.') + + get_pubkey_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True) + get_pubkey_parser.add_argument('-f', '--output-format', choices=get_pubkey.OutputFormaters.keys(), default='pem') + get_pubkey_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), default=sys.stdout) + + keygen_parser = subparsers.add_parser('keygen', help='Create a signing key. Not for production use') + + keygen_parser.add_argument('-t', '--type', choices=keygen.KeyGenerators.keys(), + default='secp256r1', help='The type of the key to generate') + keygen_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), default=sys.stdout) + keygen_parser.add_argument('-f', '--output-format', choices=keygen.OutputFormaters.keys(), default='pem') + keygen_parser.add_argument('-l', '--levels', help='The number of hss-lms levels', type=int, default=2) - get_uecc_pubkey_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True) + sever_parser = subparsers.add_parser('sever', help='Remove one or more severable elements from the manifest, if present.') + sever_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) + sever_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) + sever_parser.add_argument('-e', '--element', action='append', type=str, dest='elements', default=[]) + sever_parser.add_argument('-a', '--all', action='store_true', default=False) return parser diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py b/dist/tools/suit/suit-manifest-generator/suit_tool/clidriver.py similarity index 73% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/clidriver.py index 5b388139299d..5aeff9a34573 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/clidriver.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- # Copyright 2018-2020 ARM Limited or its affiliates @@ -17,21 +17,19 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------- -import logging -import sys +import logging, sys from suit_tool.argparser import MainArgumentParser -from suit_tool import create, sign, parse, get_uecc_pubkey +from suit_tool import create, sign, parse, get_pubkey, keygen, sever #, verify, cert, init -LOG = logging.getLogger(__name__) -LOG_FORMAT = '[%(levelname)s] %(asctime)s - %(name)s - %(message)s' +LOG = logging.getLogger(__name__) +LOG_FORMAT='[%(levelname)s] %(asctime)s - %(name)s - %(message)s' def main(): driver = CLIDriver() return driver.main() - class CLIDriver(object): def __init__(self): @@ -46,6 +44,10 @@ def __init__(self): logging.basicConfig(level=log_level, format=LOG_FORMAT, datefmt='%Y-%m-%d %H:%M:%S') + logging.addLevelName( logging.INFO, "\033[1;32m%s\033[1;0m" % logging.getLevelName(logging.INFO)) + logging.addLevelName( logging.WARNING, "\033[1;93m%s\033[1;0m" % logging.getLevelName(logging.WARNING)) + logging.addLevelName( logging.CRITICAL, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.CRITICAL)) + LOG.debug('CLIDriver created. Arguments parsed and logging setup.') def main(self): @@ -56,8 +58,10 @@ def main(self): # "cert": cert.main, # "init": init.main, # "update" : update.main, - "pubkey": get_uecc_pubkey.main, - "sign": sign.main + "pubkey": get_pubkey.main, + "sign": sign.main, + "keygen": keygen.main, + "sever" : sever.main, }[self.options.action](self.options) or 0 sys.exit(rc) diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py b/dist/tools/suit/suit-manifest-generator/suit_tool/compile.py similarity index 51% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/compile.py index 15101f027c9b..e42fb34b3676 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/compile.py @@ -1,7 +1,6 @@ -#!/usr/bin/python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- -# Copyright 2019 ARM Limited or its affiliates +# Copyright 2019-2020 ARM Limited or its affiliates # # SPDX-License-Identifier: Apache-2.0 # @@ -19,16 +18,28 @@ # ---------------------------------------------------------------------------- import binascii import copy +import collections +import json +import cbor2 as cbor +import sys +import textwrap +import itertools import logging +from collections import OrderedDict + from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from suit_tool.manifest import SUITComponentId, SUITCommon, SUITSequence, \ - SUITCommand, \ - SUITWrapper, SUITTryEach + suitCommonInfo, SUITCommand, SUITManifest, \ + SUITEnvelope, SUITTryEach, SUITBWrapField, SUITText, \ + SUITDigest, SUITDependencies, SUITDependency + +import suit_tool.create +import suit_tool.sign LOG = logging.getLogger(__name__) @@ -92,16 +103,30 @@ def make_sequence(cid, choices, seq, params, cmds, pcid_key=None, param_drctv='d seq.append(mkCommand(pcid, param_drctv, params)) TryEachCmd = SUITTryEach() for c in choices: - TECseq = SUITSequence() - for item, cmd in neqcmds.items(): - TECseq.append(cmd(cid, c)) + TECseq = TryEachCmd.field.obj().from_json([]) params = {} for param, pcmd in neqparams.items(): k,v = pcmd(cid, c) params[k] = v + dep_params = {} + TECseq_cmds = [] + for item, cmd in neqcmds.items(): + ncmd = cmd(cid, c) + for dp in ncmd.dep_params: + if dp in params: + dep_params[dp] = params[dp] + del params[dp] + TECseq_cmds.append(ncmd) + + if len(dep_params): + TECseq.v.append(mkCommand(pcid, param_drctv, dep_params)) + + for cmd in TECseq_cmds: + TECseq.v.append(cmd) + if len(params): - TECseq.append(mkCommand(pcid, param_drctv, params)) - if len(TECseq.items): + TECseq.v.append(mkCommand(pcid, param_drctv, params)) + if hasattr(TECseq, "v") and len(TECseq.v.items): TryEachCmd.append(TECseq) if len(TryEachCmd.items): seq.append(mkCommand(cid, 'directive-try-each', TryEachCmd)) @@ -114,14 +139,15 @@ def compile_manifest(options, m): m = copy.deepcopy(m) m['components'] += options.components # Compile list of All Component IDs - ids = set([ + # There is no ordered set, so use ordered dict instead + ids = OrderedDict.fromkeys([ SUITComponentId().from_json(id) for comp_ids in [ [c[f] for f in [ 'install-id', 'download-id', 'load-id' ] if f in c] for c in m['components'] ] for id in comp_ids ]) - cid_data = {} + cid_data = OrderedDict() for c in m['components']: if not 'install-id' in c: LOG.critical('install-id required for all components') @@ -139,7 +165,7 @@ def compile_manifest(options, m): digest, imgsize = hash_file(c['file'], hashes.SHA256()) c['install-digest'] = { 'algorithm-id' : 'sha256', - 'digest-bytes' : binascii.b2a_hex(digest.finalize()) + 'digest-bytes' : digest.finalize() } c['install-size'] = imgsize @@ -153,37 +179,106 @@ def compile_manifest(options, m): # Construct common sequence CommonCmds = { - 'offset': lambda cid, data: mkCommand(cid, 'condition-component-offset', data['offset']) + 'offset': lambda cid, data: mkCommand(cid, 'condition-component-offset', None), + 'vendor-id': lambda cid, data: mkCommand(cid, 'condition-vendor-identifier', None), + 'class-id': lambda cid, data: mkCommand(cid, 'condition-class-identifier', None), } CommonParams = { 'install-digest': lambda cid, data: ('image-digest', data['install-digest']), 'install-size': lambda cid, data: ('image-size', data['install-size']), + 'vendor-id' : lambda cid, data: ('vendor-id', data['vendor-id']), + 'class-id' : lambda cid, data: ('class-id', data['class-id']), + 'offset' : lambda cid, data: ('offset', data['offset']) } + # print('Common') CommonSeq = SUITSequence() for cid, choices in cid_data.items(): - if any(['vendor-id' in c for c in choices]): - CommonSeq.append(mkCommand(cid, 'condition-vendor-identifier', - [c['vendor-id'] for c in choices if 'vendor-id' in c][0])) - if any(['vendor-id' in c for c in choices]): - CommonSeq.append(mkCommand(cid, 'condition-class-identifier', - [c['class-id'] for c in choices if 'class-id' in c][0])) CommonSeq = make_sequence(cid, choices, CommonSeq, CommonParams, CommonCmds, param_drctv='directive-override-parameters') + + # print('Dependencies') + # If there are dependencies + DepSeq = SUITSequence() + Dependencies = SUITDependencies() + DepRequiredSequences = { k:[] for k in ['deres', 'fetch', 'install', 'validate', 'run', 'load']} + if 'dependencies' in m: + for dep in m['dependencies']: + # Prepare dependency if necessary + if "src-file" in dep: + # Create + with open(dep['src-file']) as input_fd: + with open(dep['file']+'.tmp','wb') as output_fd: + create_opts = type('',(object,),{ + # 'input_file': open(dep['src-file']), + # 'output_file': open(dep['file']+'.tmp','wb'), + 'input_file': input_fd, + 'output_file': output_fd, + 'format' : 'suit', + 'components': [], + 'log_level': options.log_level + })() + rc = suit_tool.create.main(create_opts) + if rc: + sys.exit(rc) + # Sign + with open(dep['file']+'.tmp','rb') as manifest_fd: + with open(dep['file'],'wb') as output_fd: + with open(dep['key-file'], 'rb') as private_key_fd: + sign_opts = type('',(object,),{ + 'manifest': manifest_fd, + 'output_file': output_fd, + 'private_key': private_key_fd, + 'log_level': options.log_level + })() + rc = suit_tool.sign.main(sign_opts) + if rc: + sys.exit(rc) + # Compute the dependency digest + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + mfst = {} + with open(dep['file'],'rb') as dep_fd: + dep_envelope = cbor.loads(dep_fd.read()) + cmfst = dep_envelope[SUITEnvelope.fields['manifest'].suit_key] + digest.update(cbor.dumps(cmfst)) + mfst = cbor.loads(cmfst) + did = SUITDigest().from_json({ + 'algorithm-id' : 'sha256', + 'digest-bytes' : binascii.b2a_hex(digest.finalize()) + }) + + Dependencies.append(SUITDependency().from_json({ + 'dependency-digest' : did.to_json() + })) + # Construct dependency resolution step + if 'uri' in dep: + DepSeq.append(mkCommand(did, 'directive-set-parameters', { + 'uri' : dep['uri'] + })) + DepSeq.append(mkCommand(did, 'directive-fetch', None)) + DepSeq.append(mkCommand(did, 'condition-image-match', None)) + for k, l in DepRequiredSequences.items(): + if SUITManifest.fields[k].suit_key in mfst: + l.append(mkCommand(did, 'directive-process-dependency', None)) + InstSeq = SUITSequence() FetchSeq = SUITSequence() + # print('Install/Fetch') for cid, choices in cid_data.items(): if any([c.get('install-on-download', True) and 'uri' in c for c in choices]): InstParams = { 'uri' : lambda cid, data: ('uri', data['uri']), + 'offset' : lambda cid, data: ('offset', data['offset']), } if any(['compression-info' in c and not c.get('decompress-on-load', False) for c in choices]): InstParams['compression-info'] = lambda cid, data: data.get('compression-info') InstCmds = { 'offset': lambda cid, data: mkCommand( - cid, 'condition-component-offset', data['offset']) + cid, 'condition-component-offset', None) } InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds) + for cmd in DepRequiredSequences['install']: + InstSeq.append(cmd) InstSeq.append(mkCommand(cid, 'directive-fetch', None)) InstSeq.append(mkCommand(cid, 'condition-image-match', None)) @@ -191,7 +286,8 @@ def compile_manifest(options, m): FetchParams = { 'uri' : lambda cid, data: ('uri', data['uri']), 'download-digest' : lambda cid, data : ( - 'image-digest', data.get('download-digest', data['install-digest'])) + 'image-digest', data.get('download-digest', data['install-digest'])), + 'offset' : lambda cid, data: ('offset', data['offset']), } if any(['compression-info' in c and not c.get('decompress-on-load', False) for c in choices]): FetchParams['compression-info'] = lambda cid, data: data.get('compression-info') @@ -199,12 +295,18 @@ def compile_manifest(options, m): FetchCmds = { 'offset': lambda cid, data: mkCommand( cid, 'condition-component-offset', data['offset']), - 'fetch' : lambda cid, data: mkCommand( - data.get('download-id', cid.to_json()), 'directive-fetch', None), - 'match' : lambda cid, data: mkCommand( - data.get('download-id', cid.to_json()), 'condition-image-match', None) + # 'fetch' : lambda cid, data: mkCommand( + # data.get('download-id', cid.to_json()), 'directive-fetch', None), + # 'match' : lambda cid, data: mkCommand( + # data.get('download-id', cid.to_json()), 'condition-image-match', None) } + did = SUITComponentId().from_json([c['download-id'] for c in choices if 'download-id' in c][0]) FetchSeq = make_sequence(cid, choices, FetchSeq, FetchParams, FetchCmds, 'download-id') + for cmd in DepRequiredSequences['fetch']: + FetchSeq.append(cmd) + + FetchSeq.append(mkCommand(did, 'directive-fetch', None)) + FetchSeq.append(mkCommand(did, 'condition-image-match', None)) InstParams = { 'download-id' : lambda cid, data : ('source-component', data['download-id']) @@ -212,31 +314,40 @@ def compile_manifest(options, m): InstCmds = { } InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds) + for cmd in DepRequiredSequences['install']: + InstSeq.append(cmd) InstSeq.append(mkCommand(cid, 'directive-copy', None)) InstSeq.append(mkCommand(cid, 'condition-image-match', None)) - # TODO: Dependencies - # If there are dependencies - # Construct dependency resolution step ValidateSeq = SUITSequence() RunSeq = SUITSequence() LoadSeq = SUITSequence() + # print('Validate/Load/Run') # If any component is marked bootable for cid, choices in cid_data.items(): - if any([c.get('bootable', False) for c in choices]): - # TODO: Dependencies - # If there are dependencies - # Verify dependencies - # Process dependencies - ValidateSeq.append(mkCommand(cid, 'condition-image-match', None)) + ValidateCmds = { + # 'install-digest' : lambda cid, data : mkCommand(cid, 'condition-image-match', None) + } + ValidateParams = { + } + ValidateSeq = make_sequence(cid, choices, ValidateSeq, ValidateParams, ValidateCmds) + for cmd in DepRequiredSequences['validate']: + ValidateSeq.append(cmd) + ValidateSeq.append(mkCommand(cid, 'condition-image-match', None)) + # if any([c.get('bootable', False) for c in choices]): + # TODO: Dependencies + # If there are dependencies + # Verify dependencies + # Process dependencies + if any(['loadable' in c for c in choices]): # Generate image load section LoadParams = { - 'install-id' : lambda cid, data : ('source-component', c['install-id']), - 'load-digest' : ('image-digest', c.get('load-digest', c['install-digest'])), - 'load-size' : ('image-size', c.get('load-size', c['install-size'])) + 'install-id' : lambda cid, data : ('source-component', c['install-id']), + 'load-digest' : lambda cid, data : ('image-digest', c.get('load-digest', c['install-digest'])), + 'load-size' : lambda cid, data : ('image-size', c.get('load-size', c['install-size'])), } if 'compression-info' in c and c.get('decompress-on-load', False): LoadParams['compression-info'] = lambda cid, data: ('compression-info', c['compression-info']) @@ -244,7 +355,9 @@ def compile_manifest(options, m): # Move each loadable component } load_id = SUITComponentId().from_json(choices[0]['load-id']) - LoadSeq = make_sequence(load_id, choices, ValidateSeq, LoadParams, LoadCmds) + LoadSeq = make_sequence(load_id, choices, LoadSeq, LoadParams, LoadCmds) + for cmd in DepRequiredSequences['load']: + LoadSeq.append(cmd) LoadSeq.append(mkCommand(load_id, 'directive-copy', None)) LoadSeq.append(mkCommand(load_id, 'condition-image-match', None)) @@ -252,13 +365,15 @@ def compile_manifest(options, m): bootable_components = [x for x in m['components'] if x.get('bootable')] if len(bootable_components) == 1: c = bootable_components[0] + for cmd in DepRequiredSequences['run']: + RunSeq.append(cmd) RunSeq.append(SUITCommand().from_json({ 'component-id' : runable_id(c), 'command-id' : 'directive-run', 'command-arg' : None })) else: - t = [] + te = [] for c in bootable_components: pass # TODO: conditions @@ -266,24 +381,75 @@ def compile_manifest(options, m): # # ) #TODO: Text + # print('Common') common = SUITCommon().from_json({ - 'components': [id.to_json() for id in ids], + 'components': [id.to_json() for id in ids.keys()], 'common-sequence': CommonSeq.to_json(), }) + if len(Dependencies.items): + common.dependencies = Dependencies + # print('manifest') jmanifest = { 'manifest-version' : m['manifest-version'], 'manifest-sequence-number' : m['manifest-sequence-number'], 'common' : common.to_json() } + # for k,v in {'deres':DepSeq, 'fetch': FetchSeq, 'install':InstSeq, 'validate':ValidateSeq, 'run':RunSeq, 'load':LoadSeq}.items(): + # # print('sequence:{}'.format(k)) + # v.to_json() + jmanifest.update({k:v for k,v in { - 'payload-fetch' : FetchSeq.to_json(), + 'deres' : DepSeq.to_json(), + 'fetch' : FetchSeq.to_json(), 'install' : InstSeq.to_json(), 'validate' : ValidateSeq.to_json(), 'run' : RunSeq.to_json(), 'load' : LoadSeq.to_json() }.items() if v}) - wrapped_manifest = SUITWrapper().from_json({'manifest' : jmanifest}) + mtext = {} + for k in ['manifest-description', 'update-description']: + if k in m: + mtext[k] = m[k] + for c in m['components']: + ctext = {} + cfields = [ + 'vendor-name', + 'model-name', + 'vendor-domain', + 'model-info', + 'component-description', + 'component-version', + 'version-required', + ] + for k in cfields: + if k in c: + ctext[k] = c[k] + if len(ctext): + cid = SUITComponentId().from_json(c['install-id']).to_suit() + mtext[cid] = ctext + + jenvelope = { + 'authentication-wrapper' : [], + 'manifest' : jmanifest + } + + if len(mtext): + text = SUITText().from_json(mtext) + digest_alg = m.get('digest-algorithm', 'sha256') + suit_text = cbor.dumps(text.to_suit(), canonical=True) + digest = hashes.Hash(SUITEnvelope.digest_algorithms.get(digest_alg)(), backend=default_backend()) + digest.update(suit_text) + + jenvelope['manifest'].update({'text' : { + 'algorithm-id' : digest_alg, + 'digest-bytes' : digest.finalize() + }}) + jenvelope.update({'text' : mtext}) + + # print('building envelope') + wrapped_manifest = SUITEnvelope().from_json(jenvelope) + return wrapped_manifest diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py b/dist/tools/suit/suit-manifest-generator/suit_tool/create.py similarity index 78% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/create.py index a930a3c91425..5201da628efd 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/create.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- # Copyright 2019 ARM Limited or its affiliates @@ -19,18 +18,20 @@ # ---------------------------------------------------------------------------- from suit_tool.compile import compile_manifest import json -import cbor +import cbor2 as cbor import itertools import textwrap +from collections import OrderedDict def main(options): - m = json.loads(options.input_file.read()) + m = json.loads(options.input_file.read(), object_pairs_hook=OrderedDict) nm = compile_manifest(options, m) - if hasattr(options, 'severable') and options.severable: - nm = nm.to_severable() + print('create done. Serializing') + if m.get('severable') or (hasattr(options, 'severable') and options.severable): + nm = nm.to_severable('sha256') output = { - 'suit' : lambda x: cbor.dumps(x.to_suit(), sort_keys=True), + 'suit' : lambda x: cbor.dumps(x.to_suit(), canonical=True), 'suit-debug' : lambda x: '\n'.join(itertools.chain.from_iterable( map(textwrap.wrap, x.to_debug('').split('\n')) )).encode('utf-8'), diff --git a/dist/tools/suit/suit-manifest-generator/suit_tool/get_pubkey.py b/dist/tools/suit/suit-manifest-generator/suit_tool/get_pubkey.py new file mode 100644 index 000000000000..720fefa08b0f --- /dev/null +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/get_pubkey.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +import textwrap +import binascii + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 +from cryptography.hazmat.primitives.asymmetric import utils as asymmetric_utils +from cryptography.hazmat.primitives import serialization as ks + + +def to_uecc_pubkey(pk): + if not isinstance(pk, ec.EllipticCurvePrivateKey): + raise Exception('Private key of type {} is not supported'.format(type(pk))) + public_numbers = pk.public_key().public_numbers() + x = public_numbers.x + y = public_numbers.y + uecc_bytes = x.to_bytes( + (x.bit_length() + 7) // 8, byteorder='big' + ) + y.to_bytes( + (y.bit_length() + 7) // 8, byteorder='big' + ) + uecc_c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( + ', '.join(['{:0=#4x}'.format(x) for x in uecc_bytes]), + 76 + ) + return '\n '.join(uecc_c_def) + '\n};\n' + + +def to_header(pk): + if isinstance(pk, ec.EllipticCurvePrivateKey): + return to_uecc_pubkey(pk) + if isinstance(pk, ed25519.Ed25519PrivateKey): + public_bytes = pk.public_key().public_bytes(ks.Encoding.Raw, + ks.PublicFormat.Raw) + public_c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( + ', '.join(['{:0=#4x}'.format(x) for x in public_bytes]), + 76 + ) + return str.encode('\n '.join(public_c_def) + '\n};\n') + + +OutputFormaters = { + 'uecc' : to_uecc_pubkey, + 'header': to_header, + 'pem' : lambda pk: pk.public_key().public_bytes(ks.Encoding.PEM, ks.PublicFormat.SubjectPublicKeyInfo), + 'der' : lambda pk: pk.public_key().public_bytes(ks.Encoding.DER, ks.PublicFormat.SubjectPublicKeyInfo), + 'hsslms' : lambda pk: pk.publicKey().serialize(), + 'c-hsslms' : lambda pk: ('\n '.join(['const uint8_t hsslms_public_key[] = {'] + textwrap.wrap( + ', '.join(['{:0=#4x}'.format(x) for x in pk.publicKey().serialize()]), + 76 + )) + '\n};\n').encode('utf-8') +} + + +def main(options): + private_key = None + # This test is here because the cryptography module doesn't know about hss-lms keys + if options.output_format in ('pem', 'der', 'uecc', 'header'): + private_key = ks.load_pem_private_key( + options.private_key.read(), + password=None, + backend=default_backend() + ) + + odata = OutputFormaters.get(options.output_format)(private_key) + + try: + odata = odata.decode('utf-8') + except: + odata = binascii.b2a_hex(odata).decode('utf-8') + + odata = '\n'.join( + [line for lines in [textwrap.wrap(line, 80) + for line in odata.split('\n')] for line in lines] + ) + '\n' + options.output_file.write(odata) + + return 0 diff --git a/dist/tools/suit/suit-manifest-generator/suit_tool/keygen.py b/dist/tools/suit/suit-manifest-generator/suit_tool/keygen.py new file mode 100644 index 000000000000..84d62ec14220 --- /dev/null +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/keygen.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019-2020 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.asymmetric import utils as asymmetric_utils +from cryptography.hazmat.primitives import serialization as ks + +import logging +import binascii +import textwrap + +LOG = logging.getLogger(__name__) + +KeyGenerators = { + 'secp256r1' : lambda o: ec.generate_private_key(ec.SECP256R1(), default_backend()), + 'secp384r1' : lambda o: ec.generate_private_key(ec.SECP384R1(), default_backend()), + 'secp521r1' : lambda o: ec.generate_private_key(ec.SECP521R1(), default_backend()), + 'ed25519' : lambda o: ed25519.Ed25519PrivateKey.generate(), +} +OutputFormaters = { + 'pem' : lambda pk: pk.private_bytes(ks.Encoding.PEM, ks.PrivateFormat.PKCS8, ks.NoEncryption()), + 'der' : lambda pk: pk.private_bytes(ks.Encoding.DER, ks.PrivateFormat.PKCS8, ks.NoEncryption()), + 'c-hss-lms' : lambda pk: pk.serialize(), +} + +def main(options): + if options.type == 'hsslms': + options.output_format = 'c-hss-lms' + # Read the manifest wrapper + private_key = KeyGenerators.get(options.type) (options) + + odata = OutputFormaters.get(options.output_format)(private_key) + + if options.output_file.isatty(): + try: + odata = odata.decode('utf-8') + except: + odata = binascii.b2a_hex(odata).decode('utf-8') + odata = '\n'.join(textwrap.wrap(odata, 64)) + '\n' + options.output_file.write(odata) + + return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py b/dist/tools/suit/suit-manifest-generator/suit_tool/manifest.py similarity index 63% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/manifest.py index 57760f50578c..9541eae4634b 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/manifest.py @@ -1,7 +1,6 @@ -#!/usr/bin/python3 # -*- coding: utf-8 -*- # ---------------------------------------------------------------------------- -# Copyright 2019 ARM Limited or its affiliates +# Copyright 2019-2020 ARM Limited or its affiliates # # SPDX-License-Identifier: Apache-2.0 # @@ -19,12 +18,20 @@ # ---------------------------------------------------------------------------- import collections import binascii -import cbor +import cbor2 as cbor +import json import copy import uuid from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes +from collections import OrderedDict + +import logging +LOG = logging.getLogger(__name__) + +TreeBranch = [] + ManifestKey = collections.namedtuple( 'ManifestKey', [ @@ -34,6 +41,8 @@ ] ) def to_bytes(s): + if isinstance(s,bytes): + return s try: return binascii.a2b_hex(s) except: @@ -42,21 +51,29 @@ def to_bytes(s): except: if isinstance(s,str): return s.encode('utf-8') - elif isinstance(s,bytes): - return s else: return str(s).encode('utf-8') +class SUITException(Exception): + def __init__(self, m, data, tree_branch): + super().__init__(m) + self.data = data + self.tree_branch = tree_branch + class SUITCommonInformation: def __init__(self): self.component_ids = [] + self.dependencies = [] self.current_index = 0 self.indent_size = 4 def component_id_to_index(self, cid): id = -1 for i, c in enumerate(self.component_ids): if c == cid and i >= 0: - id = i + id = componentIndex(i) + for i, d in enumerate(self.dependencies): + if d.digest == cid and i >= 0: + id = dependencyIndex(i) return id suitCommonInfo = SUITCommonInformation() @@ -69,7 +86,9 @@ def from_json(self, v): def to_json(self): return self.v def from_suit(self, v): + TreeBranch.append(type(self)) self.v = int(v) + TreeBranch.pop() return self def to_suit(self): return self.v @@ -78,21 +97,40 @@ def to_debug(self, indent): class SUITPosInt(SUITInt): def from_json(self, v): + TreeBranch.append(type(self)) _v = int(v) + # print (_v) if _v < 0: raise Exception('Positive Integers must be >= 0') self.v = _v + TreeBranch.pop() return self def from_suit(self, v): return self.from_json(v) class SUITManifestDict: def mkfields(d): - # rd = {} + # rd = OderedDict() return {k: ManifestKey(*v) for k,v in d.items()} def __init__(self): pass + + def __eq__(self, rhs): + if not isinstance(rhs, type(self)): + return False + + for f, info in self.fields: + if hasattr(self, f) != hasattr(rhs, f): + return False + if hasattr(self, f) and hasattr(rhs, f) and getattr(self, f) != getattr(rhs, f): + return False + + for a,b in zip(self.items, rhs.items): + if not a == b: + return False + return True + def from_json(self, data): for k, f in self.fields.items(): v = data.get(f.json_key, None) @@ -100,7 +138,7 @@ def from_json(self, data): return self def to_json(self): - j = {} + j = OrderedDict() for k, f in self.fields.items(): v = getattr(self, k) if v: @@ -108,14 +146,18 @@ def to_json(self): return j def from_suit(self, data): + TreeBranch.append(type(self)) for k, f in self.fields.items(): + TreeBranch.append(k) v = data.get(f.suit_key, None) d = f.obj().from_suit(v) if v is not None else None setattr(self, k, d) + TreeBranch.pop() + TreeBranch.pop() return self def to_suit(self): - sd = {} + sd = OrderedDict() for k, f in self.fields.items(): v = getattr(self, k) if v: @@ -136,8 +178,12 @@ def to_debug(self, indent): class SUITManifestNamedList(SUITManifestDict): def from_suit(self, data): + TreeBranch.append(type(self)) for k, f in self.fields.items(): + TreeBranch.append(k) setattr(self, k, f.obj().from_suit(data[f.suit_key])) + TreeBranch.pop() + TreeBranch.pop() return self def to_suit(self): @@ -172,18 +218,34 @@ def from_json(self, d): def to_suit(self): return self.v def from_suit(self, d): + TreeBranch.append(type(self)) self.v = self.keymap[self.rkeymap[d]] + TreeBranch.pop() return self def to_debug(self, indent): - s = str(self.v) + ' / ' + self.to_json() + ' /' + s = str(self.v) + ' / ' + json.dumps(self.to_json(),sort_keys = True) + ' /' return s def SUITBWrapField(c): class SUITBWrapper: def to_suit(self): - return cbor.dumps(self.v.to_suit(), sort_keys=True) + return cbor.dumps(self.v.to_suit(), canonical=True) def from_suit(self, d): - self.v = c().from_suit(cbor.loads(d)) + TreeBranch.append(type(self)) + try: + self.v = c().from_suit(cbor.loads(d)) + except SUITException as e: + raise e + except Exception as e: + LOG.debug('At {}: failed to load "{}" as CBOR'.format(type(self),binascii.b2a_hex(d).decode('utf-8'))) + LOG.debug('Path: {}'.format(TreeBranch)) + # LOG.debug('At {}: failed to load "{}" as CBOR'.format(type(self),binascii.b2a_hex(d).decode('utf-8'))) + raise SUITException( + m = 'At {}: failed to load "{}" as CBOR'.format(type(self),binascii.b2a_hex(d).decode('utf-8')), + data = d, + tree_branch = TreeBranch + ) + TreeBranch.pop() return self def to_json(self): return self.v.to_json() @@ -204,6 +266,8 @@ class SUITManifestArray: def __init__(self): self.items=[] def __eq__(self, rhs): + if not isinstance(rhs, type(self)): + return False if len(self.items) != len(rhs.items): return False for a,b in zip(self.items, rhs.items): @@ -225,8 +289,12 @@ def to_json(self): def from_suit(self, data): self.items = [] + TreeBranch.append(type(self)) for d in data: + TreeBranch.append(len(self.items)) self.items.append(self.field.obj().from_suit(d)) + TreeBranch.pop() + TreeBranch.pop() return self def to_suit(self): @@ -270,7 +338,7 @@ def from_suit(self, d): self.v = uuid.UUID(bytes=d).bytes return self def to_debug(self, indent): - return 'h\'' + self.to_json() + '\' / ' + str(uuid.UUID(bytes=self.v)) + ' /' + return 'h\'' + json.dumps(self.to_json(), sort_keys=True) + '\' / ' + str(uuid.UUID(bytes=self.v)) + ' /' class SUITRaw: @@ -315,6 +383,9 @@ def to_debug(self, indent): class SUITComponentId(SUITManifestArray): field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITBytes) + def to_suit(self): + return tuple(super(SUITComponentId, self).to_suit()) + def to_debug(self, indent): newindent = indent + one_indent s = '[' + ''.join([v.to_debug(newindent) for v in self.items]) + ']' @@ -337,7 +408,6 @@ def to_debug(self, indent): ) return s - class SUITComponents(SUITManifestArray): field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITComponentId) @@ -364,6 +434,8 @@ class SUITDigest(SUITManifestNamedList): 'algo' : ('algorithm-id', 0, SUITDigestAlgo), 'digest' : ('digest-bytes', 1, SUITBytes) }) + def __hash__(self): + return hash(tuple([getattr(self, k) for k in self.fields.keys() if hasattr(self, k)])) class SUITCompressionInfo(SUITKeyMap): rkeymap, keymap = SUITKeyMap.mkKeyMaps({ @@ -376,11 +448,14 @@ class SUITCompressionInfo(SUITKeyMap): class SUITParameters(SUITManifestDict): fields = SUITManifestDict.mkfields({ - 'digest' : ('image-digest', 11, SUITDigest), - 'size' : ('image-size', 12, SUITPosInt), - 'uri' : ('uri', 6, SUITTStr), - 'src' : ('source-component', 10, SUITComponentIndex), - 'compress' : ('compression-info', 8, SUITCompressionInfo) + 'vendor-id' : ('vendor-id', 1, SUITUUID), + 'class-id' : ('class-id', 2, SUITUUID), + 'digest' : ('image-digest', 3, SUITBWrapField(SUITDigest)), + 'size' : ('image-size', 14, SUITPosInt), + 'uri' : ('uri', 21, SUITTStr), + 'src' : ('source-component', 22, SUITComponentIndex), + 'compress' : ('compression-info', 19, SUITCompressionInfo), + 'offset' : ('offset', 5, SUITPosInt) }) def from_json(self, j): return super(SUITParameters, self).from_json(j) @@ -388,10 +463,18 @@ def from_json(self, j): class SUITTryEach(SUITManifestArray): pass -def SUITCommandContainer(jkey, skey, argtype): +class dependencyIndex(int): + def __new__(cls, value): + return super(cls, cls).__new__(cls, value) +class componentIndex(int): + def __new__(cls, value): + return super(cls, cls).__new__(cls, value) + +def SUITCommandContainer(jkey, skey, argtype, dp=[]): class SUITCmd(SUITCommand): json_key = jkey suit_key = skey + dep_params = dp def __init__(self): pass def to_suit(self): @@ -408,17 +491,25 @@ def to_json(self): def from_json(self, j): if j['command-id'] != self.json_key: raise Except('JSON Key mismatch error') - if self.json_key != 'directive-set-component-index': - self.cid = SUITComponentId().from_json(j['component-id']) + if self.json_key != 'directive-set-component-index' and self.json_key != 'directive-set-dependency-index': + try: + self.cid = SUITComponentId().from_json(j['component-id']) + except: + self.cid = SUITDigest().from_json(j['component-id']) self.arg = argtype().from_json(j['command-arg']) return self def from_suit(self, s): if s[0] != self.suit_key: raise Except('SUIT Key mismatch error') if self.json_key == 'directive-set-component-index': - suitCommonInfo.current_index = s[1] + suitCommonInfo.current_index = componentIndex(s[1]) + elif self.json_key == 'directive-set-dependency-index': + suitCommonInfo.current_index = dependencyIndex(s[1]) else: - self.cid = suitCommonInfo.component_ids[suitCommonInfo.current_index] + if isinstance(suitCommonInfo.current_index, dependencyIndex): + self.cid = suitCommonInfo.dependencies[suitCommonInfo.current_index] + else: + self.cid = suitCommonInfo.component_ids[suitCommonInfo.current_index] self.arg = argtype().from_suit(s[1]) return self def to_debug(self, indent): @@ -427,6 +518,14 @@ def to_debug(self, indent): return s return SUITCmd +def mkPolicy(policy): + class SUITReportingPolicy(SUITPosInt): + default_policy = policy + def from_json(self, j): + if j is None: + j = self.default_policy + return super(SUITReportingPolicy, self).from_json(j) + return SUITReportingPolicy class SUITCommand: def from_json(self, j): @@ -435,31 +534,30 @@ def from_suit(self, s): return self.scommands[s[0]]().from_suit(s) SUITCommand.commands = [ - SUITCommandContainer('condition-vendor-identifier', 1, SUITUUID), - SUITCommandContainer('condition-class-identifier', 2, SUITUUID), - SUITCommandContainer('condition-image-match', 3, SUITNil), - SUITCommandContainer('condition-use-before', 4, SUITRaw), - SUITCommandContainer('condition-component-offset', 5, SUITRaw), - SUITCommandContainer('condition-custom', 6, SUITRaw), - SUITCommandContainer('condition-device-identifier', 24, SUITRaw), - SUITCommandContainer('condition-image-not-match', 25, SUITRaw), - SUITCommandContainer('condition-minimum-battery', 26, SUITRaw), - SUITCommandContainer('condition-update-authorised', 27, SUITRaw), - SUITCommandContainer('condition-version', 28, SUITRaw), + SUITCommandContainer('condition-vendor-identifier', 1, mkPolicy(policy=0xF), dp=['vendor-id']), + SUITCommandContainer('condition-class-identifier', 2, mkPolicy(policy=0xF), dp=['class-id']), + SUITCommandContainer('condition-image-match', 3, mkPolicy(policy=0xF), dp=['digest']), + SUITCommandContainer('condition-use-before', 4, mkPolicy(policy=0xA)), + SUITCommandContainer('condition-component-offset', 5, mkPolicy(policy=0x5), dp=['offset']), + SUITCommandContainer('condition-device-identifier', 24, mkPolicy(policy=0xF)), + SUITCommandContainer('condition-image-not-match', 25, mkPolicy(policy=0xF)), + SUITCommandContainer('condition-minimum-battery', 26, mkPolicy(policy=0xA)), + SUITCommandContainer('condition-update-authorised', 27, mkPolicy(policy=0x3)), + SUITCommandContainer('condition-version', 28, mkPolicy(policy=0xF)), SUITCommandContainer('directive-set-component-index', 12, SUITPosInt), - SUITCommandContainer('directive-set-dependency-index', 13, SUITRaw), - SUITCommandContainer('directive-abort', 14, SUITRaw), + SUITCommandContainer('directive-set-dependency-index', 13, SUITPosInt), + SUITCommandContainer('directive-abort', 14, mkPolicy(policy=0x2)), SUITCommandContainer('directive-try-each', 15, SUITTryEach), - SUITCommandContainer('directive-process-dependency', 18, SUITRaw), + SUITCommandContainer('directive-process-dependency', 18, mkPolicy(policy=0)), SUITCommandContainer('directive-set-parameters', 19, SUITParameters), SUITCommandContainer('directive-override-parameters', 20, SUITParameters), - SUITCommandContainer('directive-fetch', 21, SUITNil), - SUITCommandContainer('directive-copy', 22, SUITRaw), - SUITCommandContainer('directive-run', 23, SUITRaw), - SUITCommandContainer('directive-wait', 29, SUITRaw), + SUITCommandContainer('directive-fetch', 21, mkPolicy(policy=0x2)), + SUITCommandContainer('directive-copy', 22, mkPolicy(policy=0x2)), + SUITCommandContainer('directive-run', 23, mkPolicy(policy=0x2)), + SUITCommandContainer('directive-wait', 29, mkPolicy(policy=0x2)), SUITCommandContainer('directive-run-sequence', 30, SUITRaw), SUITCommandContainer('directive-run-with-arguments', 31, SUITRaw), - SUITCommandContainer('directive-swap', 32, SUITRaw), + SUITCommandContainer('directive-swap', 32, mkPolicy(policy=0x2)), ] SUITCommand.jcommands = { c.json_key : c for c in SUITCommand.commands} SUITCommand.scommands = { c.suit_key : c for c in SUITCommand.commands} @@ -472,17 +570,27 @@ def to_suit(self): suitCommonInfo.current_index = 0 if len(suitCommonInfo.component_ids) == 1 else None for i in self.items: if i.json_key == 'directive-set-component-index': - suitCommonInfo.current_index = i.arg.v + suitCommonInfo.current_index = componentIndex(i.arg.v) + elif i.json_key == 'directive-set-dependency-index': + suitCommonInfo.current_index = dependencyIndex(i.arg.v) else: + # Option 1: current & command index same class, same number, + # Do nothing + # Option 2: current & command not equal, command is component + # set component index + # Option 3: current & command not equal, command is dependency + # set dependency index cidx = suitCommonInfo.component_id_to_index(i.cid) if cidx != suitCommonInfo.current_index: - # Change component - cswitch = SUITCommand().from_json({ - 'command-id' : 'directive-set-component-index', - 'command-arg' : cidx - }) + op = 'directive-set-component-index' + if isinstance(cidx, dependencyIndex): + op = 'directive-set-dependency-index' + # Change component/dependency suitCommonInfo.current_index = cidx - suit_l += cswitch.to_suit() + suit_l += SUITCommand().from_json({ + 'command-id' : op, + 'command-arg' : int(cidx) + }).to_suit() suit_l += i.to_suit() return suit_l def to_debug(self, indent): @@ -491,7 +599,7 @@ def from_suit(self, s): self.items = [SUITCommand().from_suit(i) for i in zip(*[iter(s)]*2)] return self -SUITTryEach.field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITSequence) +SUITTryEach.field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITBWrapField(SUITSequence)) class SUITSequenceComponentReset(SUITSequence): def to_suit(self): @@ -520,28 +628,111 @@ def to_suit(self): def to_debug(self, indent): return self.v.to_debug(indent) return SUITSeverableField -# class SUITSequenceOrDigest() + +class SUITDependency(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'digest' : ('dependency-digest', 1, SUITDigest), + 'prefix' : ('dependency-prefix', 2, SUITComponentId), + }) + +class SUITDependencies(SUITManifestArray): + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITDependency) + + def from_suit(self, data): + super(SUITDependencies, self).from_suit(data) + suitCommonInfo.dependencies = self.items + return self + + def from_json(self, j): + super(SUITDependencies, self).from_json(j) + suitCommonInfo.dependencies = self.items + return self class SUITCommon(SUITManifestDict): fields = SUITManifestNamedList.mkfields({ - # 'dependencies' : ('dependencies', 1, SUITBWrapField(SUITDependencies)), - 'components' : ('components', 2, SUITBWrapField(SUITComponents)), - # 'dependency_components' : ('dependency-components', 3, SUITBWrapField(SUITDependencies)), + 'dependencies' : ('dependencies', 1, SUITBWrapField(SUITDependencies)), + 'components' : ('components', 2, SUITComponents), 'common_sequence' : ('common-sequence', 4, SUITBWrapField(SUITSequenceComponentReset)), }) +class SUITComponentText(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'vendorname' : ('vendor-name', 1, SUITTStr), + 'modelname' : ('model-name', 2, SUITTStr), + 'vendordomain' : ('vendor-domain', 3, SUITTStr), + 'modelinfo' : ('json-source', 4, SUITTStr), + 'cdesc' : ('component-description', 5, SUITTStr), + 'version' : ('version', 6, SUITTStr), + 'reqversion' : ('required-version', 7, SUITTStr), + }) + +class SUITText(SUITManifestDict): + fields = SUITManifestDict.mkfields({ + 'mdesc' : ('manifest-description', 1, SUITTStr), + 'udesc' : ('update-description', 2, SUITTStr), + 'json' : ('json-source', 3, SUITTStr), + 'yaml' : ('yaml-source', 4, SUITTStr), + }) + components={} + + def to_json(self): + d = super(SUITText, self).to_json() + d.update({k.to_json() : v.to_json() for k,v in self.components.items()}) + return d + + def from_json(self, data): + # Handle components + for k,v in data.items(): + if not isinstance(v, str): + self.components[SUITComponentId().from_json(k)] = SUITComponentText().from_json(v) + # Treat everything else as a normal manifestDict + return super(SUITText, self).from_json(data) + + def to_suit(self): + d = super(SUITText, self).to_suit() + d.update({k.to_suit() : v.to_suit() for k,v in self.components.items()}) + return d + + def from_suit(self, data): + # Handle components + for k,v in data.items(): + if not isinstance(v, str): + self.components[SUITComponentId().from_suit(k)] = SUITComponentText().from_suit(v) + # Treat everything else as a normal manifestDict + return super(SUITText, self).from_json(data) + + def to_debug(self, indent): + s = '{' + newindent = indent + one_indent + + for k, f in self.fields.items(): + v = getattr(self, k) + if v: + s += '\n{ind}/ {jk} / {sk}:'.format(ind=newindent, jk=f.json_key, sk=f.suit_key) + s += v.to_debug(newindent) + ',' + for k, f in self.components.items(): + s += '\n' + newindent + '{}:'.format(k.to_debug(newindent + one_indent)) + s += f.to_debug(newindent + one_indent) + + s += '\n' + indent + '}' + + return s + class SUITManifest(SUITManifestDict): fields = SUITManifestDict.mkfields({ 'version' : ('manifest-version', 1, SUITPosInt), 'sequence' : ('manifest-sequence-number', 2, SUITPosInt), 'common' : ('common', 3, SUITBWrapField(SUITCommon)), + 'refuri' : ('reference-uri', 4, SUITTStr), 'deres' : ('dependency-resolution', 7, SUITMakeSeverableField(SUITSequenceComponentReset)), 'fetch' : ('payload-fetch', 8, SUITMakeSeverableField(SUITSequenceComponentReset)), 'install' : ('install', 9, SUITMakeSeverableField(SUITSequenceComponentReset)), 'validate' : ('validate', 10, SUITBWrapField(SUITSequenceComponentReset)), 'load' : ('load', 11, SUITBWrapField(SUITSequenceComponentReset)), 'run' : ('run', 12, SUITBWrapField(SUITSequenceComponentReset)), + 'text' : ('text', 13, SUITMakeSeverableField(SUITText)), + 'coswid' : ('coswid', 14, SUITBytes), }) class COSE_Algorithms(SUITKeyMap): @@ -588,7 +779,7 @@ def to_suit(self): for k, f in self.fields.items(): v = getattr(self, k, None) if v: - return cbor.Tag(tag=f.suit_key, value=v.to_suit()) + return cbor.CBORTag(tag=f.suit_key, value=v.to_suit()) return None def from_suit(self, data): @@ -618,11 +809,11 @@ class COSETaggedAuth(COSETagChoice): }) class COSEList(SUITManifestArray): - field = collections.namedtuple('ArrayElement', 'obj')(obj=COSETaggedAuth) + field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITBWrapField(COSETaggedAuth)) def from_suit(self, data): return super(COSEList, self).from_suit(data) -class SUITWrapper(SUITManifestDict): +class SUITEnvelope(SUITManifestDict): fields = SUITManifestDict.mkfields({ 'auth' : ('authentication-wrapper', 2, SUITBWrapField(COSEList)), 'manifest' : ('manifest', 3, SUITBWrapField(SUITManifest)), @@ -632,9 +823,10 @@ class SUITWrapper(SUITManifestDict): 'validate': ('validate', 10, SUITBWrapField(SUITSequence)), 'load': ('load', 11, SUITBWrapField(SUITSequence)), 'run': ('run', 12, SUITBWrapField(SUITSequence)), - # 'text': ('text', 13, SUITBWrapField(SUITSequence)), + 'text': ('text', 13, SUITBWrapField(SUITText)), + 'coswid': ('coswid', 14, SUITBytes), }) - severable_fields = {'deres', 'fetch', 'install'} #, 'text'} + severable_fields = {'deres', 'fetch', 'install', 'text', 'coswid'} digest_algorithms = { 'sha224' : hashes.SHA224, 'sha256' : hashes.SHA256, @@ -650,14 +842,14 @@ def to_severable(self, digest_alg): v = getattr(sev.manifest.v, k) if v is None: continue - cbor_field = cbor.dumps(v.to_suit(), sort_keys=True) - digest = hashes.Hash(digest_algorithms.get(digest_alg)(), backend=default_backend()) + cbor_field = cbor.dumps(v.to_suit(), canonical=True) + digest = hashes.Hash(self.digest_algorithms.get(digest_alg)(), backend=default_backend()) digest.update(cbor_field) field_digest = SUITDigest().from_json({ 'algorithm-id' : digest_alg, 'digest-bytes' : digest.finalize() }) - cbor_digest = cbor.dumps(field_digest.to_suit(), sort_keys=True) + cbor_digest = cbor.dumps(field_digest.to_suit(), canonical=True) if len(cbor_digest) < len(cbor_field): setattr(sev.manifest.v, k, field_digest) setattr(sev,k,v) @@ -673,11 +865,11 @@ def from_severable(self): if v is None: continue # Verify digest - cbor_field = cbor.dumps(v.to_suit(), sort_keys=True) + cbor_field = cbor.dumps(v.to_suit(), canonical=True) digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(cbor_field) actual_digest = digest.finalize() - field_digest = getattr(sev.nsev.v, k) + field_digest = getattr(nsev.v, k) expected_digest = field_digest.to_suit()[1] if digest != expected_digest: raise Exception('Field Digest mismatch: For {}, expected: {}, got {}'.format( diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py b/dist/tools/suit/suit-manifest-generator/suit_tool/parse.py similarity index 88% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/parse.py index 9544778d64ec..bf75667fb081 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/parse.py @@ -17,17 +17,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------- -import cbor +import cbor2 as cbor import json import itertools import textwrap -from suit_tool.manifest import SUITWrapper +from suit_tool.manifest import SUITEnvelope def main(options): # Read the manifest wrapper decoded_cbor_wrapper = cbor.loads(options.manifest.read()) - wrapper = SUITWrapper().from_suit(decoded_cbor_wrapper) + # print(decoded_cbor_wrapper) + wrapper = SUITEnvelope().from_suit(decoded_cbor_wrapper) if options.json: print (json.dumps(wrapper.to_json(),indent=2)) else: diff --git a/dist/tools/suit/suit-manifest-generator/suit_tool/sever.py b/dist/tools/suit/suit-manifest-generator/suit_tool/sever.py new file mode 100644 index 000000000000..a50c1ce89a1a --- /dev/null +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/sever.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2019 ARM Limited or its affiliates +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +from suit_tool.compile import compile_manifest +import json +import cbor2 as cbor +import itertools +import textwrap +from collections import OrderedDict + +from suit_tool.manifest import SUITEnvelope +def main(options): + # Read the manifest wrapper + envelope = cbor.loads(options.manifest.read()) + + if hasattr(options, 'all'): + options.elements = SUITEnvelope.severable_fields + + for e in options.elements: + eid = SUITEnvelope.fields[e].suit_key + if eid in envelope: + del(envelope[eid]) + + output = cbor.dumps(envelope, canonical=True) + options.output_file.write(output) + + return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py b/dist/tools/suit/suit-manifest-generator/suit_tool/sign.py similarity index 69% rename from dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py rename to dist/tools/suit/suit-manifest-generator/suit_tool/sign.py index f57333137448..b4d831a8eaed 100644 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py +++ b/dist/tools/suit/suit-manifest-generator/suit_tool/sign.py @@ -17,7 +17,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------- -import cbor +import cbor2 as cbor +import json from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes @@ -26,31 +27,40 @@ from cryptography.hazmat.primitives.asymmetric import utils as asymmetric_utils from cryptography.hazmat.primitives import serialization as ks - -from suit_tool.manifest import COSE_Sign1, COSEList, \ - SUITWrapper, SUITBytes, SUITBWrapField +from suit_tool.manifest import COSE_Sign1, COSEList, SUITDigest,\ + SUITEnvelope, SUITBytes, SUITBWrapField, \ + COSETaggedAuth import logging import binascii LOG = logging.getLogger(__name__) -def get_cose_es_bytes(private_key, sig_val): +def get_cose_es_bytes(options, private_key, sig_val): ASN1_signature = private_key.sign(sig_val, ec.ECDSA(hashes.SHA256())) r,s = asymmetric_utils.decode_dss_signature(ASN1_signature) ssize = private_key.key_size signature_bytes = r.to_bytes(ssize//8, byteorder='big') + s.to_bytes(ssize//8, byteorder='big') return signature_bytes -def get_cose_ed25519_bytes(private_key, sig_val): +def get_cose_ed25519_bytes(options, private_key, sig_val): return private_key.sign(sig_val) +def get_hsslms_bytes(options, private_key, sig_val): + sig = private_key.sign(sig_val) + key_file_name = options.private_key.name + options.private_key.close() + with open(key_file_name, 'wb') as fd: + fd.write(private_key.serialize()) + return sig + def main(options): # Read the manifest wrapper wrapper = cbor.loads(options.manifest.read()) private_key = None digest = None + private_key_buffer = options.private_key.read() try: - private_key = ks.load_pem_private_key(options.private_key.read(), password=None, backend=default_backend()) + private_key = ks.load_pem_private_key(private_key_buffer, password=None, backend=default_backend()) if isinstance(private_key, ec.EllipticCurvePrivateKey): options.key_type = 'ES{}'.format(private_key.key_size) elif isinstance(private_key, ed25519.Ed25519PrivateKey): @@ -65,13 +75,10 @@ def main(options): 'EdDSA' : hashes.Hash(hashes.SHA256(), backend=default_backend()), }.get(options.key_type) except: - digest= hashes.Hash(hashes.SHA256(), backend=default_backend()) - # private_key = None - # TODO: Implement loading of DSA keys not supported by python cryptography LOG.critical('Non-library key type not implemented') - # return 1 + return 1 - digest.update(cbor.dumps(wrapper[SUITWrapper.fields['manifest'].suit_key])) + digest.update(cbor.dumps(wrapper[SUITEnvelope.fields['manifest'].suit_key])) cose_signature = COSE_Sign1().from_json({ 'protected' : { @@ -80,7 +87,7 @@ def main(options): 'unprotected' : {}, 'payload' : { 'algorithm-id' : 'sha256', - 'digest-bytes' : binascii.b2a_hex(digest.finalize()) + 'digest-bytes' : digest.finalize() } }) @@ -89,23 +96,24 @@ def main(options): cose_signature.protected.to_suit(), b'', cose_signature.payload.to_suit(), - ], sort_keys = True) - sig_val = Sig_structure + ], canonical = True) + LOG.debug('Signing: {}'.format(binascii.b2a_hex(Sig_structure).decode('utf-8'))) signature_bytes = { 'ES256' : get_cose_es_bytes, 'ES384' : get_cose_es_bytes, 'ES512' : get_cose_es_bytes, 'EdDSA' : get_cose_ed25519_bytes, - }.get(options.key_type)(private_key, sig_val) + 'HSS-LMS' : get_hsslms_bytes, + }.get(options.key_type)(options, private_key, Sig_structure) cose_signature.signature = SUITBytes().from_suit(signature_bytes) - auth = SUITBWrapField(COSEList)().from_json([{ + auth = SUITBWrapField(COSEList)().from_suit(wrapper[SUITEnvelope.fields['auth'].suit_key]) + auth.v.append(auth.v.field.obj().from_json({ 'COSE_Sign1_Tagged' : cose_signature.to_json() - }]) - - wrapper[SUITWrapper.fields['auth'].suit_key] = auth.to_suit() + })) + wrapper[SUITEnvelope.fields['auth'].suit_key] = auth.to_suit() - options.output_file.write(cbor.dumps(wrapper, sort_keys=True)) + options.output_file.write(cbor.dumps(wrapper, canonical=True)) return 0 diff --git a/dist/tools/suit_v3/suit-manifest-generator/.gitignore b/dist/tools/suit_v3/suit-manifest-generator/.gitignore deleted file mode 100644 index 8b2f59d6b821..000000000000 --- a/dist/tools/suit_v3/suit-manifest-generator/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*__pycache__ -*.pyc -*.DS_Store -*.hex -examples/*.cbor diff --git a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py b/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py deleted file mode 100644 index 6993044a685b..000000000000 --- a/dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright 2020 ARM Limited or its affiliates -# -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- -import textwrap - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization as ks - -def main(options): - private_key = ks.load_pem_private_key( - options.private_key.read(), - password=None, - backend=default_backend() - ) - #public_numbers = private_key.public_key().public_numbers() - #x = public_numbers.x - #y = public_numbers.y - #uecc_bytes = x.to_bytes( - # (x.bit_length() + 7) // 8, byteorder='big' - #) + y.to_bytes( - # (y.bit_length() + 7) // 8, byteorder='big' - #) - #uecc_c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( - # ', '.join(['{:0=#4x}'.format(x) for x in uecc_bytes]), - # 76 - #) - public_bytes = private_key.public_key().public_bytes( - encoding=ks.Encoding.Raw, - format=ks.PublicFormat.Raw - ) - - c_def = ['const uint8_t public_key[] = {'] + textwrap.wrap( - ', '.join(['{:0=#4x}'.format(x) for x in public_bytes]), - 76 - ) - print('\n '.join(c_def) + '\n};') - return 0 diff --git a/makefiles/suit.base.inc.mk b/makefiles/suit.base.inc.mk index ab0d84c1034a..9e1df547ac4b 100644 --- a/makefiles/suit.base.inc.mk +++ b/makefiles/suit.base.inc.mk @@ -1,6 +1,6 @@ # # path to suit-tool -SUIT_TOOL ?= $(RIOTBASE)/dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool +SUIT_TOOL ?= $(RIOTBASE)/dist/tools/suit/suit-manifest-generator/bin/suit-tool # # SUIT encryption keys @@ -27,14 +27,14 @@ BUILDDEPS += $(SUIT_PUB_HDR) $(SUIT_SEC): $(CLEAN) @echo suit: generating key in $(SUIT_KEY_DIR) @mkdir -p $(SUIT_KEY_DIR) - @$(RIOTBASE)/dist/tools/suit_v3/gen_key.py $(SUIT_SEC) + @$(RIOTBASE)/dist/tools/suit/gen_key.py $(SUIT_SEC) # set FORCE so switching between keys using "SUIT_KEY=foo make ..." # triggers a rebuild even if the new key would otherwise not (because the other # key's mtime is too far back). $(SUIT_PUB_HDR): $(SUIT_SEC) FORCE | $(CLEAN) @mkdir -p $(SUIT_PUB_HDR_DIR) - @$(SUIT_TOOL) pubkey -k $(SUIT_SEC) \ + @$(SUIT_TOOL) pubkey -f header -k $(SUIT_SEC) \ | '$(LAZYSPONGE)' $(LAZYSPONGE_FLAGS) '$@' suit/genkey: $(SUIT_SEC) diff --git a/makefiles/suit.inc.mk b/makefiles/suit.inc.mk index e343b2bac195..631ce9c04d27 100644 --- a/makefiles/suit.inc.mk +++ b/makefiles/suit.inc.mk @@ -10,13 +10,13 @@ SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot # -SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv3.$(APP_VER).bin -SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv3.latest.bin -SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv3_signed.$(APP_VER).bin -SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv3_signed.latest.bin +SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suit.$(APP_VER).bin +SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suit.latest.bin +SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suit_signed.$(APP_VER).bin +SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suit_signed.latest.bin SUIT_NOTIFY_VERSION ?= latest -SUIT_NOTIFY_MANIFEST ?= $(APPLICATION)-riot.suitv3_signed.$(SUIT_NOTIFY_VERSION).bin +SUIT_NOTIFY_MANIFEST ?= $(APPLICATION)-riot.suit_signed.$(SUIT_NOTIFY_VERSION).bin # Long manifest names require more buffer space when parsing export CFLAGS += -DCONFIG_SOCK_URLPATH_MAXLEN=128 @@ -27,7 +27,7 @@ SUIT_CLASS ?= $(BOARD) # $(SUIT_MANIFEST): $(SLOT0_RIOT_BIN) $(SLOT1_RIOT_BIN) - $(RIOTBASE)/dist/tools/suit_v3/gen_manifest.py \ + $(RIOTBASE)/dist/tools/suit/gen_manifest.py \ --urlroot $(SUIT_COAP_ROOT) \ --seqnr $(SUIT_SEQNR) \ --uuid-vendor $(SUIT_VENDOR) \ diff --git a/sys/include/suit.h b/sys/include/suit.h index f352abe86f2d..c7e3c2a03964 100644 --- a/sys/include/suit.h +++ b/sys/include/suit.h @@ -14,10 +14,10 @@ * @experimental * * @note The current implementation of this specification is based on the - * IETF-SUIT-v3 draft. The module is still experimental and will change to + * IETF-SUIT-v9 draft. The module is still experimental and will change to * match future draft specifications * - * @see https://tools.ietf.org/html/draft-ietf-suit-manifest-03 + * @see https://tools.ietf.org/html/draft-ietf-suit-manifest-09 * * @{ * @@ -52,7 +52,9 @@ extern "C" { /** * @brief Maximum number of components supported in a SUIT manifest */ -#define SUIT_COMPONENT_MAX (1U) +#ifndef CONFIG_SUIT_COMPONENT_MAX +#define CONFIG_SUIT_COMPONENT_MAX (1U) +#endif /** * @brief Current SUIT serialization format version @@ -126,13 +128,59 @@ enum { }; /** - * @brief SUIT component struct + * @name SUIT parameters + * @{ + */ +typedef enum { + SUIT_PARAMETER_VENDOR_IDENTIFIER = 1, + SUIT_PARAMETER_CLASS_IDENTIFIER = 2, + SUIT_PARAMETER_IMAGE_DIGEST = 3, + SUIT_PARAMETER_USE_BEFORE = 4, + SUIT_PARAMETER_COMPONENT_OFFSET = 5, + SUIT_PARAMETER_STRICT_ORDER = 12, + SUIT_PARAMETER_SOFT_FAILURE = 13, + SUIT_PARAMETER_IMAGE_SIZE = 14, + SUIT_PARAMETER_ENCRYPTION_INFO = 18, + SUIT_PARAMETER_COMPRESSION_INFO = 19, + SUIT_PARAMETER_UNPACK_INFO = 20, + SUIT_PARAMETER_URI = 21, + SUIT_PARAMETER_SOURCE_COMPONENT = 22, + SUIT_PARAMETER_RUN_ARGS = 23, + SUIT_PARAMETER_DEVICE_IDENTIFIER = 24, + SUIT_PARAMETER_MINIMUM_BATTERY = 26, + SUIT_PARAMETER_UPDATE_PRIORITY = 27, + SUIT_PARAMETER_VERSION = 28, + SUIT_PARAMETER_WAIT_INFO = 29, + SUIT_PARAMETER_URI_LIST = 30, +} suit_parameter_t; +/** @} */ + +/** + * @brief SUIT parameter reference + * + * A 16-bit offset is enough to reference content inside the manifest itself. + */ +typedef struct { + uint16_t offset; /**< offset to the start of the content */ +} suit_param_ref_t; + +/** + * @brief SUIT component struct as decoded from the manifest + * + * The parameters are references to CBOR-encoded information in the manifest. */ typedef struct { - uint32_t size; /**< Size */ - nanocbor_value_t identifier; /**< Identifier */ - nanocbor_value_t url; /**< Url */ - nanocbor_value_t digest; /**< Digest */ + suit_param_ref_t identifier; /**< Component identifier */ + suit_param_ref_t param_vendor_id; /**< Vendor ID */ + suit_param_ref_t param_class_id; /**< Class ID */ + suit_param_ref_t param_digest; /**< Payload verification digest */ + suit_param_ref_t param_uri; /**< Payload fetch URI */ + suit_param_ref_t param_size; /**< Payload size */ + + /** + * @brief Component offset inside the device memory. + */ + suit_param_ref_t param_component_offset; } suit_component_t; /** @@ -146,9 +194,9 @@ typedef struct { uint32_t validated; /**< bitfield of validated policies */ uint32_t state; /**< bitfield holding state information */ /** List of components in the manifest */ - suit_component_t components[SUIT_COMPONENT_MAX]; + suit_component_t components[CONFIG_SUIT_COMPONENT_MAX]; unsigned components_len; /**< Current number of components */ - uint32_t component_current; /**< Current component index */ + uint8_t component_current; /**< Current component index */ riotboot_flashwrite_t *writer; /**< Pointer to the riotboot flash writer */ /** Manifest validation buffer */ uint8_t validation_buf[SUIT_COSE_BUF_SIZE]; @@ -165,6 +213,20 @@ typedef struct { */ #define SUIT_MANIFEST_HAVE_IMAGE (0x2) +/** + * @brief Component index representing all components + * + * Used when suit-directive-set-component-index = True + */ +#define SUIT_MANIFEST_COMPONENT_ALL (UINT8_MAX) + +/** + * @brief Component index representing no components + * + * Used when suit-directive-set-component-index = False + */ +#define SUIT_MANIFEST_COMPONENT_NONE (SUIT_MANIFEST_COMPONENT_ALL - 1) + /** * @brief Parse a manifest * diff --git a/sys/include/suit/handlers.h b/sys/include/suit/handlers.h index 538bf7e124b4..95529f927d38 100644 --- a/sys/include/suit/handlers.h +++ b/sys/include/suit/handlers.h @@ -135,14 +135,14 @@ extern const suit_manifest_handler_t suit_command_sequence_handlers[]; extern const size_t suit_command_sequence_handlers_len; /** - * @brief SUIT container handlers reference + * @brief SUIT envelope handlers reference */ -extern const suit_manifest_handler_t suit_container_handlers[]; +extern const suit_manifest_handler_t suit_envelope_handlers[]; /** - * @brief length of the SUIT container handlers + * @brief length of the SUIT envelope handlers */ -extern const size_t suit_container_handlers_len; +extern const size_t suit_envelope_handlers_len; /** * @brief SUIT common handlers reference @@ -193,6 +193,36 @@ int suit_handle_manifest_structure_bstr(suit_manifest_t *manifest, const suit_manifest_handler_t *handlers, size_t handlers_len); +/** + * @brief Create an internal @ref suit_param_ref_t from a NanoCBOR value + * reference. + * + * The resulting @p ref only contains a 16 bit offset to the location of the + * NanoCBOR value, saving some RAM for each stored reference. + * + * @param manifest SUIT manifest context + * @param ref reference to store + * @param val NanoCBOR value to convert to a reference + * + * @returns The offset of the nanocbor value inside the manifest + * bytestring + */ +uint16_t suit_param_ref_to_cbor(suit_manifest_t *manifest, + suit_param_ref_t *ref, + nanocbor_value_t *val); + +/** + * @brief Create a NanoCBOR value reference from an internal + * @ref suit_param_ref_t. + * + * @param manifest SUIT manifest context + * @param ref reference to parse + * @param val NanoCBOR value to restore + */ +void suit_param_cbor_to_ref(suit_manifest_t *manifest, + suit_param_ref_t *ref, + nanocbor_value_t *val); + #ifdef __cplusplus } #endif diff --git a/sys/suit/handlers.c b/sys/suit/handlers.c index 5d98c4ce3891..e83782c05d44 100644 --- a/sys/suit/handlers.c +++ b/sys/suit/handlers.c @@ -20,6 +20,7 @@ #include #include +#include #include "suit/handlers.h" #include "suit.h" @@ -36,6 +37,24 @@ static suit_manifest_handler_t _get_handler(int key, return map[key]; } +uint16_t suit_param_ref_to_cbor(suit_manifest_t *manifest, + suit_param_ref_t *ref, + nanocbor_value_t *val) +{ + size_t len = manifest->len - ref->offset; + const uint8_t *start = manifest->buf + ref->offset; + nanocbor_decoder_init(val, start, len); + return ref->offset; +} + +void suit_param_cbor_to_ref(suit_manifest_t *manifest, + suit_param_ref_t *ref, + nanocbor_value_t *val) +{ + assert(val->cur >= manifest->buf); + ref->offset = val->cur - manifest->buf; +} + int suit_handle_manifest_structure(suit_manifest_t *manifest, nanocbor_value_t *it, const suit_manifest_handler_t *handlers, diff --git a/sys/suit/handlers_command_seq.c b/sys/suit/handlers_command_seq.c index 3a5bb3cc4346..3883460f7999 100644 --- a/sys/suit/handlers_command_seq.c +++ b/sys/suit/handlers_command_seq.c @@ -24,6 +24,7 @@ #include #include +#include #include "kernel_defines.h" #include "suit/conditions.h" @@ -39,22 +40,33 @@ #include "log.h" +static suit_component_t *_get_component(suit_manifest_t *manifest) +{ + /* Out-of-bounds check has been done in the _dtv_set_comp_idx, True/False + * not handled here intentionally */ + assert(manifest->component_current < CONFIG_SUIT_COMPONENT_MAX); + return &manifest->components[manifest->component_current]; +} + static int _validate_uuid(suit_manifest_t *manifest, - nanocbor_value_t *it, + suit_param_ref_t *ref, uuid_t *uuid) { - (void)manifest; const uint8_t *uuid_manifest_ptr; size_t len = sizeof(uuid_t); - char uuid_str[UUID_STR_LEN + 1]; - char uuid_str2[UUID_STR_LEN + 1]; - if (nanocbor_get_bstr(it, &uuid_manifest_ptr, &len) < 0) { + nanocbor_value_t it; + + if ((suit_param_ref_to_cbor(manifest, ref, &it) == 0) || + (nanocbor_get_bstr(&it, &uuid_manifest_ptr, &len) < 0)) { return SUIT_ERR_INVALID_MANIFEST; } + char uuid_str[UUID_STR_LEN + 1]; + char uuid_str2[UUID_STR_LEN + 1]; uuid_to_string((uuid_t *)uuid_manifest_ptr, uuid_str); uuid_to_string(uuid, uuid_str2); LOG_INFO("Comparing %s to %s from manifest\n", uuid_str2, uuid_str); + return uuid_equal(uuid, (uuid_t *)uuid_manifest_ptr) ? SUIT_OK : SUIT_ERR_COND; @@ -65,8 +77,11 @@ static int _cond_vendor_handler(suit_manifest_t *manifest, nanocbor_value_t *it) { (void)key; + (void)it; LOG_INFO("validating vendor ID\n"); - int rc = _validate_uuid(manifest, it, suit_get_vendor_id()); + suit_component_t *comp = _get_component(manifest); + int rc = _validate_uuid(manifest, &comp->param_vendor_id, + suit_get_vendor_id()); if (rc == SUIT_OK) { LOG_INFO("validating vendor ID: OK\n"); manifest->validated |= SUIT_VALIDATED_VENDOR; @@ -79,8 +94,11 @@ static int _cond_class_handler(suit_manifest_t *manifest, nanocbor_value_t *it) { (void)key; + (void)it; LOG_INFO("validating class id\n"); - int rc = _validate_uuid(manifest, it, suit_get_class_id()); + suit_component_t *comp = _get_component(manifest); + int rc = _validate_uuid(manifest, &comp->param_class_id, + suit_get_class_id()); if (rc == SUIT_OK) { LOG_INFO("validating class id: OK\n"); manifest->validated |= SUIT_VALIDATED_CLASS; @@ -95,18 +113,24 @@ static int _cond_comp_offset(suit_manifest_t *manifest, (void)manifest; (void)key; uint32_t offset; + uint32_t report; - int rc = nanocbor_get_uint32(it, &offset); - if (rc < 0) { - LOG_WARNING("_cond_comp_offset(): expected int, got rc=%i type=%i\n", - rc, nanocbor_get_type(it)); + suit_component_t *comp = _get_component(manifest); + + /* Grab offset from param */ + if (nanocbor_get_uint32(it, &report) < 0) { + LOG_WARNING("_cond_comp_offset(): expected None param\n"); return SUIT_ERR_INVALID_MANIFEST; } + nanocbor_value_t param_offset; + suit_param_ref_to_cbor(manifest, &comp->param_component_offset, + ¶m_offset); + nanocbor_get_uint32(¶m_offset, &offset); uint32_t other_offset = (uint32_t)riotboot_slot_offset( riotboot_slot_other()); - LOG_INFO("Comparing manifest offset %u with other slot offset %u\n", - (unsigned)offset, (unsigned)other_offset); + LOG_INFO("Comparing manifest offset %"PRIx32" with other slot offset %"PRIx32"\n", + offset, other_offset); return other_offset == offset ? SUIT_OK : SUIT_ERR_COND; } @@ -115,17 +139,26 @@ static int _dtv_set_comp_idx(suit_manifest_t *manifest, nanocbor_value_t *it) { (void)key; - if (nanocbor_get_type(it) == NANOCBOR_TYPE_FLOAT) { - LOG_DEBUG("_dtv_set_comp_idx() ignoring boolean and floats\n)"); - nanocbor_skip(it); + bool index = false; + uint32_t new_index; + + /* It can be a bool, meaning all or none of the components */ + if (nanocbor_get_bool(it, &index) >= 0) { + new_index = index ? + SUIT_MANIFEST_COMPONENT_ALL : SUIT_MANIFEST_COMPONENT_NONE; } - else if (nanocbor_get_uint32(it, &manifest->component_current) < 0) { + /* It can be a positive integer, meaning one of the components */ + else if (nanocbor_get_uint32(it, &new_index) < 0) { return SUIT_ERR_INVALID_MANIFEST; } - if (manifest->component_current >= SUIT_COMPONENT_MAX) { + /* And if it is an integer it must be within the allowed bounds */ + else if (new_index >= CONFIG_SUIT_COMPONENT_MAX) { return SUIT_ERR_INVALID_MANIFEST; } - LOG_DEBUG("Setting component index to %d\n", + + /* Update the manifest context */ + manifest->component_current = new_index; + LOG_INFO("Setting component index to %d\n", (int)manifest->component_current); return 0; } @@ -158,9 +191,9 @@ static int _dtv_try_each(suit_manifest_t *manifest, nanocbor_value_t _container = container; /* `_container` should be CBOR _bstr wrapped according to the spec, but * it is not */ - res = suit_handle_manifest_structure(manifest, &_container, - suit_command_sequence_handlers, - suit_command_sequence_handlers_len); + res = suit_handle_manifest_structure_bstr(manifest, &_container, + suit_command_sequence_handlers, + suit_command_sequence_handlers_len); nanocbor_skip(&container); @@ -172,32 +205,6 @@ static int _dtv_try_each(suit_manifest_t *manifest, return res; } -static int _param_get_uri_list(suit_manifest_t *manifest, - nanocbor_value_t *it) -{ - LOG_DEBUG("got url list\n"); - manifest->components[manifest->component_current].url = *it; - return 0; -} -static int _param_get_digest(suit_manifest_t *manifest, nanocbor_value_t *it) -{ - LOG_DEBUG("got digest\n"); - manifest->components[manifest->component_current].digest = *it; - return 0; -} - -static int _param_get_img_size(suit_manifest_t *manifest, - nanocbor_value_t *it) -{ - int res = nanocbor_get_uint32(it, &manifest->components[0].size); - - if (res < 0) { - LOG_DEBUG("error getting image size\n"); - return res; - } - return res; -} - static int _dtv_set_param(suit_manifest_t *manifest, int key, nanocbor_value_t *it) { @@ -207,34 +214,50 @@ static int _dtv_set_param(suit_manifest_t *manifest, int key, nanocbor_enter_map(it, &map); + suit_component_t *comp = _get_component(manifest); + while (!nanocbor_at_end(&map)) { /* map points to the key of the param */ int32_t param_key; - nanocbor_get_int32(&map, ¶m_key); - LOG_DEBUG("Current component index: %" PRIi32 "\n", - manifest->component_current); + if (nanocbor_get_int32(&map, ¶m_key) < 0) { + return SUIT_ERR_INVALID_MANIFEST; + } LOG_DEBUG("param_key=%" PRIi32 "\n", param_key); - int res; + unsigned int type = nanocbor_get_type(&map); + /* Filter 'complex' types and only allow int, nint, bstr and tstr types + * for parameter values */ + if (type > NANOCBOR_TYPE_TSTR) { + return SUIT_ERR_INVALID_MANIFEST; + } + suit_param_ref_t *ref; switch (param_key) { - case 6: /* SUIT URI LIST */ - res = _param_get_uri_list(manifest, &map); + case SUIT_PARAMETER_VENDOR_IDENTIFIER: + ref = &comp->param_vendor_id; break; - case 11: /* SUIT DIGEST */ - res = _param_get_digest(manifest, &map); + case SUIT_PARAMETER_CLASS_IDENTIFIER: + ref = &comp->param_class_id; break; - case 12: /* SUIT IMAGE SIZE */ - res = _param_get_img_size(manifest, &map); + case SUIT_PARAMETER_IMAGE_DIGEST: + ref = &comp->param_digest; + break; + case SUIT_PARAMETER_COMPONENT_OFFSET: + ref = &comp->param_component_offset; + break; + case SUIT_PARAMETER_IMAGE_SIZE: + ref = &comp->param_size; + break; + case SUIT_PARAMETER_URI: + ref = &comp->param_uri; break; default: LOG_DEBUG("Unsupported parameter %" PRIi32 "\n", param_key); - res = SUIT_ERR_UNSUPPORTED; + return SUIT_ERR_UNSUPPORTED; } - nanocbor_skip(&map); + suit_param_cbor_to_ref(manifest, ref, &map); - if (res) { - return res; - } + /* Simple skip is sufficient to skip non-complex types */ + nanocbor_skip(&map); } return SUIT_OK; } @@ -247,8 +270,12 @@ static int _dtv_fetch(suit_manifest_t *manifest, int key, const uint8_t *url; size_t url_len; + suit_component_t *comp = _get_component(manifest); - int err = nanocbor_get_tstr(&manifest->components[0].url, &url, &url_len); + nanocbor_value_t param_uri; + suit_param_ref_to_cbor(manifest, &comp->param_uri, + ¶m_uri); + int err = nanocbor_get_tstr(¶m_uri, &url, &url_len); if (err < 0) { LOG_DEBUG("URL parsing failed\n)"); return err; @@ -293,6 +320,25 @@ static int _dtv_fetch(suit_manifest_t *manifest, int key, return SUIT_OK; } +static int _get_digest(nanocbor_value_t *bstr, const uint8_t **digest, size_t *digest_len) +{ + /* Bstr is a byte string with a cbor array containing the type and the + * digest */ + + const uint8_t *digest_struct; + size_t digest_struct_len; + uint32_t digest_type; + nanocbor_value_t digest_it; + nanocbor_value_t arr_it; + + nanocbor_get_bstr(bstr, &digest_struct, &digest_struct_len); + + nanocbor_decoder_init(&digest_it, digest_struct, digest_struct_len); + nanocbor_enter_array(&digest_it, &arr_it); + nanocbor_get_uint32(&arr_it, &digest_type); + return nanocbor_get_bstr(&arr_it, digest, digest_len); +} + static int _dtv_verify_image_match(suit_manifest_t *manifest, int key, nanocbor_value_t *_it) { @@ -301,21 +347,30 @@ static int _dtv_verify_image_match(suit_manifest_t *manifest, int key, const uint8_t *digest; size_t digest_len; int target_slot = riotboot_slot_other(); + suit_component_t *comp = _get_component(manifest); + + uint32_t img_size; + nanocbor_value_t param_size; + if ((suit_param_ref_to_cbor(manifest, &comp->param_size, ¶m_size) == 0) || + (nanocbor_get_uint32(¶m_size, &img_size) < 0)) { + return SUIT_ERR_INVALID_MANIFEST; + } LOG_INFO("Verifying image digest\n"); - nanocbor_value_t _v = manifest->components[0].digest; - int res = nanocbor_get_subcbor(&_v, &digest, &digest_len); + nanocbor_value_t _v; + if (suit_param_ref_to_cbor(manifest, &comp->param_digest, &_v) == 0) { + return SUIT_ERR_INVALID_MANIFEST; + } + + int res = _get_digest(&_v, &digest, &digest_len); + if (res < 0) { LOG_DEBUG("Unable to parse digest structure\n"); return SUIT_ERR_INVALID_MANIFEST; } - /* "digest" points to a 36 byte string that includes the digest type. - * riotboot_flashwrite_verify_sha256() is only interested in the 32b digest, - * so shift the pointer accordingly. - */ - res = riotboot_flashwrite_verify_sha256(digest + 4, - manifest->components[0].size, + res = riotboot_flashwrite_verify_sha256(digest, + img_size, target_slot); if (res != 0) { return SUIT_ERR_COND; diff --git a/sys/suit/handlers_common.c b/sys/suit/handlers_common.c index 6bfac92bfbe9..06ee864d98da 100644 --- a/sys/suit/handlers_common.c +++ b/sys/suit/handlers_common.c @@ -35,11 +35,6 @@ static int _component_handler(suit_manifest_t *manifest, int key, { (void)manifest; (void)key; - const uint8_t *subcbor; - size_t sub_size; - nanocbor_value_t _it; - nanocbor_get_bstr(it, &subcbor, &sub_size); - nanocbor_decoder_init(&_it, subcbor, sub_size); /* This is a list of lists, something like: * [ @@ -48,18 +43,26 @@ static int _component_handler(suit_manifest_t *manifest, int key, * ] * */ nanocbor_value_t arr; - if (nanocbor_enter_array(&_it, &arr) < 0) { + if (nanocbor_enter_array(it, &arr) < 0) { LOG_DEBUG("components field not an array %d\n", nanocbor_get_type(it)); return SUIT_ERR_INVALID_MANIFEST; } unsigned n = 0; while (!nanocbor_at_end(&arr)) { + if (n >= CONFIG_SUIT_COMPONENT_MAX) { + LOG_INFO("Too many components found: %u, Supported: %u\n", + n, CONFIG_SUIT_COMPONENT_MAX); + return SUIT_ERR_UNSUPPORTED; + } + suit_component_t *component = &manifest->components[n]; nanocbor_value_t comp; if (nanocbor_enter_array(&arr, &comp) < 0) { LOG_DEBUG("component elements field not an array %d\n", nanocbor_get_type(it)); return SUIT_ERR_INVALID_MANIFEST; } + suit_param_cbor_to_ref(manifest, &component->identifier, &comp); + /* Ensure that all parts of the identifier are a bstr */ while (!nanocbor_at_end(&comp)) { const uint8_t *identifier; size_t id_len; @@ -71,11 +74,10 @@ static int _component_handler(suit_manifest_t *manifest, int key, nanocbor_leave_container(&arr, &comp); n++; } - if (n > 1) { - LOG_INFO("More than 1 component found, exiting\n"); - return SUIT_ERR_UNSUPPORTED; + manifest->components_len = n; + if (n) { + manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; } - manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; return 0; } diff --git a/sys/suit/handlers_container.c b/sys/suit/handlers_envelope.c similarity index 60% rename from sys/suit/handlers_container.c rename to sys/suit/handlers_envelope.c index a3beba5dd7dc..0b955d73a535 100644 --- a/sys/suit/handlers_container.c +++ b/sys/suit/handlers_envelope.c @@ -38,18 +38,23 @@ static int _auth_handler(suit_manifest_t *manifest, int key, (void)key; cose_sign_dec_t verify; const uint8_t *cose_buf; - const uint8_t *cose_container; - size_t container_len; + const uint8_t *auth_container; + size_t auth_container_len; size_t cose_len = 0; /* It is a list of cose signatures */ - int res = nanocbor_get_bstr(it, &cose_container, &container_len); - if (res < 0) { - LOG_INFO("Unable to get COSE signature\n"); + if (nanocbor_get_bstr(it, &auth_container, &auth_container_len) < 0) { + LOG_INFO("Unable to get auth container\n"); return SUIT_ERR_INVALID_MANIFEST; } + /* Initialize key from hardcoded public key */ + cose_key_t pkey; + cose_key_init(&pkey); + cose_key_set_keys(&pkey, COSE_EC_CURVE_ED25519, COSE_ALGO_EDDSA, + (uint8_t *)public_key, NULL, NULL); + nanocbor_value_t _cont, arr; - nanocbor_decoder_init(&_cont, cose_container, container_len); + nanocbor_decoder_init(&_cont, auth_container, auth_container_len); int rc = nanocbor_enter_array(&_cont, &arr); if (rc < 0) { @@ -57,49 +62,46 @@ static int _auth_handler(suit_manifest_t *manifest, int key, return SUIT_ERR_INVALID_MANIFEST; } - uint32_t tag; - nanocbor_get_tag(&arr, &tag); - arr.remaining++; - res = nanocbor_get_subcbor(&arr, &cose_buf, &cose_len); - if (res < 0) { - LOG_INFO("Unable to get subcbor: %d\n", res); - } - - res = cose_sign_decode(&verify, cose_buf, cose_len); - if (res < 0) { - LOG_INFO("Unable to parse COSE signature\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - - /* Iterate over signatures, should only be a single signature */ - cose_signature_dec_t signature; - - cose_sign_signature_iter_init(&signature); - if (!cose_sign_signature_iter(&verify, &signature)) { - LOG_INFO("Unable to get signature iteration\n"); - return SUIT_ERR_INVALID_MANIFEST; - } - - /* Initialize key from hardcoded public key */ - cose_key_t pkey; - cose_key_init(&pkey); - cose_key_set_keys(&pkey, COSE_EC_CURVE_ED25519, COSE_ALGO_EDDSA, - (uint8_t *)public_key, NULL, NULL); - - LOG_INFO("suit: verifying manifest signature\n"); - int verification = cose_sign_verify(&verify, &signature, - &pkey, manifest->validation_buf, - SUIT_COSE_BUF_SIZE); - if (verification != 0) { - LOG_INFO("Unable to validate signature: %d\n", verification); - return SUIT_ERR_SIGNATURE; + int res = SUIT_ERR_SIGNATURE; + + while (!nanocbor_at_end(&arr)) { + res = nanocbor_get_bstr(&arr, &cose_buf, &cose_len); + if (res < 0) { + LOG_INFO("Unable to get COSE bstr: %d\n", res); + return SUIT_ERR_INVALID_MANIFEST; + } + if (!(manifest->state & SUIT_STATE_COSE_AUTHENTICATED)) { + res = cose_sign_decode(&verify, cose_buf, cose_len); + if (res < 0) { + LOG_INFO("Unable to parse COSE signature\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + /* Iterate over signatures, should only be a single signature */ + cose_signature_dec_t signature; + + cose_sign_signature_iter_init(&signature); + if (!cose_sign_signature_iter(&verify, &signature)) { + LOG_INFO("Unable to get signature iteration\n"); + return SUIT_ERR_INVALID_MANIFEST; + } + LOG_INFO("suit: verifying manifest signature\n"); + int verification = cose_sign_verify(&verify, &signature, + &pkey, manifest->validation_buf, + SUIT_COSE_BUF_SIZE); + if (verification == 0) { + manifest->state |= SUIT_STATE_COSE_AUTHENTICATED; + res = SUIT_OK; + manifest->cose_payload = verify.payload; + manifest->cose_payload_len = verify.payload_len; + } + else { + LOG_INFO("Unable to validate signature: %d\n", verification); + } + } } - manifest->cose_payload = verify.payload; - manifest->cose_payload_len = verify.payload_len; - manifest->state |= SUIT_STATE_COSE_AUTHENTICATED; - return 0; + return res; } static int _manifest_handler(suit_manifest_t *manifest, int key, @@ -142,10 +144,10 @@ static int _manifest_handler(suit_manifest_t *manifest, int key, } /* begin{code-style-ignore} */ -const suit_manifest_handler_t suit_container_handlers[] = { +const suit_manifest_handler_t suit_envelope_handlers[] = { [SUIT_WRAPPER_AUTHENTICATION] = _auth_handler, [SUIT_WRAPPER_MANIFEST] = _manifest_handler, }; /* end{code-style-ignore} */ -const size_t suit_container_handlers_len = ARRAY_SIZE(suit_container_handlers); +const size_t suit_envelope_handlers_len = ARRAY_SIZE(suit_envelope_handlers); diff --git a/sys/suit/suit.c b/sys/suit/suit.c index 7ca96bc33338..147398204947 100644 --- a/sys/suit/suit.c +++ b/sys/suit/suit.c @@ -42,8 +42,8 @@ int suit_parse(suit_manifest_t *manifest, const uint8_t *buf, manifest->buf = buf; manifest->len = len; nanocbor_decoder_init(&it, buf, len); - LOG_DEBUG("Starting container sequence handler\n"); + LOG_DEBUG("Starting envelope sequence handler\n"); return suit_handle_manifest_structure(manifest, &it, - suit_container_handlers, - suit_container_handlers_len); + suit_envelope_handlers, + suit_envelope_handlers_len); } diff --git a/sys/suit/transport/coap.c b/sys/suit/transport/coap.c index 93bb4bee7348..6c63841e5e75 100644 --- a/sys/suit/transport/coap.c +++ b/sys/suit/transport/coap.c @@ -42,6 +42,7 @@ #ifdef MODULE_SUIT #include "suit.h" +#include "suit/handlers.h" #endif #if defined(MODULE_PROGRESS_BAR) @@ -75,14 +76,26 @@ static char _url[SUIT_URL_MAX]; static uint8_t _manifest_buf[SUIT_MANIFEST_BUFSIZE]; #ifdef MODULE_SUIT -static inline void _print_download_progress(size_t offset, size_t len, - uint32_t image_size) +static inline void _print_download_progress(suit_manifest_t *manifest, + size_t offset, size_t len) { + (void)manifest; (void)offset; (void)len; - (void)image_size; DEBUG("_suit_flashwrite(): writing %u bytes at pos %u\n", len, offset); #if defined(MODULE_PROGRESS_BAR) + uint32_t image_size; + nanocbor_value_t param_size; + suit_param_ref_t *ref_size = + &manifest->components[manifest->component_current].param_size; + + /* Grab the total image size from the manifest */ + if ((suit_param_ref_to_cbor(manifest, ref_size, ¶m_size) == 0) || + (nanocbor_get_uint32(¶m_size, &image_size) < 0)) { + /* Early exit if the total image size can't be determined */ + return; + } + if (image_size != 0) { char _suffix[7] = { 0 }; uint8_t _progress = 100 * (offset + len) / image_size; @@ -419,7 +432,7 @@ int suit_flashwrite_helper(void *arg, size_t offset, uint8_t *buf, size_t len, return -1; } - _print_download_progress(offset, len, manifest->components[0].size); + _print_download_progress(manifest, offset, len); return riotboot_flashwrite_putbytes(writer, buf, len, more); } diff --git a/tests/suit_manifest/Makefile b/tests/suit_manifest/Makefile index c4daddf12a94..d85237ab2d0d 100644 --- a/tests/suit_manifest/Makefile +++ b/tests/suit_manifest/Makefile @@ -17,9 +17,12 @@ BLOBS += $(MANIFEST_DIR)/manifest0.bin BLOBS += $(MANIFEST_DIR)/manifest1.bin BLOBS += $(MANIFEST_DIR)/manifest2.bin BLOBS += $(MANIFEST_DIR)/manifest3.bin +BLOBS += $(MANIFEST_DIR)/manifest4.bin USEMODULE += suit_transport_mock +CFLAGS += -DCONFIG_SUIT_COMPONENT_MAX=2 + # Use a version of 'native' that includes flash page support ifeq (native, $(BOARD)) EXTERNAL_BOARD_DIRS = $(CURDIR)/native_flashpage diff --git a/tests/suit_manifest/create_test_data.sh b/tests/suit_manifest/create_test_data.sh index 79a3af71f560..1cc9c980744c 100644 --- a/tests/suit_manifest/create_test_data.sh +++ b/tests/suit_manifest/create_test_data.sh @@ -9,13 +9,13 @@ gen_manifest() { shift - "${RIOTBASE}/dist/tools/suit_v3/gen_manifest.py" \ + "${RIOTBASE}/dist/tools/suit/gen_manifest.py" \ --urlroot "test://test" \ --seqnr "$seqnr" \ --uuid-vendor "riot-os.org" \ --uuid-class "${BOARD}" \ -o "$out.tmp" \ - "${1}:$((0x1000))" "${2}:$((0x2000))" + "${@}" ${SUIT_TOOL} create -f suit -i "$out.tmp" -o "$out" @@ -34,14 +34,18 @@ echo foo > "${MANIFEST_DIR}/file1.bin" echo bar > "${MANIFEST_DIR}/file2.bin" # random valid cbor (manifest but not signed, missing cose auth) -gen_manifest "${MANIFEST_DIR}/manifest0.bin" 1 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin" +gen_manifest "${MANIFEST_DIR}/manifest0.bin" 1 "${MANIFEST_DIR}/file1.bin:$((0x1000))" "${MANIFEST_DIR}/file2.bin:$((0x2000))" # manifest with invalid seqnr sign_manifest "${MANIFEST_DIR}/manifest0.bin" "${MANIFEST_DIR}/manifest1.bin" -(BOARD=invalid gen_manifest "${MANIFEST_DIR}/manifest2.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin") +(BOARD=invalid gen_manifest "${MANIFEST_DIR}/manifest2.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin:$((0x1000))" "${MANIFEST_DIR}/file2.bin:$((0x2000))") sign_manifest "${MANIFEST_DIR}/manifest2.bin".unsigned "${MANIFEST_DIR}/manifest2.bin" # valid manifest, valid seqnr, signed -gen_manifest "${MANIFEST_DIR}/manifest3.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin" +gen_manifest "${MANIFEST_DIR}/manifest3.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin:$((0x1000))" "${MANIFEST_DIR}/file2.bin:$((0x2000))" sign_manifest "${MANIFEST_DIR}/manifest3.bin".unsigned "${MANIFEST_DIR}/manifest3.bin" + +# valid manifest, valid seqnr, signed, 2 components +gen_manifest "${MANIFEST_DIR}/manifest4.bin".unsigned 2 "${MANIFEST_DIR}/file1.bin" "${MANIFEST_DIR}/file2.bin" +sign_manifest "${MANIFEST_DIR}/manifest4.bin".unsigned "${MANIFEST_DIR}/manifest4.bin" diff --git a/tests/suit_manifest/main.c b/tests/suit_manifest/main.c index dfb89c1fd1f3..4103589eb745 100644 --- a/tests/suit_manifest/main.c +++ b/tests/suit_manifest/main.c @@ -32,6 +32,7 @@ #include TEST_MANIFEST_INCLUDE(manifest1.bin.h) #include TEST_MANIFEST_INCLUDE(manifest2.bin.h) #include TEST_MANIFEST_INCLUDE(manifest3.bin.h) +#include TEST_MANIFEST_INCLUDE(manifest4.bin.h) #define SUIT_URL_MAX 128 @@ -47,6 +48,7 @@ const manifest_blob_t manifest_blobs[] = { { manifest1_bin, sizeof(manifest1_bin), SUIT_ERR_SEQUENCE_NUMBER }, { manifest2_bin, sizeof(manifest2_bin), SUIT_ERR_COND }, { manifest3_bin, sizeof(manifest3_bin), SUIT_OK }, + { manifest4_bin, sizeof(manifest4_bin), SUIT_OK }, }; const unsigned manifest_blobs_numof = ARRAY_SIZE(manifest_blobs);