* [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates
@ 2026-07-28 11:35 ` Sergio Prado
2026-07-28 11:35 ` [PATCH v6 1/5] binman: x509_cert: document Entry_x509_cert properties Sergio Prado
` (5 more replies)
0 siblings, 6 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
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.
This series makes binman usable with any PKCS#11-capable HSM (YubiKey,
TPM, network HSM, SoftHSM2 for development, etc.) when signing X509
certificates.
Design
------
As Quentin pointed out during the v5 review, most of this already works:
the 'keyfile' entry argument is handed straight to 'openssl -key', which
on OpenSSL 3.x resolves it through the STORE API, so a PKCS#11 URI is
already accepted today. What was missing was documentation, tests, and a
convenient way to feed a build with a key and a PIN which do not live in
the source tree. The series is therefore split in five:
1/5 documents the Entry_x509_cert properties (unchanged from v5)
2/5 documents the PKCS#11 URI support which already exists, together
with the OpenSSL configuration it needs and the two ways of
supplying the token PIN, and adds tests for it. No code change.
3/5 drops a redundant re-read of the 'keyfile' entry argument from the
two TI K3 subclasses of Entry_x509_cert, which would otherwise
discard whatever the parent decided. No functional change on its
own, but 4/5 depends on it.
4/5 adds the PKCS11_PIN environment variable, so a CI job can pass the
PIN as a secret instead of writing it into openssl.cnf or into the
URI. It is a fallback: a URI which already names a PIN source is
left untouched.
5/5 adds the BINMAN_X509_KEYFILE make variable, which overrides the
'keyfile' of every x509 certificate entry in the build::
make BINMAN_X509_KEYFILE="pkcs11:token=mytk;object=mykey;type=private" \
OPENSSL_CONF=/path/to/openssl.cnf
Two URI forms work on OpenSSL 3.x: the provider path (recommended, via
the pkcs11-provider package) and the engine path, prefixed with
org.openssl.engine:<engine>: so that the STORE API routes it to the
engine. OpenSSL 4.0 removed the ENGINE API altogether, so the engine form
is specific to 3.x. OpenSSL 1.x is not supported.
PIN precedence
--------------
Quentin asked in the v5 review what wins when a PIN is configured in more
than one place. Measured with SoftHSM2 and the pkcs11 provider on OpenSSL
3.4.1, one freshly-initialised token per case:
- wrong PIN in openssl.cnf + correct pin-value in the URI: signing
succeeds
- correct PIN in openssl.cnf + wrong pin-value in the URI: signing
fails
- with pin-value given twice in the URI, the last one is used
So the URI wins over openssl.cnf, and blindly appending a second
pin-value - which v5 did - would silently override a PIN the user had
put in the URI. 4/5 therefore skips the append when the URI already has
a pin-value or pin-source attribute, and this is documented.
Testing
-------
Tested on a Toradex Verdin AM62 (verdin-am62_a53_defconfig) with both the
engine path and the provider path, using SoftHSM2 and a YubiKey 5 NFC.
The binman test suite gains seven tests:
- testX509CertPkcs11 signs against a SoftHSM2 token with the PIN in the
URI
- testX509CertPkcs11Pin does the same with the PIN in PKCS11_PIN
- testX509CertPkcs11PinEngine checks that an engine-prefixed URI
reaches the openssl invocation unchanged, with only the PIN appended;
openssl is forced missing so no engine has to be installed
- testX509CertPkcs11PinNotUri checks that a keyfile which merely
contains 'pkcs11:' rather than starting with it is left alone
- testX509CertPkcs11PinSubclass checks that the PIN survives into the
TI K3 subclasses, ti-secure and ti-secure-rom, which are the entries
this series exists for
- testX509CertAddPkcs11Pin unit-tests the URI/PIN combiner, including
percent-encoding and the pin-value/pin-source passthrough
- testX509CertKeyfile checks that the 'keyfile' property is read and
that the entry argument overrides it
The two signing tests skip cleanly when the OpenSSL pkcs11 provider is
not installed. The whole series was run patch by patch; each commit
leaves the suite with the same result as the base commit.
Not in this series
------------------
Quentin also spotted that Bintoolopenssl.x509_cert() takes a 'cn'
argument which it never uses - the config template says
CN = {cert_fname} - so the CN of a generic certificate ends up being the
output temp filename. That is an older bug, unrelated to HSM signing, and
will be sent as a separate fix.
Changes in v6:
- Split the feature commit into three: documentation and tests for the
PKCS#11 URI support which already works (no code change), the
PKCS11_PIN environment variable, and the make variable (Quentin)
- Drop the redundant 'keyfile' re-read from Entry_ti_secure and
Entry_ti_secure_rom, in a preparatory patch. Both re-read the entry
argument straight after super().ReadNode() had already read it, which
discarded the PIN and left every TI K3 entry - the whole point of the
series - still prompting for it. Found by testing v6 on a Verdin AM62;
v5 was unaffected because the rewrite happened in GetCertificate(),
which the subclasses reach through super()
- Rename BINMAN_X509_KEY_URI to BINMAN_X509_KEYFILE; it is a generic
keyfile override which happens to accept a URI, not a URI-only knob
(Quentin)
- Move the PIN rewrite from GetCertificate() to ReadNode() so that it
runs once per entry; the local-variable workaround for
ProcessContents() calling GetCertificate() twice is gone (Quentin)
- Guard the rewrite with startswith(('pkcs11:',
'org.openssl.engine:pkcs11:')) instead of testing for 'pkcs11:'
anywhere in the value (Simon). The guard sits at the call site in
ReadNode(), so that _add_pkcs11_pin() only ever sees a PKCS#11 URI and
a plain key file is visibly left alone
- Leave a URI which already has a pin-value or pin-source attribute
untouched, instead of appending a second pin-value which would
override it; document the precedence, which was measured rather than
assumed (Quentin)
- Reword the commit message: with a PIN appended the URI is no longer
forwarded "as-is" (Simon)
- Give the PKCS#11 setup its own section in binman.rst and list all the
forms 'keyfile' accepts, including a plain key file; the
Entry_x509_cert docstring keeps a three-line description of the
argument which references that section (Quentin)
- State plainly that OpenSSL 1.x is not supported, and note that OpenSSL
4.0 removed the ENGINE API, so the engine form is 3.x-only (Quentin)
- Add a test for the engine-prefixed URI form which asserts the prefixed
URI reaches the openssl invocation unchanged, without needing an
engine installed (Simon)
- Add security/x509_cert_keyfile.dts, whose 'keyfile' property names a
file which does not exist, and a test that the property is read and
that the entry argument overrides it. This is a separate image
description rather than a change to security/x509_cert.dts, so that a
deliberately invalid key cannot trip up the tests which just want a
signed certificate (Quentin)
- Add an integration test which puts the PIN in the URI directly, with
no environment variable involved (Quentin)
- Clarify that PKCS11_PIN is read from the environment by binman itself,
which is why it goes before 'make' rather than after it (Quentin)
- Carry the Reviewed-by tags from Simon and Quentin on patch 1
Changes in v5:
- Split the Entry_x509_cert docstring expansion into a separate
preparatory commit (Simon)
- Unify URI support under the existing 'keyfile' entry argument
rather than introducing a new 'x509-key-uri' arg; drop the
Entry_x509_cert state and override logic that v4 added (Quentin)
- Percent-encode the PIN before appending to the URI using
urllib.parse.quote(), per RFC 7512, and add a unit test
covering PINs with reserved characters (Simon)
- Detect the OpenSSL pkcs11 provider via
'openssl list -providers -provider pkcs11' rather than discovering
the provider .so path manually; drops the 'openssl version -m'
MODULESDIR lookup (Quentin)
- Drop the 'p11-kit print-config' / softhsm2 .so discovery from the
test; the simpler openssl.cnf relies on softhsm2 being registered
with p11-kit globally (Quentin). As a side effect this also fixes
the test on Ubuntu 22.04, where 'p11-kit print-config' is not a
valid subcommand
- Move the test openssl.cnf in-tree as
tools/binman/test/fit/openssl_provider.conf and trim it to the
minimum needed (no 'module = ', no 'pkcs11-module-path = ') (Quentin)
- Replace 'pkcs11-tool --keypairgen' with 'softhsm2-util --import' of
tools/binman/test/fit/rsa2048.key so the test runs faster and drops
the pkcs11-tool dependency (Quentin)
- Recommend pkcs11-module-token-pin in openssl.cnf as the primary
way to deliver the PIN; PKCS11_PIN env var is now documented as
the convenience fallback (Quentin)
- Drop the incorrect claim that PKCS11_PIN keeps the PIN out of
shell history (Quentin)
- Rephrase the URI intro in binman.rst and clarify that
'pkcs11-provider' is a Debian package name, not a path (Quentin)
- Drop the inheritance notes added to Entry_ti_secure and
Entry_ti_secure_rom in v4; with the v5 keyfile unification the
original motivation (an inherited x509-key-uri property) no
longer applies
Changes in v4:
- Drop the v3 bintool extra_env commit entirely; binman no longer
sets any PKCS#11-related environment variables (Quentin)
- Drop BINMAN_PKCS11_MODULE / pkcs11-module entry argument; the
PKCS#11 module path must be configured externally via openssl.cnf
(Quentin)
- Drop provider/engine auto-detection (_pkcs11_use_provider,
_build_key_args, _run_cmd_pkcs11) along with the threading.Lock;
the user selects provider or engine via OPENSSL_CONF and the URI
form (Quentin)
- Rename the v3 BINMAN_PKCS11_URI / pkcs11-uri to BINMAN_X509_KEY_URI
/ x509-key-uri to scope the names to x509 certificate entries
without locking them to a specific URI scheme (Quentin)
- Document that the engine path is supported on OpenSSL 3.x by
prefixing the URI with org.openssl.engine:<engine_name>: (Quentin)
- Replace the mocked openssl test with a real SoftHSM2-based
integration test using the provider path and OPENSSL_CONF (Quentin)
- Use 'p11-kit print-config' to locate the softhsm2 library at test
time instead of hardcoding a distro-specific path (Quentin)
- Use 'openssl version -m' (MODULESDIR) to locate the OpenSSL pkcs11
provider .so file, so multiarch paths like
/usr/lib/x86_64-linux-gnu/ossl-modules on Debian/Ubuntu are handled
correctly
- Generate the test RSA keypair with pkcs11-tool; softhsm2-util has
no key-generation action (only --import) and silently exits 0 on an
unknown --generate-keypair option, which would leave the token
empty and make the openssl step fail with 'Could not read private
key'
- Add self._CheckBintool() to all PKCS#11 test paths so tests skip
cleanly when bintools are missing (Quentin)
- Extract the URI/PIN combiner into Entry_x509_cert._build_pkcs11_key()
and add a unit test for it
- Document that PKCS11_PIN keeps the PIN out of the make command line
but is still visible in 'ps' output via the openssl invocation; for
improved isolation, configure the PIN in openssl.cnf
- Document all Entry_x509_cert properties (content, keyfile,
x509-key-uri, cert-ca, cert-revision-int, sw-rev) in its docstring
(Quentin)
- Add inheritance notes to Entry_ti_secure and Entry_ti_secure_rom
docstrings, pointing out that they extend Entry_x509_cert via
super() and therefore accept its properties (notably x509-key-uri)
(Quentin)
Changes in v3:
- Split into two patches: bintool infrastructure (1/2) and x509_cert
feature (2/2)
- Fix global environment mutation: _run_cmd_pkcs11() no longer writes
to os.environ directly; it now uses the new extra_env parameter so
module paths are scoped to the subprocess only, which is both
cleaner and safe under concurrent execution
- Add module-level threading.Lock to serialise concurrent PKCS#11
signing calls and fix intermittent login failures caused by binman's
ThreadPoolExecutor
- Fix URI query string separator: use '&' when the URI already
contains '?' (e.g. module-path already present), '?' otherwise
- Test cases updated
Changes in v2:
- Add tests for _build_key_args() (PEM path, PKCS#11 provider, PKCS#11
engine, PIN appending), _pkcs11_use_provider() (caching),
_run_cmd_pkcs11() (with and without module path), and end-to-end
x509_cert signing with a PKCS#11 URI (testX509CertPkcs11), ensuring
btool/openssl.py and etype/x509_cert.py have 100% test coverage
Sergio Prado (5):
binman: x509_cert: document Entry_x509_cert properties
binman: x509_cert: document PKCS#11 URI support in keyfile
binman: ti_secure: drop the redundant keyfile re-read
binman: x509_cert: support PKCS11_PIN environment variable
binman: Add BINMAN_X509_KEYFILE to override the signing key
Makefile | 1 +
tools/binman/binman.rst | 75 +++++++
tools/binman/etype/ti_secure.py | 3 -
tools/binman/etype/ti_secure_rom.py | 3 -
tools/binman/etype/x509_cert.py | 43 +++-
tools/binman/ftest.py | 201 ++++++++++++++++++
tools/binman/test/fit/openssl_provider.conf | 14 ++
.../test/security/x509_cert_keyfile.dts | 25 +++
8 files changed, 358 insertions(+), 7 deletions(-)
create mode 100644 tools/binman/test/fit/openssl_provider.conf
create mode 100644 tools/binman/test/security/x509_cert_keyfile.dts
--
2.34.1
^ permalink raw reply [flat|nested] 12+ messages in thread
* [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
* [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
* [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
* [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 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
* 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
* 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 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
* 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 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
end of thread, other threads:[~2026-08-07 13:19 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <4LN_e4Mi0TK99Phw-qnFJv5fIiv9tloFlexssVWY2btc8KqK4_JhOcNLziBlaQYe1fRY5-gnFHrYEYQjBOTiPw==@protonmail.internalid>
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-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
2026-08-05 18:58 ` Simon Glass
2026-08-07 13:19 ` Sergio Prado
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
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
2026-07-28 14:40 ` [PATCH v6 0/5] binman: add PKCS#11/HSM signing support for X509 certificates Rasmus Villemoes via U-Boot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.