Hi Sergio,
Thanks for your patience!
I think this could be split in multiple commits. I could identify the
following unrelated changes:
1) documentation on how to use PKCS11 signing (+ test for it). I believe
PKCS11 is already supported via OPENSSL_CONF for the provider, with a
pkcs11 uri in keyfile property in DT + additionally the pin either in
OPENSSL_CONF or in the keyfile property directly, I haven't tested
though so maybe I'm wrong
2) support for PKCS11_PIN environment variable to pass the pin, (+ test
for it)
3) support for overriding the keyfile property via a make variable
(BINMAN_X509_KEY_URI), (+ test for it)
Do you agree with this split?
There's quite a few comments below, but I don't think they require big
changes.
On 5/25/26 3:28 PM, Sergio Prado wrote:
[...]
The PIN-rewriting step in GetCertificate() uses a local variable
rather than mutating self.key_fname, because ProcessContents() calls
GetCertificate() a second time on the same entry; an in-place rewrite
would double-append the PIN on the second call.
Should we rather move this logic to ReadNode() instead then? Hopefully
that is run only once? What made you chose GetCertificate() for this
logic? Maybe I'm missing some clue.
[...]
+BINMAN_X509_KEY_URI
+ URI of a key used to sign x509 certificate entries via an HSM instead of a
+ PEM key file on disk. When set, it is passed as ``-a keyfile=<uri>`` to
+ binman, which overrides the ``keyfile`` entry argument for all x509
+ certificate signing operations.
+
This isn't specific to HSMI/PKCS11 though, is it? So it's just a way to
override the keyfile entry argument by an environment variable. Yes, the
typical usecase is to use it for an HSM but nothing forces you to use it
for an HSM?
[...]
+
+ URIs currently supported:
+
+ - PKCS#11 URI (RFC 7512)
+
Aren't we missing "local PEM key file" here?
+ PKCS#11 signing requires OpenSSL 3.x. The provider or engine and the
Is it requiring OpenSSL 3.x though or did you only test on OpenSSL 3.x?
Since engines were supported in OpenSSL 1.x, I would assume PKCS11
signing also worked back then? Are you aware of a limitation? Also, does
it NOT work with OpenSSL 4.x (which distro will slowly start to migrate to)?
What I'm trying to figure out is whether we should reword this to
"PKCS11 signing is only tested on OpenSSL 3.x".
+ PKCS#11 module must be configured externally via an ``openssl.cnf`` file.
+ Either rely on the system default (``/etc/ssl/openssl.cnf``) or point to a
+ custom file via ``OPENSSL_CONF``.
+
+ Two URI forms are supported on OpenSSL 3.x:
+
+ 1. Provider path (recommended) — requires the pkcs11 provider, e.g. via
+ the ``pkcs11-provider`` package::
+
+ make
BINMAN_X509_KEY_URI="pkcs11:token=mytoken;object=mykey;type=private" \
+ OPENSSL_CONF=/path/to/openssl.cnf
+
+ 2. Engine path — for setups where only an OpenSSL engine is available
+ (e.g. ``libengine-pkcs11-openssl``). Prefix the URI with
+ ``org.openssl.engine:<engine_name>:`` so OpenSSL 3.x's STORE API
+ routes it to the engine without requiring ``-engine``/``-keyform``
Ah yes, now I remember why it's OpenSSL 3+ only! Mmmmm then wondering
what to do for OpenSSL 4.x, any chance you can test whether it works
with OpenSSL 4? If not, then keep the previous paragraph as is and
ignore my comment.
[...]
+ OpenSSL 1.x engine usage is not supported transparently; users on
+ OpenSSL 1.x need to provide the engine flags through other means.
+
Yeah, or just say "OpenSSL 1.x is not supported." and be done with it :)
[...]
+ PKCS11_PIN=1234 make BINMAN_X509_KEY_URI="pkcs11:..." \
+ OPENSSL_CONF=/path/to/openssl.cnf
+
question: why suddenly the variable before `make`? All the others are
after so it seems like an odd choice to me.
+ Note: ``PKCS11_PIN`` keeps the PIN out of the ``BINMAN_X509_KEY_URI``
+ value itself, but the PIN is still passed to ``openssl`` as part of its
+ command line, where it is visible via ``ps`` and may appear in verbose
+ build logs. For stronger isolation, configure the PIN inside
+ ``openssl.cnf`` as described above.
+
The third option is to have the pin in the URI directly (which is
equally bad, but is possible no?).
I'm wondering if we should support replacing the PIN inside the URI if
there's one in it and we also have PKCS11_PIN.
Side question, what happens if we have a pin set in openssl.cnf and also
via PKCS11_PIN/pin in the URI?
Trying to figure out if we should explicitly document those
incompatibilities (if they exist!) or test them if we want to support
those cases.
[...]
@@ -88,6 +93,15 @@ class Entry_x509_cert(Entry_collection):
if input_data is None:
return None
+ # When keyfile is a PKCS#11 URI and PKCS11_PIN is set, append the
+ # PIN to the URI so signing runs non-interactively. The preferred
+ # way to deliver a PIN is to configure pkcs11-module-token-pin in
+ # openssl.cnf; PKCS11_PIN is the convenience fallback.
+ key_fname = self.key_fname
+ pin = os.environ.get('PKCS11_PIN')
+ if pin and 'pkcs11:' in key_fname:
I agree with Simon here, we need to check the key starts with pkcs11:
(and if we want to support OpenSSL engines with OpenSSL 3.x like we
document if in here, then also org.openssl.engine:pkcs11:).
[...]
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 9a3811c17322..5ee118993390 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
@@ -6905,6 +6906,97 @@ fdt fdtmap Extract the devicetree
blob from the fdtmap
err = stderr.getvalue()
self.assertRegex(err, "Image 'image'.*missing bintools.*: openssl")
+ def testX509CertPkcs11(self):
+ """Test X509 certificate signing via a PKCS#11 URI in keyfile"""
+ openssl = bintool.Bintool.create('openssl')
+ self._CheckBintool(openssl)
+
+ # Detect the OpenSSL pkcs11 provider. -provider asks OpenSSL to load
+ # the named provider, so the command succeeds only when the provider
+ # module is installed; this works regardless of OPENSSL_CONF state.
+ 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')
+
Idly wondering if we shouldn't do a simple try:except and let
raise_on_error to True and skip in the except.
[...]
+ with unittest.mock.patch.dict('os.environ',
+ {'SOFTHSM2_CONF': softhsm2_conf,
+ 'OPENSSL_CONF': openssl_conf,
+ 'PKCS11_PIN': pin}):
Please add a test for when the pin is passed via the URI directly (as I
assume this is supported) and not through the env variable.
+ 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)
+
+ uri = f'pkcs11:token={token};object={key_label};type=private'
+ entry_args = {
+ # 'keyfile' is set to a PKCS#11 URI rather than a filesystem
+ # path. binman forwards it to 'openssl -key', which resolves
+ # it via the pkcs11 provider configured in OPENSSL_CONF.
+ # PKCS11_PIN is appended to the URI by Entry_x509_cert before
+ # the openssl invocation, exercising _build_pkcs11_key().
+ 'keyfile': uri,
+ }
+ data = self._DoReadFileDtb('security/x509_cert.dts',
+ entry_args=entry_args)[0]
+ self.assertEqual(U_BOOT_DATA, data[-4:])
We should add a keyfile property in the test device tree to make sure we
override it with the entryarg.
I'm also surprised tools/binman/test/security/x509_cert.dts doesn't have
a keyfile property, isn't it marked as required in
tools/binman/etype/x509_cert.py? AH! It seems like we pass it via an
entryarg in testX509Cert and testX509CertMissing.
It'd be nice to check the engine works as well with the prefix. Not sure
how to properly test this, if it's easier, I would be fine with checking
we pass the key with the org.openssl.engine: prefix, at that point if it
fails it's an openssl issue and we've already checked with the provider
the code path is correct so that'd be an okay compromise to avoid
testing the whole code path.
Cheers,
Quentin