The 'keyfile' entry argument of an x509 certificate entry is passed straight to 'openssl -key', which on OpenSSL 3.x resolves it through the STORE API. Any URI the STORE API understands therefore already works, including a PKCS#11 URI (RFC 7512) naming a key held in a hardware security module.
Nothing in binman had to change for that, but nothing said so either. Document the forms 'keyfile' accepts - a PEM key file on disk, a PKCS#11 URI, or a PKCS#11 URI prefixed with 'org.openssl.engine:<engine>:' for setups which only have the older pkcs11 engine - along with the OpenSSL configuration they need and the two ways of supplying the token PIN for unattended signing. PKCS#11 signing needs OpenSSL 3.x. The provider API and the 'org.openssl.engine:' STORE scheme both appeared in 3.0, and OpenSSL 4.0 removed the ENGINE API altogether [1], so the engine form is specific to 3.x while the provider form is the one to build on. OpenSSL 1.x is not supported. When a PIN is given both in openssl.cnf via pkcs11-module-token-pin and as a pin-value attribute in the URI, the URI wins. This was measured with SoftHSM2 and the pkcs11 provider on OpenSSL 3.4.1. Add testX509CertPkcs11, which signs against a SoftHSM2 token with the PIN carried in the URI and skips cleanly when the OpenSSL pkcs11 provider is not installed. Add testX509CertKeyfile too, which checks that the 'keyfile' property is read and that the entry argument overrides it; it uses an image description of its own rather than x509_cert.dts, so that the file naming a nonexistent key cannot trip up tests which just want a signed certificate. [1] https://openssl-library.org/post/2025-12-18-remove-engines/ Signed-off-by: Sergio Prado <[email protected]> --- tools/binman/binman.rst | 52 ++++++++++ tools/binman/etype/x509_cert.py | 4 +- tools/binman/ftest.py | 96 +++++++++++++++++++ tools/binman/test/fit/openssl_provider.conf | 14 +++ .../test/security/x509_cert_keyfile.dts | 25 +++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tools/binman/test/fit/openssl_provider.conf create mode 100644 tools/binman/test/security/x509_cert_keyfile.dts diff --git a/tools/binman/binman.rst b/tools/binman/binman.rst index 366491089ad9..86e15741fa35 100644 --- a/tools/binman/binman.rst +++ b/tools/binman/binman.rst @@ -1543,6 +1543,58 @@ If you want to sign and replace a FIT container in place:: which will sign the FIT container with a private key and replace it immediately inside your image. +.. _`SigningX509Hsm`: + +Signing x509 certificates with a key stored in an HSM +----------------------------------------------------- + +x509 certificate entries (see :ref:`etype_x509_cert`) are signed with the key +named by their ``keyfile`` property or entry argument. Instead of a key file on +disk, ``keyfile`` accepts a PKCS#11 URI (RFC 7512) naming a key held in a +hardware security module, so that the private key never leaves the device:: + + keyfile = "pkcs11:token=mytoken;object=mykey;type=private"; + +binman passes ``keyfile`` to ``openssl -key``, which resolves it through +OpenSSL's STORE API. Everything else - loading the pkcs11 provider or engine +and pointing it at the PKCS#11 module - is configured outside binman, in an +``openssl.cnf`` file selected either by the system default +(``/etc/ssl/openssl.cnf``) or by the ``OPENSSL_CONF`` environment variable. + +This requires OpenSSL 3.x; OpenSSL 1.x is not supported. Two forms of URI can +be used: + +1. Provider path (recommended). This needs the pkcs11 provider, e.g. the + ``pkcs11-provider`` package on Debian and Ubuntu:: + + pkcs11:token=mytoken;object=mykey;type=private + +2. Engine path, for setups where only the older pkcs11 engine is available + (e.g. ``libengine-pkcs11-openssl``). Prefixing the URI with + ``org.openssl.engine:<engine>:`` routes it to the engine through the STORE + API, so that no ``-engine`` / ``-keyform`` command-line flags are needed:: + + org.openssl.engine:pkcs11:pkcs11:token=mytoken;object=mykey;type=private + + Note that OpenSSL 4.0 removed the ENGINE API altogether, so this form only + works on OpenSSL 3.x. New setups should use the provider. + +Unattended signing needs the token PIN. This can be supplied in +``openssl.cnf``, under the section which activates the pkcs11 provider:: + + [pkcs11_provider] + activate = 1 + pkcs11-module-token-pin = 1234 + +or as a ``pin-value`` attribute in the URI itself:: + + keyfile = "pkcs11:token=mytoken;object=mykey;type=private?pin-value=1234"; + +If both are given, the ``pin-value`` in the URI wins. Note that a PIN placed in +``keyfile`` ends up on the ``openssl`` command line, where it is visible via +``ps`` and may be recorded in build logs; keeping the PIN in ``openssl.cnf`` +avoids that. + .. _`BinmanLogging`: Logging diff --git a/tools/binman/etype/x509_cert.py b/tools/binman/etype/x509_cert.py index efa85f9553e7..6d7883af58ef 100644 --- a/tools/binman/etype/x509_cert.py +++ b/tools/binman/etype/x509_cert.py @@ -19,7 +19,9 @@ 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. + - keyfile: Key used to sign the binary. This is either the filename of + a PEM key file on disk, or a PKCS#11 URI naming a key stored in a + hardware security module. See :ref:`SigningX509Hsm`. - cert-ca: Common Name (CN) embedded in the certificate. Used when generating a generic x509 certificate. - cert-revision-int: Integer certificate revision number. Used when diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py index bf98b268ac15..10373a2fa899 100644 --- a/tools/binman/ftest.py +++ b/tools/binman/ftest.py @@ -6905,6 +6905,102 @@ fdt fdtmap Extract the devicetree blob from the fdtmap err = stderr.getvalue() self.assertRegex(err, "Image 'image'.*missing bintools.*: openssl") + def testX509CertKeyfile(self): + """Test that the keyfile entry arg overrides the keyfile property""" + # The keyfile property in this image description names a file which + # does not exist, so signing fails when there is no entry arg + with self.assertRaises(ValueError) as e: + self._DoReadFileDtb('security/x509_cert_keyfile.dts') + self.assertIn('keyfile-from-dts.key', str(e.exception)) + + # With the entry arg present, the key it names is used instead + entry_args = { + 'keyfile': self.TestFile('security/key.key'), + } + data = self._DoReadFileDtb('security/x509_cert_keyfile.dts', + entry_args=entry_args)[0] + self.assertEqual(U_BOOT_DATA, data[-4:]) + + def _CheckPkcs11Provider(self): + """Skip the current test if the OpenSSL pkcs11 provider is missing""" + openssl = bintool.Bintool.create('openssl') + self._CheckBintool(openssl) + + # '-provider pkcs11' asks OpenSSL to load the named provider, so this + # succeeds only when the provider module is installed, whatever + # OPENSSL_CONF happens to point at + result = openssl.run_cmd_result('list', '-providers', '-provider', + 'pkcs11', raise_on_error=False) + if result is None or result.return_code != 0: + self.skipTest('OpenSSL pkcs11 provider not available') + + def _SetupPkcs11Token(self, prefix, token, key_label, pin): + """Set up a SoftHSM2 token holding the test signing key + + Creates a SoftHSM2 configuration and token store private to the + calling test, then imports the in-tree test key into a fresh token. + + Args: + prefix (str): Prefix for the temporary files to create + token (str): Label of the token to create + key_label (str): Label to give to the imported private key + pin (str): User PIN to set on the token + + Returns: + dict: Environment variables which make both the token and the + OpenSSL pkcs11 provider visible to openssl + """ + softhsm2_util = bintool.Bintool.create('softhsm2_util') + self._CheckBintool(softhsm2_util) + + # Per-test SoftHSM2 token store, isolated from the host configuration + data = tools.read_file(self.TestFile('fit/softhsm2.conf')) + softhsm2_conf = self._MakeInputFile(f'{prefix}softhsm2.conf', data) + softhsm2_tokens_dir = self._MakeInputDir(f'{prefix}softhsm2.tokens') + with open(softhsm2_conf, 'a') as f: + f.write(f'directories.tokendir = {softhsm2_tokens_dir}\n') + + # Minimal in-tree openssl.cnf which activates the pkcs11 provider. It + # relies on the provider module living in OpenSSL's MODULESDIR and on + # softhsm2 being registered with p11-kit globally, both of which hold + # when pkcs11-provider and softhsm2 are installed normally. + env = {'SOFTHSM2_CONF': softhsm2_conf, + 'OPENSSL_CONF': self.TestFile('fit/openssl_provider.conf')} + + # rsa2048.key is already a PKCS#8 PEM, which is what + # 'softhsm2-util --import' requires + private_key = self.TestFile('fit/rsa2048.key') + with unittest.mock.patch.dict('os.environ', env): + softhsm2_util.run_cmd('--init-token', '--free', '--label', token, + '--pin', pin, '--so-pin', '000000') + softhsm2_util.run_cmd('--import', private_key, '--token', token, + '--label', key_label, '--id', '01', + '--pin', pin) + return env + + def testX509CertPkcs11(self): + """Test X509 certificate signing with a key stored in an HSM""" + self._CheckPkcs11Provider() + + token = 'x509-test' + key_label = 'testkey' + pin = '1234' + env = self._SetupPkcs11Token('testX509CertPkcs11.', token, key_label, + pin) + + # 'keyfile' is a PKCS#11 URI rather than a filesystem path. binman + # passes it to 'openssl -key', which resolves it through the pkcs11 + # provider activated by OPENSSL_CONF. The PIN is carried by the URI + # itself, so signing runs without any further configuration. + entry_args = { + 'keyfile': (f'pkcs11:token={token};object={key_label};' + f'type=private?pin-value={pin}'), + } + with unittest.mock.patch.dict('os.environ', env): + data = self._DoReadFileDtb('security/x509_cert.dts', + entry_args=entry_args)[0] + self.assertEqual(U_BOOT_DATA, data[-4:]) + def testPackRockchipTpl(self): """Test that an image with a Rockchip TPL binary can be created""" data = self._DoReadFile('vendor/rockchip_tpl.dts') diff --git a/tools/binman/test/fit/openssl_provider.conf b/tools/binman/test/fit/openssl_provider.conf new file mode 100644 index 000000000000..579452ca84c4 --- /dev/null +++ b/tools/binman/test/fit/openssl_provider.conf @@ -0,0 +1,14 @@ +openssl_conf = openssl_init + +[openssl_init] +providers = providers_section + +[providers_section] +default = default_provider +pkcs11 = pkcs11_provider + +[default_provider] +activate = 1 + +[pkcs11_provider] +activate = 1 diff --git a/tools/binman/test/security/x509_cert_keyfile.dts b/tools/binman/test/security/x509_cert_keyfile.dts new file mode 100644 index 000000000000..468c6b4406b1 --- /dev/null +++ b/tools/binman/test/security/x509_cert_keyfile.dts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + x509-cert { + cert-ca = "IOT2050 Firmware Signature"; + cert-revision-int = <0>; + /* + * Deliberately names a file which does not exist, so + * that a test can tell whether this property or the + * 'keyfile' entry argument was used + */ + keyfile = "keyfile-from-dts.key"; + content = <&u_boot>; + }; + + u_boot: u-boot { + }; + }; +}; -- 2.34.1
