1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-01-18 12:52:44 +01:00

dist/tools/suit_v3: Add ietf-v3 manifest generator

Co-authored-by: Kaspar Schleiser <kaspar@schleiser.de>
This commit is contained in:
Koen Zandberg 2020-02-26 14:44:13 +01:00
parent ef8daaf7be
commit 009a317b14
No known key found for this signature in database
GPG Key ID: 0E63411F8FCA8247
16 changed files with 1991 additions and 0 deletions

38
dist/tools/suit_v3/gen_key.py vendored Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020 Kaspar Schleiser <kaspar@schleiser.de>
# 2020 Inria
# 2020 Freie Universität Berlin
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#
import sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import PrivateFormat
from cryptography.hazmat.primitives.serialization import NoEncryption
def main():
if len(sys.argv) != 2:
print("usage: gen_key.py <secret filename>")
sys.exit(1)
pk = Ed25519PrivateKey.generate()
pem = pk.private_bytes(encoding=Encoding.PEM,
format=PrivateFormat.PKCS8,
encryption_algorithm=NoEncryption()
)
with open(sys.argv[1], "wb") as f:
f.write(pem)
if __name__ == '__main__':
main()

89
dist/tools/suit_v3/gen_manifest.py vendored Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019 Inria
# 2019 FU Berlin
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#
import argparse
import json
import os
import uuid
def str2int(x):
if x.startswith("0x"):
return int(x, 16)
else:
return int(x)
def parse_arguments():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--urlroot', '-u', help='',
default="coap://example.org")
parser.add_argument('--seqnr', '-s', default=0,
help='Sequence number of the manifest')
parser.add_argument('--output', '-o', default="out.json",
help='Manifest output binary file path')
parser.add_argument('--uuid-vendor', '-V', default="riot-os.org",
help='Manifest vendor uuid')
parser.add_argument('--uuid-class', '-C', default="native",
help='Manifest class uuid')
parser.add_argument('slotfiles', nargs="+",
help='The list of slot file paths')
return parser.parse_args()
def main(args):
uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, args.uuid_vendor)
uuid_class = uuid.uuid5(uuid_vendor, args.uuid_class)
template = {}
template["manifest-version"] = int(1)
template["manifest-sequence-number"] = int(args.seqnr)
images = []
for filename_offset in args.slotfiles:
split = filename_offset.split(":")
if len(split) == 1:
filename, offset = split[0], 0
else:
filename, offset = split[0], str2int(split[1])
images.append((filename, offset))
template["components"] = []
for slot, image in enumerate(images):
filename, offset = image
uri = os.path.join(args.urlroot, os.path.basename(filename))
component = {
"install-id": ["00"],
"vendor-id": uuid_vendor.hex,
"class-id": uuid_class.hex,
"file": filename,
"uri": uri,
"bootable": True,
}
if offset:
component.update({"offset": offset})
template["components"].append(component)
with open(args.output, 'w') as f:
json.dump(template, f, indent=4)
if __name__ == "__main__":
_args = parse_arguments()
main(_args)

View File

@ -0,0 +1,5 @@
*__pycache__
*.pyc
*.DS_Store
*.hex
examples/*.cbor

View File

@ -0,0 +1,165 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.

View File

@ -0,0 +1,209 @@
# Manifest Generator
This repository contains a tool to generate manifests following the specification in https://tools.ietf.org/html/draft-ietf-suit-manifest-03.
# Installing
First clone this repo:
```
$ git clone https://github.com/ARMmbed/suit-manifest-generator.git
```
Next, use pip to install the repo:
```
$ cd suit-manifest-generator
$ python3 -m pip install --user --upgrade .
```
# Input File Description
The input file is organised into four high-level elements:
* `manifest-version` (a positive integer), the version of the manifest specification
* `manifest-sequence-number` (a positive integer), the anti-rollback counter of the manifest
* `components`, a list of components that are described by the manifest
Each component is a JSON map that may contain the following elements. Some elements are required for the target to be able to install the component.
Required elements:
* `install-id` (a Component ID), the identifier of the location to install the described component.
* `install-digest` (a SUIT Digest), the digest of the component after installation.
* `install-size` (a positive integer), the size of the component after installation.
* `vendor-id` (a RFC 4122 UUID), the UUID for the component vendor. This must match the UUID that the manifest processor expects for the specified `install-id`. The suit-tool expects at least one component to have a `vendor-id`
* `class-id` (a RFC 4122 UUID), the UUID for the component. This must match the UUID that the manifest processor expects for the specified `install-id`. The `suit-tool` expects at least one component with a `vendor-id` to also have a `class-id`
* `file` (a string), the path to a payload file. The `install-digest` and `install-size` will be calculated from this file.
Some elements are not required by the tool, but are necessary in order to accomplish one or more use-cases.
Optional elements:
* `bootable` (a boolean, default: `false`), when set to true, the `suit-tool` will generate commands to execute the component, either from `install-id` or from `load-id` (see below)
* `uri` (a text string), the location at which to find the payload. This element is required in order to generate the `payload-fetch` and `install` sections.
* `loadable` (a boolean, default: `false`), when set to true, the `suit-tool` loads this component in the `load` section.
* `compression-info` (a choice of string values), indicates how a payload is compressed. When specified, payload is decompressed before installation. The `install-size` must match the decompressed size of the payload and the install-digest must match the decompressed payload. N.B. The suit-tool does not perform compression. Supported values are:
* `gzip`
* `bzip2`
* `deflate`
* `lz4`
* `lzma`
* `download-digest` (a SUIT Digest), a digest of the component after download. Only required if `compression-info` is present and `decompress-on-load` is `false`.
* `decompress-on-load` (a boolean, default: `false`), when set to true, payload is not decompressed during installation. Instead, the payload is decompressed during loading. This element has no effect if `loadable` is `false`.
* `load-digest` (a SUIT Digest), a digest of the component after loading. Only required if `decompress-on-load` is `true`.
* `install-on-download` (boolean, default: true), If true, payload is written to `install-id` during download, otherwise, payload is written to `download-id`.
* `download-id` (a component id), the location where a downloaded payload should be stored before installation--only used when `install-on-download` is `false`.
## Component ID
The `suit-tool` expects component IDs to be a JSON list of strings. The `suit-tool` converts the strings to bytes by:
1. Attempting to convert from hex
2. Attempting to convert from base64
3. Encoding the string to UTF-8
For example,
* `["00"]` will encode to `814100` (`[h'00']`)
* `["0"]` will encode to `814130` (`[h'30']`)
* `["MTIzNA=="]` will encode to `814431323334` (`[h'31323334']`)
* `["example"]` will encode to `81476578616D706C65` (`[h'6578616d706c65']`)
N.B. Be careful that certain strings can appear to be hex or base64 and will be treated as such. Any characters outside the set `[0-9a-fA-F]` ensure that the string is not treated as hex. Any characters outside the set `[0-9A-Za-z+/]` or a number of characters not divisible by 4 will ensure that the string is not treated as base64.
## SUIT Digest
The format of a digest is a JSON map:
```JSON
{
"algorithm-id" : "sha256",
"digest-bytes" : "base64-or-hex"
}
```
The `algorithm-id` must be one of:
* `sha224`
* `sha256`
* `sha384`
* `sha512`
The `digest-bytes` is a string of either hex- or base64-encoded bytes. The same decoding rules as those in Component ID are applied.
## Example Input File
```JSON
{
"components" : [
{
"download-id" : ["01"],
"install-id" : ["00"],
"install-digest": {
"algorithm-id": "sha256",
"digest-bytes": "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210"
},
"install-size" : 34768,
"uri": "http://example.com/file.bin",
"vendor-id" : "fa6b4a53-d5ad-5fdf-be9d-e663e4d41ffe",
"class-id" : "1492af14-2569-5e48-bf42-9b2d51f2ab45",
"bootable" : true,
"install-on-download" : false,
"loadable" : true,
"decompress-on-load" : true,
"load-id" : ["02"],
"compression-info" : "gzip",
"load-digest" : {
"algorithm-id": "sha256",
"digest-bytes": "0011223344556677889901234567899876543210aabbccddeeffabcdeffedcba"
},
},
{
"install-id" : ["03", "01"],
"install-digest": {
"algorithm-id": "sha256",
"digest-bytes": "0123456789abcdeffedcba987654321000112233445566778899aabbccddeeff"
},
"install-size" : 76834,
"uri": "http://example.com/file2.bin"
}
],
"manifest-version": 1,
"manifest-sequence-number": 7
}
```
# Invoking the suit-tool
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.
The `suit-tool` has a configurable log level, specified with `-l`:
* `suit-tool -l debug` verbose output
* `suit-tool -l info` normal output
* `suit-tool -l warning` suppress informational messages
* `suit-tool -l exception` suppress warning and informational messages
## Create
To create a manifest, invoke the `suit-tool` with:
```sh
suit-tool create -i IFILE -o OFILE
```
The format of `IFILE` is as described above. `OFILE` defaults to a CBOR-encoded SUIT manifest.
`-f` specifies the output format:
* `suit`: CBOR-encoded SUIT manifest
* `suit-debug`: CBOR-debug SUIT manifest
* `json`: JSON-representation of a SUIT manifest
The `suit-tool` can generate a manifest with severable fields. To enable this mode, add the `-s` flag.
To add a component to the manifest from the command-line, use the following syntax:
```
-c 'FIELD1=VALUE1,FIELD2=VALUE2'
```
The supported fields are:
* `file` the path to a file to use as a payload file.
* `inst` the `install-id`.
* `uri` the URI where the file will be found.
## Sign
To sign an existing manifest, invoke the `suit-tool` with:
```sh
suit-tool sign -m MANIFEST -k PRIVKEY -o OFILE
```
`PRIVKEY` must be a secp256r1 ECC private key in PEM format.
If the COSE Signature needs to indicate the key ID, add a key id with:
```
-i KEYID
```
## Parse
To parse an existing manifest, invoke the `suit-tool` with:
```sh
suit-tool parse -m MANIFEST
```
If a json-representation is needed, add the '-j' flag.

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2016-2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
import sys
import os
suittoolPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..')
sys.path.insert(0,suittoolPath)
from suit_tool import clidriver
def main():
return clidriver.main()
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,67 @@
#!/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 setuptools
import os
import suit_tool
with open('README.md', 'r') as fd:
long_description = fd.read()
if os.name == 'nt':
entry_points={
"console_scripts": [
"suit-tool=suit_tool.clidriver:main",
],
}
scripts = []
else:
platform_deps = []
# entry points are nice, but add ~100ms to startup time with all the
# pkg_resources infrastructure, so we use scripts= instead on unix-y
# platforms:
scripts = ['bin/suit-tool', ]
entry_points = {}
setuptools.setup (
name = 'ietf-suit-tool',
version = suit_tool.__version__,
author = 'Brendan Moran',
author_email = 'brendan.moran@arm.com',
description = 'A tool for constructing SUIT manifests',
long_description = long_description,
url = 'https://github.com/ARMmbed/suit-manifest-generator',
packages = setuptools.find_packages(exclude=['examples*', 'parser_examples*', '.git*']),
python_requires ='>=3.6',
scripts = scripts,
entry_points = entry_points,
zip_safe = False,
install_requires = [
'cbor>=1.0.0',
'colorama>=0.4.0',
'cryptography>=2.8'
],
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Development Status :: 3 - Alpha",
"Operating System :: OS Independent"
],
long_description_content_type = 'text/markdown'
)

View File

@ -0,0 +1,20 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2016-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.
#
__version__ = '0.0.1'

View File

@ -0,0 +1,85 @@
#!/usr/bin/python3
# -*- 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.
# ----------------------------------------------------------------------------
import sys
import argparse
from suit_tool import __version__
import re
def str_to_component(s):
types = {
'file' : ('file', 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):
self.parser = self._make_parser()
def _make_parser(self):
parser = argparse.ArgumentParser(description = 'Create or transform a manifest.'
' Use {} [command] -h for help on each command.'.format(sys.argv[0]))
# Add all top-level commands
parser.add_argument('-l', '--log-level', choices=['debug','info','warning','exception'], default='info',
help='Set the verbosity level of console output.')
parser.add_argument('--version', action='version', version=__version__,
help='display the version'
)
subparsers = parser.add_subparsers(dest="action")
subparsers.required = True
create_parser = subparsers.add_parser('create', help='Create a new manifest')
# 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.')
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.')
create_parser.add_argument('-c', '--add-component', action='append', type=str_to_component, dest='components', default=[])
sign_parser = subparsers.add_parser('sign', help='Sign a manifest')
sign_parser.add_argument('-m', '--manifest', metavar='FILE', type=argparse.FileType('rb'), required=True)
sign_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True)
sign_parser.add_argument('-i', '--key-id', metavar='ID', type=str)
sign_parser.add_argument('-o', '--output-file', metavar='FILE', type=argparse.FileType('wb'), required=True)
parse_parser = subparsers.add_parser('parse', help='Parse a manifest')
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_uecc_pubkey_parser.add_argument('-k', '--private-key', metavar='FILE', type=argparse.FileType('rb'), required=True)
return parser
def parse_args(self, args=None):
self.options = self.parser.parse_args(args)
return self

View File

@ -0,0 +1,63 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2018-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 logging
import sys
from suit_tool.argparser import MainArgumentParser
from suit_tool import create, sign, parse, get_uecc_pubkey
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):
self.options = MainArgumentParser().parse_args().options
log_level = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'exception': logging.CRITICAL
}[self.options.log_level]
logging.basicConfig(level=log_level,
format=LOG_FORMAT,
datefmt='%Y-%m-%d %H:%M:%S')
LOG.debug('CLIDriver created. Arguments parsed and logging setup.')
def main(self):
rc = {
"create": create.main,
"parse": parse.main,
# "verify": verify.main,
# "cert": cert.main,
# "init": init.main,
# "update" : update.main,
"pubkey": get_uecc_pubkey.main,
"sign": sign.main
}[self.options.action](self.options) or 0
sys.exit(rc)

View File

@ -0,0 +1,289 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
import binascii
import copy
import logging
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from suit_tool.manifest import SUITComponentId, SUITCommon, SUITSequence, \
SUITCommand, \
SUITWrapper, SUITTryEach
LOG = logging.getLogger(__name__)
def runable_id(c):
id = c['install-id']
if c.get('loadable'):
id = c['load-id']
return id
def hash_file(fname, alg):
imgsize = 0
digest = hashes.Hash(alg, backend=default_backend())
with open(fname, 'rb') as fd:
def read_in_chunks():
while True:
data = fd.read(1024)
if not data:
break
yield data
for chunk in read_in_chunks():
imgsize += len(chunk)
digest.update(chunk)
return digest, imgsize
def mkCommand(cid, name, arg):
if hasattr(arg, 'to_json'):
jarg = arg.to_json()
else:
jarg = arg
return SUITCommand().from_json({
'component-id' : cid.to_json(),
'command-id' : name,
'command-arg' : jarg
})
def check_eq(ids, choices):
eq = {}
neq = {}
check = lambda x: x[:-1]==x[1:]
get = lambda k, l: [d.get(k) for d in l]
eq = { k: ids[k] for k in ids if any([k in c for c in choices]) and check(get(k, choices)) }
check = lambda x: not x[:-1]==x[1:]
neq = { k: ids[k] for k in ids if any([k in c for c in choices]) and check(get(k, choices)) }
return eq, neq
def make_sequence(cid, choices, seq, params, cmds, pcid_key=None, param_drctv='directive-set-parameters'):
eqcmds, neqcmds = check_eq(cmds, choices)
eqparams, neqparams = check_eq(params, choices)
if not pcid_key:
pcid = cid
else:
pcid = SUITComponentId().from_json(choices[0][pcid_key])
params = {}
for param, pcmd in eqparams.items():
k,v = pcmd(pcid, choices[0])
params[k] = v
if len(params):
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))
params = {}
for param, pcmd in neqparams.items():
k,v = pcmd(cid, c)
params[k] = v
if len(params):
TECseq.append(mkCommand(pcid, param_drctv, params))
if len(TECseq.items):
TryEachCmd.append(TECseq)
if len(TryEachCmd.items):
seq.append(mkCommand(cid, 'directive-try-each', TryEachCmd))
# Finally, and equal commands
for item, cmd in eqcmds.items():
seq.append(cmd(cid, choices[0]))
return seq
def compile_manifest(options, m):
m = copy.deepcopy(m)
m['components'] += options.components
# Compile list of All Component IDs
ids = set([
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 = {}
for c in m['components']:
if not 'install-id' in c:
LOG.critical('install-id required for all components')
raise Exception('No install-id')
cid = SUITComponentId().from_json(c['install-id'])
if not cid in cid_data:
cid_data[cid] = [c]
else:
cid_data[cid].append(c)
for id, choices in cid_data.items():
for c in choices:
if 'file' in c:
digest, imgsize = hash_file(c['file'], hashes.SHA256())
c['install-digest'] = {
'algorithm-id' : 'sha256',
'digest-bytes' : binascii.b2a_hex(digest.finalize())
}
c['install-size'] = imgsize
if not any(c.get('vendor-id', None) for c in m['components']):
LOG.critical('A vendor-id is required for at least one component')
raise Exception('No Vendor ID')
if not any(c.get('class-id', None) for c in m['components'] if 'vendor-id' in c):
LOG.critical('A class-id is required for at least one component that also has a vendor-id')
raise Exception('No Class ID')
# Construct common sequence
CommonCmds = {
'offset': lambda cid, data: mkCommand(cid, 'condition-component-offset', data['offset'])
}
CommonParams = {
'install-digest': lambda cid, data: ('image-digest', data['install-digest']),
'install-size': lambda cid, data: ('image-size', data['install-size']),
}
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')
InstSeq = SUITSequence()
FetchSeq = SUITSequence()
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']),
}
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'])
}
InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds)
InstSeq.append(mkCommand(cid, 'directive-fetch', None))
InstSeq.append(mkCommand(cid, 'condition-image-match', None))
elif any(['uri' in c for c in choices]):
FetchParams = {
'uri' : lambda cid, data: ('uri', data['uri']),
'download-digest' : lambda cid, data : (
'image-digest', data.get('download-digest', data['install-digest']))
}
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')
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)
}
FetchSeq = make_sequence(cid, choices, FetchSeq, FetchParams, FetchCmds, 'download-id')
InstParams = {
'download-id' : lambda cid, data : ('source-component', data['download-id'])
}
InstCmds = {
}
InstSeq = make_sequence(cid, choices, InstSeq, InstParams, InstCmds)
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()
# 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))
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']))
}
if 'compression-info' in c and c.get('decompress-on-load', False):
LoadParams['compression-info'] = lambda cid, data: ('compression-info', c['compression-info'])
LoadCmds = {
# Move each loadable component
}
load_id = SUITComponentId().from_json(choices[0]['load-id'])
LoadSeq = make_sequence(load_id, choices, ValidateSeq, LoadParams, LoadCmds)
LoadSeq.append(mkCommand(load_id, 'directive-copy', None))
LoadSeq.append(mkCommand(load_id, 'condition-image-match', None))
# Generate image invocation section
bootable_components = [x for x in m['components'] if x.get('bootable')]
if len(bootable_components) == 1:
c = bootable_components[0]
RunSeq.append(SUITCommand().from_json({
'component-id' : runable_id(c),
'command-id' : 'directive-run',
'command-arg' : None
}))
else:
t = []
for c in bootable_components:
pass
# TODO: conditions
# t.append(
#
# )
#TODO: Text
common = SUITCommon().from_json({
'components': [id.to_json() for id in ids],
'common-sequence': CommonSeq.to_json(),
})
jmanifest = {
'manifest-version' : m['manifest-version'],
'manifest-sequence-number' : m['manifest-sequence-number'],
'common' : common.to_json()
}
jmanifest.update({k:v for k,v in {
'payload-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})
return wrapped_manifest

View File

@ -0,0 +1,41 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
from suit_tool.compile import compile_manifest
import json
import cbor
import itertools
import textwrap
def main(options):
m = json.loads(options.input_file.read())
nm = compile_manifest(options, m)
if hasattr(options, 'severable') and options.severable:
nm = nm.to_severable()
output = {
'suit' : lambda x: cbor.dumps(x.to_suit(), sort_keys=True),
'suit-debug' : lambda x: '\n'.join(itertools.chain.from_iterable(
map(textwrap.wrap, x.to_debug('').split('\n'))
)).encode('utf-8'),
'json' : lambda x : json.dumps(x.to_json(), indent=2).encode('utf-8')
}.get(options.format)(nm)
options.output_file.write(output)
return 0

View File

@ -0,0 +1,53 @@
#!/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

View File

@ -0,0 +1,688 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
import collections
import binascii
import cbor
import copy
import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
ManifestKey = collections.namedtuple(
'ManifestKey',
[
'json_key',
'suit_key',
'obj'
]
)
def to_bytes(s):
try:
return binascii.a2b_hex(s)
except:
try:
return binascii.a2b_base64(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 SUITCommonInformation:
def __init__(self):
self.component_ids = []
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
return id
suitCommonInfo = SUITCommonInformation()
one_indent = ' '
class SUITInt:
def from_json(self, v):
self.v = int(v)
return self
def to_json(self):
return self.v
def from_suit(self, v):
self.v = int(v)
return self
def to_suit(self):
return self.v
def to_debug(self, indent):
return str(self.v)
class SUITPosInt(SUITInt):
def from_json(self, v):
_v = int(v)
if _v < 0:
raise Exception('Positive Integers must be >= 0')
self.v = _v
return self
def from_suit(self, v):
return self.from_json(v)
class SUITManifestDict:
def mkfields(d):
# rd = {}
return {k: ManifestKey(*v) for k,v in d.items()}
def __init__(self):
pass
def from_json(self, data):
for k, f in self.fields.items():
v = data.get(f.json_key, None)
setattr(self, k, f.obj().from_json(v) if v is not None else None)
return self
def to_json(self):
j = {}
for k, f in self.fields.items():
v = getattr(self, k)
if v:
j[f.json_key] = v.to_json()
return j
def from_suit(self, data):
for k, f in self.fields.items():
v = data.get(f.suit_key, None)
d = f.obj().from_suit(v) if v is not None else None
setattr(self, k, d)
return self
def to_suit(self):
sd = {}
for k, f in self.fields.items():
v = getattr(self, k)
if v:
sd[f.suit_key] = v.to_suit()
return sd
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) + ','
s += '\n' + indent + '}'
return s
class SUITManifestNamedList(SUITManifestDict):
def from_suit(self, data):
for k, f in self.fields.items():
setattr(self, k, f.obj().from_suit(data[f.suit_key]))
return self
def to_suit(self):
sd = [None] * (max([f.suit_key for k, f in self.fields.items()]) + 1)
for k, f in self.fields.items():
v = getattr(self, k)
if v:
sd[f.suit_key] = v.to_suit()
return sd
def to_debug(self, indent):
newindent = indent + one_indent
items = []
for k, f in self.fields.items():
v = getattr(self, k)
if v:
items.append('/ ' + f.json_key + ' / ' + v.to_debug(newindent))
s = '[\n{newindent}{items}\n{indent}]'.format(
newindent=newindent,
indent=indent,
items=',\n{newindent}'.format(newindent=newindent).join(items)
)
return s
class SUITKeyMap:
def mkKeyMaps(m):
return {v:k for k,v in m.items()}, m
def to_json(self):
return self.rkeymap[self.v]
def from_json(self, d):
self.v = self.keymap[d]
return self
def to_suit(self):
return self.v
def from_suit(self, d):
self.v = self.keymap[self.rkeymap[d]]
return self
def to_debug(self, indent):
s = str(self.v) + ' / ' + self.to_json() + ' /'
return s
def SUITBWrapField(c):
class SUITBWrapper:
def to_suit(self):
return cbor.dumps(self.v.to_suit(), sort_keys=True)
def from_suit(self, d):
self.v = c().from_suit(cbor.loads(d))
return self
def to_json(self):
return self.v.to_json()
def from_json(self, d):
self.v = c().from_json(d)
return self
def to_debug(self, indent):
s = 'h\''
s += binascii.b2a_hex(self.to_suit()).decode('utf-8')
s += '\' / '
s += self.v.to_debug(indent)
s += ' /'
return s
return SUITBWrapper
class SUITManifestArray:
def __init__(self):
self.items=[]
def __eq__(self, rhs):
if len(self.items) != len(rhs.items):
return False
for a,b in zip(self.items, rhs.items):
if not a == b:
return False
return True
def from_json(self, data):
self.items = []
for d in data:
self.items.append(self.field.obj().from_json(d))
return self
def to_json(self):
j = []
for i in self.items:
j.append(i.to_json())
return j
def from_suit(self, data):
self.items = []
for d in data:
self.items.append(self.field.obj().from_suit(d))
return self
def to_suit(self):
l = []
for i in self.items:
l.append(i.to_suit())
return l
def append(self, element):
if not isinstance(element, self.field.obj):
raise Exception('element {} is not a {}'.format(element, self.field.obj))
self.items.append(element)
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' + indent + ']'
return s
class SUITBytes:
def to_json(self):
return binascii.b2a_hex(self.v).decode('utf-8')
def from_json(self, d):
self.v = to_bytes(d)
return self
def from_suit(self, d):
self.v = bytes(d)
return self
def to_suit(self):
return self.v
def to_debug(self, indent):
return 'h\'' + self.to_json() + '\''
def __eq__(self, rhs):
return self.v == rhs.v
class SUITUUID(SUITBytes):
def from_json(self, d):
self.v = uuid.UUID(d).bytes
return self
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)) + ' /'
class SUITRaw:
def to_json(self):
return self.v
def from_json(self, d):
self.v = d
return self
def to_suit(self):
return self.v
def from_suit(self, d):
self.v = d
return self
def to_debug(self, indent):
return str(self.v)
class SUITNil:
def to_json(self):
return None
def from_json(self, d):
if d is not None:
raise Exception('Expected Nil')
return self
def to_suit(self):
return None
def from_suit(self, d):
if d is not None:
raise Exception('Expected Nil')
return self
def to_debug(self, indent):
return 'F6 / nil /'
class SUITTStr(SUITRaw):
def from_json(self, d):
self.v = str(d)
return self
def from_suit(self, d):
self.v = str(d)
return self
def to_debug(self, indent):
return '\''+ str(self.v) + '\''
class SUITComponentId(SUITManifestArray):
field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITBytes)
def to_debug(self, indent):
newindent = indent + one_indent
s = '[' + ''.join([v.to_debug(newindent) for v in self.items]) + ']'
return s
def __hash__(self):
return hash(tuple([i.v for i in self.items]))
class SUITComponentIndex(SUITComponentId):
def to_suit(self):
return suitCommonInfo.component_id_to_index(self)
def from_suit(self, d):
return super(SUITComponentIndex, self).from_suit(
suitCommonInfo.component_ids[d].to_suit()
)
def to_debug(self, indent):
newindent = indent + one_indent
s = '{suit} / [{dbg}] /'.format(
suit=self.to_suit(),
dbg=''.join([v.to_debug(newindent) for v in self.items])
)
return s
class SUITComponents(SUITManifestArray):
field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITComponentId)
def from_suit(self, data):
super(SUITComponents, self).from_suit(data)
suitCommonInfo.component_ids = self.items
return self
def from_json(self, j):
super(SUITComponents, self).from_json(j)
suitCommonInfo.component_ids = self.items
return self
class SUITDigestAlgo(SUITKeyMap):
rkeymap, keymap = SUITKeyMap.mkKeyMaps({
'sha224' : 1,
'sha256' : 2,
'sha384' : 3,
'sha512' : 4
})
class SUITDigest(SUITManifestNamedList):
fields = SUITManifestNamedList.mkfields({
'algo' : ('algorithm-id', 0, SUITDigestAlgo),
'digest' : ('digest-bytes', 1, SUITBytes)
})
class SUITCompressionInfo(SUITKeyMap):
rkeymap, keymap = SUITKeyMap.mkKeyMaps({
'gzip' : 1,
'bzip2' : 2,
'deflate' : 3,
'lz4' : 4,
'lzma' : 7
})
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)
})
def from_json(self, j):
return super(SUITParameters, self).from_json(j)
class SUITTryEach(SUITManifestArray):
pass
def SUITCommandContainer(jkey, skey, argtype):
class SUITCmd(SUITCommand):
json_key = jkey
suit_key = skey
def __init__(self):
pass
def to_suit(self):
return [self.suit_key, self.arg.to_suit()]
def to_json(self):
if self.json_key == 'directive-set-component-index':
return {}
else:
return {
'command-id' : self.json_key,
'command-arg' : self.arg.to_json(),
'component-id' : self.cid.to_json()
}
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'])
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]
else:
self.cid = suitCommonInfo.component_ids[suitCommonInfo.current_index]
self.arg = argtype().from_suit(s[1])
return self
def to_debug(self, indent):
s = '/ {} / {},'.format(self.json_key, self.suit_key)
s += self.arg.to_debug(indent)
return s
return SUITCmd
class SUITCommand:
def from_json(self, j):
return self.jcommands[j['command-id']]().from_json(j)
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('directive-set-component-index', 12, SUITPosInt),
SUITCommandContainer('directive-set-dependency-index', 13, SUITRaw),
SUITCommandContainer('directive-abort', 14, SUITRaw),
SUITCommandContainer('directive-try-each', 15, SUITTryEach),
SUITCommandContainer('directive-process-dependency', 18, SUITRaw),
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-run-sequence', 30, SUITRaw),
SUITCommandContainer('directive-run-with-arguments', 31, SUITRaw),
SUITCommandContainer('directive-swap', 32, SUITRaw),
]
SUITCommand.jcommands = { c.json_key : c for c in SUITCommand.commands}
SUITCommand.scommands = { c.suit_key : c for c in SUITCommand.commands}
class SUITSequence(SUITManifestArray):
field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITCommand)
def to_suit(self):
suit_l = []
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
else:
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
})
suitCommonInfo.current_index = cidx
suit_l += cswitch.to_suit()
suit_l += i.to_suit()
return suit_l
def to_debug(self, indent):
return super(SUITSequence, SUITSequence().from_suit(self.to_suit())).to_debug(indent)
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)
class SUITSequenceComponentReset(SUITSequence):
def to_suit(self):
suitCommonInfo.current_index = None
return super(SUITSequenceComponentReset, self).to_suit()
def SUITMakeSeverableField(c):
class SUITSeverableField:
objtype = SUITBWrapField(c)
def from_json(self, data):
if 'algorithm-id' in data:
self.v = SUITDigest().from_json(data)
else:
self.v = self.objtype().from_json(data)
return self
def from_suit(self, data):
if isinstance(data, list):
self.v = SUITDigest().from_suit(data)
else:
self.v = self.objtype().from_suit(data)
return self
def to_json(self):
return self.v.to_json()
def to_suit(self):
return self.v.to_suit()
def to_debug(self, indent):
return self.v.to_debug(indent)
return SUITSeverableField
# class SUITSequenceOrDigest()
class SUITCommon(SUITManifestDict):
fields = SUITManifestNamedList.mkfields({
# 'dependencies' : ('dependencies', 1, SUITBWrapField(SUITDependencies)),
'components' : ('components', 2, SUITBWrapField(SUITComponents)),
# 'dependency_components' : ('dependency-components', 3, SUITBWrapField(SUITDependencies)),
'common_sequence' : ('common-sequence', 4, SUITBWrapField(SUITSequenceComponentReset)),
})
class SUITManifest(SUITManifestDict):
fields = SUITManifestDict.mkfields({
'version' : ('manifest-version', 1, SUITPosInt),
'sequence' : ('manifest-sequence-number', 2, SUITPosInt),
'common' : ('common', 3, SUITBWrapField(SUITCommon)),
'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)),
})
class COSE_Algorithms(SUITKeyMap):
rkeymap, keymap = SUITKeyMap.mkKeyMaps({
'ES256' : -7,
'ES384' : -35,
'ES512' : -36,
'EdDSA' : -8,
'HSS-LMS' : -46,
})
class COSE_CritList(SUITManifestArray):
field = collections.namedtuple('ArrayElement', 'obj')(obj=SUITInt)
class COSE_header_map(SUITManifestDict):
fields = SUITManifestDict.mkfields({
# 1: algorithm Identifier
'alg' : ('alg', 1, COSE_Algorithms),
# 2: list of critical headers (criticality)
# 3: content type
# 4: key id
'kid' : ('kid', 4, SUITBytes),
# 5: IV
# 6: partial IV
# 7: counter signature(s)
})
class COSE_Sign:
pass
class COSE_Sign1(SUITManifestNamedList):
fields = SUITManifestDict.mkfields({
'protected' : ('protected', 0, SUITBWrapField(COSE_header_map)),
'unprotected' : ('unprotected', 1, COSE_header_map),
'payload' : ('payload', 2, SUITBWrapField(SUITDigest)),
'signature' : ('signature', 3, SUITBytes)
})
class COSE_Mac:
pass
class COSE_Mac0:
pass
class COSETagChoice(SUITManifestDict):
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 None
def from_suit(self, data):
for k, f in self.fields.items():
if data.tag == f.suit_key:
v = data.value
d = f.obj().from_suit(v) if v is not None else None
setattr(self, k, d)
return self
def to_debug(self, indent):
s = ''
for k, f in self.fields.items():
if hasattr(self, k):
v = getattr(self, k)
newindent = indent + one_indent
s = '{tag}({value})'.format(tag=f.suit_key, value=v.to_debug(newindent))
return s
class COSETaggedAuth(COSETagChoice):
fields = SUITManifestDict.mkfields({
'cose_sign' : ('COSE_Sign_Tagged', 98, COSE_Sign),
'cose_sign1' : ('COSE_Sign1_Tagged', 18, COSE_Sign1),
'cose_mac' : ('COSE_Mac_Tagged', 97, COSE_Mac),
'cose_mac0' : ('COSE_Mac0_Tagged', 17, COSE_Mac0)
})
class COSEList(SUITManifestArray):
field = collections.namedtuple('ArrayElement', 'obj')(obj=COSETaggedAuth)
def from_suit(self, data):
return super(COSEList, self).from_suit(data)
class SUITWrapper(SUITManifestDict):
fields = SUITManifestDict.mkfields({
'auth' : ('authentication-wrapper', 2, SUITBWrapField(COSEList)),
'manifest' : ('manifest', 3, SUITBWrapField(SUITManifest)),
'deres': ('dependency-resolution', 7, SUITBWrapField(SUITSequence)),
'fetch': ('payload-fetch', 8, SUITBWrapField(SUITSequence)),
'install': ('install', 9, SUITBWrapField(SUITSequence)),
'validate': ('validate', 10, SUITBWrapField(SUITSequence)),
'load': ('load', 11, SUITBWrapField(SUITSequence)),
'run': ('run', 12, SUITBWrapField(SUITSequence)),
# 'text': ('text', 13, SUITBWrapField(SUITSequence)),
})
severable_fields = {'deres', 'fetch', 'install'} #, 'text'}
digest_algorithms = {
'sha224' : hashes.SHA224,
'sha256' : hashes.SHA256,
'sha384' : hashes.SHA384,
'sha512' : hashes.SHA512
}
def to_severable(self, digest_alg):
sev = copy.deepcopy(self)
for k in sev.severable_fields:
f = sev.manifest.v.fields[k]
if not hasattr(sev.manifest.v, k):
continue
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())
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)
if len(cbor_digest) < len(cbor_field):
setattr(sev.manifest.v, k, field_digest)
setattr(sev,k,v)
return sev
def from_severable(self):
raise Exception('From Severable unimplemented')
nsev = copy.deepcopy(self)
for k in nsev.severable_fields:
f = nsev.fields[k]
if not hasattr(nsev, k):
continue
v = getattr(nsev, k)
if v is None:
continue
# Verify digest
cbor_field = cbor.dumps(v.to_suit(), sort_keys=True)
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(cbor_field)
actual_digest = digest.finalize()
field_digest = getattr(sev.nsev.v, k)
expected_digest = field_digest.to_suit()[1]
if digest != expected_digest:
raise Exception('Field Digest mismatch: For {}, expected: {}, got {}'.format(
f.json_key, expected_digest, actual_digest
))
setattr(nsev.manifest.v, k, v)
delattr(nsev, k)
return nsev

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
import cbor
import json
import itertools
import textwrap
from suit_tool.manifest import SUITWrapper
def main(options):
# Read the manifest wrapper
decoded_cbor_wrapper = cbor.loads(options.manifest.read())
wrapper = SUITWrapper().from_suit(decoded_cbor_wrapper)
if options.json:
print (json.dumps(wrapper.to_json(),indent=2))
else:
print ('\n'.join(itertools.chain.from_iterable(
[textwrap.wrap(t, 70) for t in wrapper.to_debug('').split('\n')]
)))
return 0

View File

@ -0,0 +1,111 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
import cbor
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
from suit_tool.manifest import COSE_Sign1, COSEList, \
SUITWrapper, SUITBytes, SUITBWrapField
import logging
import binascii
LOG = logging.getLogger(__name__)
def get_cose_es_bytes(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):
return private_key.sign(sig_val)
def main(options):
# Read the manifest wrapper
wrapper = cbor.loads(options.manifest.read())
private_key = None
digest = None
try:
private_key = ks.load_pem_private_key(options.private_key.read(), 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):
options.key_type = 'EdDSA'
else:
LOG.critical('Unrecognized key: {}'.format(type(private_key).__name__))
return 1
digest = {
'ES256' : hashes.Hash(hashes.SHA256(), backend=default_backend()),
'ES384' : hashes.Hash(hashes.SHA384(), backend=default_backend()),
'ES512' : hashes.Hash(hashes.SHA512(), backend=default_backend()),
'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
digest.update(cbor.dumps(wrapper[SUITWrapper.fields['manifest'].suit_key]))
cose_signature = COSE_Sign1().from_json({
'protected' : {
'alg' : options.key_type
},
'unprotected' : {},
'payload' : {
'algorithm-id' : 'sha256',
'digest-bytes' : binascii.b2a_hex(digest.finalize())
}
})
Sig_structure = cbor.dumps([
"Signature1",
cose_signature.protected.to_suit(),
b'',
cose_signature.payload.to_suit(),
], sort_keys = True)
sig_val = Sig_structure
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)
cose_signature.signature = SUITBytes().from_suit(signature_bytes)
auth = SUITBWrapField(COSEList)().from_json([{
'COSE_Sign1_Tagged' : cose_signature.to_json()
}])
wrapper[SUITWrapper.fields['auth'].suit_key] = auth.to_suit()
options.output_file.write(cbor.dumps(wrapper, sort_keys=True))
return 0