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