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

imgtool: Add tests for imgtool commands part 1 #1983

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/zephyr_build.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (c) 2022-2023 Nordic Semiconductor ASA
# Copyright (c) 2024, Arm Limited
# SPDX-License-Identifier: Apache-2.0

name: Build Zephyr samples with Twister
Expand Down Expand Up @@ -91,6 +92,7 @@ jobs:
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
echo "Using Zephyr version: ${{ env.ZEPHYR_VERSION }}"
echo "Using Mcuboot version: ${{ env.MCUBOOT_VERSION }}"
pip install -r ../bootloader/mcuboot/scripts/requirements.txt
./scripts/twister --inline-logs -v -N -M --integration --overflow-as-errors --retry-failed 2 ${test_paths}

- name: Upload Tests Results
Expand Down
5 changes: 4 additions & 1 deletion ci/fih_test_docker/execute_test.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash -x

# Copyright (c) 2020-2023 Arm Limited
# Copyright (c) 2020-2024 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,6 +30,9 @@ else
CMAKE_FIH_LEVEL="-DMCUBOOT_FIH_PROFILE=\"$FIH_LEVEL\""
fi

# Install imgtool dependencies
pip install -r $MCUBOOT_PATH/scripts/requirements.txt

# build TF-M with MCUBoot
mkdir -p $TFM_BUILD_PATH $TFM_SPE_BUILD_PATH

Expand Down
13 changes: 10 additions & 3 deletions scripts/imgtool/dumpinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@

import click
import yaml
from intelhex import IntelHex

from imgtool import image
from imgtool.image import INTEL_HEX_EXT

HEADER_ITEMS = ("magic", "load_addr", "hdr_size", "protected_tlv_size",
"img_size", "flags", "version")
Expand Down Expand Up @@ -129,11 +131,16 @@ def dump_imginfo(imgfile, outfile=None, silent=False):
trailer = {}
key_field_len = None

ext = os.path.splitext(imgfile)[1][1:].lower()
try:
with open(imgfile, "rb") as f:
b = f.read()
if ext == INTEL_HEX_EXT:
ih = IntelHex(imgfile)
b = ih.tobinstr()
else:
with open(imgfile, "rb") as f:
b = f.read()
except FileNotFoundError:
raise click.UsageError("Image file not found ({})".format(imgfile))
raise click.UsageError(f"Image file not found: {imgfile}")

# Parsing the image header
_header = struct.unpack('IIHHIIBBHI', b[:28])
Expand Down
4 changes: 2 additions & 2 deletions scripts/imgtool/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def load(self, path):
self.infile_data = f.read()
self.payload = copy.copy(self.infile_data)
except FileNotFoundError:
raise click.UsageError("Input file not found")
raise click.UsageError(f"Image file not found: {path}")
self.image_size = len(self.payload)

# Add the image header if needed.
Expand Down Expand Up @@ -779,7 +779,7 @@ def verify(imgfile, key):
with open(imgfile, 'rb') as f:
b = f.read()
except FileNotFoundError:
raise click.UsageError(f"Image file {imgfile} not found")
raise click.UsageError(f"Image file not found: {imgfile}")

magic, _, header_size, _, img_size = struct.unpack('IIHHI', b[:16])
version = struct.unpack('BBHI', b[20:28])
Expand Down
16 changes: 13 additions & 3 deletions scripts/imgtool/keys/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright 2017 Linaro Limited
# Copyright 2023 Arm Limited
# Copyright 2023-2024 Arm Limited
#
# SPDX-License-Identifier: Apache-2.0
#
Expand Down Expand Up @@ -58,14 +58,24 @@ def load(path, passwd=None):
except TypeError as e:
msg = str(e)
if "private key is encrypted" in msg:
print(msg)
return None
raise e
except ValueError:
except ValueError as e:
msg1 = str(e)
# This seems to happen if the key is a public key, let's try
# loading it as a public key.
pk = serialization.load_pem_public_key(
try:
pk = serialization.load_pem_public_key(
raw_pem,
backend=default_backend())
except ValueError as e:
# If loading as public key also fails, that indicates wrong
# passphrase input
msg2 = str(e)
if ("password may be incorrect" in msg1 and
"Are you sure this is a public key" in msg2):
raise Exception("Invalid passphrase")

if isinstance(pk, RSAPrivateKey):
if pk.key_size not in RSA_KEY_SIZES:
Expand Down
72 changes: 41 additions & 31 deletions scripts/imgtool/keys/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

# SPDX-License-Identifier: Apache-2.0

import binascii
import io
import os
import sys

from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, PublicKeyTypes
from cryptography.hazmat.primitives.hashes import Hash, SHA256

from imgtool import keys

AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"


def key_types_matching(key: PrivateKeyTypes, enckey: PublicKeyTypes):
type_dict = {keys.ECDSA256P1: keys.ECDSA256P1Public,
keys.ECDSA384P1: keys.ECDSA384P1Public,
keys.Ed25519: keys.X25519Public,
keys.RSA: keys.RSAPublic}
return type_dict[type(key)] == type(enckey)


class FileHandler(object):
def __init__(self, file, *args, **kwargs):
self.file_in = file
Expand All @@ -34,7 +44,7 @@ def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout,
len_format=None):
with FileHandler(file, 'w') as file:
self._emit_to_output(header, trailer, encoded_bytes, indent,
file, len_format)
file, len_format)

def _emit_to_output(self, header, trailer, encoded_bytes, indent, file,
len_format):
Expand Down Expand Up @@ -62,27 +72,27 @@ def _emit_raw(self, encoded_bytes, file):

def emit_c_public(self, file=sys.stdout):
self._emit(
header="const unsigned char {}_pub_key[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=self.get_public_bytes(),
indent=" ",
len_format="const unsigned int {}_pub_key_len = {{}};"
.format(self.shortname()),
file=file)
header="const unsigned char {}_pub_key[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=self.get_public_bytes(),
indent=" ",
len_format="const unsigned int {}_pub_key_len = {{}};"
.format(self.shortname()),
file=file)

def emit_c_public_hash(self, file=sys.stdout):
digest = Hash(SHA256())
digest.update(self.get_public_bytes())
self._emit(
header="const unsigned char {}_pub_key_hash[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=digest.finalize(),
indent=" ",
len_format="const unsigned int {}_pub_key_hash_len = {{}};"
.format(self.shortname()),
file=file)
header="const unsigned char {}_pub_key_hash[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=digest.finalize(),
indent=" ",
len_format="const unsigned int {}_pub_key_hash_len = {{}};"
.format(self.shortname()),
file=file)

def emit_raw_public(self, file=sys.stdout):
self._emit_raw(self.get_public_bytes(), file=file)
Expand All @@ -94,22 +104,22 @@ def emit_raw_public_hash(self, file=sys.stdout):

def emit_rust_public(self, file=sys.stdout):
self._emit(
header="static {}_PUB_KEY: &[u8] = &["
.format(self.shortname().upper()),
trailer="];",
encoded_bytes=self.get_public_bytes(),
indent=" ",
file=file)
header="static {}_PUB_KEY: &[u8] = &["
.format(self.shortname().upper()),
trailer="];",
encoded_bytes=self.get_public_bytes(),
indent=" ",
file=file)

def emit_public_pem(self, file=sys.stdout):
with FileHandler(file, 'w') as file:
print(str(self.get_public_pem(), 'utf-8'), file=file, end='')

def emit_private(self, minimal, format, file=sys.stdout):
self._emit(
header="const unsigned char enc_priv_key[] = {",
trailer="};",
encoded_bytes=self.get_private_bytes(minimal, format),
indent=" ",
len_format="const unsigned int enc_priv_key_len = {};",
file=file)
header="const unsigned char enc_priv_key[] = {",
trailer="};",
encoded_bytes=self.get_private_bytes(minimal, format),
indent=" ",
len_format="const unsigned int enc_priv_key_len = {};",
file=file)
51 changes: 29 additions & 22 deletions scripts/imgtool/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#! /usr/bin/env python3
#
# Copyright 2017-2020 Linaro Limited
# Copyright 2019-2023 Arm Limited
# Copyright 2019-2024 Arm Limited
#
# SPDX-License-Identifier: Apache-2.0
#
Expand All @@ -28,6 +28,7 @@
import hashlib
import base64
from imgtool import image, imgtool_version
from imgtool.keys.general import key_types_matching
from imgtool.version import decode_version
from imgtool.dumpinfo import dump_imginfo
from .keys import (
Expand Down Expand Up @@ -99,12 +100,29 @@ def save_signature(sigfile, sig):


def load_key(keyfile):
# TODO: better handling of invalid pass-phrase
key = keys.load(keyfile)
try:
key = keys.load(keyfile)
except FileNotFoundError as e:
print(f"Key file not found: {keyfile}")
raise e
if key is not None:
return key

# Key is password protected
passwd = getpass.getpass("Enter key passphrase: ").encode('utf-8')
return keys.load(keyfile, passwd)
try:
key = keys.load(keyfile, passwd)
except Exception as e:
msg = str(e)
if "Invalid passphrase" in msg:
print(msg)
exit(1)
else:
raise e
if key is None:
print("Could not load key for unknown error")
exit(1)
return key


def get_password():
Expand Down Expand Up @@ -157,9 +175,8 @@ def getpub(key, encoding, lang, output):

if not output:
output = sys.stdout
if key is None:
print("Invalid passphrase")
elif lang == 'c' or encoding == 'lang-c':

if lang == 'c' or encoding == 'lang-c':
key.emit_c_public(file=output)
elif lang == 'rust' or encoding == 'lang-rust':
key.emit_rust_public(file=output)
Expand Down Expand Up @@ -189,9 +206,8 @@ def getpubhash(key, output, encoding):

if not output:
output = sys.stdout
if key is None:
print("Invalid passphrase")
elif encoding == 'lang-c':

if encoding == 'lang-c':
key.emit_c_public_hash(file=output)
elif encoding == 'raw':
key.emit_raw_public_hash(file=output)
Expand All @@ -212,8 +228,6 @@ def getpubhash(key, output, encoding):
@click.command(help='Dump private key from keypair')
def getpriv(key, minimal, format):
key = load_key(key)
if key is None:
print("Invalid passphrase")
try:
key.emit_private(minimal, format)
except (RSAUsageError, ECDSAUsageError, Ed25519UsageError,
Expand Down Expand Up @@ -460,16 +474,9 @@ def sign(key, public_key_format, align, version, pad_sig, header_size,
img.load(infile)
key = load_key(key) if key else None
enckey = load_key(encrypt) if encrypt else None
if enckey and key:
if ((isinstance(key, keys.ECDSA256P1) and
not isinstance(enckey, keys.ECDSA256P1Public))
or (isinstance(key, keys.ECDSA384P1) and
not isinstance(enckey, keys.ECDSA384P1Public))
or (isinstance(key, keys.RSA) and
not isinstance(enckey, keys.RSAPublic))):
# FIXME
raise click.UsageError("Signing and encryption must use the same "
"type of key")
if enckey and key and not key_types_matching(key, enckey):
raise click.UsageError("Encryption must use the public pair of the "
"same type of key used for signing")

if pad_sig and hasattr(key, 'pad_sig'):
key.pad_sig = True
Expand Down
1 change: 1 addition & 0 deletions scripts/tests/assets/images/one.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
Loading
Loading