[PATCH v3 2/2] binman: x509_cert: add PKCS#11/HSM signing support

Quentin Schulz quentin.schulz at cherry.de
Mon Apr 27 12:20:21 CEST 2026


Hi Sergio,

On 4/23/26 5:45 PM, Sergio Prado wrote:
> Allow X509 certificates used for TI K3 secure boot to be signed via an
> HSM using the PKCS#11 standard, so that the private key never leaves
> the hardware token.
> 
> Two new make variables are introduced:
> 
>    BINMAN_PKCS11_URI    PKCS#11 URI identifying the signing key, e.g.
>                         pkcs11:token=mytoken;object=mykey;type=private
>    BINMAN_PKCS11_MODULE Path to the PKCS#11 shared library (.so), e.g.
>                         /usr/lib/softhsm/libsofthsm2.so
> 
> When BINMAN_PKCS11_URI is set, it is forwarded to binman as the
> pkcs11-uri entry argument, overriding the keyfile property at signing
> time. BINMAN_PKCS11_MODULE is optional and only needed when the PKCS#11
> library is not already configured via openssl.cnf or the
> PKCS11_MODULE_PATH environment variable.
> 
> The openssl bintool gains three helper methods:
> 
>    _pkcs11_use_provider() auto-detects whether the OpenSSL pkcs11
>      provider (OpenSSL >= 3.1, pkcs11-provider) or the legacy pkcs11
>      engine (libp11) is available, by inspecting 'openssl list
>      -providers'.
> 
>    _build_key_args() builds the -key/-provider/-engine arguments for the
>      openssl command line. When PKCS11_PIN is set in the environment,
>      its value is appended as ?pin-value=<pin> to the URI so that
>      non-interactive signing works with both the provider and engine
>      paths.
> 
>    _run_cmd_pkcs11() sets PKCS11_MODULE_PATH and PKCS11_PROVIDER_MODULE
>      before invoking openssl when a module path is given, and serialises
>      concurrent signing calls with a module-level lock. The lock is
>      required because binman's ThreadPoolExecutor may invoke multiple
>      signing operations simultaneously, and HSMs (including SoftHSM2) do
>      not support concurrent logins from different threads.
> 
> Existing behavior is unchanged when neither BINMAN_PKCS11_URI nor
> BINMAN_PKCS11_MODULE is set.
> 
> Tested with SoftHSM2 and a YubiKey using the verdin-am62_a53_defconfig
> configuration and both engine and provider OpenSSL APIs.
> 
> Signed-off-by: Sergio Prado <sergio.prado at e-labworks.com>
> ---
>   Makefile                        |   2 +
>   tools/binman/binman.rst         |  18 +++++
>   tools/binman/btool/openssl.py   | 117 +++++++++++++++++++++++++++-----
>   tools/binman/etype/x509_cert.py |  47 +++++++++++--
>   tools/binman/ftest.py           | 110 ++++++++++++++++++++++++++++++
>   5 files changed, 274 insertions(+), 20 deletions(-)
> 
> diff --git a/Makefile b/Makefile
> index dfc95d314ddd..8d6c584b2731 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1701,6 +1701,8 @@ cmd_binman = $(srctree)/tools/binman/binman $(if $(BINMAN_DEBUG),-D) \
>   		-a vpl-dtb=$(CONFIG_VPL_OF_REAL) \
>   		-a pre-load-key-path=${PRE_LOAD_KEY_PATH} \
>   		-a of-spl-remove-props=$(CONFIG_OF_SPL_REMOVE_PROPS) \
> +		$(if $(BINMAN_PKCS11_URI),-a pkcs11-uri="$(BINMAN_PKCS11_URI)") \
> +		$(if $(BINMAN_PKCS11_MODULE),-a pkcs11-module="$(BINMAN_PKCS11_MODULE)") \
>   		$(BINMAN_$(@F))
>   
>   OBJCOPYFLAGS_u-boot.ldr.hex := -I binary -O ihex
> diff --git a/tools/binman/binman.rst b/tools/binman/binman.rst
> index 366491089ad9..e60eb7f572eb 100644
> --- a/tools/binman/binman.rst
> +++ b/tools/binman/binman.rst
> @@ -2161,6 +2161,24 @@ BINMAN_INDIRS
>       Sets the search path for input files used by binman by adding one or more
>       `-I` arguments. See :ref:`External blobs`.
>   
> +BINMAN_PKCS11_URI
> +    PKCS#11 URI used to sign boot artifacts via an HSM instead of a PEM key
> +    file on disk. When set, it is passed as ``-a pkcs11-uri=<uri>`` to binman,
> +    which overrides the ``keyfile`` entry argument for all signing operations.
> +    Example::
> +
> +        make BINMAN_PKCS11_URI="pkcs11:token=mytoken;object=mykey;type=private"
> +
> +BINMAN_PKCS11_MODULE
> +    Path to the PKCS#11 shared library (.so) for HSM signing. When set, it is
> +    passed as ``-a pkcs11-module=<path>`` to binman. Only needed when the
> +    module is not already configured via ``openssl.cnf`` or the
> +    ``PKCS11_MODULE_PATH`` environment variable. Typically used together with
> +    ``BINMAN_PKCS11_URI``::
> +
> +        make BINMAN_PKCS11_URI="pkcs11:token=mytoken;object=mykey;type=private" \
> +             BINMAN_PKCS11_MODULE="/usr/lib/pkcs11/libsofthsm2.so"
> +

OK, but why do we want to have this variable for storing the path to the 
module lib instead of using an openssl.cnf?

Also, this is stating it's for signing boot artifacts with an HSM, but 
that isn't true. It won't work with FIT images, which use DT properties 
instead (see fit,engine, fit,engine-keydir/key-name-hint.).

I'm assuming it's possible we want different keys for different boot 
artifacts, so this is also a bit too generic of a name?

>   BINMAN_TOOLPATHS
>       Sets the search path for external tool used by binman by adding one or more
>       `--toolpath` arguments. See :ref:`External tools`.
> diff --git a/tools/binman/btool/openssl.py b/tools/binman/btool/openssl.py
> index b26f087c4470..ec477a46a3d7 100644
> --- a/tools/binman/btool/openssl.py
> +++ b/tools/binman/btool/openssl.py
> @@ -11,10 +11,16 @@ Source code is at https://www.openssl.org/
>   """
>   
>   import hashlib
> +import os
> +import threading
>   
>   from binman import bintool
>   from u_boot_pylib import tools
>   
> +# Serializes concurrent PKCS#11 signing operations. SoftHSM2 and many real
> +# HSMs do not support simultaneous logins from multiple threads, so binman's
> +# ThreadPoolExecutor would otherwise cause intermittent login failures.
> +_pkcs11_lock = threading.Lock()
>   

Is there really no external mechanism to make sure this cannot happen? 
or maybe some sort of session concept?

binman tests will run on multiple processes (see ConcurrentTestSuite) 
when not instructed to run on one thread (via -T 0, used for test 
coverage). My worry is whether this is multiprocess safe (hunch is no). 
Look at what we did for the FIT signing tests, we use different softhsm 
and openssl config files per file and I hope this is enough for openssl 
and softhsm to not get confused. Considering there's a 
multiprocessing.Lock(), there must be a reason for it no?

>   VALID_SHAS = [256, 384, 512, 224]
>   SHA_OIDS = {256:'2.16.840.1.101.3.4.2.1',
> @@ -35,18 +41,89 @@ class Bintoolopenssl(bintool.Bintool):
>           super().__init__(
>               name, 'openssl cryptography toolkit',
>               version_regex=r'OpenSSL (.*) \(', version_args='version')
> +        self._use_provider = None
> +
> +    def _pkcs11_use_provider(self):
> +        """Return True if the provider API should be used for PKCS#11 signing.
> +
> +        Checks whether the pkcs11 provider is available by running
> +        'openssl list -providers'. The result is cached after the first call
> +        to avoid spawning a subprocess on every signing operation.
> +
> +        Returns:
> +            bool: True if provider should be used, False if engine should be used
> +        """
> +        if self._use_provider is None:
> +            out = self.run_cmd('list', '-providers') or ''
> +            self._use_provider = 'pkcs11' in out
> +        return self._use_provider
> +

Why force the user to use providers if one is available? Can't they use 
an engine even if a pkcs11 provider is available (e.g. you have both 
pkcs11 provider support via softhsm2, but only engine support for your 
HSM). Isn't this something the user can specify via openssl.cnf?

> +    def _build_key_args(self, key_fname):
> +        """Build openssl CLI arguments for the signing key.
> +
> +        Always add '-key key_fname', and for a PKCS#11 URI (pkcs11:...),
> +        prepends either '-provider default -provider pkcs11' (if provider API
> +        is available) or '-engine pkcs11 -keyform engine' (legacy fallback).
> +        If the PKCS11_PIN environment variable is set, appends pin-value=<pin>
> +        to the URI.
> +
> +        Args:
> +            key_fname (str): Filename of .pem file or PKCS#11 URI
> +
> +        Returns:
> +            list: openssl arguments for the key selection
> +        """
> +        if key_fname.startswith('pkcs11:'):
> +            pin = os.environ.get('PKCS11_PIN')
> +            if pin:
> +                sep = '&' if '?' in key_fname else '?'
> +                key_fname = f'{key_fname}{sep}pin-value={pin}'
> +            if self._pkcs11_use_provider():
> +                args = ['-provider', 'default', '-provider', 'pkcs11', '-key', key_fname]
> +            else:
> +                args = ['-engine', 'pkcs11', '-keyform', 'engine', '-key', key_fname]
> +        else:
> +            args = ['-key', key_fname]
> +        return args
> +
> +    def _run_cmd_pkcs11(self, pkcs11_module, *args):
> +        """Run an openssl command, optionally passing PKCS#11 module env vars.
> +
> +        When pkcs11_module is set, PKCS11_MODULE_PATH (engine) and
> +        PKCS11_PROVIDER_MODULE (provider) are passed as extra environment
> +        variables.
> +        If already configured via openssl.cnf or the calling environment,
> +        pkcs11_module can be None.
> +
> +        Args:
> +            pkcs11_module (str or None): Path to the PKCS#11 shared library
> +            *args: Arguments forwarded to run_cmd
> +
> +        Returns:
> +            str: openssl output
> +        """
> +        if pkcs11_module:
> +            extra_env = {
> +                'PKCS11_MODULE_PATH': pkcs11_module,
> +                'PKCS11_PROVIDER_MODULE': pkcs11_module,
> +            }
> +            with _pkcs11_lock:
> +                return self.run_cmd(*args, extra_env=extra_env)
> +        return self.run_cmd(*args)
>   
>       def x509_cert(self, cert_fname, input_fname, key_fname, cn, revision,
> -                  config_fname):
> +                  config_fname, pkcs11_module=None):
>           """Create a certificate
>   
>           Args:
>               cert_fname (str): Filename of certificate to create
>               input_fname (str): Filename containing data to sign
> -            key_fname (str): Filename of .pem file
> +            key_fname (str): Filename of .pem file or PKCS#11 URI
>               cn (str): Common name
>               revision (int): Revision number
>               config_fname (str): Filename to write fconfig into
> +            pkcs11_module (str or None): Path to PKCS#11 shared library, or
> +                None if already configured via openssl.cnf or the environment
>   
>           Returns:
>               str: Tool output
> @@ -76,19 +153,20 @@ shaType                = OID:2.16.840.1.101.3.4.2.3
>   shaValue               = FORMAT:HEX,OCT:{hashval}
>   imageSize              = INTEGER:{len(indata)}
>   ''', file=outf)
> -        args = ['req', '-new', '-x509', '-key', key_fname, '-nodes',
> +        args = ['req', '-new', '-x509', *self._build_key_args(key_fname), '-nodes',
>                   '-outform', 'DER', '-out', cert_fname, '-config', config_fname,
>                   '-sha512']
> -        return self.run_cmd(*args)
> +        return self._run_cmd_pkcs11(pkcs11_module, *args)
>   
>       def x509_cert_sysfw(self, cert_fname, input_fname, key_fname, sw_rev,
> -                  config_fname, req_dist_name_dict, firewall_cert_data):
> +                  config_fname, req_dist_name_dict, firewall_cert_data,
> +                  pkcs11_module=None):
>           """Create a certificate to be booted by system firmware
>   
>           Args:
>               cert_fname (str): Filename of certificate to create
>               input_fname (str): Filename containing data to sign
> -            key_fname (str): Filename of .pem file
> +            key_fname (str): Filename of .pem file or PKCS#11 URI
>               sw_rev (int): Software revision
>               config_fname (str): Filename to write fconfig into
>               req_dist_name_dict (dict): Dictionary containing key-value pairs of
> @@ -101,6 +179,8 @@ imageSize              = INTEGER:{len(indata)}
>                   extended certificate
>                 - certificate (str): Extended firewall certificate with
>                   the information for the firewall configurations.
> +            pkcs11_module (str or None): Path to PKCS#11 shared library, or
> +                None if already configured via openssl.cnf or the environment
>   
>           Returns:
>               str: Tool output
> @@ -146,20 +226,20 @@ authInPlace = INTEGER:{hex(firewall_cert_data['auth_in_place'])}
>   numFirewallRegions = INTEGER:{firewall_cert_data['num_firewalls']}
>   {firewall_cert_data['certificate']}
>   ''', file=outf)
> -        args = ['req', '-new', '-x509', '-key', key_fname, '-nodes',
> +        args = ['req', '-new', '-x509', *self._build_key_args(key_fname), '-nodes',
>                   '-outform', 'DER', '-out', cert_fname, '-config', config_fname,
>                   '-sha512']
> -        return self.run_cmd(*args)
> +        return self._run_cmd_pkcs11(pkcs11_module, *args)
>   
>       def x509_cert_rom(self, cert_fname, input_fname, key_fname, sw_rev,
>                     config_fname, req_dist_name_dict, cert_type, bootcore,
> -                  bootcore_opts, load_addr, sha, debug):
> +                  bootcore_opts, load_addr, sha, debug, pkcs11_module=None):
>           """Create a certificate
>   
>           Args:
>               cert_fname (str): Filename of certificate to create
>               input_fname (str): Filename containing data to sign
> -            key_fname (str): Filename of .pem file
> +            key_fname (str): Filename of .pem file or PKCS#11 URI
>               sw_rev (int): Software revision
>               config_fname (str): Filename to write fconfig into
>               req_dist_name_dict (dict): Dictionary containing key-value pairs of
> @@ -170,6 +250,8 @@ numFirewallRegions = INTEGER:{firewall_cert_data['num_firewalls']}
>               bootcore_opts(int): Booting core option, lockstep (0) or split (2) mode
>               load_addr (int): Load address of image
>               sha (int): Hash function
> +            pkcs11_module (str or None): Path to PKCS#11 shared library, or
> +                None if already configured via openssl.cnf or the environment
>   
>           Returns:
>               str: Tool output
> @@ -231,10 +313,10 @@ emailAddress           = {req_dist_name_dict['emailAddress']}
>    coreDbgEn = INTEGER:0
>    coreDbgSecEn = INTEGER:0
>   ''', file=outf)
> -        args = ['req', '-new', '-x509', '-key', key_fname, '-nodes',
> +        args = ['req', '-new', '-x509', *self._build_key_args(key_fname), '-nodes',
>                   '-outform', 'DER', '-out', cert_fname, '-config', config_fname,
>                   '-sha512']
> -        return self.run_cmd(*args)
> +        return self._run_cmd_pkcs11(pkcs11_module, *args)
>   
>       def x509_cert_rom_combined(self, cert_fname, input_fname, key_fname, sw_rev,
>                     config_fname, req_dist_name_dict, load_addr, sha, total_size, num_comps,
> @@ -242,13 +324,14 @@ emailAddress           = {req_dist_name_dict['emailAddress']}
>                     imagesize_sbl, hashval_sbl, load_addr_sysfw, imagesize_sysfw,
>                     hashval_sysfw, load_addr_sysfw_data, imagesize_sysfw_data,
>                     hashval_sysfw_data, sysfw_inner_cert_ext_boot_block,
> -                  dm_data_ext_boot_block, bootcore_opts, debug):
> +                  dm_data_ext_boot_block, bootcore_opts, debug,
> +                  pkcs11_module=None):
>           """Create a certificate
>   
>           Args:
>               cert_fname (str): Filename of certificate to create
>               input_fname (str): Filename containing data to sign
> -            key_fname (str): Filename of .pem file
> +            key_fname (str): Filename of .pem file or PKCS#11 URI
>               sw_rev (int): Software revision
>               config_fname (str): Filename to write fconfig into
>               req_dist_name_dict (dict): Dictionary containing key-value pairs of
> @@ -259,6 +342,8 @@ emailAddress           = {req_dist_name_dict['emailAddress']}
>               load_addr (int): Load address of image
>               sha (int): Hash function
>               bootcore_opts (int): Booting core option, lockstep (0) or split (2) mode
> +            pkcs11_module (str or None): Path to PKCS#11 shared library, or
> +                None if already configured via openssl.cnf or the environment
>   
>           Returns:
>               str: Tool output
> @@ -342,10 +427,10 @@ coreDbgSecEn = INTEGER:0
>   
>   {dm_data_ext_boot_block}
>           ''', file=outf)
> -        args = ['req', '-new', '-x509', '-key', key_fname, '-nodes',
> +        args = ['req', '-new', '-x509', *self._build_key_args(key_fname), '-nodes',
>                   '-outform', 'DER', '-out', cert_fname, '-config', config_fname,
>                   '-sha512']
> -        return self.run_cmd(*args)
> +        return self._run_cmd_pkcs11(pkcs11_module, *args)
>   
>       def fetch(self, method):
>           """Fetch handler for openssl
> diff --git a/tools/binman/etype/x509_cert.py b/tools/binman/etype/x509_cert.py
> index b6e8b0b4fb09..cdeff1a227a1 100644
> --- a/tools/binman/etype/x509_cert.py
> +++ b/tools/binman/etype/x509_cert.py
> @@ -19,6 +19,18 @@ class Entry_x509_cert(Entry_collection):
>   
>       Properties / Entry arguments:
>           - content: List of phandles to entries to sign
> +        - keyfile: Filename of the PEM key file used to sign the binary
> +        - pkcs11-uri: PKCS#11 URI for signing via an HSM, e.g.
> +            ``pkcs11:token=mytoken;object=mykey;type=private``. When provided
> +            via ``-a pkcs11-uri=...`` on the binman command line (or via the
> +            ``BINMAN_PKCS11_URI`` make variable), it overrides ``keyfile`` for
> +            the signing operation.
> +        - pkcs11-module: Path to the PKCS#11 shared library (.so), e.g.
> +            ``/usr/lib/pkcs11/libsofthsm2.so``. Only needed when signing via
> +            HSM and the module is not already configured via ``openssl.cnf`` or
> +            the ``PKCS11_MODULE_PATH`` environment variable. Provided via
> +            ``-a pkcs11-module=...`` or the ``BINMAN_PKCS11_MODULE`` make
> +            variable.
>   
>       Output files:
>           - input.<unique_name> - input file passed to openssl
> @@ -27,6 +39,16 @@ class Entry_x509_cert(Entry_collection):
>   
>       openssl signs the provided data, writing the signature in this entry. This
>       allows verification that the data is genuine
> +
> +    To sign with an HSM, pass the PKCS#11 URI and optionally the module path
> +    at build time::
> +
> +        make BINMAN_PKCS11_URI="pkcs11:token=mytoken;object=mykey;type=private" \\
> +             BINMAN_PKCS11_MODULE="/usr/lib/pkcs11/libsofthsm2.so" \\
> +             PKCS11_PIN="1234"
> +
> +    The ``PKCS11_PIN`` environment variable is used by the openssl bintool to
> +    append ``?pin-value=<pin>`` to the URI when required.
>       """
>       def __init__(self, section, etype, node):
>           super().__init__(section, etype, node)
> @@ -53,6 +75,8 @@ class Entry_x509_cert(Entry_collection):
>           self.dm_data_ext_boot_block = None
>           self.firewall_cert_data = None
>           self.debug = False
> +        self.pkcs11_uri = None
> +        self.pkcs11_module = None
>   
>       def ReadNode(self):
>           super().ReadNode()
> @@ -61,6 +85,10 @@ class Entry_x509_cert(Entry_collection):
>           self.key_fname = self.GetEntryArgsOrProps([
>               EntryArg('keyfile', str)], required=True)[0]
>           self.sw_rev = fdt_util.GetInt(self._node, 'sw-rev', 1)
> +        self.pkcs11_uri = self.GetEntryArgsOrProps([
> +            EntryArg('pkcs11-uri', str)], required=False)[0]
> +        self.pkcs11_module = self.GetEntryArgsOrProps([
> +            EntryArg('pkcs11-module', str)], required=False)[0]
>   
>       def GetCertificate(self, required, type='generic'):
>           """Get the contents of this entry
> @@ -80,6 +108,13 @@ class Entry_x509_cert(Entry_collection):
>           if input_data is None:
>               return None
>   
> +        # Override keyfile with the PKCS#11 URI if provided. This must be
> +        # done here rather than in ReadNode(), because subclasses (e.g.
> +        # ti_secure_rom, ti_secure) call super().ReadNode() and then
> +        # overwrite self.key_fname with the DTS 'keyfile' property.
> +        if self.pkcs11_uri:
> +            self.key_fname = self.pkcs11_uri
> +
>           uniq = self.GetUniqueName()
>           output_fname = tools.get_output_filename('cert.%s' % uniq)
>           input_fname = tools.get_output_filename('input.%s' % uniq)
> @@ -93,7 +128,8 @@ class Entry_x509_cert(Entry_collection):
>                   key_fname=self.key_fname,
>                   cn=self._cert_ca,
>                   revision=self._cert_rev,
> -                config_fname=config_fname)
> +                config_fname=config_fname,
> +                pkcs11_module=self.pkcs11_module)
>           elif type == 'sysfw':
>               stdout = self.openssl.x509_cert_sysfw(
>                   cert_fname=output_fname,
> @@ -102,7 +138,8 @@ class Entry_x509_cert(Entry_collection):
>                   config_fname=config_fname,
>                   sw_rev=self.sw_rev,
>                   req_dist_name_dict=self.req_dist_name,
> -                firewall_cert_data=self.firewall_cert_data)
> +                firewall_cert_data=self.firewall_cert_data,
> +                pkcs11_module=self.pkcs11_module)
>           elif type == 'rom':
>               stdout = self.openssl.x509_cert_rom(
>                   cert_fname=output_fname,
> @@ -116,7 +153,8 @@ class Entry_x509_cert(Entry_collection):
>                   bootcore_opts=self.bootcore_opts,
>                   load_addr=self.load_addr,
>                   sha=self.sha,
> -                debug=self.debug
> +                debug=self.debug,
> +                pkcs11_module=self.pkcs11_module
>               )
>           elif type == 'rom-combined':
>               stdout = self.openssl.x509_cert_rom_combined(
> @@ -143,7 +181,8 @@ class Entry_x509_cert(Entry_collection):
>                   sysfw_inner_cert_ext_boot_block=self.sysfw_inner_cert_ext_boot_block,
>                   dm_data_ext_boot_block=self.dm_data_ext_boot_block,
>                   bootcore_opts=self.bootcore_opts,
> -                debug=self.debug
> +                debug=self.debug,
> +                pkcs11_module=self.pkcs11_module
>               )
>           if stdout is not None:
>               data = tools.read_file(output_fname)
> diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
> index ca5149ee654a..a23719c767c8 100644
> --- a/tools/binman/ftest.py
> +++ b/tools/binman/ftest.py
> @@ -6884,6 +6884,116 @@ fdt         fdtmap                Extract the devicetree blob from the fdtmap
>           err = stderr.getvalue()
>           self.assertRegex(err, "Image 'image'.*missing bintools.*: openssl")
>   
> +    def testOpenSslBuildKeyArgsPem(self):
> +        """Test _build_key_args with a regular PEM key file"""
> +        openssl = bintool.Bintool.create('openssl')

Please always check the bintool is present with

self._CheckBintool(bintool)

such that we can skip tests if it isn't present instead of failing hard 
later.

> +        args = openssl._build_key_args('/path/to/key.pem')
> +        self.assertEqual(['-key', '/path/to/key.pem'], args)
> +
> +    def testOpenSslBuildKeyArgsPkcs11Provider(self):
> +        """Test _build_key_args with a PKCS#11 URI when provider API is available"""
> +        uri = 'pkcs11:token=mytoken;object=mykey'
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                        return_value='pkcs11 provider'):
> +            args = openssl._build_key_args(uri)
> +        self.assertEqual(['-provider', 'default', '-provider', 'pkcs11',
> +                          '-key', uri], args)
> +
> +    def testOpenSslBuildKeyArgsPkcs11Engine(self):
> +        """Test _build_key_args with a PKCS#11 URI when only engine API is available"""
> +        uri = 'pkcs11:token=mytoken;object=mykey'
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.object(openssl, 'run_cmd', return_value=''):
> +            args = openssl._build_key_args(uri)
> +        self.assertEqual(['-engine', 'pkcs11', '-keyform', 'engine',
> +                          '-key', uri], args)
> +
> +    def testOpenSslBuildKeyArgsPkcs11Pin(self):
> +        """Test _build_key_args appends pin-value to URI when PKCS11_PIN is set"""
> +        uri = 'pkcs11:token=mytoken;object=mykey'
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.dict('os.environ', {'PKCS11_PIN': '1234'}):
> +            with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                            return_value='pkcs11'):
> +                args = openssl._build_key_args(uri)
> +        self.assertIn(f'{uri}?pin-value=1234', args)
> +
> +    def testOpenSslBuildKeyArgsPkcs11PinExistingQuery(self):
> +        """Test _build_key_args uses '&' separator when URI already has query params"""
> +        uri = 'pkcs11:token=mytoken;object=mykey?module-path=/path/to/lib'
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.dict('os.environ', {'PKCS11_PIN': '1234'}):
> +            with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                            return_value='pkcs11'):
> +                args = openssl._build_key_args(uri)
> +        self.assertIn(f'{uri}&pin-value=1234', args)
> +
> +    def testOpenSslPkcs11UseProviderCached(self):
> +        """Test that _pkcs11_use_provider caches its result after the first call"""
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                        return_value='pkcs11') as mock_cmd:
> +            result1 = openssl._pkcs11_use_provider()
> +            result2 = openssl._pkcs11_use_provider()
> +        self.assertTrue(result1)
> +        self.assertTrue(result2)
> +        mock_cmd.assert_called_once_with('list', '-providers')
> +
> +    def testOpenSslRunCmdPkcs11NoModule(self):
> +        """Test _run_cmd_pkcs11 without a module just forwards to run_cmd"""
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                        return_value='output') as mock_cmd:
> +            result = openssl._run_cmd_pkcs11(None, 'version')
> +        mock_cmd.assert_called_once_with('version')
> +        self.assertEqual('output', result)
> +
> +    def testOpenSslRunCmdPkcs11WithModule(self):
> +        """Test _run_cmd_pkcs11 passes module paths to run_cmd via extra_env"""
> +        module = '/usr/lib/pkcs11/libsofthsm2.so'
> +        openssl = bintool.Bintool.create('openssl')
> +        with unittest.mock.patch.object(openssl, 'run_cmd',
> +                                        return_value='output') as mock_cmd:
> +            openssl._run_cmd_pkcs11(module, 'version')
> +        mock_cmd.assert_called_once_with('version', extra_env={
> +            'PKCS11_MODULE_PATH': module,
> +            'PKCS11_PROVIDER_MODULE': module,
> +        })
> +
> +    def testX509CertPkcs11(self):
> +        """Test X509 certificate creation using a PKCS#11 URI instead of a key file"""
> +        PKCS11_URI = 'pkcs11:token=test;object=mykey;type=private'
> +        PKCS11_MODULE = '/usr/lib/pkcs11/libsofthsm2.so'

FYI, the path isn't stable across all distros. Hence why we went for 
checking the path with p11-kit print-config to identify where it's located.

> +        original = bintool.Bintool.run_cmd
> +        signing_args = []
> +
> +        def fake_openssl(self_tool, *args, binary=False, extra_env=None):
> +            if self_tool.name != 'openssl':
> +                return original(self_tool, *args, binary=binary)
> +            arg_list = list(args)
> +            if arg_list == ['list', '-providers']:
> +                return 'pkcs11 provider'
> +            if '-out' in arg_list:
> +                signing_args.extend(arg_list)
> +                tools.write_file(arg_list[arg_list.index('-out') + 1],
> +                                 b'\x00' * 32)
> +            return ''
> +

We need to check with a real openssl too, otherwise we just check the 
interface with openssl and not that it is actually doing what's expected 
(and may change, e.g. with a newer version of openssl).

> +        entry_args = {
> +            'keyfile': self.TestFile('security/key.key'),
> +            'pkcs11-uri': PKCS11_URI,
> +            'pkcs11-module': PKCS11_MODULE,
> +        }
> +        with unittest.mock.patch.object(bintool.Bintool, 'run_cmd',
> +                                        new=fake_openssl):
> +            data = self._DoReadFileDtb('security/x509_cert.dts',
> +                                       entry_args=entry_args)[0]
> +        self.assertGreater(len(data), len(U_BOOT_DATA))
> +        self.assertEqual(U_BOOT_DATA, data[-4:])
> +        self.assertIn('-provider pkcs11', ' '.join(signing_args))
> +        self.assertIn(PKCS11_URI, signing_args)
> +

Why going to such length to avoid actually using openssl? 
Generate/import a certificate in softhsm2 and check that the bintool 
actually returns what's expected? We do this for signing FIT images so 
I'm assuming this is something you could do here as well?

Cheers,
Quentin


More information about the U-Boot mailing list