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

Reply via email to