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: initial sanity test (for CI build) #1819

Merged
merged 4 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/imgtool.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
branches:
- main
- v*-branch
pull_request:

name: imgtool

Expand All @@ -11,7 +12,38 @@ concurrency:
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.x", "pypy3.9"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pipenv'
cache-dependency-path: |
scripts/setup.py
- name: Install dependencies
run: |
pip install --user pipenv
- name: Run tests
run: |
cd scripts
pipenv run pip install pytest -e .
pipenv run pytest --junitxml=../junit/pytest-results-${{ matrix.python-version }}.xml
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: pytest-results-${{ matrix.python-version }}
path: |
junit/pytest-results-${{ matrix.python-version }}*.xml
if-no-files-found: ignore
environment:
mingulov marked this conversation as resolved.
Show resolved Hide resolved
if: ${{ github.event_name == 'push' }}
mingulov marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions scripts/imgtool.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let
python37.pkgs.intelhex
python37.pkgs.setuptools
python37.pkgs.cbor2
python37.pkgs.pyyaml
]
);
in
Expand Down
58 changes: 37 additions & 21 deletions scripts/imgtool/keys/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,39 @@

# SPDX-License-Identifier: Apache-2.0

import binascii
import io
import os
import sys
from cryptography.hazmat.primitives.hashes import Hash, SHA256

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


class FileHandler(object):
def __init__(self, file, *args, **kwargs):
self.file_in = file
self.args = args
self.kwargs = kwargs

def __enter__(self):
if isinstance(self.file_in, (str, bytes, os.PathLike)):
self.file = open(self.file_in, *self.args, **self.kwargs)
else:
self.file = self.file_in
return self.file

def __exit__(self, *args):
if self.file != self.file_in:
self.file.close()


class KeyClass(object):
def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout,
len_format=None):
if file and file is not sys.stdout:
with open(file, 'w') as file:
self._emit_to_output(header, trailer, encoded_bytes, indent,
file, len_format)
else:
with FileHandler(file, 'w') as file:
self._emit_to_output(header, trailer, encoded_bytes, indent,
sys.stdout, len_format)
file, len_format)

def _emit_to_output(self, header, trailer, encoded_bytes, indent, file,
len_format):
Expand All @@ -33,6 +50,16 @@ def _emit_to_output(self, header, trailer, encoded_bytes, indent, file,
if len_format is not None:
print(len_format.format(len(encoded_bytes)), file=file)

def _emit_raw(self, encoded_bytes, file):
with FileHandler(file, 'wb') as file:
try:
# file.buffer is not part of the TextIOBase API
# and may not exist in some implementations.
file.buffer.write(encoded_bytes)
except AttributeError:
# raw binary data, can be for example io.BytesIO
file.write(encoded_bytes)

def emit_c_public(self, file=sys.stdout):
self._emit(
header="const unsigned char {}_pub_key[] = {{"
Expand All @@ -58,20 +85,12 @@ def emit_c_public_hash(self, file=sys.stdout):
file=file)

def emit_raw_public(self, file=sys.stdout):
if file and file is not sys.stdout:
with open(file, 'wb') as file:
file.write(self.get_public_bytes())
else:
sys.stdout.buffer.write(self.get_public_bytes())
self._emit_raw(self.get_public_bytes(), file=file)

def emit_raw_public_hash(self, file=sys.stdout):
digest = Hash(SHA256())
digest.update(self.get_public_bytes())
if file and file is not sys.stdout:
with open(file, 'wb') as file:
file.write(digest.finalize())
else:
sys.stdout.buffer.write(digest.finalize())
self._emit_raw(digest.finalize(), file=file)

def emit_rust_public(self, file=sys.stdout):
self._emit(
Expand All @@ -83,11 +102,8 @@ def emit_rust_public(self, file=sys.stdout):
file=file)

def emit_public_pem(self, file=sys.stdout):
if file and file is not sys.stdout:
with open(file, 'w') as file:
print(str(self.get_public_pem(), 'utf-8'), file=file, end='')
else:
print(str(self.get_public_pem(), 'utf-8'), file=sys.stdout, end='')
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(
Expand Down
19 changes: 19 additions & 0 deletions scripts/imgtool/keys/rsa_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,34 @@ def test_emit(self):
for key_size in RSA_KEY_SIZES:
k = RSA.generate(key_size=key_size)

pubpem = io.StringIO()
k.emit_public_pem(pubpem)
self.assertIn("BEGIN PUBLIC KEY", pubpem.getvalue())
self.assertIn("END PUBLIC KEY", pubpem.getvalue())

ccode = io.StringIO()
k.emit_c_public(ccode)
self.assertIn("rsa_pub_key", ccode.getvalue())
self.assertIn("rsa_pub_key_len", ccode.getvalue())

hashccode = io.StringIO()
k.emit_c_public_hash(hashccode)
self.assertIn("rsa_pub_key_hash", hashccode.getvalue())
self.assertIn("rsa_pub_key_hash_len", hashccode.getvalue())

rustcode = io.StringIO()
k.emit_rust_public(rustcode)
self.assertIn("RSA_PUB_KEY", rustcode.getvalue())

# raw data - bytes
pubraw = io.BytesIO()
k.emit_raw_public(pubraw)
self.assertTrue(len(pubraw.getvalue()) > 0)

hashraw = io.BytesIO()
k.emit_raw_public_hash(hashraw)
self.assertTrue(len(hashraw.getvalue()) > 0)

def test_emit_pub(self):
"""Basic sanity check on the code emitters, from public key."""
pubname = self.tname("public.pem")
Expand Down
1 change: 1 addition & 0 deletions scripts/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'intelhex>=2.2.1',
'click',
'cbor2',
'pyyaml',
],
entry_points={
"console_scripts": ["imgtool=imgtool.main:imgtool"]
Expand Down
31 changes: 31 additions & 0 deletions scripts/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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 pytest

# List of tests expected to fail for some reason
XFAILED_TESTS = {
"tests/test_keys.py::test_getpriv[openssl-ed25519]",
"tests/test_keys.py::test_getpriv[openssl-x25519]",
"tests/test_keys.py::test_getpriv[pkcs8-rsa-2048]",
"tests/test_keys.py::test_getpriv[pkcs8-rsa-3072]",
"tests/test_keys.py::test_getpriv[pkcs8-ed25519]",
"tests/test_keys.py::test_getpub[pem-ed25519]",
"tests/test_keys.py::test_sign_verify[x25519]",
}


def pytest_runtest_setup(item):
if item.nodeid in XFAILED_TESTS:
pytest.xfail()
112 changes: 112 additions & 0 deletions scripts/tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 pytest

from click.testing import CliRunner
from imgtool.main import imgtool
from imgtool import imgtool_version

# all available imgtool commands
COMMANDS = [
"create",
"dumpinfo",
"getpriv",
"getpub",
"getpubhash",
"keygen",
"sign",
"verify",
"version",
]


def test_new_command():
"""Check that no new commands had been added,
so that tests would be updated in such case"""
for cmd in imgtool.commands:
assert cmd in COMMANDS


def test_help():
"""Simple test for the imgtool's help option,
mostly just to see that it can be started"""
runner = CliRunner()

result_short = runner.invoke(imgtool, ["-h"])
assert result_short.exit_code == 0

result_long = runner.invoke(imgtool, ["--help"])
assert result_long.exit_code == 0
assert result_short.output == result_long.output

# by default help should be also produced
result_empty = runner.invoke(imgtool)
assert result_empty.exit_code == 0
assert result_empty.output == result_short.output


def test_version():
"""Check that some version info is produced"""
runner = CliRunner()

result = runner.invoke(imgtool, ["version"])
assert result.exit_code == 0
assert result.output == imgtool_version + "\n"

result_help = runner.invoke(imgtool, ["version", "-h"])
assert result_help.exit_code == 0
assert result_help.output != result.output


def test_unknown():
"""Check that unknown command will be handled"""
runner = CliRunner()

result = runner.invoke(imgtool, ["unknown"])
assert result.exit_code != 0


@pytest.mark.parametrize("command", COMMANDS)
def test_cmd_help(command):
"""Check that all commands have some help"""
runner = CliRunner()

result_short = runner.invoke(imgtool, [command, "-h"])
assert result_short.exit_code == 0

result_long = runner.invoke(imgtool, [command, "--help"])
assert result_long.exit_code == 0

assert result_short.output == result_long.output


@pytest.mark.parametrize("command1", COMMANDS)
@pytest.mark.parametrize("command2", COMMANDS)
def test_cmd_dif_help(command1, command2):
"""Check that all commands have some different help"""
runner = CliRunner()

result_general = runner.invoke(imgtool, "--help")
assert result_general.exit_code == 0

result_cmd1 = runner.invoke(imgtool, [command1, "--help"])
assert result_cmd1.exit_code == 0
assert result_cmd1.output != result_general.output

if command1 != command2:
result_cmd2 = runner.invoke(imgtool, [command2, "--help"])
assert result_cmd2.exit_code == 0

assert result_cmd1.output != result_cmd2.output