diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index b3da4ec922ec7c19406992fcd55043b7f3bbcd6d..43eeb54092e9f3406f6ff8b52b24a1143855a24f --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ *__pycache__ *.pyc *.DS_Store +.vscode *.hex examples/*.cbor examples/*.json.txt -examples/*.suit \ No newline at end of file +examples/*.suit +build/* diff --git a/cddl/.gitignore b/cddl/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5be6db3bbbd6308bf83afb1d792a86ee5ce723c1 --- /dev/null +++ b/cddl/.gitignore @@ -0,0 +1,2 @@ +*.cddl +*.xml \ No newline at end of file diff --git a/cddl/cose_cddl.py b/cddl/cose_cddl.py new file mode 100755 index 0000000000000000000000000000000000000000..f49acded62cfc7545a5abe7499155c46f6477273 --- /dev/null +++ b/cddl/cose_cddl.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2022 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 xml.etree.ElementTree as ET +import requests + +cose_source = 'https://www.ietf.org/archive/id/draft-ietf-cose-msg-24.xml' +cose_filename = 'cose.xml' +cose_cddl_filename = 'cose.cddl' + +with open(cose_filename, 'w') as fd: + r = requests.get(cose_source) + r.raise_for_status() + fd.write(r.text) + +# Load cose xml +xml_cose = ET.parse(cose_filename) +cddl_elements = xml_cose.findall("//artwork[@type='CDDL']") + +cddl = '\n'.join([e.text for e in cddl_elements]) + +with open(cose_cddl_filename, 'w') as fd: + fd.write(cddl) diff --git a/cddl/suit-complete.py b/cddl/suit-complete.py new file mode 100755 index 0000000000000000000000000000000000000000..f92d34980868cd3a5cc7f9696571a1a019c04078 --- /dev/null +++ b/cddl/suit-complete.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2022 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 cose_cddl +import suit_cddl + +suit_complete_cddl = 'suit-complete.cddl' + +with open(suit_complete_cddl, 'w') as sc_fd: + with open(suit_cddl.suit_cddl_filename) as s_fd: + sc_fd.write(s_fd.read()) + with open(cose_cddl.cose_cddl_filename) as c_fd: + sc_fd.write(c_fd.read()) diff --git a/cddl/suit_cddl.py b/cddl/suit_cddl.py new file mode 100755 index 0000000000000000000000000000000000000000..90b8b016c59d08658619421f559c66851db2da50 --- /dev/null +++ b/cddl/suit_cddl.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2022 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 requests + +suit_source = 'https://raw.githubusercontent.com/suit-wg/manifest-spec/master/draft-ietf-suit-manifest.cddl' +suit_cddl_filename = 'suit.cddl' + +with open(suit_cddl_filename, 'w') as fd: + r = requests.get(suit_source) + r.raise_for_status() + fd.write(r.text) diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..18690f6f770d3f9388e171ce949643db0e842c4c --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,3 @@ +*.cbor +*.json.txt +*.suit diff --git a/examples/example8.json b/examples/example8.json new file mode 100644 index 0000000000000000000000000000000000000000..85b79565f9482d3d5ff2647dcaddd934c39f59bb --- /dev/null +++ b/examples/example8.json @@ -0,0 +1,19 @@ +{ + "components" : [ + { + "install-id" : ["00"], + "bootable" : true, + "install-digest": { + "algorithm-id": "sha256", + "digest-bytes": "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210" + }, + "vendor-id" : "fa6b4a53-d5ad-5fdf-be9d-e663e4d41ffe", + "class-id" : "1492af14-2569-5e48-bf42-9b2d51f2ab45", + "install-size" : 34768 + } + ], + "manifest-version": 1, + "manifest-sequence-number": 0, + "manifest-component-id" : ["manifest"], + "reference-uri" : "http://suit.example/cid-example.suit" +} diff --git a/examples/make-examples.sh b/examples/make-examples.sh old mode 100644 new mode 100755 index d01161a2f70aad58d529a57e377e33c4ff70e98b..1161aad3c55126389c53b5bcacbf8e4f45087186 --- a/examples/make-examples.sh +++ b/examples/make-examples.sh @@ -3,10 +3,13 @@ set -e set -x STOOL="python3 ../bin/suit-tool" SRCS=`ls *.json` +CDDLTOOL=cddl rm -f examples.txt for SRC in $SRCS ; do $STOOL create -i $SRC -o $SRC.suit + $CDDLTOOL ../cddl/suit-complete.cddl validate $SRC.suit $STOOL sign -m $SRC.suit -k ../private_key.pem -o signed-$SRC.suit + $CDDLTOOL ../cddl/suit-complete.cddl validate signed-$SRC.suit $STOOL parse -m signed-$SRC.suit > signed-$SRC.txt rm -f $SRC.txt @@ -19,6 +22,8 @@ for SRC in $SRCS ; do if python3 -c 'import json, sys; sys.exit(0 if json.load(open(sys.argv[1])).get("severable") else 1)' $SRC ; then $STOOL sever -a -m $SRC.suit -o severed-$SRC.suit + $CDDLTOOL ../cddl/suit-complete.cddl validate severed-$SRC.suit + echo "Total size of the Envelope without COSE authentication object or Severable Elements: " `stat -f "%z" severed-$SRC.suit` >> $SRC.txt echo "" >> $SRC.txt echo "Envelope:">> $SRC.txt diff --git a/suit_tool/__init__.py b/suit_tool/__init__.py old mode 100644 new mode 100755 index dd088ece5db2c04a75aa86240614678ae4ea40cf..f1533d9c1f5c7c5bd3519aaa57c6b4cc3483e13b --- a/suit_tool/__init__.py +++ b/suit_tool/__init__.py @@ -16,4 +16,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = '0.0.2' +__version__ = '0.19.2' diff --git a/suit_tool/argparser.py b/suit_tool/argparser.py index e6e9911a23d6bd5acc93e8e6fe7c90d813721358..a28cf4335d3c6cad36fb5c3a04852676dbde35b5 100644 --- a/suit_tool/argparser.py +++ b/suit_tool/argparser.py @@ -92,6 +92,17 @@ class MainArgumentParser(object): sever_parser.add_argument('-e', '--element', action='append', type=str, dest='elements', default=[]) sever_parser.add_argument('-a', '--all', action='store_true', default=False) + tag_parser = subparsers.add_parser('tag', help='operate on the manifest tag') + tag_sub_parsers = tag_parser.add_subparsers(dest="tag_action") + + tag_remove_parser = tag_sub_parsers.add_parser('rm', help='Remove the SUIT Envelope tag if present') + tag_remove_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) + tag_remove_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) + + tag_add_parser = tag_sub_parsers.add_parser('add', help='Add the SUIT Envelope tag if not present') + tag_add_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True) + tag_add_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True) + return parser diff --git a/suit_tool/clidriver.py b/suit_tool/clidriver.py index 6c0149cc7ec470b744c6a7c2eb42951a3d1e910d..b07769bc29329e1c118a3ce69372f202933eae58 100644 --- a/suit_tool/clidriver.py +++ b/suit_tool/clidriver.py @@ -20,7 +20,7 @@ import logging, sys from suit_tool.argparser import MainArgumentParser -from suit_tool import create, sign, parse, get_pubkey, keygen, sever #, verify, cert, init +from suit_tool import create, sign, parse, get_pubkey, keygen, sever, tag #, verify, cert, init # from suit_tool import update import colorama colorama.init() @@ -65,6 +65,7 @@ class CLIDriver(object): "sign": sign.main, "keygen": keygen.main, "sever" : sever.main, + "tag" : tag.main, }[self.options.action](self.options) or 0 sys.exit(rc) diff --git a/suit_tool/compile.py b/suit_tool/compile.py old mode 100644 new mode 100755 index 54458e408178ae4e3287e230354130da4756d3aa..a6f3b085cfa75bca08b590ef7aeac62fa0024501 --- a/suit_tool/compile.py +++ b/suit_tool/compile.py @@ -36,7 +36,7 @@ from cryptography.hazmat.primitives import hashes from suit_tool.manifest import SUITComponentId, SUITCommon, SUITSequence, \ suitCommonInfo, SUITCommand, SUITManifest, \ - SUITEnvelope, SUITTryEach, SUITBWrapField, SUITText, \ + SUITEnvelope, SUITTryEach, SUITBWrapField, \ SUITDigest, SUITDependencies, SUITDependency, SUITEnvelopeTagged \ import suit_tool.create @@ -396,7 +396,7 @@ def compile_manifest(options, m): # print('Common') common = SUITCommon().from_json({ 'components': [id.to_json() for id in ids.keys()], - 'common-sequence': CommonSeq.to_json(), + 'shared-sequence': CommonSeq.to_json(), }) if len(Dependencies.items): common.dependencies = Dependencies @@ -407,17 +407,27 @@ def compile_manifest(options, m): 'manifest-sequence-number' : m['manifest-sequence-number'], 'common' : common.to_json() } + if 'manifest-component-id' in m: + jmanifest['manifest-component-id'] = m['manifest-component-id'] + if 'reference-uri' in m: + jmanifest['reference-uri'] = m['reference-uri'] # for k,v in {'deres':DepSeq, 'fetch': FetchSeq, 'install':InstSeq, 'validate':ValidateSeq, 'run':RunSeq, 'load':LoadSeq}.items(): # # print('sequence:{}'.format(k)) # print(v.to_json()) + # Text has to be internationalized. The manifest defines multiple sections + # where each section has an internationalization string + # First get the map of descriptions at the top level. mtext = {} - for k in ['manifest-description', 'update-description']: - if k in m: - mtext[k] = m[k] + # Collect all the descriptions: + for lang, desc in m.get('description',{}).items(): + d = {k:desc[k] for k in ['manifest-description', 'update-description'] if k in desc} + if len(d): + mtext[lang] = d + + # Collect all the components with internationalization for c in m['components']: - ctext = {} cfields = [ 'vendor-name', 'model-name', @@ -427,13 +437,11 @@ def compile_manifest(options, m): '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 - + cid = SUITComponentId().from_json(c['install-id']).to_suit() + for lang, desc in c.get('description',{}).items(): + if not lang in mtext: + mtext[lang]={} + mtext[lang].update({cid: {k:desc[k] for k in cfields if k in desc}}) jmanifest.update({k:v for k,v in { 'dependency-resolution' : DepSeq.to_json(), 'payload-fetch' : FetchSeq.to_json(), diff --git a/suit_tool/manifest.py b/suit_tool/manifest.py old mode 100644 new mode 100755 index 1fe16b9e3eb60655dc925e5b1bfaed122cdc81f4..3e85589fe49c24d2bfdf276b5758618791b2ed9f --- a/suit_tool/manifest.py +++ b/suit_tool/manifest.py @@ -100,7 +100,6 @@ class SUITPosInt(SUITInt): def from_json(self, v): TreeBranch.append(type(self)) _v = int(v) - # print (_v) if _v < 0: raise SUITException( m = 'Positive Integers must be >= 0. Got {}.'.format(_v), @@ -164,15 +163,16 @@ class SUITManifestDict: sd[f.suit_key] = v.to_suit() return sd def to_debug(self, indent): - s = '{' newindent = indent + one_indent + rows = [] for k, f in self.fields.items(): v = getattr(self, k) if v: - s += '\n{ind}/ {jk} / {sk}:'.format(ind=newindent, jk=f.json_key, sk=f.suit_key) - s += v.to_debug(newindent) + ',' - s += '\n' + indent + '}' + rows.append('{ind}/ {jk} / {sk}:{v}'.format( + ind=newindent, jk=f.json_key, sk=f.suit_key, v=v.to_debug(newindent) + )) + s = '{\n' + ',\n'.join(rows) + '\n' + indent + '}' return s @@ -314,7 +314,7 @@ class SUITManifestArray: def to_debug(self, indent): newindent = indent + one_indent s = '[\n' - s += ' ,\n'.join([newindent + v.to_debug(newindent) for v in self.items]) + s += ',\n'.join([newindent + v.to_debug(newindent) for v in self.items]) s += '\n' + indent + ']' return s class SUITBytes: @@ -372,7 +372,7 @@ class SUITNil: raise Exception('Expected Nil') return self def to_debug(self, indent): - return 'F6 / nil /' + return 'null / nil /' class SUITTStr(SUITRaw): def from_json(self, d): @@ -710,7 +710,7 @@ class SUITCommon(SUITManifestDict): fields = SUITManifestNamedList.mkfields({ 'dependencies' : ('dependencies', 1, SUITDependencies), 'components' : ('components', 2, SUITComponents), - 'common_sequence' : ('common-sequence', 4, SUITBWrapField(SUITSequenceComponentReset)), + 'shared_sequence' : ('shared-sequence', 4, SUITBWrapField(SUITSequenceComponentReset)), }) class SUITComponentText(SUITManifestDict): @@ -731,7 +731,9 @@ class SUITText(SUITManifestDict): 'json' : ('json-source', 3, SUITTStr), 'yaml' : ('yaml-source', 4, SUITTStr), }) - components={} + def __init__(self): + super(SUITText,self).__init__() + self.components = {} def to_json(self): d = super(SUITText, self).to_json() @@ -757,32 +759,66 @@ class SUITText(SUITManifestDict): 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) + return super(SUITText, self).from_suit(data) def to_debug(self, indent): - s = '{' newindent = indent + one_indent - + rows = [] for k, f in self.fields.items(): v = getattr(self, k) if v: - s += '\n{ind}/ {jk} / {sk}:'.format(ind=newindent, jk=f.json_key, sk=f.suit_key) - s += v.to_debug(newindent) + ',' + s = '{ind}/ {jk} / {sk}:'.format(ind=newindent, jk=f.json_key, sk=f.suit_key) + s += v.to_debug(newindent) + rows.append(s) for k, f in self.components.items(): - s += '\n' + newindent + '{}:'.format(k.to_debug(newindent + one_indent)) + s = '\n' + newindent + '{}:'.format(k.to_debug(newindent + one_indent)) s += f.to_debug(newindent + one_indent) + rows.append(s) + s = '{\n' + ',\n'.join(rows) + '\n' + indent + '}' + return s +class SUITInternationalText(SUITManifestDict): + def __init__(self): + super(SUITInternationalText,self).__init__() + self.languages = {} + def to_json(self): + return { + lang : desc.to_json() for lang,desc in self.languages.items() + } + def from_json(self, data): + for lang,desc in data.items(): + self.languages[lang] = SUITText().from_json(desc) + return self + def to_suit(self): + return { + lang : desc.to_suit() for lang,desc in self.languages.items() + } + def from_suit(self, data): + print(f'{type(self)}.from_suit({data})') + for lang, desc in data.items(): + print(f'decoding from suit: {lang}:{desc} =>') + d = SUITText().from_suit(desc) + print(f'{d}') + self.languages[lang] = d + print(self.languages) + return self + def to_debug(self, indent): + s = '{' + newindent = indent + one_indent + for lang, desc in self.languages.items(): + s += f'\n{newindent}\'{lang}\' : {desc.to_debug(newindent+one_indent)},' + if len(self.languages): + s = s[:-1] 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), + 'component' : ('manifest-component-id', 5, SUITComponentId), 'validate' : ('validate', 7, SUITBWrapField(SUITSequenceComponentReset)), 'load' : ('load', 8, SUITBWrapField(SUITSequenceComponentReset)), @@ -790,9 +826,9 @@ class SUITManifest(SUITManifestDict): 'deres' : ('dependency-resolution', 15, SUITMakeSeverableField(SUITSequenceComponentReset)), 'fetch' : ('payload-fetch', 16, SUITMakeSeverableField(SUITSequenceComponentReset)), - 'install' : ('install', 17, SUITMakeSeverableField(SUITSequenceComponentReset)), + 'install' : ('install', 20, SUITMakeSeverableField(SUITSequenceComponentReset)), - 'text' : ('text', 23, SUITMakeSeverableField(SUITText)), + 'text' : ('text', 23, SUITMakeSeverableField(SUITInternationalText)), 'coswid' : ('coswid', 24, SUITBytes), }) @@ -893,10 +929,7 @@ class COSEList(SUITManifestArray): if len(self.items): s += ',\n' # show signatures - # s += indent1 + 'signatures: [\n' - indent2 = indent1 - s += ' ,\n'.join([indent2 + '/ signature: / ' + v.to_debug(indent2) for v in self.items]) - s += '\n' + indent1 + ']' + s += ' ,\n'.join([indent1 + '/ signature: / ' + v.to_debug(indent) for v in self.items]) s += '\n{}]'.format(indent) return s @@ -911,9 +944,9 @@ class SUITEnvelope(SUITManifestDict): 'deres': ('dependency-resolution', 15, SUITBWrapField(SUITSequence)), 'fetch': ('payload-fetch', 16, SUITBWrapField(SUITSequence)), - 'install': ('install', 17, SUITBWrapField(SUITSequence)), + 'install': ('install', 20, SUITBWrapField(SUITSequence)), - 'text': ('text', 23, SUITBWrapField(SUITText)), + 'text': ('text', 23, SUITBWrapField(SUITInternationalText)), 'coswid': ('coswid', 14, SUITBytes), }) severable_fields = {'deres', 'fetch', 'install', 'text', 'coswid'} diff --git a/suit_tool/tag.py b/suit_tool/tag.py new file mode 100644 index 0000000000000000000000000000000000000000..6b3c63958ddef2c8a7e9ee8a525de73416ca1ff9 --- /dev/null +++ b/suit_tool/tag.py @@ -0,0 +1,51 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright 2022 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 cbor2 as cbor +from suit_tool.manifest import SUITEnvelopeTagged +envelope_tag_value = SUITEnvelopeTagged.fields['suit_envelope'].suit_key + +def rm(options, envelope): + if isinstance(envelope,cbor.CBORTag) and envelope.tag == envelope_tag_value: + return envelope.value + return envelope + + +def add(options, envelope): + if isinstance(envelope,cbor.CBORTag) and envelope.tag == envelope_tag_value: + return envelope + return cbor.CBORTag(envelope_tag_value, envelope) + +def main(options): + # Read the manifest wrapper + envelope = cbor.loads(options.manifest.read()) + if not hasattr(options, 'tag_action'): + return 1 + + new_envelope = { + 'rm' : rm, + 'add' : add + }.get(options.tag_action)(options, envelope) + + if not new_envelope: + return 1 + + options.output_file.write(cbor.dumps(new_envelope, canonical=True)) + + return 0