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