Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sys/suit: initial support for SUIT firmware updates #11818

Merged
merged 5 commits into from
Oct 10, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ results/
# Clangd compile flags (language server)
compile_commands.json
compile_flags.txt

# suit manifest keys
keys/
25 changes: 25 additions & 0 deletions Makefile.dep
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,31 @@ ifneq (,$(filter sock_dtls, $(USEMODULE)))
USEMODULE += sock_udp
endif

ifneq (,$(filter suit_v4_%,$(USEMODULE)))
USEMODULE += suit_v4
endif

ifneq (,$(filter suit_v4,$(USEMODULE)))
USEPKG += tinycbor
USEPKG += libcose
USEMODULE += libcose_crypt_hacl
USEMODULE += suit_conditions

# SUIT depends on riotboot support and some extra riotboot modules
FEATURES_REQUIRED += riotboot
USEMODULE += riotboot_slot
USEMODULE += riotboot_flashwrite
USEMODULE += riotboot_flashwrite_verify_sha256
endif

ifneq (,$(filter suit_conditions,$(USEMODULE)))
USEMODULE += uuid
endif

ifneq (,$(filter suit_%,$(USEMODULE)))
USEMODULE += suit
endif

# Enable periph_gpio when periph_gpio_irq is enabled
ifneq (,$(filter periph_gpio_irq,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio
Expand Down
7 changes: 6 additions & 1 deletion dist/tools/flake8/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ cd $RIOTBASE
: "${RIOTTOOLS:=${RIOTBASE}/dist/tools}"
. "${RIOTTOOLS}"/ci/changed_files.sh

EXCLUDE='^(.+/vendor/|dist/tools/cc2538-bsl|dist/tools/mcuboot|dist/tools/uhcpd|dist/tools/stm32loader)'
EXCLUDE="^(.+/vendor/\
|dist/tools/cc2538-bsl\
|dist/tools/mcuboot\
|dist/tools/uhcpd\
|dist/tools/stm32loader\
|dist/tools/suit_v4/suit_manifest_encoder_04)"
FILEREGEX='(\.py$|pyterm$)'
FILES=$(FILEREGEX=${FILEREGEX} EXCLUDE=${EXCLUDE} changed_files)

Expand Down
33 changes: 33 additions & 0 deletions dist/tools/suit_v4/gen_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env python3

#
# Copyright (C) 2019 Inria
# 2019 FU Berlin
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#

import sys
import ed25519


def main():
if len(sys.argv) != 3:
print("usage: gen_key.py <secret filename> <public filename>")
sys.exit(1)

_signing_key, _verifying_key = ed25519.create_keypair()
with open(sys.argv[1], "wb") as f:
f.write(_signing_key.to_bytes())

with open(sys.argv[2], "wb") as f:
f.write(_verifying_key.to_bytes())

vkey_hex = _verifying_key.to_ascii(encoding="hex")
print("Generated public key: '{}'".format(vkey_hex.decode()))


if __name__ == '__main__':
main()
94 changes: 94 additions & 0 deletions dist/tools/suit_v4/gen_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python3

#
# Copyright (C) 2019 Inria
# 2019 FU Berlin
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#

import os
import hashlib
import json
import uuid
import argparse

from suit_manifest_encoder_04 import compile_to_suit


def str2int(x):
if x.startswith("0x"):
return int(x, 16)
else:
return int(x)


def sha256_from_file(filepath):
sha256 = hashlib.sha256()
sha256.update(open(filepath, "rb").read())
return sha256.digest()


def parse_arguments():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--template', '-t', help='Manifest template file path')
parser.add_argument('--urlroot', '-u', help='')
parser.add_argument('--offsets', '-O', help='')
parser.add_argument('--seqnr', '-s',
help='Sequence number of the manifest')
parser.add_argument('--output', '-o', nargs='?',
help='Manifest output binary file path')
parser.add_argument('--uuid-vendor', '-V',
help='Manifest vendor uuid')
parser.add_argument('--uuid-class', '-C',
help='Manifest class uuid')
parser.add_argument('slotfiles', nargs=2,
help='The list of slot file paths')
return parser.parse_args()


def main(args):
uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, args.uuid_vendor)
uuid_class = uuid.uuid5(uuid_vendor, args.uuid_class)
with open(args.template, 'r') as f:
template = json.load(f)

template["sequence-number"] = int(args.seqnr)
template["conditions"] = [
{"condition-vendor-id": uuid_vendor.hex},
{"condition-class-id": uuid_class.hex},
]

offsets = [str2int(offset) for offset in args.offsets.split(",")]

for slot, slotfile in enumerate(args.slotfiles):
filename = slotfile
size = os.path.getsize(filename)
uri = os.path.join(args.urlroot, os.path.basename(filename))
offset = offsets[slot]

_image_slot = template["components"][0]["images"][slot]
_image_slot.update({
"file": filename,
"uri": uri,
"size": size,
"digest": sha256_from_file(slotfile),
})

_image_slot["conditions"][0]["condition-component-offset"] = offset
_image_slot["file"] = filename

result = compile_to_suit(template)
if args.output is not None:
with open(args.output, 'wb') as f:
f.write(result)
else:
print(result)


if __name__ == "__main__":
_args = parse_arguments()
main(_args)
154 changes: 154 additions & 0 deletions dist/tools/suit_v4/sign-04.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright 2018-2019 ARM Limited or its affiliates
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
"""
This is a demo script that is intended to act as a reference for SUIT manifest
signing.

NOTE: It is expected that C and C++ parser implementations will be written
against this script, so it does not adhere to PEP8 in order to maintain
similarity between the naming in this script and that of C/C++ implementations.
"""

import sys
import copy

import cbor
import ed25519

from pprint import pprint

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization

# Private key in arg 1
# Public key in arg 2
# Input file in arg 3
# Output file in arg 4

COSE_Sign_Tag = 98
APPLICATION_OCTET_STREAM_ID = 42
ES256 = -7
EDDSA = -8


def signWrapper(algo, private_key, public_key, encwrapper):
wrapper = cbor.loads(encwrapper)

pprint(wrapper[1])
COSE_Sign = copy.deepcopy(wrapper[1])
if not COSE_Sign:
protected = cbor.dumps({
3: APPLICATION_OCTET_STREAM_ID, # Content Type
})
unprotected = {
}
signatures = []

# Create a COSE_Sign_Tagged object
COSE_Sign = [
protected,
unprotected,
b'',
signatures
]

if algo == EDDSA:
public_bytes = public_key.to_bytes()
else:
public_bytes = public_key.public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo)

# NOTE: Using RFC7093, Method 4
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(public_bytes)
kid = digest.finalize()
# Sign the payload
protected = cbor.dumps({
1: algo, # alg
})
# Create the signing object
unprotected = {
4: kid # kid
}

Sig_structure = [
"Signature", # Context
COSE_Sign[0], # Body Protected
protected, # signature protected
b'', # External AAD
wrapper[2] # payload
]
sig_str = cbor.dumps(Sig_structure, sort_keys=True)

if algo == EDDSA:
signature = private_key.sign(sig_str)
else:
signature = private_key.sign(
sig_str,
ec.ECDSA(hashes.SHA256())
)

COSE_Signature = [
protected,
unprotected,
signature
]
COSE_Sign[3].append(COSE_Signature)
wrapper[1] = cbor.dumps(cbor.Tag(COSE_Sign_Tag, COSE_Sign), sort_keys=True)
return wrapper


def main():
private_key = None
algo = ES256
with open(sys.argv[1], 'rb') as fd:
priv_key_bytes = fd.read()
try:
private_key = serialization.load_pem_private_key(
priv_key_bytes, password=None, backend=default_backend())
except ValueError:
algo = EDDSA
private_key = ed25519.SigningKey(priv_key_bytes)

public_key = None
with open(sys.argv[2], 'rb') as fd:
pub_key_bytes = fd.read()
try:
public_key = serialization.load_pem_public_key(
pub_key_bytes, backend=default_backend())
except ValueError:
public_key = ed25519.VerifyingKey(pub_key_bytes)

# Read the input file
doc = None
with open(sys.argv[3], 'rb') as fd:
doc = fd.read()

outDoc = signWrapper(algo, private_key, public_key, doc)

with open(sys.argv[4], 'wb') as fd:
fd.write(cbor.dumps(outDoc, sort_keys=True))


if __name__ == '__main__':
main()
Loading