The pre-load entry type imports a private key from a file and signs with
Cryptodome, so the key must be readable as a file. Keys held in an HSM
or on a smart card cannot be used. The FIT entry type is not affected as
it delegates signing to mkimage, which signs through the OpenSSL engine
API.

Add an optional 'engine' property. When set, sign with openssl using
that engine and interpret 'key-name' as the engine-specific key
identifier, instead of reading a key from 'pre-load-key-path'.

For the pkcs11 engine, expand a 'key-name' that is not already a PKCS#11
URI to:

    pkcs11:object=<key-name>;type=private

With an engine, binman never loads the key itself and so cannot check
its size before signing. Check the length of the resulting RSA signature
instead, which is the modulus size either way.

Behavior is otherwise unchanged when 'engine' is absent.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Lorenz Kofler <[email protected]>
---
 tools/binman/btool/openssl.py  | 28 +++++++++++
 tools/binman/etype/pre_load.py | 87 +++++++++++++++++++++++++++++-----
 tools/binman/ftest.py          | 12 +++--
 3 files changed, 112 insertions(+), 15 deletions(-)

diff --git a/tools/binman/btool/openssl.py b/tools/binman/btool/openssl.py
index b26f087c447..20e4e16e537 100644
--- a/tools/binman/btool/openssl.py
+++ b/tools/binman/btool/openssl.py
@@ -36,6 +36,34 @@ class Bintoolopenssl(bintool.Bintool):
             name, 'openssl cryptography toolkit',
             version_regex=r'OpenSSL (.*) \(', version_args='version')
 
+    def sign(self, input_fname, output_fname, key, hash_name, padding_name,
+             engine=None):
+        """Sign a file
+
+        The key is a filename, or, when an engine is used, anything the engine
+        accepts as a key identifier, e.g. a PKCS#11 URI.
+
+        Args:
+            input_fname (str): Filename holding the data to sign
+            output_fname (str): Filename to write the signature to
+            key (str): Key filename, or key identifier for the engine
+            hash_name (str): Hash algorithm to use, e.g. 'sha256'
+            padding_name (str): Padding to use, 'pkcs-1.5' or 'pss'
+            engine (str): OpenSSL engine to use for signing, or None to sign
+                with a local key file
+
+        Returns:
+            str: Tool output
+        """
+        args = ['dgst', f'-{hash_name}', '-sign', key, '-out', output_fname]
+        if engine:
+            args += ['-engine', engine, '-keyform', 'engine']
+        if padding_name == 'pss':
+            args += ['-sigopt', 'rsa_padding_mode:pss',
+                     '-sigopt', 'rsa_pss_saltlen:max']
+        args.append(input_fname)
+        return self.run_cmd(*args)
+
     def x509_cert(self, cert_fname, input_fname, key_fname, cn, revision,
                   config_fname):
         """Create a certificate
diff --git a/tools/binman/etype/pre_load.py b/tools/binman/etype/pre_load.py
index 0cf635ab23a..e6842732a59 100644
--- a/tools/binman/etype/pre_load.py
+++ b/tools/binman/etype/pre_load.py
@@ -42,13 +42,21 @@ class Entry_pre_load(Entry_collection):
         - content: List of phandles to entries to sign
         - algo-name: Hash and signature algo to use for the signature
         - padding-name: Name of the padding (pkcs-1.5 or pss)
-        - key-name: Filename of the private key to sign
+        - key-name: Filename of the private key to sign, or the key
+          identifier when 'engine' is used
         - header-size: Total size of the header
         - version: Version of the header
+        - engine: OpenSSL engine to sign with, instead of a key file
+          (optional)
 
     This entry creates a pre-load header that contains a global
     image signature.
 
+    If 'engine' is set, openssl signs with that engine, so the private key
+    does not have to be a file. With engine = "pkcs11" a key-name that is not
+    already a PKCS#11 URI becomes ``pkcs11:object=<key-name>;type=private``
+    and pre-load-key-path is unused.
+
     For example, this creates an image with a pre-load header and a binary::
 
         binman {
@@ -69,6 +77,18 @@ class Entry_pre_load(Entry_collection):
                 };
             };
         };
+
+    With an engine the key is named by an identifier rather than a file::
+
+        pre-load {
+            content = <&image>;
+            algo-name = "sha256,rsa2048";
+            padding-name = "pss";
+            key-name = "pkcs11:object=preload;type=private";
+            engine = "pkcs11";
+            header-size = <4096>;
+            version = <1>;
+        };
     """
 
     def __init__(self, section, etype, node):
@@ -78,6 +98,7 @@ class Entry_pre_load(Entry_collection):
         self.key_name = fdt_util.GetString(self._node, 'key-name')
         self.header_size = fdt_util.GetInt(self._node, 'header-size')
         self.version = fdt_util.GetInt(self._node, 'version')
+        self.engine = fdt_util.GetString(self._node, 'engine')
 
     def ReadNode(self):
         super().ReadNode()
@@ -86,18 +107,49 @@ class Entry_pre_load(Entry_collection):
         if self.key_path is None:
             self.key_path = ''
 
-    def _sign(self, data, key, hash_name, padding_name):
-        """Sign data with a key
+    def _sign_engine(self, data, hash_name, padding_name):
+        """Sign data with an OpenSSL engine
 
         Args:
             data (bytes): Data to sign
-            key (RsaKey): Key to sign with
+            hash_name (str): Hash algorithm to use
+            padding_name (str): Padding to use
+
+        Returns:
+            bytes: Signature, or zeroes if openssl is missing
+        """
+        key_id = self.key_name
+        if self.engine == 'pkcs11' and not key_id.startswith('pkcs11:'):
+            key_id = f'pkcs11:object={key_id};type=private'
+
+        uniq = self.GetUniqueName()
+        input_fname = tools.get_output_filename(f'pre-load.in.{uniq}')
+        sig_fname = tools.get_output_filename(f'pre-load.sig.{uniq}')
+        tools.write_file(input_fname, data)
+
+        if self.openssl.sign(input_fname, sig_fname, key_id, hash_name,
+                             padding_name, self.engine) is None:
+            self.record_missing_bintool(self.openssl)
+            sign_name = self.algo_name.split(',')[1]
+            return tools.get_bytes(0, int(RSAS[sign_name]))
+
+        return tools.read_file(sig_fname)
+
+    def _sign(self, data, key, hash_name, padding_name):
+        """Sign data with an engine or with a key file
+
+        Args:
+            data (bytes): Data to sign
+            key (RsaKey): Key to sign with, None when an engine is used
             hash_name (str): Hash algorithm to use
             padding_name (str): Padding to use
 
         Returns:
             bytes: Signature
         """
+        if self.engine:
+            return self._sign_engine(data, hash_name, padding_name)
+
         hash_data = SHAS[hash_name].new()
         hash_data.update(data)
 
@@ -115,7 +167,7 @@ class Entry_pre_load(Entry_collection):
         """Create a pre load header"""
         hash_name, sign_name = self.algo_name.split(',')
         padding_name = self.padding_name
-        key_name = os.path.join(self.key_path, self.key_name)
+        key = None
 
         # Check hash and signature name/type
         if hash_name not in SHAS:
@@ -123,21 +175,28 @@ class Entry_pre_load(Entry_collection):
         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")
-
         if padding_name is None:
             padding_name = "pkcs-1.5"
         if padding_name not in ("pss", "pkcs-1.5"):
             self.Raise(padding_name + " is not supported")
 
+        # Read the key, unless an engine holds it
+        if not self.engine:
+            key_name = os.path.join(self.key_path, self.key_name)
+            key = RSA.import_key(tools.read_file(key_name))
+
+            # Check file-backed keys before attempting a signing operation
+            if key.size_in_bytes() != RSAS[sign_name]:
+                self.Raise("The key " + self.key_name +
+                           " don't have the expected size")
+
         # Compute the signature
         sig = self._sign(self.image, key, hash_name, padding_name)
 
+        # An engine does not expose its key, so infer its size from the 
signature
+        if self.engine and len(sig) != RSAS[sign_name]:
+            self.Raise("The key " + self.key_name + " don't have the expected 
size")
+
         hash_sig = SHA256.new()
         hash_sig.update(sig)
 
@@ -171,3 +230,7 @@ class Entry_pre_load(Entry_collection):
         self.image = self.GetContents(True)
         data = self._CreateHeader()
         return self.ProcessContentsUpdate(data)
+
+    def AddBintools(self, btools):
+        super().AddBintools(btools)
+        self.openssl = self.AddBintool(btools, 'openssl')
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index a0e8bde7901..63fbf50089f 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5888,12 +5888,18 @@ fdt         fdtmap                Extract the 
devicetree blob from the fdtmap
 
     def testPreLoadInvalidKey(self):
         """Test an image with a pre-load header with an invalid key"""
+        from binman.etype import pre_load
+
         entry_args = {
             'pre-load-key-path': os.path.join(self._binman_dir, 'test'),
         }
-        with self.assertRaises(ValueError) as e:
-            data = self._DoReadFileDtb('security/pre_load_invalid_key.dts',
-                                       entry_args=entry_args)
+        with unittest.mock.patch.object(
+                pre_load.Entry_pre_load, '_sign',
+                side_effect=AssertionError('signing should not be attempted')):
+            with self.assertRaisesRegex(ValueError,
+                                        "don't have the expected size"):
+                self._DoReadFileDtb('security/pre_load_invalid_key.dts',
+                                    entry_args=entry_args)
 
     def testPreLoadEncryptedFit(self):
         """Test an encrypted FIT image with a pre-load header"""
-- 
2.55.0

Reply via email to