[meta-swupdate][PATCH] swupdate-common: add support for multiple CMS signers

13 views
Skip to first unread message

Ayoub Zaki

unread,
Jul 14, 2026, 3:58:25 AM (2 days ago) Jul 14
to swup...@googlegroups.com, Ayoub Zaki, Ayoub Zaki
From: Ayoub Zaki <ayoub...@googlemail.com>

So far SWUPDATE_CMS_CERT and SWUPDATE_CMS_KEY accepted only a single
certificate and private key. Allow both variables to hold a space
delimited list paired by position and emit one "-signer"/"-inkey"
pair per entry on the openssl command line.

This enables hybrid signing e.g. a classical RSA or ECDSA signature combined
with a post-quantum ML-DSA signature in the same CMS structure. Note
that SWUpdate's CMS_verify() treats the signers as an AND: every listed
signer's certificate must be present in the verification trust store on
the target.

A single certificate/key pair produces exactly the same openssl command
as before, so existing recipes are unaffected.

Signed-off-by: Ayoub Zaki <ayoub...@embetrix.com>
---
README | 14 +++++++++++---
classes-recipe/swupdate-common.bbclass | 23 ++++++++++++++---------
2 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/README b/README
index f28bbce..7b638d4 100644
--- a/README
+++ b/README
@@ -60,9 +60,15 @@ There are 3 signing mechanisms supported by meta-swupdate at the moment:

* Set variable: `SWUPDATE_SIGNING = "CMS"`

- * Set `SWUPDATE_CMS_CERT` to the full path of certificate file
+ * Set `SWUPDATE_CMS_CERT` to the full path of certificate file. A space
+ delimited list of certificates may be given to produce a CMS with multiple
+ signers (e.g. hybrid classic + post-quantum signing). SWUpdate verifies the
+ signers as an AND, so every certificate must be present in the target trust
+ store.

- * Set `SWUPDATE_CMS_KEY ` to the full path of private key file
+ * Set `SWUPDATE_CMS_KEY ` to the full path of private key file. When several
+ certificates are listed in `SWUPDATE_CMS_CERT`, list the matching private
+ keys here in the same order; the two lists must have the same length.

* (Optional) Set `SWUPDATE_CMS_EXTRA_CERTS` to a space delimited list of intermediate certificate files

@@ -70,7 +76,9 @@ There are 3 signing mechanisms supported by meta-swupdate at the moment:
`openssl cms -sign` via `-md` (e.g. `sha256`, `sha512`). When unset, openssl
picks the signing key's default digest. This is required for signing keys
that have no default digest such as ML-DSA where openssl otherwise fails
- with "no default digest".
+ with "no default digest". `openssl cms` uses a single digest for all
+ signers, so with multiple certificates choose one strong enough for the
+ strongest key (e.g. `sha512` when any of ML-DSA-65/87 is used).

3. Custom signing tool:

diff --git a/classes-recipe/swupdate-common.bbclass b/classes-recipe/swupdate-common.bbclass
index 1359e09..aee4816 100644
--- a/classes-recipe/swupdate-common.bbclass
+++ b/classes-recipe/swupdate-common.bbclass
@@ -218,19 +218,24 @@ def prepare_sw_description(d):
signcmd = ["openssl", "dgst", "-sha256", "-sign", privkey] + get_pwd_file_args(d, 'SWUPDATE_PASSWORD_FILE') + \
["-sigopt", "rsa_padding_mode:pss", "-sigopt", "rsa_pss_saltlen:-2", "-out", sw_desc_sig, sw_desc]
elif signing == "CMS":
- cms_cert = d.getVar('SWUPDATE_CMS_CERT')
- if not cms_cert:
+ cms_certs = (d.getVar('SWUPDATE_CMS_CERT') or "").split()
+ if not cms_certs:
bb.fatal("SWUPDATE_CMS_CERT is not set")
- if not os.path.exists(cms_cert):
- bb.fatal("SWUPDATE_CMS_CERT %s doesn't exist" % (cms_cert))
- cms_key = d.getVar('SWUPDATE_CMS_KEY')
- if not cms_key:
+ cms_keys = (d.getVar('SWUPDATE_CMS_KEY') or "").split()
+ if not cms_keys:
bb.fatal("SWUPDATE_CMS_KEY isn't set")
- if not os.path.exists(cms_key):
- bb.fatal("SWUPDATE_CMS_KEY %s doesn't exist" % (cms_key))
+ if len(cms_certs) != len(cms_keys):
+ bb.fatal("SWUPDATE_CMS_CERT and SWUPDATE_CMS_KEY must list the same number of entries (got %d certs, %d keys)" % (len(cms_certs), len(cms_keys)))
+ signer_args = []
+ for cms_cert, cms_key in zip(cms_certs, cms_keys):
+ if not os.path.exists(cms_cert):
+ bb.fatal("SWUPDATE_CMS_CERT %s doesn't exist" % (cms_cert))
+ if not os.path.exists(cms_key):
+ bb.fatal("SWUPDATE_CMS_KEY %s doesn't exist" % (cms_key))
+ signer_args += ["-signer", cms_cert, "-inkey", cms_key]
cms_md = d.getVar('SWUPDATE_CMS_MD')
md_args = ["-md", cms_md] if cms_md else []
- signcmd = ["openssl", "cms", "-sign", "-in", sw_desc, "-out", sw_desc_sig, "-signer", cms_cert, "-inkey", cms_key] + \
+ signcmd = ["openssl", "cms", "-sign", "-in", sw_desc, "-out", sw_desc_sig] + signer_args + \
["-outform", "DER", "-nosmimecap", "-binary"] + md_args + \
get_pwd_file_args(d, 'SWUPDATE_PASSWORD_FILE') + \
get_certfile_args(d)
--
2.43.0

Reply all
Reply to author
Forward
0 new messages