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.
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'. 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. Signed-off-by: Lorenz Kofler <[email protected]> --- Changes in v2: - Reject a missing engine key-name before attempting to sign. - Remove automatic PKCS#11 URI construction. Pass the engine key identifiers unchanged. - Explain that, when OpenSSL is missing, _sign_engine() returns exactly RSAS[sign_name] zero-filled bytes so binman can report the missing tool without failing the signature-size check. - Move the change to the testPreLoadInvalidKey() test to patch 1. tools/binman/btool/openssl.py | 28 +++++++++++ tools/binman/etype/pre_load.py | 88 +++++++++++++++++++++++++++++----- 2 files changed, 104 insertions(+), 12 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 @@ def __init__(self, name): 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..d3687ebebc9 100644 --- a/tools/binman/etype/pre_load.py +++ b/tools/binman/etype/pre_load.py @@ -42,13 +42,20 @@ 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 and pre-load-key-path is unused. With + engine = "pkcs11", supply a full PKCS#11 URI as key-name. + For example, this creates an image with a pre-load header and a binary:: binman { @@ -69,6 +76,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 +97,7 @@ def __init__(self, section, etype, node): 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 +106,45 @@ def ReadNode(self): 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 + """ + 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, self.key_name, 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 +162,7 @@ def _CreateHeader(self): """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 +170,34 @@ def _CreateHeader(self): 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") + if self.engine and not self.key_name: + self.Raise("key-name must be provided when using an engine") + + # 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 openssl is missing, _sign_engine() returns a placeholder through + # _sign() containing exactly RSAS[sign_name] zero-filled bytes, so this + # check passes and binman can report the missing tool later. + 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 +231,7 @@ def ProcessContents(self): 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') -- 2.55.0
