[PATCH v4] Add support for OpenSSL Provider API
Quentin Schulz
quentin.schulz at cherry.de
Wed May 20 12:28:16 CEST 2026
Hi Eddie,
Thanks for working on this, this was a good base for me to work on
supporting pkcs11 and custom OpenSSL providers for signing FIT images
with binman.
On 4/29/26 8:02 PM, Eddie Kovsky wrote:
> The Engine API has been deprecated since the release of OpenSSL 3.0. End
> users have been advised to migrate to the new Provider interface.
> Several distributions have already removed support for engines, which is
> preventing U-Boot from being compiled in those environments.
>
> Add support for the Provider API while continuing to support the existing
> Engine API on distros shipping older releases of OpenSSL.
>
> This is based on similar work contributed by Jan Stancek updating Linux
> to use the Provider interface.
>
> commit 558bdc45dfb2669e1741384a0c80be9c82fa052c
> Author: Jan Stancek <jstancek at redhat.com>
> Date: Fri Sep 20 19:52:48 2024 +0300
>
> sign-file,extract-cert: use pkcs11 provider for OPENSSL MAJOR >= 3
>
> The changes have been tested with the FIT signature verification vboot
> tests on Fedora 42 and Debian 13. All 30 tests pass with both the legacy
> Engine library installed and with the Provider API.
>
> Tested-by Enric Balletbo i Serra <eballetb at redhat.com>
> Tested-by Mark Kettenis <mark.kettenis at xs4all.nl>
> Signed-off-by: Eddie Kovsky <ekovsky at redhat.com>
> ---
> Changes in v4:
> - Add comment that @engine pointer is null when using pkcs11 provider
> - Remove extra line break
> - Add pkcs11-provider package to build dependencies
> v3: https://lore.kernel.org/u-boot/20260120164524.253188-1-ekovsky@redhat.com/
>
> Changes in v3:
> - Removed Kconfig option
> - Changed macro symbol from CONFIG_OPENSSL_NO_DEPRECATED to
> USE_PKCS11_PROVIDER or USE_PKCS11_ENGINE
> v2: https://lore.kernel.org/u-boot/20251027195834.71109-1-ekovsky@redhat.com/
>
> Changes in v2:
> - Remove default for new Kconfig option
> - Use #ifdef instead of IS_ENABLED macro
> - Remove comment after #endif
> - Remove unrelated checkpatch cleanup of 'sslErr' variable name
> v1: https://lore.kernel.org/u-boot/20251017171329.255689-1-ekovsky@redhat.com/
> ---
> doc/build/gcc.rst | 4 +-
> lib/aes/aes-encrypt.c | 4 +-
> lib/rsa/rsa-sign.c | 102 ++++++++++++++++++++++++++++++++++++++--
> tools/docker/Dockerfile | 1 +
> 4 files changed, 103 insertions(+), 8 deletions(-)
>
> diff --git a/doc/build/gcc.rst b/doc/build/gcc.rst
> index 1fef718ceecb..29a6a632e7e3 100644
> --- a/doc/build/gcc.rst
> +++ b/doc/build/gcc.rst
> @@ -25,8 +25,8 @@ Depending on the build targets further packages maybe needed
>
> sudo apt-get install bc bison build-essential coccinelle \
> device-tree-compiler dfu-util efitools flex gdisk graphviz imagemagick \
> - libgnutls28-dev libguestfs-tools libncurses-dev \
> - libpython3-dev libsdl2-dev libssl-dev lz4 lzma lzma-alone openssl \
> + libgnutls28-dev libguestfs-tools libncurses-dev libpython3-dev \
> + libsdl2-dev libssl-dev lz4 lzma lzma-alone openssl pkcs11-provider \
> pkg-config python3 python3-asteval python3-coverage python3-filelock \
> python3-pkg-resources python3-pycryptodome python3-pyelftools \
> python3-pytest python3-pytest-xdist python3-sphinxcontrib.apidoc \
> diff --git a/lib/aes/aes-encrypt.c b/lib/aes/aes-encrypt.c
> index 90e1407b4f09..4fc4ce232478 100644
> --- a/lib/aes/aes-encrypt.c
> +++ b/lib/aes/aes-encrypt.c
> @@ -16,7 +16,9 @@
> #include <openssl/err.h>
> #include <openssl/ssl.h>
> #include <openssl/evp.h>
> -#include <openssl/engine.h>
> +#if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0)
> +# include <openssl/engine.h>
> +#endif
> #include <uboot_aes.h>
>
> #if OPENSSL_VERSION_NUMBER >= 0x10000000L
> diff --git a/lib/rsa/rsa-sign.c b/lib/rsa/rsa-sign.c
> index 0e38c9e802fd..f456f3c58e65 100644
> --- a/lib/rsa/rsa-sign.c
> +++ b/lib/rsa/rsa-sign.c
> @@ -19,7 +19,47 @@
> #include <openssl/err.h>
> #include <openssl/ssl.h>
> #include <openssl/evp.h>
> -#include <openssl/engine.h>
> +#if OPENSSL_VERSION_MAJOR >= 3
> +# define USE_PKCS11_PROVIDER
> +# include <err.h>
> +# include <openssl/provider.h>
> +# include <openssl/store.h>
> +#else
> +# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0)
> +# define USE_PKCS11_ENGINE
> +# include <openssl/engine.h>
> +# endif
> +#endif
> +
> +#ifdef USE_PKCS11_PROVIDER
> +#define ERR(cond, fmt, ...) \
> + do { \
> + bool __cond = (cond); \
> + drain_openssl_errors(__LINE__, 0); \
> + if (__cond) { \
> + errx(1, fmt, ## __VA_ARGS__); \
> + } \
> + } while (0)
> +
> +static void drain_openssl_errors(int l, int silent)
> +{
> + const char *file;
> + char buf[120];
> + int e, line;
> +
> + if (ERR_peek_error() == 0)
> + return;
> + if (!silent)
> + fprintf(stderr, "At main.c:%d:\n", l);
> +
> + while ((e = ERR_peek_error_line(&file, &line))) {
> + ERR_error_string(e, buf);
buf must be at least 256 bytes long, and it's 120 here. Either increase
buf[] size to 256 or use ERR_error_string_n(e, buf, 120), c.f.
https://docs.openssl.org/3.0/man3/ERR_error_string/#description.
> + if (!silent)
> + fprintf(stderr, "- SSL %s: %s:%d\n", buf, file, line);
> + ERR_get_error();
> + }
> +}
> +#endif
>
OK so this was somehow unclear to me this was imported from Linux even
though you stated that in your commit log. In the kernel, the macro and
function are stored in scripts/ssl-common.h, I'm wondering if we can
import it in a similar way (separate file) so that it's easy to keep
track of changes made to the file in Linux and backport them to U-Boot
whenever necessary. The origin is at least clear that way.
> static int rsa_err(const char *msg)
> {
> @@ -94,10 +134,11 @@ err_cert:
> *
> * @keydir: Key prefix
> * @name Name of key
> - * @engine Engine to use
> + * @engine Engine to use or NULL when using pkcs11 provider
No, because this function doesn't exist whenever provider support is
enabled. Since I would like to support both providers and engines in
parallel (see the other review), this function can actually be built
even if provider support is enabled. However, we shouldn't be calling
this function with a provider. If engine is NULL, this function isn't
called, see rsa_get_pub_key().
> * @evpp Returns EVP_PKEY object, or NULL on failure
> * Return: 0 if ok, -ve on error (in which case *evpp will be set to NULL)
> */
> +#ifdef USE_PKCS11_ENGINE
> static int rsa_engine_get_pub_key(const char *keydir, const char *name,
> ENGINE *engine, EVP_PKEY **evpp)
> {
> @@ -157,21 +198,24 @@ static int rsa_engine_get_pub_key(const char *keydir, const char *name,
>
> return 0;
> }
> +#endif
>
> /**
> * rsa_get_pub_key() - read a public key
> *
> * @keydir: Directory containing the key (PEM file) or key prefix (engine)
> * @name Name of key file (will have a .crt extension)
> - * @engine Engine to use
> + * @engine Engine to use or NULL when using pkcs11 provider
Well, a provider isn't an engine, so I think it's pretty clear engine is
supposed to be NULL when you don't want to use an engine? Also, this
applies to all providers, not only pkcs11.
> * @evpp Returns EVP_PKEY object, or NULL on failure
> * Return: 0 if ok, -ve on error (in which case *evpp will be set to NULL)
> */
> static int rsa_get_pub_key(const char *keydir, const char *name,
> ENGINE *engine, EVP_PKEY **evpp)
> {
> +#ifdef USE_PKCS11_ENGINE
> if (engine)
> return rsa_engine_get_pub_key(keydir, name, engine, evpp);
> +#endif
> return rsa_pem_get_pub_key(keydir, name, evpp);
> }
>
> @@ -207,13 +251,44 @@ static int rsa_pem_get_priv_key(const char *keydir, const char *name,
> return -ENOENT;
> }
>
> +#ifdef USE_PKCS11_PROVIDER
> + EVP_PKEY *private_key = NULL;
> + OSSL_STORE_CTX *store;
> +
This is sneaky here because it's not in the git context, but above here
there is:
"""
if (keydir && name)
snprintf(path, sizeof(path), "%s/%s.key", keydir, name);
else if (keyfile)
snprintf(path, sizeof(path), "%s", keyfile);
else
return -EINVAL;
f = fopen(path, "r");
if (!f) {
fprintf(stderr, "Couldn't open RSA private key: '%s':
%s\n",
path, strerror(errno));
return -ENOENT;
}
"""
This means this function will fail if path cannot be found on disk,
which I am assuming is the case for every provider. I'm wondering if you
actually tested this with a provider or only with a file? (the
OSSL_STORE_open() supports files in path but also providers (with the
scheme (e.g. pkcs11:) as prefix)) (my grep-fu may be lacking but nothing
in test/ seems to do anything with pkcs11?) If your only worry is to
build with an OpenSSL without engine support but have no real interest
in supporting providers, then just ifdef the engine support and let
provider support be done by someone else (I'm looking into it now for
signing FIT images, I've got something locally already).
Also, we have some logic for generating the proper path for pkcs11 from
keydir, name and keyfile for the pkcs11 engine, c.f.
rsa_engine_get_pub_key(), I'm assuming we may want something similar for
pkcs11 provider.
Locally, I've gone for:
"""
if (keydir && !strncmp(pkcs11_schema, keydir,
strlen(pkcs11_schema))) {
// Create the URI for the PKCS11 provider
if (strstr(keydir, "object=") || strstr(keydir, "id="))
snprintf(path, sizeof(path), "%s;type=private",
keydir);
else
snprintf(path, sizeof(path),
"%s;object=%s;type=private",
keydir, name);
} else if (keydir && strstr(keydir, ":")) {
// Create the URI for specified provider from keydir
and name
snprintf(path, sizeof(path), "%s%s", keydir,
name ?: "");
} else if (keydir && name) {
snprintf(path, sizeof(path), "%s/%s.key", keydir, name);
} else if (keyfile) {
snprintf(path, sizeof(path), "%s", keyfile);
} else {
return -EINVAL;
}
"""
> + if (!OSSL_PROVIDER_try_load(NULL, "pkcs11", true))
> + ERR(1, "OSSL_PROVIDER_try_load(pkcs11)");
> + if (!OSSL_PROVIDER_try_load(NULL, "default", true))
> + ERR(1, "OSSL_PROVIDER_try_load(default)");
> +
You can ignore this with
export OPENSSL_CONF=/path/to/my/openssl.conf
"""
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
"""
This will allow supporting any provider, not only pkcs11 and allow to
build without pkcs11-provider installed if another provider is desired.
Note that you're modifying code used to sign with RSA, it's not
necessarily pkcs11 (unlike in the kernel from where you took inspiration
which is explicitly pkcs11-only). This also allows to pass a pin for the
pkcs11 provider without U-Boot needing to know about it.
> + store = OSSL_STORE_open(path, NULL, NULL, NULL, NULL);
> + ERR(!store, "OSSL_STORE_open");
> +
> + while (!OSSL_STORE_eof(store)) {
> + OSSL_STORE_INFO *info = OSSL_STORE_load(store);
> +
> + if (!info) {
> + drain_openssl_errors(__LINE__, 0);
> + continue;
> + }
> + if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
> + private_key = OSSL_STORE_INFO_get1_PKEY(info);
> + ERR(!private_key, "OSSL_STORE_INFO_get1_PKEY");
> + }
I have been told that calling OSSL_STORE_expect() with the expected type
should make it easier to get the object we want directly.
Something like
OSSL_STORE_expect(store, OSSL_STORE_INFO_PKEY);
right after OSSL_STORE_open(). I'm assuming this should allow us to get
rid of the OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY check
since OSS_STORE_load() would only return objects of expected type.
c.f. https://docs.openssl.org/3.0/man3/OSSL_STORE_expect/#description
I've not tested this myself yet.
> + OSSL_STORE_INFO_free(info);
> + if (private_key)
> + break;
> + }
> + OSSL_STORE_close(store);
> +
> + *evpp = private_key;
> +#else
> if (!PEM_read_PrivateKey(f, evpp, NULL, path)) {
> rsa_err("Failure reading private key");
> fclose(f);
> return -EPROTO;
> }
> fclose(f);
> -
> +#endif
> return 0;
Not much is actually shared in rsa_pem_get_priv_key between provider and
no provider support so I think it may make sense to ifdef the whole
content of the function or maybe the whole function even (or move the
content into other functions).
> }
>
> @@ -226,6 +301,7 @@ static int rsa_pem_get_priv_key(const char *keydir, const char *name,
> * @evpp Returns EVP_PKEY object, or NULL on failure
> * Return: 0 if ok, -ve on error (in which case *evpp will be set to NULL)
> */
> +#ifdef USE_PKCS11_ENGINE
> static int rsa_engine_get_priv_key(const char *keydir, const char *name,
> const char *keyfile,
> ENGINE *engine, EVP_PKEY **evpp)
> @@ -293,22 +369,25 @@ static int rsa_engine_get_priv_key(const char *keydir, const char *name,
>
> return 0;
> }
> +#endif
>
> /**
> * rsa_get_priv_key() - read a private key
> *
> * @keydir: Directory containing the key (PEM file) or key prefix (engine)
> * @name Name of key
> - * @engine Engine to use for signing
> + * @engine Engine to use or NULL when using pkcs11 provider
Same remark as above.
Cheers,
Quentin
More information about the U-Boot
mailing list