diff --git a/tools/idf_py_actions/serial_ext.py b/tools/idf_py_actions/serial_ext.py index a8a1630821..57cc406114 100644 --- a/tools/idf_py_actions/serial_ext.py +++ b/tools/idf_py_actions/serial_ext.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import json import os @@ -290,7 +290,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: format: str, md5_disable: str, flash_offset: str, - fill_flash_size: str) -> None: + fill_flash_size: str, + merge_args: str) -> None: ensure_build_directory(args, ctx.info_name) project_desc = _get_project_desc(ctx, args) merge_bin_args = [PYTHON, '-m', 'esptool'] @@ -320,7 +321,10 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: yellow_print('idf.py merge-bin: --fill-flash-size is only valid for RAW format, option will be ignored.') else: merge_bin_args += ['--fill-flash-size', fill_flash_size] - merge_bin_args += ['@flash_args'] + if not merge_args: + merge_bin_args += ['@flash_args'] + else: + merge_bin_args += ['@{}'.format(merge_args)] print(f'Merged binary {output} will be created in the build directory...') RunTool('merge_bin', merge_bin_args, args.build_dir, build_dir=args.build_dir, hints=not args.no_hints)() @@ -424,6 +428,15 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: generate_signing_key_args += [extra_args['keyfile']] RunTool('espsecure', generate_signing_key_args, args.project_dir)() + def secure_generate_key_digest(action: str, ctx: click.core.Context, args: PropertyDict, keyfile:str, output:str, **extra_args: str) -> None: + ensure_build_directory(args, ctx.info_name) + generate_key_digest_args = [PYTHON, '-m', 'espsecure', 'digest_sbv2_public_key'] + if keyfile: + generate_key_digest_args += ['--keyfile', keyfile] + if output: + generate_key_digest_args += ['--output', output] + RunTool('espsecure', generate_key_digest_args, args.project_dir)() + def secure_sign_data(action: str, ctx: click.core.Context, args: PropertyDict, @@ -468,6 +481,35 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: verify_signature_args += [extra_args['datafile']] RunTool('espsecure', verify_signature_args, args.build_dir)() + def secure_generate_nvs_partition_key(action: str, + ctx: click.core.Context, + args: PropertyDict, + encryption_scheme: str, + keyfile: str, + hmac_keyfile: str, + **extra_args: str) -> None: + ensure_build_directory(args, ctx.info_name) + generate_nvs_partition_key_args = [PYTHON, '-m', 'esp_idf_nvs_partition_gen', 'generate-key'] + if encryption_scheme == 'HMAC': + generate_nvs_partition_key_args += ['--key_protect_hmac'] + generate_nvs_partition_key_args += ['--kp_hmac_keygen'] + generate_nvs_partition_key_args += ['--kp_hmac_keyfile', hmac_keyfile] + generate_nvs_partition_key_args += ['--keyfile', keyfile] + + RunTool('espsecure', generate_nvs_partition_key_args, args.project_dir)() + + def secure_encrypt_nvs_partition(action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, **extra_args: str) -> None: + ensure_build_directory(args, ctx.info_name) + encrypt_nvs_partition_args = [PYTHON, '-m', 'esp_idf_nvs_partition_gen', 'encrypt'] + encrypt_nvs_partition_args += ['--inputkey', keyfile] + if extra_args['input_file']: + encrypt_nvs_partition_args += [extra_args['input_file']] + if extra_args['output_file']: + encrypt_nvs_partition_args += [extra_args['output_file']] + if extra_args['partition_size']: + encrypt_nvs_partition_args += [extra_args['partition_size']] + RunTool('espsecure', encrypt_nvs_partition_args, args.project_dir)() + def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: Dict) -> List: efuse_args = [] if args.port: @@ -639,6 +681,13 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: 'help': ('[ONLY RAW] If set, the final binary file will be padded with FF bytes up to this flash size.'), 'type': click.Choice(['256KB', '512KB', '1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB']), }, + { + 'names': ['--merge-args'], + 'help': ( + 'Filepath to specify which binaries should be merged with their respective addresses. ' + 'The file format should be the same as flash_args.' + ), + } ], 'dependencies': ['all'], # all = build }, @@ -781,6 +830,20 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: }, ], }, + 'secure-generate-key-digest': { + 'callback': secure_generate_key_digest, + 'help': ('Generate a digest of a puiblic key file for use with secure boot.'), + 'options': [ + { + 'names': ['--keyfile', '-k'], + 'help': ('Public key file for digest generation.'), + }, + { + 'names': ['--output', '-o'], + 'help': ('Output file for key digest.'), + }, + ], + }, 'secure-sign-data': { 'callback': secure_sign_data, 'help': ('Sign a data file for use with secure boot. Signing algorithm is deterministic ECDSA w/ SHA-512 (V1) or either RSA-' @@ -851,6 +914,51 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: }, ], }, + 'secure-generate-nvs-partition-key': { + 'callback': secure_generate_nvs_partition_key, + 'help': 'Generate a key for NVS partition encryption.', + 'options': [ + { + 'names': ['--keyfile', '-k'], + 'help': 'File to store the generated key.', + }, + { + 'names': ['--encryption-scheme', '-s'], + 'help': 'Encryption scheme to use.', + 'type': click.Choice(['HMAC', 'Flash']), + 'default': 'HMAC', + }, + { + 'names': ['--hmac-keyfile', '-l'], + 'help': 'File to store the generated HMAC key.', + + } + ], + }, + 'secure-encrypt-nvs-partition': { + 'callback': secure_encrypt_nvs_partition, + 'help': 'Encrypt the NVS partition.', + 'options': [ + { + 'names': ['--keyfile', '-k'], + 'help': 'File with NVS partition key.', + } + ], + 'arguments': [ + { + 'names': ['input_file'], + 'nargs': 1, + }, + { + 'names': ['output_file'], + 'nargs': 1, + }, + { + 'names': ['partition_size'], + 'nargs': 1, + } + ] + }, 'efuse-burn': { 'callback': efuse_burn, 'help': 'Burn the eFuse with the specified name.', @@ -878,8 +986,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: 'names': ['--force-write-always'], 'is_flag': True, 'help': ( - "Write the eFuse even if it looks like it's already been written, or is write protected." - "Note that this option can't disable write protection, or clear any bit which has already been set." + 'Write the eFuse even if it looks like it\'s already been written, or is write protected.' + 'Note that this option can\'t disable write protection, or clear any bit which has already been set.' ), }, { @@ -977,7 +1085,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: 'Baud rate for monitor. ' 'If this option is not provided IDF_MONITOR_BAUD and MONITORBAUD ' 'environment variables, global baud rate and project_description.json in build directory ' - "(generated by CMake from project's sdkconfig) " + '(generated by CMake from project\'s sdkconfig) ' 'will be checked for default value.' ), }, diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index 4e5826cc0f..b715a78945 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import json import os @@ -409,6 +409,7 @@ class TestSecureCommands(TestWrapperCommands): subprocess.run([sys.executable, idf_py_path, 'build'], stdout=subprocess.DEVNULL) cls.flash_encryption_key = 'test_key.bin' cls.signing_key = 'test_signing_key.pem' + cls.nvs_partition_key = 'nvs_partition_key.bin' def secure_generate_flash_encryption_key(self): generate_key_command = [sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', self.flash_encryption_key] @@ -449,19 +450,6 @@ class TestSecureCommands(TestWrapperCommands): self.assertIn('Using 256-bit key', output) self.assertIn('Done', output) - def secure_generate_signing_key(self): - generate_key_command = [sys.executable, - idf_py_path, - 'secure-generate-signing-key', - '--version', - '2', - '--scheme', - 'rsa3072', - self.signing_key] - output = self.call_command(generate_key_command) - self.assertIn(f'RSA 3072 private key in PEM format written to {self.signing_key}', output) - self.assertIn('Done', output) - def secure_sign_data(self): self.secure_generate_signing_key() sign_command = [sys.executable, @@ -490,6 +478,43 @@ class TestSecureCommands(TestWrapperCommands): output = self.call_command(sign_command) self.assertIn('verification successful', output) + def secure_generate_signing_key(self): + generate_key_command = [sys.executable, + idf_py_path, + 'secure-generate-signing-key', + '--version', + '2', + '--scheme', + 'rsa3072', + self.signing_key] + output = self.call_command(generate_key_command) + self.assertIn(f'RSA 3072 private key in PEM format written to {self.signing_key}', output) + + def test_secure_generate_key_digest(self): + self.secure_generate_signing_key() + digest_command = [sys.executable, + idf_py_path, + 'secure-generate-key-digest', + '--keyfile', + f'{self.signing_key}', + '--output', + 'key_digest.bin'] + output = self.call_command(digest_command) + self.assertIn(f'Writing the public key digest of {self.signing_key} to key_digest.bin', output) + + def test_secure_generate_nvs_partition_key(self): + generate_key_command = [sys.executable, + idf_py_path, + 'secure-generate-nvs-partition-key', + '--keyfile', + f'{self.nvs_partition_key}', + '--encryption-scheme', + 'HMAC', + '--hmac-keyfile', + 'nvs_partition_key.bin'] + output = self.call_command(generate_key_command) + self.assertIn('Created encryption keys:', output) + class TestMergeBinCommands(TestWrapperCommands): """