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:
parent
ef8daaf7be
commit
009a317b14
38
dist/tools/suit_v3/gen_key.py
vendored
Executable file
38
dist/tools/suit_v3/gen_key.py
vendored
Executable 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
89
dist/tools/suit_v3/gen_manifest.py
vendored
Executable 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)
|
5
dist/tools/suit_v3/suit-manifest-generator/.gitignore
vendored
Normal file
5
dist/tools/suit_v3/suit-manifest-generator/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*__pycache__
|
||||
*.pyc
|
||||
*.DS_Store
|
||||
*.hex
|
||||
examples/*.cbor
|
165
dist/tools/suit_v3/suit-manifest-generator/LICENSE
vendored
Normal file
165
dist/tools/suit_v3/suit-manifest-generator/LICENSE
vendored
Normal 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.
|
209
dist/tools/suit_v3/suit-manifest-generator/README.md
vendored
Normal file
209
dist/tools/suit_v3/suit-manifest-generator/README.md
vendored
Normal 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.
|
31
dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool
vendored
Executable file
31
dist/tools/suit_v3/suit-manifest-generator/bin/suit-tool
vendored
Executable 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())
|
67
dist/tools/suit_v3/suit-manifest-generator/setup.py
vendored
Normal file
67
dist/tools/suit_v3/suit-manifest-generator/setup.py
vendored
Normal 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'
|
||||
)
|
20
dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py
vendored
Normal file
20
dist/tools/suit_v3/suit-manifest-generator/suit_tool/__init__.py
vendored
Normal 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'
|
85
dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py
vendored
Normal file
85
dist/tools/suit_v3/suit-manifest-generator/suit_tool/argparser.py
vendored
Normal 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
|
63
dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py
vendored
Normal file
63
dist/tools/suit_v3/suit-manifest-generator/suit_tool/clidriver.py
vendored
Normal 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)
|
289
dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py
vendored
Normal file
289
dist/tools/suit_v3/suit-manifest-generator/suit_tool/compile.py
vendored
Normal 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
|
41
dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py
vendored
Normal file
41
dist/tools/suit_v3/suit-manifest-generator/suit_tool/create.py
vendored
Normal 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
|
53
dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py
vendored
Normal file
53
dist/tools/suit_v3/suit-manifest-generator/suit_tool/get_uecc_pubkey.py
vendored
Normal 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
|
688
dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py
vendored
Normal file
688
dist/tools/suit_v3/suit-manifest-generator/suit_tool/manifest.py
vendored
Normal 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
|
37
dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py
vendored
Normal file
37
dist/tools/suit_v3/suit-manifest-generator/suit_tool/parse.py
vendored
Normal 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
|
111
dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py
vendored
Normal file
111
dist/tools/suit_v3/suit-manifest-generator/suit_tool/sign.py
vendored
Normal 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
|
Loading…
Reference in New Issue
Block a user