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