Extend the pre-load entry type to support signing via hardware security modules (HSM) using PKCS#11. The implementation integrates with the pkcs11_tool btool to perform signing operations.
Signed-off-by: Yan WANG <[email protected]> --- tools/binman/etype/pre_load.py | 285 +++++++++++++++++++++++++-------- 1 file changed, 214 insertions(+), 71 deletions(-) diff --git a/tools/binman/etype/pre_load.py b/tools/binman/etype/pre_load.py index 0d953cb258e..26e2c5eb3be 100644 --- a/tools/binman/etype/pre_load.py +++ b/tools/binman/etype/pre_load.py @@ -2,7 +2,7 @@ # Copyright (c) 2022 Softathome # Written by Philippe Reynes <[email protected]> # -# Entry-type for the global header +# Entry-type for the global header with file-based and HSM signing support # import os @@ -22,9 +22,9 @@ from Cryptodome.Signature import pss PRE_LOAD_MAGIC = b'UBSH' RSAS = { - 'rsa1024': 1024 / 8, - 'rsa2048': 2048 / 8, - 'rsa4096': 4096 / 8 + 'rsa1024': 1024 // 8, + 'rsa2048': 2048 // 8, + 'rsa4096': 4096 // 8 } SHAS = { @@ -33,8 +33,9 @@ SHAS = { 'sha512': SHA512 } + class Entry_pre_load(Entry_collection): - """Pre load image header + """Pre load image header with file-based or HSM signing support Properties / Entry arguments: - pre-load-key-path: Path of the directory that store key (provided by @@ -46,29 +47,57 @@ class Entry_pre_load(Entry_collection): - header-size: Total size of the header - version: Version of the header - This entry creates a pre-load header that contains a global - image signature. + HSM Signing Properties (optional): + - hsm-support: Enable HSM signing (boolean) + - hsm-library: Path to PKCS#11 library (.so file) + - hsm-token-label: HSM token label + - hsm-token-serial: HSM token serial number (alternative to token-label) + - hsm-key-label: HSM private key label + - hsm-key-id: HSM private key ID (alternative to key-label) + - hsm-pin: HSM user PIN + + This entry creates a pre-load header that contains a global image signature. + The signature can be created using either a traditional RSA key file or a + Hardware Security Module (HSM) via PKCS#11. - For example, this creates an image with a pre-load header and a binary:: + Example with file-based signing:: binman { - image2 { - filename = "sandbox.bin"; - - pre-load { - content = <&image>; - algo-name = "sha256,rsa2048"; - padding-name = "pss"; - key-name = "private.pem"; - header-size = <4096>; - version = <1>; - }; - - image: blob-ext { - filename = "sandbox.itb"; - }; + pre-load { + content = <&image>; + algo-name = "sha256,rsa2048"; + padding-name = "pss"; + key-name = "private.pem"; + header-size = <4096>; + version = <1>; + }; + + image: blob-ext { + filename = "sandbox.itb"; + }; + }; + + Example with HSM signing (SoftHSM):: + + binman { + pre-load { + content = <&image>; + algo-name = "sha256,rsa2048"; + padding-name = "pss"; + header-size = <4096>; + version = <1>; + hsm-support; + hsm-library = "/usr/lib/softhsm/libsofthsm2.so"; + hsm-token-label = "uboot-hsm"; + hsm-key-label = "uboot-key"; + hsm-pin = "1234"; + }; + + image: blob-ext { + filename = "sandbox.itb"; }; }; + """ def __init__(self, section, etype, node): @@ -79,6 +108,19 @@ class Entry_pre_load(Entry_collection): self.header_size = fdt_util.GetInt(self._node, 'header-size') self.version = fdt_util.GetInt(self._node, 'version') + # HSM properties (optional) + self.hsm_support = fdt_util.GetBool(self._node, 'hsm-support', False) + self.hsm_library = fdt_util.GetString(self._node, 'hsm-library') + self.hsm_token_label = fdt_util.GetString(self._node, 'hsm-token-label') + self.hsm_token_serial = fdt_util.GetString(self._node, 'hsm-token-serial') + self.hsm_slot_index = fdt_util.GetString(self._node, 'hsm-slot-index') + self.hsm_key_label = fdt_util.GetString(self._node, 'hsm-key-label') + self.hsm_key_id = fdt_util.GetString(self._node, 'hsm-key-id') + self.hsm_pin = fdt_util.GetString(self._node, 'hsm-pin') + + # Will be set in AddBintools if HSM is used + self.pkcs11_tool = None + def ReadNode(self): super().ReadNode() self.key_path, = self.GetEntryArgsOrProps( @@ -86,68 +128,169 @@ class Entry_pre_load(Entry_collection): if self.key_path is None: self.key_path = '' - def _CreateHeader(self): - """Create a pre load header""" + def AddBintools(self, btools): + """Add required btools + + Register pkcs11-tool btool if HSM signing is enabled. + """ + super().AddBintools(btools) + if self.hsm_support: + self.pkcs11_tool = self.AddBintool(btools, 'pkcs11_tool') + + def _SignData(self, data): + """Sign data using either file-based key or HSM + + This method provides a unified interface for signing data regardless + of whether a traditional RSA key file or an HSM is used. The signing + backend is selected based on the hsm-support property. + + Args: + data (bytes): Data to sign + + Returns: + bytes: Signature + + Raises: + ValueError: If configuration is invalid + RuntimeError: If signing operation fails + """ hash_name, sign_name = self.algo_name.split(',') - padding_name = self.padding_name - key_name = os.path.join(self.key_path, self.key_name) - - # Check hash and signature name/type - if hash_name not in SHAS: - self.Raise(hash_name + " is not supported") - if sign_name not in RSAS: - self.Raise(sign_name + " is not supported") - - # Read the key - key = RSA.import_key(tools.read_file(key_name)) - - # Check if the key has the expected size - if key.size_in_bytes() != RSAS[sign_name]: - self.Raise("The key " + self.key_name + " don't have the expected size") - - # Compute the hash - hash_image = SHAS[hash_name].new() - hash_image.update(self.image) - - # Compute the signature - if padding_name is None: - padding_name = "pkcs-1.5" - padding = None - padding_args = None - if padding_name == "pss": - salt_len = key.size_in_bytes() - hash_image.digest_size - 2 - padding = pss - padding_args = {'salt_bytes': salt_len} - elif padding_name == "pkcs-1.5": - padding = pkcs1_15 - padding_args = {} + padding_name = self.padding_name or 'pkcs-1.5' + padding_btool = None + + if self.hsm_support: + # HSM-based signing via pkcs11-tool btool + if not self.pkcs11_tool.is_present(): + self.Raise("pkcs11-tool btool not available for HSM signing") + + if not self.hsm_library: + self.Raise("hsm-library is required for HSM signing") + + # Normalize padding name for btool + if padding_name == 'pss': + padding_btool = 'pss' + elif padding_name == 'pkcs-1.5': + padding_btool = 'pkcs1v15' + else: + self.Raise(f"Unsupported padding scheme: {padding_name}") + + # Use slot index if provided (simpler than serial lookup) + try: + signature = self.pkcs11_tool.sign( + data=data, + module=self.hsm_library, + token_label=self.hsm_token_label, + token_serial=self.hsm_token_serial, + slot_index=self.hsm_slot_index, + key_label=self.hsm_key_label, + key_id=self.hsm_key_id, + pin=self.hsm_pin, + hash_algo=hash_name, + sign_algo=sign_name, + padding=padding_btool + ) + except (ValueError, RuntimeError) as e: + self.Raise(f"HSM signing failed: {e}") + else: - self.Raise(padding_name + " is not supported") + # File-based signing (traditional method) + if not self.key_name: + self.Raise("key-name is required for file-based signing") + + key_name = os.path.join(self.key_path, self.key_name) + + # Check hash and signature name/type + if hash_name not in SHAS: + self.Raise(f"{hash_name} is not supported") + if sign_name not in RSAS: + self.Raise(f"{sign_name} is not supported") + + # Read the key + key = RSA.import_key(tools.read_file(key_name)) + + # Check if the key has the expected size + if key.size_in_bytes() != RSAS[sign_name]: + self.Raise( + f"The key {self.key_name} doesn't have the expected size " + f"({key.size_in_bytes()} bytes vs expected " + f"{RSAS[sign_name]} bytes)" + ) - sig = padding.new(key, **padding_args).sign(hash_image) + # Compute the hash + hash_obj = SHAS[hash_name].new() + hash_obj.update(data) - hash_sig = SHA256.new() - hash_sig.update(sig) + # Setup padding + padding_module = None + padding_args = {} + if padding_name == "pss": + salt_len = key.size_in_bytes() - hash_obj.digest_size - 2 + padding_module = pss + padding_args = {'salt_bytes': salt_len} + elif padding_name == "pkcs-1.5": + padding_module = pkcs1_15 + padding_args = {} + else: + self.Raise(f"{padding_name} is not supported") + + # Sign + signature = padding_module.new(key, **padding_args).sign(hash_obj) + + return signature + + def _CreateHeader(self): + """Create a pre-load header with signature + This method creates the pre-load header regardless of whether signing + is done via file-based keys or HSM. The signing backend is abstracted + in _SignData(), eliminating code duplication. + + The header format is: + - First header (64 bytes): magic, version, sizes, hash of image signature + - Header signature (variable): signature of first header + - Image signature (variable): signature of image data + - Padding: zeros to fill header_size + + Returns: + bytes: Complete pre-load header with padding + """ + # Sign the image data + img_sig = self._SignData(self.image) + + # Compute hash of image signature (always SHA256 for header) + hash_sig = SHA256.new(img_sig) + + # Build header fields version = self.version header_size = self.header_size image_size = len(self.image) - ofs_img_sig = 64 + len(sig) + ofs_img_sig = 64 + len(img_sig) flags = 0 reserved0 = 0 reserved1 = 0 - first_header = struct.pack('>4sIIIIIII32s', PRE_LOAD_MAGIC, - version, header_size, image_size, - ofs_img_sig, flags, reserved0, - reserved1, hash_sig.digest()) + # Pack first header (64 bytes, big-endian) + first_header = struct.pack( + '>4sIIIIIII32s', + PRE_LOAD_MAGIC, # 4 bytes: magic number 'UBSH' + version, # 4 bytes: header version + header_size, # 4 bytes: total header size + image_size, # 4 bytes: image size + ofs_img_sig, # 4 bytes: offset to image signature + flags, # 4 bytes: flags (reserved) + reserved0, # 4 bytes: reserved + reserved1, # 4 bytes: reserved + hash_sig.digest() # 32 bytes: SHA256 hash of image signature + ) + + # Sign the header + hdr_sig = self._SignData(first_header) - hash_first_header = SHAS[hash_name].new() - hash_first_header.update(first_header) - sig_first_header = padding.new(key, **padding_args).sign(hash_first_header) + # Assemble complete header: first_header + header_sig + image_sig + data = first_header + hdr_sig + img_sig - data = first_header + sig_first_header + sig - pad = bytearray(self.header_size - len(data)) + # Add padding to reach header_size + pad = bytearray(self.header_size - len(data)) return data + pad -- 2.43.0
