[PATCH 1/3] Check all certs in chain against CRL

11 views
Skip to first unread message

wesdm...@gmail.com

unread,
Aug 11, 2026, 10:02:04 PMAug 11
to swup...@googlegroups.com, Wes Malone
From: Wes Malone <wesdm...@gmail.com>

When using only X509_V_FLAG_CRL_CHECK, only leaf certificates are
revoked. This allows intermediate certificates in the chain to escape
revocation.

my_root
-> intermediate_1 (revoked!)
-> leaf_1 (revoked!)

If intermediate_1 issues a new leaf_2 cert it validates successfully,
because the intermediate chain is not checked against the CRL. Setting
X509_V_FLAG_CRL_CHECK_ALL ensures that all certs in the chain are
checked against the revocation list.

The --crl-check-all argument/config option makes this behavior opt-in
to maintain backwards compatibility (default: signer cert only).

Signed-off-by: Wes Malone <wesdm...@gmail.com>
---
core/swupdate.c | 10 ++++++++++
crypto/swupdate_cms_verify_openssl.c | 13 ++++++++++---
doc/source/signed_images.rst | 15 ++++++++++++---
include/swupdate.h | 1 +
4 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/core/swupdate.c b/core/swupdate.c
index 3bc1fded..8e287e4d 100644
--- a/core/swupdate.c
+++ b/core/swupdate.c
@@ -102,6 +102,7 @@ static struct option long_options[] = {
{"forced-signer-name", required_argument, NULL, '2'},
#if defined(CONFIG_SSL_IMPL_OPENSSL)
{"crl-path", required_argument, NULL, '9'},
+ {"crl-check-all", no_argument, NULL, 'A'},
#endif
#endif
#ifdef CONFIG_SIGALG_GPG
@@ -178,6 +179,7 @@ static void usage(char *programname)
" --ca-path : path to the Certificate Authority (PEM)\n"
#if defined(CONFIG_SIGALG_CMS) && defined(CONFIG_SSL_IMPL_OPENSSL)
" --crl-path : path to the Certificate Revocation List (CRL, PEM or DER)\n"
+ " --crl-check-all : check CRLs for the full cert chain (default: signer cert only)\n"
#endif
#ifdef CONFIG_SIGALG_GPG
" For GnuPG only:\n"
@@ -391,6 +393,8 @@ static int read_globals_settings(void *elem, void *data)
"ca-path", sw->publickeyfname);
GET_FIELD_STRING(LIBCFG_PARSER, elem,
"crl-path", sw->crlfname);
+ GET_FIELD_BOOL(LIBCFG_PARSER, elem,
+ "crl-check-all", &sw->crl_check_all);
GET_FIELD_STRING(LIBCFG_PARSER, elem,
"aes-key-file", sw->aeskeyfname);
GET_FIELD_STRING(LIBCFG_PARSER, elem,
@@ -635,6 +639,9 @@ int main(int argc, char **argv)
#endif
#ifdef CONFIG_SIGNED_IMAGES
strcat(main_options, "k:");
+#if defined(CONFIG_SIGALG_CMS) && defined(CONFIG_SSL_IMPL_OPENSSL)
+ strcat(main_options, "A");
+#endif
#endif
#ifdef CONFIG_ENCRYPTED_IMAGES
strcat(main_options, "K:");
@@ -854,6 +861,9 @@ int main(int argc, char **argv)
optarg,
sizeof(swcfg.crlfname));
break;
+ case 'A':
+ swcfg.crl_check_all = true;
+ break;
#ifdef CONFIG_ENCRYPTED_IMAGES
case 'K':
if (optarg) strlcpy(swcfg.aeskeyfname,
diff --git a/crypto/swupdate_cms_verify_openssl.c b/crypto/swupdate_cms_verify_openssl.c
index e202817e..bada8778 100644
--- a/crypto/swupdate_cms_verify_openssl.c
+++ b/crypto/swupdate_cms_verify_openssl.c
@@ -164,7 +164,8 @@ static void report_crl_err(unsigned long err, const char *crl_file)
ERR_clear_error();
}

-static int add_crl_to_store(X509_STORE *castore, const char *crl_file)
+static int add_crl_to_store(X509_STORE *castore, const char *crl_file,
+ bool check_all)
{
BIO *fp = BIO_new_file(crl_file, "rb");
if (!fp) {
@@ -232,7 +233,12 @@ static int add_crl_to_store(X509_STORE *castore, const char *crl_file)
ERR_clear_error();
}

- if (!X509_STORE_set_flags(castore, X509_V_FLAG_CRL_CHECK)) {
+ unsigned long crl_flags = X509_V_FLAG_CRL_CHECK;
+
+ if (check_all)
+ crl_flags |= X509_V_FLAG_CRL_CHECK_ALL;
+
+ if (!X509_STORE_set_flags(castore, crl_flags)) {
TRACE("Error setting CRL flags");
return 0;
}
@@ -406,7 +412,8 @@ static int openssl_cms_dgst_init(struct swupdate_cfg *sw, const char *keyfile)

if (strlen(sw->crlfname)) {
TRACE("Loading CRL from %s", sw->crlfname);
- if (!add_crl_to_store(dgst->certs, sw->crlfname)) {
+ if (!add_crl_to_store(dgst->certs, sw->crlfname,
+ sw->crl_check_all)) {
ERROR("Error loading CRL from %s", sw->crlfname);
ret = -EINVAL;
goto dgst_init_error;
diff --git a/doc/source/signed_images.rst b/doc/source/signed_images.rst
index d7ec5813..c682714a 100644
--- a/doc/source/signed_images.rst
+++ b/doc/source/signed_images.rst
@@ -325,9 +325,18 @@ For a DER-encoded CRL, pass the binary file directly:
--crl-path /etc/swupdate/revocations.der

Revocation is only checked against the signer certificate. Revoking an
-intermediate CA in the trust chain would require the
-``X509_V_FLAG_CRL_CHECK_ALL`` verification flag, which SWUpdate does not
-currently set.
+intermediate CA in the trust chain requires enabling full-chain checking
+with ``X509_V_FLAG_CRL_CHECK_ALL``.
+
+By default, SWUpdate sets only ``X509_V_FLAG_CRL_CHECK``. To enable
+full-chain checks, pass ``--crl-check-all`` (or set
+``crl-check-all = true`` in the ``globals`` section of ``swupdate.cfg``).
+
+::
+
+ swupdate -i image.swu -k /etc/swupdate/ca-chain.pem \
+ --crl-path /etc/swupdate/revocations.pem \
+ --crl-check-all

For GPG signing, CONFIG_SIGALG_GPG needs to be enabled. The GPG key will
need to be imported to the device's GnuPG home directory. To do this, the
diff --git a/include/swupdate.h b/include/swupdate.h
index 1c5729b9..ff9d6dd2 100644
--- a/include/swupdate.h
+++ b/include/swupdate.h
@@ -84,6 +84,7 @@ struct swupdate_cfg {
char output_swversions[SWUPDATE_GENERAL_STRING_SIZE];
char publickeyfname[SWUPDATE_GENERAL_STRING_SIZE];
char crlfname[SWUPDATE_GENERAL_STRING_SIZE];
+ bool crl_check_all;
char aeskeyfname[SWUPDATE_GENERAL_STRING_SIZE];
char mtdblacklist[SWUPDATE_GENERAL_STRING_SIZE];
char forced_signer_name[SWUPDATE_GENERAL_STRING_SIZE];
--
2.43.0

wesdm...@gmail.com

unread,
Aug 11, 2026, 10:02:12 PMAug 11
to swup...@googlegroups.com, Wes Malone
From: Wes Malone <wesdm...@gmail.com>

Unit test cases to validate the X509_V_FLAG_CRL_CHECK_ALL behavior.
The test Makefile generates a multi-level CA hierarchy (root
CA -> intermediate CA -> signer).

Tests added (in test/test_verify.c):
- _with_revoked_intermediate_crl_default:
intermediate revoked, check_all=false → pass (current behavior)
- _with_revoked_intermediate_crl_check_all:
same scenario with check_all=true → rejected

The paired *_default / *_check_all tests show that without
CRL_CHECK_ALL, a revoked intermediate CA can issue a valid-looking new
leaf cert that passes verification.

Signed-off-by: Wes Malone <wesdm...@gmail.com>
---
test/Makefile | 33 +++++++++++++++--
.../data/cms-test-ca/intermediate-openssl.cnf | 31 ++++++++++++++++
test/data/cms-test-ca/openssl.cnf | 3 ++
test/test_verify.c | 37 +++++++++++++++++++
4 files changed, 101 insertions(+), 3 deletions(-)
create mode 100644 test/data/cms-test-ca/intermediate-openssl.cnf

diff --git a/test/Makefile b/test/Makefile
index ca66f7b0..ffe613ad 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -108,7 +108,7 @@ $(obj)/test_verify.o: PREPARE_DATA $(DATADIR)/signature $(DATADIR)/signing-pubke

ifeq ($(CONFIG_SSL_IMPL_OPENSSL),y)
ifeq ($(CONFIG_SIGALG_CMS),y)
-$(obj)/test_verify.o: $(DATADIR)/signature.cms $(DATADIR)/cms-ca.cert.pem $(DATADIR)/cms-ca.crl.pem $(DATADIR)/cms-ca.crl.der $(DATADIR)/cms-ca.crl.empty.pem
+$(obj)/test_verify.o: $(DATADIR)/signature.cms $(DATADIR)/cms-ca.cert.pem $(DATADIR)/cms-ca.crl.pem $(DATADIR)/cms-ca.crl.der $(DATADIR)/cms-ca.crl.empty.pem $(DATADIR)/signature-chain.cms $(DATADIR)/cms-chain.crl.pem
endif
endif

@@ -129,11 +129,12 @@ $(DATADIR)/signing-secret.pem:

CMS_TEST_CA_DIR := $(DATADIR)/cms-test-ca
CMS_TEST_CA_CONFIG := $(CMS_TEST_CA_DIR)/openssl.cnf
+CMS_TEST_INTERMEDIATE_CA_CONFIG := $(CMS_TEST_CA_DIR)/intermediate-openssl.cnf

-$(DATADIR)/signature.cms $(DATADIR)/cms-ca.cert.pem $(DATADIR)/cms-ca.crl.pem $(DATADIR)/cms-ca.crl.der $(DATADIR)/cms-ca.crl.empty.pem &: $(DATADIR)/to-be-signed $(CMS_TEST_CA_CONFIG) | PREPARE_DATA
+$(DATADIR)/signature.cms $(DATADIR)/cms-ca.cert.pem $(DATADIR)/cms-ca.crl.pem $(DATADIR)/cms-ca.crl.der $(DATADIR)/cms-ca.crl.empty.pem $(DATADIR)/signature-chain.cms $(DATADIR)/cms-chain.crl.pem &: $(DATADIR)/to-be-signed $(CMS_TEST_CA_CONFIG) $(CMS_TEST_INTERMEDIATE_CA_CONFIG) | PREPARE_DATA
$(if $(Q),@echo " GEN CMS/CRL test data")
$(Q)mkdir -p $(CMS_TEST_CA_DIR)
- $(Q)find $(CMS_TEST_CA_DIR) -mindepth 1 ! -name openssl.cnf -exec rm -rf {} +
+ $(Q)find $(CMS_TEST_CA_DIR) -mindepth 1 ! -name openssl.cnf ! -name intermediate-openssl.cnf -exec rm -rf {} +
$(Q)mkdir -p $(CMS_TEST_CA_DIR)/newcerts
$(Q)printf "1000\n" > $(CMS_TEST_CA_DIR)/serial
$(Q)printf "1000\n" > $(CMS_TEST_CA_DIR)/crlnumber
@@ -152,14 +153,40 @@ $(DATADIR)/signature.cms $(DATADIR)/cms-ca.cert.pem $(DATADIR)/cms-ca.crl.pem $(
$(Q)openssl cms -sign -in $(DATADIR)/to-be-signed -out $(DATADIR)/signature.cms \
-signer $(DATADIR)/cms-signer.cert.pem -inkey $(DATADIR)/cms-signer.key.pem \
-outform DER -nosmimecap -binary > /dev/null 2>&1
+ $(Q)openssl req -new -newkey rsa:2048 -nodes \
+ -keyout $(DATADIR)/cms-intermediate.key.pem -out $(DATADIR)/cms-intermediate.csr.pem \
+ -subj "/CN=SWUpdate Test Intermediate CA" > /dev/null 2>&1
+ $(Q)openssl ca -batch -config $(CMS_TEST_CA_CONFIG) -extensions intermediate_ca \
+ -in $(DATADIR)/cms-intermediate.csr.pem -out $(DATADIR)/cms-intermediate.cert.pem \
+ -days 365 > /dev/null 2>&1
+ $(Q)mkdir -p $(CMS_TEST_CA_DIR)/intermediate/newcerts
+ $(Q)printf "1000\n" > $(CMS_TEST_CA_DIR)/intermediate/serial
+ $(Q)printf "1000\n" > $(CMS_TEST_CA_DIR)/intermediate/crlnumber
+ $(Q)touch $(CMS_TEST_CA_DIR)/intermediate/index.txt
+ $(Q)openssl req -new -newkey rsa:2048 -nodes \
+ -keyout $(DATADIR)/cms-chain-signer.key.pem -out $(DATADIR)/cms-chain-signer.csr.pem \
+ -subj "/CN=SWUpdate Test Chain Signer" > /dev/null 2>&1
+ $(Q)openssl ca -batch -config $(CMS_TEST_INTERMEDIATE_CA_CONFIG) \
+ -in $(DATADIR)/cms-chain-signer.csr.pem -out $(DATADIR)/cms-chain-signer.cert.pem \
+ -days 365 > /dev/null 2>&1
+ $(Q)openssl cms -sign -in $(DATADIR)/to-be-signed -out $(DATADIR)/signature-chain.cms \
+ -signer $(DATADIR)/cms-chain-signer.cert.pem -inkey $(DATADIR)/cms-chain-signer.key.pem \
+ -certfile $(DATADIR)/cms-intermediate.cert.pem -outform DER -nosmimecap -binary > /dev/null 2>&1
$(Q)openssl ca -gencrl -config $(CMS_TEST_CA_CONFIG) \
-out $(DATADIR)/cms-ca.crl.empty.pem > /dev/null 2>&1
+ $(Q)openssl ca -gencrl -config $(CMS_TEST_INTERMEDIATE_CA_CONFIG) \
+ -out $(DATADIR)/cms-intermediate.crl.empty.pem > /dev/null 2>&1
$(Q)openssl ca -config $(CMS_TEST_CA_CONFIG) \
-revoke $(DATADIR)/cms-signer.cert.pem > /dev/null 2>&1
$(Q)openssl ca -gencrl -config $(CMS_TEST_CA_CONFIG) \
-out $(DATADIR)/cms-ca.crl.pem > /dev/null 2>&1
$(Q)openssl crl -in $(DATADIR)/cms-ca.crl.pem -out $(DATADIR)/cms-ca.crl.der \
-outform DER > /dev/null 2>&1
+ $(Q)openssl ca -config $(CMS_TEST_CA_CONFIG) \
+ -revoke $(DATADIR)/cms-intermediate.cert.pem > /dev/null 2>&1
+ $(Q)openssl ca -gencrl -config $(CMS_TEST_CA_CONFIG) \
+ -out $(DATADIR)/cms-root.crl.pem > /dev/null 2>&1
+ $(Q)cat $(DATADIR)/cms-root.crl.pem $(DATADIR)/cms-intermediate.crl.empty.pem > $(DATADIR)/cms-chain.crl.pem

ifeq ($(CONFIG_PKCS11),y)
$(obj)/test_crypt_pkcs11.o: $(DATADIR)/softshm
diff --git a/test/data/cms-test-ca/intermediate-openssl.cnf b/test/data/cms-test-ca/intermediate-openssl.cnf
new file mode 100644
index 00000000..9a5b346b
--- /dev/null
+++ b/test/data/cms-test-ca/intermediate-openssl.cnf
@@ -0,0 +1,31 @@
+# SPDX-FileCopyrightText: 2026 MTSI
+#
+# SPDX-License-Identifier: CC0-1.0
+
+[ ca ]
+default_ca = swupdate_test_intermediate_ca
+[ swupdate_test_intermediate_ca ]
+dir = test/data/cms-test-ca/intermediate
+database = $dir/index.txt
+new_certs_dir = $dir/newcerts
+certificate = test/data/cms-intermediate.cert.pem
+private_key = test/data/cms-intermediate.key.pem
+serial = $dir/serial
+crlnumber = $dir/crlnumber
+default_md = sha256
+policy = policy_any
+x509_extensions = usr_cert
+copy_extensions = copy
+default_crl_days = 365
+[ policy_any ]
+commonName = supplied
+organizationName = optional
+organizationalUnitName = optional
+countryName = optional
+stateOrProvinceName = optional
+localityName = optional
+emailAddress = optional
+[ usr_cert ]
+basicConstraints = critical,CA:FALSE
+keyUsage = critical,digitalSignature
+extendedKeyUsage = emailProtection
diff --git a/test/data/cms-test-ca/openssl.cnf b/test/data/cms-test-ca/openssl.cnf
index 0eb9d10f..c6695ebf 100644
--- a/test/data/cms-test-ca/openssl.cnf
+++ b/test/data/cms-test-ca/openssl.cnf
@@ -29,3 +29,6 @@ emailAddress = optional
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = emailProtection
+[ intermediate_ca ]
+basicConstraints = critical,CA:true,pathlen:0
+keyUsage = critical,keyCertSign,cRLSign
diff --git a/test/test_verify.c b/test/test_verify.c
index d72f3967..a93f67f4 100644
--- a/test/test_verify.c
+++ b/test/test_verify.c
@@ -112,6 +112,41 @@ static void test_verify_cms_with_nonrevoked_crl(void **state)
DATADIR "to-be-signed", NULL);
assert_int_equal(error, 0);
}
+
+static void test_verify_cms_with_revoked_intermediate_crl_default(void **state)
+{
+ int error;
+ struct swupdate_cfg config;
+
+ (void)state;
+
+ memset(&config, 0, sizeof(config));
+ strlcpy(config.crlfname, DATADIR "cms-chain.crl.pem", sizeof(config.crlfname));
+ error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
+ assert_int_equal(error, 0);
+
+ error = swupdate_verify_file(config.dgst, DATADIR "signature-chain.cms",
+ DATADIR "to-be-signed", NULL);
+ assert_int_equal(error, 0);
+}
+
+static void test_verify_cms_with_revoked_intermediate_crl_check_all(void **state)
+{
+ int error;
+ struct swupdate_cfg config;
+
+ (void)state;
+
+ memset(&config, 0, sizeof(config));
+ strlcpy(config.crlfname, DATADIR "cms-chain.crl.pem", sizeof(config.crlfname));
+ config.crl_check_all = true;
+ error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
+ assert_int_equal(error, 0);
+
+ error = swupdate_verify_file(config.dgst, DATADIR "signature-chain.cms",
+ DATADIR "to-be-signed", NULL);
+ assert_int_not_equal(error, 0);
+}
#endif

int main(void)
@@ -124,6 +159,8 @@ int main(void)
cmocka_unit_test(test_verify_cms_with_revoked_signer_crl),
cmocka_unit_test(test_verify_cms_with_revoked_signer_der_crl),
cmocka_unit_test(test_verify_cms_with_nonrevoked_crl),
+ cmocka_unit_test(test_verify_cms_with_revoked_intermediate_crl_default),
+ cmocka_unit_test(test_verify_cms_with_revoked_intermediate_crl_check_all),
#endif
};
return cmocka_run_group_tests_name("verify", verify_tests, NULL, NULL);
--
2.43.0

wesdm...@gmail.com

unread,
Aug 11, 2026, 10:02:18 PMAug 11
to swup...@googlegroups.com, Wes Malone
From: Wes Malone <wesdm...@gmail.com>

When multiple digest providers are compiled in (e.g. SIGALG_RAWRSA and
SIGALG_CMS), swupdate defaults to the first registered one (opensslRSA)
unless the user specifies the provider via --digest-provider. Here we
mimic that param/config selection for CRL testing.

Signed-off-by: Wes Malone <wesdm...@gmail.com>
---
test/test_verify.c | 17 +++++++++++++++++
1 file changed, 17 insertions(+)

diff --git a/test/test_verify.c b/test/test_verify.c
index a93f67f4..d6cdcd26 100644
--- a/test/test_verify.c
+++ b/test/test_verify.c
@@ -46,6 +46,17 @@ static void test_verify_pkcs15(void **state)
}

#if defined(CONFIG_SIGALG_CMS) && defined(CONFIG_SSL_IMPL_OPENSSL)
+/*
+ * mimic --digest-provider opensslCMS selection for CMS tests, otherwise
+ * opensslRSA is selected
+ */
+static void select_cms_provider(struct swupdate_cfg *config)
+{
+ strlcpy(config->digest_provider, "opensslCMS",
+ sizeof(config->digest_provider));
+ assert_int_equal(set_dgstlib(config->digest_provider), 0);
+}
+
static void test_verify_cms_without_crl(void **state)
{
int error;
@@ -54,6 +65,7 @@ static void test_verify_cms_without_crl(void **state)
(void)state;

memset(&config, 0, sizeof(config));
+ select_cms_provider(&config);
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);

@@ -71,6 +83,7 @@ static void test_verify_cms_with_revoked_signer_crl(void **state)

memset(&config, 0, sizeof(config));
strlcpy(config.crlfname, DATADIR "cms-ca.crl.pem", sizeof(config.crlfname));
+ select_cms_provider(&config);
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);

@@ -88,6 +101,7 @@ static void test_verify_cms_with_revoked_signer_der_crl(void **state)

memset(&config, 0, sizeof(config));
strlcpy(config.crlfname, DATADIR "cms-ca.crl.der", sizeof(config.crlfname));
+ select_cms_provider(&config);
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);

@@ -105,6 +119,7 @@ static void test_verify_cms_with_nonrevoked_crl(void **state)

memset(&config, 0, sizeof(config));
strlcpy(config.crlfname, DATADIR "cms-ca.crl.empty.pem", sizeof(config.crlfname));
+ select_cms_provider(&config);
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);

@@ -122,6 +137,7 @@ static void test_verify_cms_with_revoked_intermediate_crl_default(void **state)

memset(&config, 0, sizeof(config));
strlcpy(config.crlfname, DATADIR "cms-chain.crl.pem", sizeof(config.crlfname));
+ select_cms_provider(&config);
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);

@@ -139,6 +155,7 @@ static void test_verify_cms_with_revoked_intermediate_crl_check_all(void **state

memset(&config, 0, sizeof(config));
strlcpy(config.crlfname, DATADIR "cms-chain.crl.pem", sizeof(config.crlfname));
+ select_cms_provider(&config);
config.crl_check_all = true;
error = swupdate_dgst_init(&config, DATADIR "cms-ca.cert.pem");
assert_int_equal(error, 0);
--
2.43.0

Reply all
Reply to author
Forward
0 new messages