1# Copyright (c) 2023 Nordic Semiconductor ASA
2#
3# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
5
6import logging
7from pathlib import Path
8
9import pytest
10from twister_harness import DeviceAdapter, MCUmgr, Shell
11from twister_harness.helpers.utils import find_in_config, match_lines, match_no_lines
12from utils import check_with_mcumgr_command, check_with_shell_command
13from west_sign_wrapper import west_sign_with_imgtool
14
15logger = logging.getLogger(__name__)
16
17
18def create_signed_image(build_dir: Path, app_build_dir: Path, version: str) -> Path:
19    image_to_test = Path(build_dir) / 'test_{}.signed.bin'.format(
20        version.replace('.', '_').replace('+', '_'))
21    origin_key_file = find_in_config(
22        Path(build_dir) / 'mcuboot' / 'zephyr' / '.config',
23        'CONFIG_BOOT_SIGNATURE_KEY_FILE'
24    )
25    west_sign_with_imgtool(
26        build_dir=Path(app_build_dir),
27        output_bin=image_to_test,
28        key_file=Path(origin_key_file),
29        version=version
30    )
31    assert image_to_test.is_file()
32    return image_to_test
33
34
35def clear_buffer(dut: DeviceAdapter) -> None:
36    disconnect = False
37    if not dut.is_device_connected():
38        dut.connect()
39        disconnect = True
40    dut.clear_buffer()
41    if disconnect:
42        dut.disconnect()
43
44
45def test_upgrade_with_confirm(dut: DeviceAdapter, shell: Shell, mcumgr: MCUmgr):
46    """
47    Verify that the application can be updated
48    1) Device flashed with MCUboot and an application that contains SMP server
49    2) Prepare an update of an application containing the SMP server
50    3) Upload the application update to slot 1 using mcumgr
51    4) Flag the application update in slot 1 as 'pending' by using mcumgr 'test'
52    5) Restart the device, verify that swapping process is initiated
53    6) Verify that the updated application is booted
54    7) Confirm the image using mcumgr
55    8) Restart the device, and verify that the new application is still booted
56    """
57    logger.info('Prepare upgrade image')
58    new_version = '0.0.2+0'
59    image_to_test = create_signed_image(dut.device_config.build_dir,
60                                        dut.device_config.app_build_dir, new_version)
61
62    logger.info('Upload image with mcumgr')
63    dut.disconnect()
64    mcumgr.image_upload(image_to_test)
65
66    logger.info('Test uploaded APP image')
67    second_hash = mcumgr.get_hash_to_test()
68    mcumgr.image_test(second_hash)
69    clear_buffer(dut)
70    mcumgr.reset_device()
71
72    dut.connect()
73    output = dut.readlines_until('Launching primary slot application')
74    match_lines(output, [
75        'Swap type: test',
76        'Starting swap using move algorithm'
77    ])
78    logger.info('Verify new APP is booted')
79    check_with_shell_command(shell, new_version, swap_type='test')
80    dut.disconnect()
81    check_with_mcumgr_command(mcumgr, new_version)
82
83    logger.info('Confirm the image')
84    mcumgr.image_confirm(second_hash)
85    mcumgr.reset_device()
86
87    dut.connect()
88    output = dut.readlines_until('Launching primary slot application')
89    match_no_lines(output, [
90        'Starting swap using move algorithm'
91    ])
92    logger.info('Verify new APP is still booted')
93    check_with_shell_command(shell, new_version)
94
95
96def test_upgrade_with_revert(dut: DeviceAdapter, shell: Shell, mcumgr: MCUmgr):
97    """
98    Verify that MCUboot will roll back an image that is not confirmed
99    1) Device flashed with MCUboot and an application that contains SMP server
100    2) Prepare an update of an application containing the SMP server
101    3) Upload the application update to slot 1 using mcumgr
102    4) Flag the application update in slot 1 as 'pending' by using mcumgr 'test'
103    5) Restart the device, verify that swapping process is initiated
104    6) Verify that the updated application is booted
105    7) Reset the device without confirming the image
106    8) Verify that MCUboot reverts update
107    """
108    origin_version = find_in_config(
109        Path(dut.device_config.app_build_dir) / 'zephyr' / '.config',
110        'CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION'
111    )
112    logger.info('Prepare upgrade image')
113    new_version = '0.0.3+0'
114    image_to_test = create_signed_image(dut.device_config.build_dir,
115                                        dut.device_config.app_build_dir, new_version)
116
117    logger.info('Upload image with mcumgr')
118    dut.disconnect()
119    mcumgr.image_upload(image_to_test)
120
121    logger.info('Test uploaded APP image')
122    second_hash = mcumgr.get_hash_to_test()
123    mcumgr.image_test(second_hash)
124    clear_buffer(dut)
125    mcumgr.reset_device()
126
127    dut.connect()
128    output = dut.readlines_until('Launching primary slot application')
129    match_lines(output, [
130        'Swap type: test',
131        'Starting swap using move algorithm'
132    ])
133    logger.info('Verify new APP is booted')
134    check_with_shell_command(shell, new_version, swap_type='test')
135    dut.disconnect()
136    check_with_mcumgr_command(mcumgr, new_version)
137
138    logger.info('Revert images')
139    mcumgr.reset_device()
140
141    dut.connect()
142    output = dut.readlines_until('Launching primary slot application')
143    match_lines(output, [
144        'Swap type: revert',
145        'Starting swap using move algorithm'
146    ])
147    logger.info('Verify that MCUboot reverts update')
148    check_with_shell_command(shell, origin_version)
149
150
151@pytest.mark.parametrize(
152    'key_file', [None, 'root-ec-p256.pem'],
153    ids=[
154        'no_key',
155        'invalid_key'
156    ])
157def test_upgrade_signature(dut: DeviceAdapter, shell: Shell, mcumgr: MCUmgr, key_file):
158    """
159    Verify that the application is not updated when app is not signed or signed with invalid key
160    1) Device flashed with MCUboot and an application that contains SMP server
161    2) Prepare an update of an application containing the SMP server that has
162       been signed:
163       a) without any key
164       b) with a different key than MCUboot was compiled with
165    3) Upload the application update to slot 1 using mcumgr
166    4) Flag the application update in slot 1 as 'pending' by using mcumgr 'test'
167    5) Restart the device, verify that swap is not started
168    """
169    if key_file:
170        origin_key_file = find_in_config(
171            Path(dut.device_config.build_dir) / 'mcuboot' / 'zephyr' / '.config',
172            'CONFIG_BOOT_SIGNATURE_KEY_FILE'
173        ).strip('"\'')
174        key_file = Path(origin_key_file).parent / key_file
175        assert key_file.is_file()
176        assert not key_file.samefile(origin_key_file)
177        image_to_test = Path(dut.device_config.build_dir) / 'test_invalid_key.bin'
178        logger.info('Sign second image with an invalid key')
179    else:
180        image_to_test = Path(dut.device_config.build_dir) / 'test_no_key.bin'
181        logger.info('Sign second imagewith no key')
182
183    west_sign_with_imgtool(
184        build_dir=Path(dut.device_config.app_build_dir),
185        output_bin=image_to_test,
186        key_file=key_file,
187        version='0.0.3+4'  # must differ from the origin version, if not then hash is not updated
188    )
189    assert image_to_test.is_file()
190
191    logger.info('Upload image with mcumgr')
192    dut.disconnect()
193    mcumgr.image_upload(image_to_test)
194
195    logger.info('Test uploaded APP image')
196    second_hash = mcumgr.get_hash_to_test()
197    mcumgr.image_test(second_hash)
198
199    logger.info('Verify that swap is not started')
200    clear_buffer(dut)
201    mcumgr.reset_device()
202
203    dut.connect()
204    output = dut.readlines_until('Launching primary slot application')
205    match_no_lines(output, ['Starting swap using move algorithm'])
206    match_lines(output, ['Image in the secondary slot is not valid'])
207