# # Copyright (c) 2017-2018 Future Electronics # Copyright (c) 2018-2019 Cypress Semiconductor Corporation # # 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 os import sys import subprocess from array import array from struct import (pack, unpack) from shutil import copy2 import json from intelhex import IntelHex, hex2bin, bin2hex from ..config import ConfigException # The size of the program data in Cypress HEX files is limited to 0x80000000 # Higher addresses contain additional metadata (chip protection, eFuse data, etc..) CY_PROGRAM_SIZE = 0x80000000 # The starting address of the program data checksum section CY_CHECKSUM_ADDR = 0x90300000 # The starting address of the .cymeta section (12 bytes) # Additional metadata include silicon revision, Silicon/JTAG ID, etc. CY_META_ADDR = 0x90500000 # The address of the silicon ID (4 bytes) CY_META_SILICON_ID_ADDR = 0x90500002 # The address of the metadata checksum (4 bytes) CY_META_CHECKSUM_ADDR = 0x90500008 # Secure Boot defines MCUBOOT_HEADER_SIZE = 1024 SPE_IMAGE_ID = 1 NSPE_IMAGE_ID = 16 SMIF_MEM_MAP_START = 0x18000000 class AddSignatureError(Exception): """ A simple class that represents all the exceptions associated with adding signature to Secure Boot image """ # Patch Cypress hex file: # - update checksum # - update metadata # - align regions to page (256 bytes) boundary def patch(message_func, ihex, hexf, align=256): update_checksum = False update_metadata = False # calculate checksum of the program section, detect metadata checksum = 0 for start, end in ihex.segments(): if start == CY_CHECKSUM_ADDR: # checksum section found in the original hex update_checksum = True if start == CY_META_ADDR: # metadata section found in the original hex update_metadata = True if start >= CY_PROGRAM_SIZE: continue segment = ihex.tobinarray(start, end) checksum += sum(segment) # only update checksum if it was found in the original hex if update_checksum: lowchecksum = checksum & 0x0FFFF message_func("Calculated checksum for %s is 0x%04x" % (hexf, lowchecksum)) checksum_str = pack('>H', lowchecksum) ihex.frombytes(array('B', checksum_str), offset=CY_CHECKSUM_ADDR) # only update metadata if it was found in the original hex if update_metadata: signature = unpack('>L', ihex.tobinstr(start=CY_META_SILICON_ID_ADDR, size=4))[0] sigcheck = pack('>L', (checksum + signature) & 0x0FFFF) ihex.frombytes(array('B', sigcheck), offset=CY_META_CHECKSUM_ADDR) # align flash segments align_mask = align - 1 alignments = IntelHex() for start, end in ihex.segments(): if start >= CY_PROGRAM_SIZE: continue aligned_start = start & ~align_mask if start != aligned_start: message_func("Aligning start from 0x%x to 0x%x" % (start, aligned_start)) alignments.frombytes(ihex.tobinarray(aligned_start, start - 1), aligned_start) aligned_end = end & ~align_mask if end != aligned_end: aligned_end += align message_func("Aligning end from 0x%x to 0x%x" % (end, aligned_end)) alignments.frombytes(ihex.tobinarray(end, aligned_end - 1), end) ihex.merge(alignments, 'ignore') def merge_images(hexf0, hexf1=None): ihex = IntelHex() ihex.padding = 0x00 ihex.loadfile(hexf0, "hex") if hexf1 is not None: # Merge the CM0+ image ihex1 = IntelHex(hexf1) ihex.merge(ihex1, 'ignore') return ihex def complete_func(message_func, elf0, hexf0, hexf1=None, dest=None): message_func("Postprocessing %s -> %s" % (elf0, hexf0)) ihex = merge_images(hexf0, hexf1) patch(message_func, ihex, hexf0) ihex.write_hex_file(dest if dest else hexf0, write_start_addr=False, byte_count=16) # Find Cortex M0 image. def find_cm0_image(toolchain, resources, elf, hexf, hex_filename): if hex_filename is None: return None # Locate user-specified image from tools.resources import FileType hex_files = resources.get_file_paths(FileType.HEX) m0hexf = next((f for f in hex_files if os.path.basename(f) == hex_filename), None) if toolchain.target.is_PSA_non_secure_target: m0hexf = next((f for f in hex_files if os.path.basename(f) == os.path.basename(hexf)), m0hexf) if m0hexf: toolchain.notify.info("M0 core image file found: %s." % m0hexf) else: toolchain.notify.info("M0 core hex image file %s not found. Aborting." % hex_filename) raise ConfigException("Required M0 core hex image not found.") return m0hexf # check if policy parameters are consistent def check_slots_integrity(toolchain, fw_cyb, target_data, fw_spe=None, fw_nspe=None): """ Function checks consistency of parameters presented in policy file used for build of Secure Boot enabled target. :param toolchain: Toolchain object of current build session :param fw_cyb: CyBootloader firmware description from policy :param target_data: Object contains description of processing target from target.json :param fw_spe: CM0p firmware descpription object from policy :param fw_nspe: CM4 firmware descpription object from policy :return: List of slots and image id corresponding to them """ slot0 = None slot1 = None if fw_spe is None: img_id = fw_nspe["id"] # check single stage scheme if not (fw_cyb["launch"] == img_id): # may be PSA NSPE part if not fw_cyb["launch"] == SPE_IMAGE_ID: toolchain.notify.debug("[PSOC6.sign_image] WARNING: ID of build image " + str(img_id) + " does not correspond launch ID in CyBootloader - " + str(fw_cyb["launch"])) else: toolchain.notify.info("[PSOC6.sign_image] INFO: NSPE image ID is " + str(img_id) + ". It will be launched by SPE part.") # check slots addresses and sizes if upgrade is set to True for slot in fw_nspe["resources"]: if slot["type"] == "BOOT": slot0 = slot if fw_nspe["upgrade"] is True: slot1 = slot if slot["type"] == "UPGRADE": if fw_nspe.get("encrypt") is True: # mark slot1 image as one, that should be encrypted slot1.update({'encrypt': True}) toolchain.notify.info("[PSOC6.sign_image] INFO: Image for UPGRADE NSPE will" " be ENCRYPTED per policy settings.") else: pass else: toolchain.notify.info("[PSOC6.sign_image] INFO: Image for UPGRADE will not" " be built per policy settings.") break if slot0 is None: toolchain.notify.debug("[PSOC6.sign_image] WARNING: BOOT section not found in policy resources") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") else: # check if PSA targets flash map correspond to slots addresses and sizes in policy if not (int(target_data["overrides"]["secure-rom-start"], 16) - MCUBOOT_HEADER_SIZE) ==\ int(fw_spe["resources"][0]["address"]): toolchain.notify.debug("[PSOC6.sign_image] WARNING: SPE start address " "does not correspond BOOT slot start address defined in policy. " "Check if MCUboot header offset 0x400 is included in SPE flash start") if not (int(target_data["overrides"]["non-secure-rom-start"], 16) - MCUBOOT_HEADER_SIZE) ==\ int(fw_nspe["resources"][0]["address"]): toolchain.notify.debug("[PSOC6.sign_image] WARNING: NSPE start address " "does not correspond BOOT slot start address defined in policy. " "Check if MCUboot header offset 0x400 is included in NSPE flash start") if (int(target_data["overrides"]["secure-rom-size"], 16) + MCUBOOT_HEADER_SIZE) >\ int(fw_spe["resources"][0]["size"]): toolchain.notify.debug("[PSOC6.sign_image] WARNING: SPE flash size " "does not fit in BOOT slot size defined in policy.") if (int(target_data["overrides"]["non-secure-rom-size"], 16) + MCUBOOT_HEADER_SIZE) >\ int(fw_nspe["resources"][0]["size"]): toolchain.notify.debug("[PSOC6.sign_image] WARNING: NSPE flash size " "does not fit in BOOT slot size defined in policy.") img_id = fw_spe["id"] # check dual stage scheme if img_id != 1: toolchain.notify.debug("[PSOC6.sign_image] ERROR: Image ID of SPE image" " is not equal to 1!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") if not (fw_cyb["launch"] == img_id): toolchain.notify.debug("[PSOC6.sign_image] ERROR: ID of build image" " does not correspond launch ID in CyBootloader!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") if not (fw_spe["launch"] == fw_nspe["id"]): toolchain.notify.debug("[PSOC6.sign_image] ERROR: ID of NSPE image" " does not correspond launch ID in SPE part!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") # check slots addresses and sizes if upgrade is set to True for slot in fw_spe["resources"]: if slot["type"] == "BOOT": slot0 = slot if fw_spe["upgrade"] is True: if slot["type"] == "UPGRADE": slot1 = slot if fw_spe.get("encrypt") is True: # mark slot1 image as one, that should be encrypted slot1.update({'encrypt': True}) toolchain.notify.info("[PSOC6.sign_image] INFO: Image for UPGRADE SPE will" " be ENCRYPTED per policy settings.") else: pass else: toolchain.notify.info("[PSOC6.sign_image] INFO: Image for UPGRADE will not" " be produced per policy settings.") if slot0 is None: toolchain.notify.debug("[PSOC6.sign_image] WARNING: BOOT section not found in policy resources") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") if slot1 is not None: # bigger or equal to 0x18000000 in hex is a start of SMIF memory if slot1["address"] >= SMIF_MEM_MAP_START: toolchain.notify.info("[PSOC6.sign_image] INFO: UPGRADE slot will be resided in external flash") if slot0["size"] != slot1["size"]: toolchain.notify.debug("[PSOC6.sign_image] WARNING: BOOT and UPGRADE slots sizes are not equal") return (slot0, slot1, img_id) else: return (slot0, None, img_id) def process_target(toolchain, target): """ Gathers and process information about target being built :param toolchain: Toolchain object of current build session :param target: Name of target being built :return: List with all data needed for adding signature """ from pathlib import Path targets_json = Path("targets/targets.json") cy_targets = Path("targets/TARGET_Cypress/TARGET_PSOC6/") sb_params_file_name = Path("secure_image_parameters.json") root_dir = Path(os.getcwd()) mbed_os_targets = root_dir / targets_json if not os.path.isfile(str(mbed_os_targets)): # try location for tests mbed_os_targets = root_dir / 'mbed-os' / targets_json root_dir = root_dir / 'mbed-os' if not os.path.isfile(str(mbed_os_targets)): toolchain.notify.debug("[PSOC6.sign_image] ERROR: targets.json not found!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") with open(str(mbed_os_targets)) as j: json_str = j.read() all_targets = json.loads(json_str) j.close() processing_target = all_targets[target] sb_params_file_path = root_dir / cy_targets / Path("TARGET_" + str(target)) / sb_params_file_name if os.path.isfile(str(sb_params_file_path)): with open(str(sb_params_file_path)) as f: json_str = f.read() sb_config = json.loads(json_str) f.close() else: toolchain.notify.debug("[PSOC6.sign_image] ERROR: secure_image_parametest.json not found!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") sdk_path = root_dir / sb_config["sdk_path"] priv_key_path = sdk_path / Path(sb_config["priv_key_file"]) if not os.path.isfile(str(priv_key_path)): toolchain.notify.debug("[PSOC6.sign_image] ERROR: Private key file not found in " + str(priv_key_path)) raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") if "_PSA" in target: # assume dual stage bootloading scheme with open(sdk_path / Path(sb_config["policy_file"])) as p: policy_str = p.read() policy_file = json.loads(policy_str) p.close() firmware_list = policy_file["boot_upgrade"]["firmware"] # collect firmware descriptions from policy for corresponding images firmware_cyb_cm0p = firmware_list[0] if "_M0_" in target: firmware_spe_cm0p = firmware_list[1] firmware_nspe_cm4 = firmware_list[2] slots = check_slots_integrity(toolchain, fw_cyb=firmware_cyb_cm0p, fw_spe=firmware_spe_cm0p, fw_nspe=firmware_nspe_cm4, target_data=processing_target) else: firmware_nspe_cm4 = firmware_list[2] slots = check_slots_integrity(toolchain, fw_cyb=firmware_cyb_cm0p, fw_nspe=firmware_nspe_cm4, target_data=processing_target) else: # consider single stage bootloading scheme with open(sdk_path / Path(sb_config["policy_file"])) as p: policy_str = p.read() policy_file = json.loads(policy_str) p.close() firmware_list = policy_file["boot_upgrade"]["firmware"] firmware_cyb_cm0p = firmware_list[0] firmware_nspe_cm4 = firmware_list[1] slots = check_slots_integrity(toolchain, fw_cyb=firmware_cyb_cm0p, fw_nspe=firmware_nspe_cm4, target_data=processing_target) target_sig_data = [{"img_data": sb_config["boot0"], "slot_data": slots[0], "key_file": sb_config["priv_key_file"], "sdk_path": sdk_path, "id": slots[2]}] if slots[1] is not None: target_sig_data.append({"img_data": sb_config["boot1"], "slot_data": slots[1], "key_file": sb_config["priv_key_file"], "sdk_path": sdk_path, "id": slots[2]}) # check if slot1 image sould be encrypted if slots[1].get("encrypt") is True: dev_pub_key = sdk_path / Path(sb_config["dev_pub_key_file"]) if not os.path.isfile(str(dev_pub_key)): toolchain.notify.debug("[PSOC6.sign_image] ERROR: Device public key file not found in " + str(dev_pub_key)) raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") aes_key_file = sdk_path / Path(sb_config["aes_key_file"]) if not os.path.isfile(str(aes_key_file)): toolchain.notify.debug("[PSOC6.sign_image] ERROR: AES-128 key file not found in " + str(aes_key_file)) raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") target_sig_data[1].update({"aes_key": sb_config["aes_key_file"], "dev_pub_key": sb_config["dev_pub_key_file"]}) else: toolchain.notify.info("[PSOC6.sign_image] INFO: Image for slot UPGRADE would not be encrypted per policy settings") return target_sig_data def sign_image(toolchain, binf): """ Adds signature to a binary file being built, prepares some intermediate binary artifacts. :param toolchain: Toolchain object of current build session :param binf: Binary file created for target """ from pathlib import PurePath target_sig_data = None # reserve name for separate NSPE image out_cm4_hex = binf[:-4] + "_cm4.hex" # preserve original hex file from mbed-os build mbed_hex = binf[:-4] + "_unsigned.hex" copy2(binf, mbed_hex) # find target name and type before processing for part in PurePath(binf).parts: if "CY" in part: target_sig_data = process_target(toolchain=toolchain, target=part) if target_sig_data is None: toolchain.notify.debug("[PSOC6.sign_image] ERROR: Target not found!") raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") for slot in target_sig_data: # first check if image for slot under processing should be encrypted if slot["slot_data"].get("encrypt") is True: # call encrypt_img to perform encryption args = [sys.executable, str(slot["sdk_path"] / "encrypted_image_runner.py"), "--sdk-path", str(slot["sdk_path"]), "--hex-file", os.getcwd() + '/' + mbed_hex, "--key-priv", str(slot["sdk_path"] / slot["key_file"]), "--key-pub", str(slot["sdk_path"] / slot["dev_pub_key"]), "--key-aes", str(slot["sdk_path"] / slot["aes_key"]), "--ver", str(slot["img_data"]["VERSION"]), "--img-id", str(slot["id"]), "--rlb-count", str(slot["img_data"]["ROLLBACK_COUNTER"]), "--slot-size", str(hex(slot["slot_data"]["size"])), "--img-offset", str(slot["slot_data"]["address"])] if slot["slot_data"]["type"] != "BOOT": args.append("--pad") process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # catch standard process pipes outputs stderr = process.communicate()[1] stdout = process.communicate()[0] rc = process.wait() toolchain.notify.info(stdout.decode("utf-8")) if rc != 0: toolchain.notify.debug("[PSOC6.sign_image] ERROR: Encryption script ended with error!") toolchain.notify.debug("[PSOC6.sign_image] Message from encryption script: " + stderr.decode("utf-8")) raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") else: toolchain.notify.info("[PSOC6.sign_image] SUCCESS: Image for slot " + slot["slot_data"]["type"] + " is signed and encrypted with no errors!") # all non ecrypted images take this path else: if slot["slot_data"]["type"] == "UPGRADE": out_hex_name = binf[:-4] + "_upgrade.hex" else: out_hex_name = binf out_bin_name = out_hex_name[:-4] + "_signed.bin" # call imgtool for signature args = [sys.executable, str(slot["sdk_path"] / "imgtool/imgtool.py"), "sign", "--key", str(slot["sdk_path"] / slot["key_file"]), "--header-size", str(hex(MCUBOOT_HEADER_SIZE)), "--pad-header", "--align", "8", "--version", str(slot["img_data"]["VERSION"]), "--image-id", str(slot["id"]), "--rollback_counter", str(slot["img_data"]["ROLLBACK_COUNTER"]), "--slot-size", str(hex(slot["slot_data"]["size"])), "--overwrite-only", mbed_hex, out_hex_name] if slot["slot_data"]["type"] != "BOOT": args.append("--pad") process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # catch stderr outputs stderr = process.communicate()[1] rc = process.wait() if rc != 0: toolchain.notify.debug("[PSOC6.sign_image] ERROR: Signature is not added!") toolchain.notify.debug("[PSOC6.sign_image] Message from imgtool: " + stderr.decode("utf-8")) raise AddSignatureError("PSOC6.sign_image finished execution with errors! Signature is not added.") else: toolchain.notify.info("[PSOC6.sign_image] SUCCESS: Image for slot " + slot["slot_data"]["type"] + " is signed with no errors!") # preserve signed binary file hex2bin(out_hex_name, out_bin_name) # preserve separate hex for cm4 # 16 is image ID for NSPE image if slot["id"] == NSPE_IMAGE_ID: copy2(out_hex_name, out_cm4_hex) # produce hex file for slot1 if slot["slot_data"]["type"] == "UPGRADE": bin2hex(out_bin_name, out_hex_name, offset=int(slot["slot_data"]["address"])) toolchain.notify.info("Image UPGRADE: " + out_hex_name + "\n") def complete(toolchain, elf0, hexf0, hexf1=None): """ Merge CM4 and CM0 images to a single binary """ complete_func(toolchain.notify.debug, elf0, hexf0, hexf1)