* [PATCH v6 1/5] binman: x509_cert: document Entry_x509_cert properties
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
@ 2026-07-28 11:35 ` Sergio Prado
2026-07-28 11:35 ` [PATCH v6 2/5] binman: x509_cert: document PKCS#11 URI support in keyfile Sergio Prado
` (4 subsequent siblings)
5 siblings, 0 replies; 12+ messages in thread
From: Sergio Prado @ 2026-07-28 11:35 UTC (permalink / raw)
To: u-boot
Cc: trini, sjg, alpernebiyasak, marek.vasut+renesas, ilias.apalodimas,
pbrobinson, sughosh.ganu, sergio.prado, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
The Entry_x509_cert class only listed 'content' in its
'Properties / Entry arguments' section, even though 'keyfile',
'cert-ca', 'cert-revision-int' and 'sw-rev' are also read from the
entry node and passed through to openssl.
Expand the docstring so the binman documentation generated from it
matches the actual behavior of the entry.
No functional change.
Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
Reviewed-by: Quentin Schulz <quentin.schulz@cherry.de>
Reviewed-by: Simon Glass <sjg@chromium.org>
---
tools/binman/etype/x509_cert.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/tools/binman/etype/x509_cert.py b/tools/binman/etype/x509_cert.py
index b6e8b0b4fb09..efa85f9553e7 100644
--- a/tools/binman/etype/x509_cert.py
+++ b/tools/binman/etype/x509_cert.py
@@ -18,7 +18,15 @@ class Entry_x509_cert(Entry_collection):
"""An entry which contains an X509 certificate
Properties / Entry arguments:
- - content: List of phandles to entries to sign
+ - content: List of phandles to entries to sign.
+ - keyfile: Filename of the PEM key file used to sign the binary.
+ - 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
+ generating a generic x509 certificate. Defaults to 0.
+ - sw-rev: Software revision number embedded in the certificate by
+ the sysfw/rom variants used by the TI K3 secure boot subclasses.
+ Defaults to 1.
Output files:
- input.<unique_name> - input file passed to openssl
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v6 2/5] binman: x509_cert: document PKCS#11 URI support in keyfile
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
2026-07-28 11:35 ` [PATCH v6 1/5] binman: x509_cert: document Entry_x509_cert properties Sergio Prado
@ 2026-07-28 11:35 ` Sergio Prado
2026-08-05 18:57 ` Simon Glass
2026-07-28 11:35 ` [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read Sergio Prado
` (3 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Sergio Prado @ 2026-07-28 11:35 UTC (permalink / raw)
To: u-boot
Cc: trini, sjg, alpernebiyasak, marek.vasut+renesas, ilias.apalodimas,
pbrobinson, sughosh.ganu, sergio.prado, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
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 <sergio.prado@e-labworks.com>
---
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
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH v6 2/5] binman: x509_cert: document PKCS#11 URI support in keyfile
2026-07-28 11:35 ` [PATCH v6 2/5] binman: x509_cert: document PKCS#11 URI support in keyfile Sergio Prado
@ 2026-08-05 18:57 ` Simon Glass
0 siblings, 0 replies; 12+ messages in thread
From: Simon Glass @ 2026-08-05 18:57 UTC (permalink / raw)
To: sergio.prado
Cc: u-boot, trini, sjg, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik,
u-boot
On 2026-07-28T11:35:30, Sergio Prado <sergio.prado@e-labworks.com> wrote:
> binman: x509_cert: document PKCS#11 URI support in keyfile
>
> 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 <sergio.prado@e-labworks.com>
>
> 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 ++++
> tools/binman/test/security/x509_cert_keyfile.dts | 25 ++++++
> 5 files changed, 190 insertions(+), 1 deletion(-)
Reviewed-by: Simon Glass <sjg@chromium.org>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
2026-07-28 11:35 ` [PATCH v6 1/5] binman: x509_cert: document Entry_x509_cert properties Sergio Prado
2026-07-28 11:35 ` [PATCH v6 2/5] binman: x509_cert: document PKCS#11 URI support in keyfile Sergio Prado
@ 2026-07-28 11:35 ` Sergio Prado
2026-08-05 18:58 ` Simon Glass
2026-07-28 11:35 ` [PATCH v6 4/5] binman: x509_cert: support PKCS11_PIN environment variable Sergio Prado
` (2 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Sergio Prado @ 2026-07-28 11:35 UTC (permalink / raw)
To: u-boot
Cc: trini, sjg, alpernebiyasak, marek.vasut+renesas, ilias.apalodimas,
pbrobinson, sughosh.ganu, sergio.prado, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
Entry_ti_secure and Entry_ti_secure_rom both call super().ReadNode() and
then read the 'keyfile' entry argument a second time, with exactly the
call that Entry_x509_cert.ReadNode() has already made. The second read is
pure duplication, and it means anything the parent does to
self.key_fname is silently discarded.
Drop it, so that the subclasses use the value their parent decided on,
and drop the EntryArg import which becomes unused.
No functional change.
Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
---
tools/binman/etype/ti_secure.py | 3 ---
tools/binman/etype/ti_secure_rom.py | 3 ---
2 files changed, 6 deletions(-)
diff --git a/tools/binman/etype/ti_secure.py b/tools/binman/etype/ti_secure.py
index f6caa0286d97..705a05342a85 100644
--- a/tools/binman/etype/ti_secure.py
+++ b/tools/binman/etype/ti_secure.py
@@ -5,7 +5,6 @@
# Support for generation of TI secured binary blobs
-from binman.entry import EntryArg
from binman.etype.x509_cert import Entry_x509_cert
from dataclasses import dataclass
@@ -110,8 +109,6 @@ class Entry_ti_secure(Entry_x509_cert):
def ReadNode(self):
super().ReadNode()
- self.key_fname = self.GetEntryArgsOrProps([
- EntryArg('keyfile', str)], required=True)[0]
auth_in_place = fdt_util.GetInt(self._node, 'auth-in-place')
if auth_in_place:
self.firewall_cert_data['auth_in_place'] = auth_in_place
diff --git a/tools/binman/etype/ti_secure_rom.py b/tools/binman/etype/ti_secure_rom.py
index 7e90c6559409..f1bc9e7577e6 100644
--- a/tools/binman/etype/ti_secure_rom.py
+++ b/tools/binman/etype/ti_secure_rom.py
@@ -5,7 +5,6 @@
# Support for generation of TI secured bootloaders booted by ROM
-from binman.entry import EntryArg
from binman.etype.x509_cert import Entry_x509_cert
import hashlib
@@ -71,8 +70,6 @@ class Entry_ti_secure_rom(Entry_x509_cert):
self.sha = fdt_util.GetInt(self._node, 'sha', 512)
self.core = fdt_util.GetString(self._node, 'core', 'secure')
self.bootcore_opts = fdt_util.GetInt(self._node, 'core-opts')
- self.key_fname = self.GetEntryArgsOrProps([
- EntryArg('keyfile', str)], required=True)[0]
if self.combined:
self.sysfw_inner_cert = fdt_util.GetBool(self._node, 'sysfw-inner-cert', False)
self.load_addr_sysfw = fdt_util.GetInt(self._node, 'load-sysfw', 0x00000000)
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read
2026-07-28 11:35 ` [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read Sergio Prado
@ 2026-08-05 18:58 ` Simon Glass
2026-08-07 13:19 ` Sergio Prado
0 siblings, 1 reply; 12+ messages in thread
From: Simon Glass @ 2026-08-05 18:58 UTC (permalink / raw)
To: sergio.prado
Cc: u-boot, trini, sjg, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik,
u-boot
Hi Sergio,
On 2026-07-28T11:35:30, Sergio Prado <sergio.prado@e-labworks.com> wrote:
> binman: ti_secure: drop the redundant keyfile re-read
>
> Entry_ti_secure and Entry_ti_secure_rom both call super().ReadNode() and
> then read the 'keyfile' entry argument a second time, with exactly the
> call that Entry_x509_cert.ReadNode() has already made. The second read is
> pure duplication, and it means anything the parent does to
> self.key_fname is silently discarded.
>
> Drop it, so that the subclasses use the value their parent decided on,
> and drop the EntryArg import which becomes unused.
>
> No functional change.
>
> Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
>
> tools/binman/etype/ti_secure.py | 3 ---
> tools/binman/etype/ti_secure_rom.py | 3 ---
> 2 files changed, 6 deletions(-)
> diff --git a/tools/binman/etype/ti_secure_rom.py b/tools/binman/etype/ti_secure_rom.py
> @@ -71,8 +70,6 @@ class Entry_ti_secure_rom(Entry_x509_cert):
> self.sha = fdt_util.GetInt(self._node, 'sha', 512)
> self.core = fdt_util.GetString(self._node, 'core', 'secure')
> self.bootcore_opts = fdt_util.GetInt(self._node, 'core-opts')
> - self.key_fname = self.GetEntryArgsOrProps([
> - EntryArg('keyfile', str)], required=True)[0]
BTW Entry_ti_secure_rom.ReadNode() also re-reads 'sw-rev' with the
parent's default (line 70) - same pattern. Folding it in would leave
no redundant re-reads at all. Could be a follow-up perhaps?
Reviewed-by: Simon Glass <sjg@chromium.org>
Regards,
Simon
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read
2026-08-05 18:58 ` Simon Glass
@ 2026-08-07 13:19 ` Sergio Prado
0 siblings, 0 replies; 12+ messages in thread
From: Sergio Prado @ 2026-08-07 13:19 UTC (permalink / raw)
To: Simon Glass
Cc: u-boot, trini, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik,
u-boot
Hi Simon,
Thanks for reviewing.
Em qua., 5 de ago. de 2026 às 15:58, Simon Glass <sjg@chromium.org> escreveu:
>
> Hi Sergio,
...
> > diff --git a/tools/binman/etype/ti_secure_rom.py b/tools/binman/etype/ti_secure_rom.py
> > @@ -71,8 +70,6 @@ class Entry_ti_secure_rom(Entry_x509_cert):
> > self.sha = fdt_util.GetInt(self._node, 'sha', 512)
> > self.core = fdt_util.GetString(self._node, 'core', 'secure')
> > self.bootcore_opts = fdt_util.GetInt(self._node, 'core-opts')
> > - self.key_fname = self.GetEntryArgsOrProps([
> > - EntryArg('keyfile', str)], required=True)[0]
>
> BTW Entry_ti_secure_rom.ReadNode() also re-reads 'sw-rev' with the
> parent's default (line 70) - same pattern. Folding it in would leave
> no redundant re-reads at all. Could be a follow-up perhaps?
Makes sense. I will send a follow-up.
Best regards,
Sergio Prado
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v6 4/5] binman: x509_cert: support PKCS11_PIN environment variable
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
` (2 preceding siblings ...)
2026-07-28 11:35 ` [PATCH v6 3/5] binman: ti_secure: drop the redundant keyfile re-read Sergio Prado
@ 2026-07-28 11:35 ` Sergio Prado
2026-08-05 18:58 ` Simon Glass
2026-07-28 11:35 ` [PATCH v6 5/5] binman: Add BINMAN_X509_KEYFILE to override the signing key Sergio Prado
2026-07-28 14:40 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Rasmus Villemoes via U-Boot
5 siblings, 1 reply; 12+ messages in thread
From: Sergio Prado @ 2026-07-28 11:35 UTC (permalink / raw)
To: u-boot
Cc: trini, sjg, alpernebiyasak, marek.vasut+renesas, ilias.apalodimas,
pbrobinson, sughosh.ganu, sergio.prado, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
Signing an x509 certificate entry with a key held in an HSM needs the
token PIN. It can be put in openssl.cnf via pkcs11-module-token-pin, or
in the PKCS#11 URI itself as a pin-value attribute, but both mean writing
the PIN into a file which is part of the build. That is not
user-friendly in CI, where the PIN typically arrives as a secret in the
environment.
Read the PIN from the PKCS11_PIN environment variable and append it to
the URI as a percent-encoded pin-value attribute, as described by RFC
7512. The rewritten URI, not the original, is what reaches
'openssl -key'.
PKCS11_PIN is a fallback. A URI which already has a pin-value or a
pin-source attribute is passed through untouched, since the PIN named by
the URI is the one OpenSSL uses; appending a second pin-value would
silently override it, as the last occurrence wins.
Note that PKCS11_PIN keeps the PIN out of the build files but not off the
openssl command line, where it is visible via 'ps' and may be recorded in
build logs. Configuring the PIN in openssl.cnf remains the option which
avoids that, and this is documented alongside the variable.
Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
---
tools/binman/binman.rst | 11 ++++
tools/binman/etype/x509_cert.py | 31 ++++++++++
tools/binman/ftest.py | 105 ++++++++++++++++++++++++++++++++
3 files changed, 147 insertions(+)
diff --git a/tools/binman/binman.rst b/tools/binman/binman.rst
index 86e15741fa35..106c34efb76c 100644
--- a/tools/binman/binman.rst
+++ b/tools/binman/binman.rst
@@ -1595,6 +1595,17 @@ If both are given, the ``pin-value`` in the URI wins. Note that a PIN placed in
``ps`` and may be recorded in build logs; keeping the PIN in ``openssl.cnf``
avoids that.
+As a third option, binman reads the PIN from the ``PKCS11_PIN`` environment
+variable and appends it to the URI as a ``pin-value`` attribute, so that the
+PIN does not have to be written into either file::
+
+ PKCS11_PIN=1234 binman build ...
+
+``PKCS11_PIN`` is a fallback only: a URI which already has a ``pin-value`` or
+``pin-source`` attribute is passed through untouched. It keeps the PIN out of
+the build files, but not off the ``openssl`` command line - for that, use
+``openssl.cnf``.
+
.. _`BinmanLogging`:
Logging
diff --git a/tools/binman/etype/x509_cert.py b/tools/binman/etype/x509_cert.py
index 6d7883af58ef..66c9987976c2 100644
--- a/tools/binman/etype/x509_cert.py
+++ b/tools/binman/etype/x509_cert.py
@@ -7,6 +7,7 @@
from collections import OrderedDict
import os
+import urllib.parse
from binman.entry import EntryArg
from binman.etype.collection import Entry_collection
@@ -70,6 +71,9 @@ class Entry_x509_cert(Entry_collection):
self._cert_rev = fdt_util.GetInt(self._node, 'cert-revision-int', 0)
self.key_fname = self.GetEntryArgsOrProps([
EntryArg('keyfile', str)], required=True)[0]
+ if self.key_fname.startswith(('pkcs11:', 'org.openssl.engine:pkcs11:')):
+ self.key_fname = self._add_pkcs11_pin(self.key_fname,
+ os.environ.get('PKCS11_PIN'))
self.sw_rev = fdt_util.GetInt(self._node, 'sw-rev', 1)
def GetCertificate(self, required, type='generic'):
@@ -178,3 +182,30 @@ class Entry_x509_cert(Entry_collection):
def AddBintools(self, btools):
super().AddBintools(btools)
self.openssl = self.AddBintool(btools, 'openssl')
+
+ @staticmethod
+ def _add_pkcs11_pin(uri, pin):
+ """Add a PIN to a PKCS#11 URI, so that signing runs unattended
+
+ Appends a 'pin-value' attribute holding the PIN, percent-encoded as
+ required by RFC 7512. A URI which already says where its PIN comes
+ from is left alone, since that takes precedence over the PIN passed
+ here.
+
+ Args:
+ uri (str): PKCS#11 URI naming the signing key
+ pin (str): PIN to add, or None if there is none
+
+ Returns:
+ str: Value to pass to 'openssl -key'
+ """
+ if not pin:
+ return uri
+
+ # Only the query component can hold pin-value / pin-source
+ query = uri.partition('?')[2]
+ if 'pin-value=' in query or 'pin-source=' in query:
+ return uri
+
+ sep = '&' if query else '?'
+ return f'{uri}{sep}pin-value={urllib.parse.quote(pin, safe="")}'
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 10373a2fa899..e91768c52fe9 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -35,6 +35,7 @@ from dtoc import fdt
from dtoc import fdt_util
from binman.etype import fdtmap
from binman.etype import image_header
+from binman.etype.x509_cert import Entry_x509_cert
from binman.image import Image
from u_boot_pylib import command
from u_boot_pylib import terminal
@@ -7001,6 +7002,110 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
entry_args=entry_args)[0]
self.assertEqual(U_BOOT_DATA, data[-4:])
+ def testX509CertPkcs11Pin(self):
+ """Test signing with a key in an HSM, with the PIN from PKCS11_PIN"""
+ self._CheckPkcs11Provider()
+
+ token = 'x509-test-pin'
+ key_label = 'testkey'
+ pin = '1234'
+ env = self._SetupPkcs11Token('testX509CertPkcs11Pin.', token,
+ key_label, pin)
+ env['PKCS11_PIN'] = pin
+
+ # This time the URI carries no PIN, so binman appends the one from
+ # PKCS11_PIN before handing the URI to openssl
+ entry_args = {
+ 'keyfile': f'pkcs11:token={token};object={key_label};type=private',
+ }
+ 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 testX509CertPkcs11PinEngine(self):
+ """Test PKCS11_PIN with an engine-prefixed PKCS#11 URI"""
+ uri = ('org.openssl.engine:pkcs11:pkcs11:token=mytoken;'
+ 'object=mykey;type=private')
+ entry_args = {
+ 'keyfile': uri,
+ }
+
+ # openssl is forced missing so that this needs no pkcs11 engine to be
+ # installed; what matters is the keyfile it would have been given
+ with unittest.mock.patch.dict('os.environ', {'PKCS11_PIN': '1234'}):
+ with terminal.capture():
+ self._DoTestFile('security/x509_cert.dts',
+ force_missing_bintools='openssl',
+ entry_args=entry_args)
+ entry = control.images['image'].GetEntries()['x509-cert']
+ self.assertEqual(f'{uri}?pin-value=1234', entry.key_fname)
+
+ def testX509CertPkcs11PinNotUri(self):
+ """Test PKCS11_PIN is ignored when keyfile is not a PKCS#11 URI"""
+ # A path which merely contains 'pkcs11:' is not a URI, so it must
+ # reach openssl untouched
+ keyfile = '/keys/pkcs11:key'
+ entry_args = {
+ 'keyfile': keyfile,
+ }
+ with unittest.mock.patch.dict('os.environ', {'PKCS11_PIN': '1234'}):
+ with terminal.capture():
+ self._DoTestFile('security/x509_cert.dts',
+ force_missing_bintools='openssl',
+ entry_args=entry_args)
+ entry = control.images['image'].GetEntries()['x509-cert']
+ self.assertEqual(keyfile, entry.key_fname)
+
+ def testX509CertPkcs11PinSubclass(self):
+ """Test PKCS11_PIN reaches the TI K3 x509 certificate subclasses"""
+ uri = 'pkcs11:token=mytoken;object=mykey;type=private'
+ entry_args = {
+ 'keyfile': uri,
+ }
+
+ # These read 'keyfile' through Entry_x509_cert.ReadNode(), so the PIN
+ # must survive into the entry they build
+ for dts, name in [('vendor/ti_secure.dts', 'ti-secure'),
+ ('vendor/ti_secure_rom.dts', 'ti-secure-rom')]:
+ with unittest.mock.patch.dict('os.environ',
+ {'PKCS11_PIN': '1234'}):
+ with terminal.capture():
+ self._DoTestFile(dts, force_missing_bintools='openssl',
+ entry_args=entry_args)
+ entry = control.images['image'].GetEntries()[name]
+ self.assertEqual(f'{uri}?pin-value=1234', entry.key_fname)
+
+ def testX509CertAddPkcs11Pin(self):
+ """Test adding a PIN to a PKCS#11 URI"""
+ add = Entry_x509_cert._add_pkcs11_pin
+ uri = 'pkcs11:token=t;object=o;type=private'
+
+ # No PIN to add, so the keyfile is unchanged
+ self.assertEqual(uri, add(uri, None))
+ self.assertEqual(uri, add(uri, ''))
+
+ # PKCS#11 URI, in both the provider and the engine form
+ self.assertEqual(f'{uri}?pin-value=1234', add(uri, '1234'))
+ engine_uri = f'org.openssl.engine:pkcs11:{uri}'
+ self.assertEqual(f'{engine_uri}?pin-value=1234',
+ add(engine_uri, '1234'))
+
+ # URI which already has a query component, so '&' separates the PIN
+ self.assertEqual(f'{uri}?module-name=softhsm2&pin-value=1234',
+ add(f'{uri}?module-name=softhsm2', '1234'))
+
+ # URI which already says where its PIN comes from, so it takes
+ # precedence and the keyfile is unchanged
+ self.assertEqual(f'{uri}?pin-value=5678',
+ add(f'{uri}?pin-value=5678', '1234'))
+ self.assertEqual(f'{uri}?pin-source=file:/etc/pin',
+ add(f'{uri}?pin-source=file:/etc/pin', '1234'))
+
+ # PIN percent-encoded as required by RFC 7512
+ self.assertEqual(f'{uri}?pin-value=a%26b%3Fc%3Dd', add(uri, 'a&b?c=d'))
+ self.assertEqual(f'{uri}?pin-value=a%20b%2Bc', add(uri, 'a b+c'))
+
def testPackRockchipTpl(self):
"""Test that an image with a Rockchip TPL binary can be created"""
data = self._DoReadFile('vendor/rockchip_tpl.dts')
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH v6 4/5] binman: x509_cert: support PKCS11_PIN environment variable
2026-07-28 11:35 ` [PATCH v6 4/5] binman: x509_cert: support PKCS11_PIN environment variable Sergio Prado
@ 2026-08-05 18:58 ` Simon Glass
0 siblings, 0 replies; 12+ messages in thread
From: Simon Glass @ 2026-08-05 18:58 UTC (permalink / raw)
To: sergio.prado
Cc: u-boot, trini, sjg, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik,
u-boot
On 2026-07-28T11:35:30, Sergio Prado <sergio.prado@e-labworks.com> wrote:
> binman: x509_cert: support PKCS11_PIN environment variable
>
> Signing an x509 certificate entry with a key held in an HSM needs the
> token PIN. It can be put in openssl.cnf via pkcs11-module-token-pin, or
> in the PKCS#11 URI itself as a pin-value attribute, but both mean writing
> the PIN into a file which is part of the build. That is not
> user-friendly in CI, where the PIN typically arrives as a secret in the
> environment.
>
> Read the PIN from the PKCS11_PIN environment variable and append it to
> the URI as a percent-encoded pin-value attribute, as described by RFC
> 7512. The rewritten URI, not the original, is what reaches
> 'openssl -key'.
>
> PKCS11_PIN is a fallback. A URI which already has a pin-value or a
> pin-source attribute is passed through untouched, since the PIN named by
> the URI is the one OpenSSL uses; appending a second pin-value would
> silently override it, as the last occurrence wins.
>
> Note that PKCS11_PIN keeps the PIN out of the build files but not off the
> openssl command line, where it is visible via 'ps' and may be recorded in
> build logs. Configuring the PIN in openssl.cnf remains the option which
> avoids that, and this is documented alongside the variable.
>
> Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
>
> tools/binman/binman.rst | 11 +++++
> tools/binman/etype/x509_cert.py | 31 ++++++++++++
> tools/binman/ftest.py | 105 ++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 147 insertions(+)
Reviewed-by: Simon Glass <sjg@chromium.org>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v6 5/5] binman: Add BINMAN_X509_KEYFILE to override the signing key
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
` (3 preceding siblings ...)
2026-07-28 11:35 ` [PATCH v6 4/5] binman: x509_cert: support PKCS11_PIN environment variable Sergio Prado
@ 2026-07-28 11:35 ` Sergio Prado
2026-08-05 18:58 ` Simon Glass
2026-07-28 14:40 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Rasmus Villemoes via U-Boot
5 siblings, 1 reply; 12+ messages in thread
From: Sergio Prado @ 2026-07-28 11:35 UTC (permalink / raw)
To: u-boot
Cc: trini, sjg, alpernebiyasak, marek.vasut+renesas, ilias.apalodimas,
pbrobinson, sughosh.ganu, sergio.prado, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
The key used to sign an x509 certificate entry comes from the image
description, either as a 'keyfile' property or as a 'keyfile' entry
argument. Neither is convenient for a build which must not carry the key,
such as one signing with an HSM.
Add a BINMAN_X509_KEYFILE make variable which, when set, passes
'-a keyfile=<value>' to binman and so overrides the image description for
every x509 certificate entry in the build::
URI="pkcs11:token=mytoken;object=mykey;type=private"
make BINMAN_X509_KEYFILE="$URI" OPENSSL_CONF=/path/to/openssl.cnf
The variable is a plain keyfile override, so it takes a path to a PEM key
file just as happily as a PKCS#11 URI; signing with an HSM is simply the
case which needs it most.
Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
---
Makefile | 1 +
tools/binman/binman.rst | 12 ++++++++++++
2 files changed, 13 insertions(+)
diff --git a/Makefile b/Makefile
index 7f5d83658d75..c418fda1981f 100644
--- a/Makefile
+++ b/Makefile
@@ -1704,6 +1704,7 @@ 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_X509_KEYFILE),-a keyfile="$(BINMAN_X509_KEYFILE)") \
$(BINMAN_$(@F))
OBJCOPYFLAGS_u-boot.ldr.hex := -I binary -O ihex
diff --git a/tools/binman/binman.rst b/tools/binman/binman.rst
index 106c34efb76c..ff61baf6d5fe 100644
--- a/tools/binman/binman.rst
+++ b/tools/binman/binman.rst
@@ -2232,6 +2232,18 @@ BINMAN_VERBOSE
Sets the logging verbosity of binman by adding a `-v` argument. See
:ref:`BinmanLogging`.
+BINMAN_X509_KEYFILE
+ Sets the key used to sign x509 certificate entries by adding an
+ `-a keyfile=<value>` argument, overriding whatever the image description
+ says. The value is either the filename of a PEM key file on disk or a
+ PKCS#11 URI naming a key held in an HSM, so this is the way to keep the
+ signing key out of the source tree::
+
+ URI="pkcs11:token=mytoken;object=mykey;type=private"
+ make BINMAN_X509_KEYFILE="$URI" OPENSSL_CONF=/path/to/openssl.cnf
+
+ See :ref:`SigningX509Hsm` for the URI forms which are accepted and the
+ OpenSSL configuration they need.
Error messages
--------------
--
2.34.1
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH v6 5/5] binman: Add BINMAN_X509_KEYFILE to override the signing key
2026-07-28 11:35 ` [PATCH v6 5/5] binman: Add BINMAN_X509_KEYFILE to override the signing key Sergio Prado
@ 2026-08-05 18:58 ` Simon Glass
0 siblings, 0 replies; 12+ messages in thread
From: Simon Glass @ 2026-08-05 18:58 UTC (permalink / raw)
To: sergio.prado
Cc: u-boot, trini, sjg, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik,
u-boot
On 2026-07-28T11:35:30, Sergio Prado <sergio.prado@e-labworks.com> wrote:
> binman: Add BINMAN_X509_KEYFILE to override the signing key
>
> The key used to sign an x509 certificate entry comes from the image
> description, either as a 'keyfile' property or as a 'keyfile' entry
> argument. Neither is convenient for a build which must not carry the key,
> such as one signing with an HSM.
>
> Add a BINMAN_X509_KEYFILE make variable which, when set, passes
> '-a keyfile=<value>' to binman and so overrides the image description for
> every x509 certificate entry in the build::
>
> URI="pkcs11:token=mytoken;object=mykey;type=private"
> make BINMAN_X509_KEYFILE="$URI" OPENSSL_CONF=/path/to/openssl.cnf
>
> The variable is a plain keyfile override, so it takes a path to a PEM key
> file just as happily as a PKCS#11 URI; signing with an HSM is simply the
> case which needs it most.
>
> Signed-off-by: Sergio Prado <sergio.prado@e-labworks.com>
>
> Makefile | 1 +
> tools/binman/binman.rst | 12 ++++++++++++
> 2 files changed, 13 insertions(+)
Reviewed-by: Simon Glass <sjg@chromium.org>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates
2026-07-28 11:35 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Sergio Prado
` (4 preceding siblings ...)
2026-07-28 11:35 ` [PATCH v6 5/5] binman: Add BINMAN_X509_KEYFILE to override the signing key Sergio Prado
@ 2026-07-28 14:40 ` Rasmus Villemoes via U-Boot
5 siblings, 0 replies; 12+ messages in thread
From: Rasmus Villemoes via U-Boot @ 2026-07-28 14:40 UTC (permalink / raw)
To: Sergio Prado
Cc: u-boot, trini, sjg, alpernebiyasak, marek.vasut+renesas,
ilias.apalodimas, pbrobinson, sughosh.ganu, wolfgang.wallner,
xypron.glpk, quentin.schulz, jj251510319013, Wojciech.Dubowik
On Tue, Jul 28 2026, "Sergio Prado" <sergio.prado@e-labworks.com> wrote:
> Motivation
> ----------
>
> TI K3 secure boot requires X509 certificates to be signed with a private
> key at build time. For production use, that key should never exist
> unprotected on a build machine - it belongs inside a Hardware Security
> Module (HSM) which enforces access control and keeps the key material
> unexportable.
Hi Sergio
I was completely unaware of this work when I sent
https://lore.kernel.org/u-boot/20260717135824.2142135-1-ravi@prevas.dk/
(and v1 of that); I assume your v5 must have been sent some time before
my v1.
We clearly have very similar goals, but somewhat different approaches. I
will look through your patches tomorrow and see if they would work for
us.
Rasmus
^ permalink raw reply [flat|nested] 12+ messages in thread