From 8a3910d2bfb6d292480dbba5b8a8febe1901f742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Hor=C3=A1k?= Date: Thu, 27 Aug 2026 14:56:31 +0200 Subject: [PATCH] - zkey: Security fixes for zkey and friends (RHEL-243143) - Resolves: RHEL-243143 --- s390utils-2.29.0-rhel.patch | 2099 ++++++++++++++++++++++++++++++++++- s390utils.spec | 6 +- 2 files changed, 2060 insertions(+), 45 deletions(-) diff --git a/s390utils-2.29.0-rhel.patch b/s390utils-2.29.0-rhel.patch index 22f4d8f..c644235 100644 --- a/s390utils-2.29.0-rhel.patch +++ b/s390utils-2.29.0-rhel.patch @@ -1,7 +1,7 @@ From a32824922cb273703bacd44e6a29cbc33ae48cf5 Mon Sep 17 00:00:00 2001 From: Ingo Franzki Date: Fri, 21 Jul 2023 14:06:18 +0200 -Subject: [PATCH 01/22] zkey: Support EP11 AES keys with prepended header to +Subject: [PATCH 01/31] zkey: Support EP11 AES keys with prepended header to retain EP11 session (RHEL-11440) The pkey kernel module supports two key blob formats for EP11 AES keys. @@ -730,13 +730,13 @@ index 3000290f..843e5544 100644 "secure AES key"); } -- -2.53.0 +2.55.0 From df0819ca69dbef1f99321f51cd9c4d33c6374992 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 7 Aug 2023 16:56:54 +0200 -Subject: [PATCH 02/22] rust/Makefile: Fix use of Cargoflags for 'make clean' +Subject: [PATCH 02/31] rust/Makefile: Fix use of Cargoflags for 'make clean' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -763,13 +763,13 @@ index cf2fda70..420bafd0 100644 rust-test: .check-cargo .no-cross-compile -- -2.53.0 +2.55.0 From b6ce8c7fc10c225c0b1d59af32edd323f5817ab7 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Mon, 7 Aug 2023 16:56:55 +0200 -Subject: [PATCH 03/22] rust/README.md: Fix some typos +Subject: [PATCH 03/31] rust/README.md: Fix some typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -833,13 +833,13 @@ index 2622bba2..61b0af80 100644 } ``` -- -2.53.0 +2.55.0 From 883d28afea6ea18b1001ebf9e3d921d86be9c593 Mon Sep 17 00:00:00 2001 From: Marc Hartmayer Date: Mon, 4 Sep 2023 14:18:50 +0200 -Subject: [PATCH 04/22] rust/**/*.rs: fix `cargo clippy` findings +Subject: [PATCH 04/31] rust/**/*.rs: fix `cargo clippy` findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -1205,13 +1205,13 @@ index 2b3e861e..bf491c61 100644 let exp = get_test_asset!("exp/asrcb/null_none_default_cuid_seven"); -- -2.53.0 +2.55.0 From 4c8072cebe9add441c42e62663d4089d14d32389 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 25 Oct 2023 15:26:14 +0200 -Subject: [PATCH 05/22] rust/pv: fix Invalid write of size 1 +Subject: [PATCH 05/31] rust/pv: fix Invalid write of size 1 Fix a valgrind finding. Fix an invalid read/write of one byte after the actual struct to clear. Not fixing this may result in a illegal write or @@ -1240,13 +1240,13 @@ index cdef9ef8..88287c8e 100644 } std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); -- -2.53.0 +2.55.0 From 49eabe2d13ea3909f4c522fefaf8db998c7ab888 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 4 Oct 2023 10:59:34 +0200 -Subject: [PATCH 06/22] rust: Create workspace +Subject: [PATCH 06/31] rust: Create workspace A workspaces simplifies the build and packaging process significantly. All build artifacts and binaries are now built in a single location @@ -1492,13 +1492,13 @@ index 30bbbc83..215381be 100644 +edition.workspace = true +license.workspace = true -- -2.53.0 +2.55.0 From be47ce72f4ee7dc7ed2dafb9b89079b0c2b154fa Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 4 Oct 2023 11:08:20 +0200 -Subject: [PATCH 07/22] rust: Update dependency files +Subject: [PATCH 07/31] rust: Update dependency files With the last patch introducing the rust workspace the location of Cargo.lock has changed. Therefore, remove all crate level lock-files and @@ -1912,13 +1912,13 @@ index 1db32c29..f7d1cf0f 100644 name = "winapi" version = "0.3.9" -- -2.53.0 +2.55.0 From c25115c0d605c9c79efd8e17d4917a35603c0766 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Tue, 21 Nov 2023 13:27:21 +0100 -Subject: [PATCH 08/22] rust: Sanitize minimal dependencies +Subject: [PATCH 08/31] rust: Sanitize minimal dependencies The crate dependencies were a bit to slack. Due to the rust dependency resolver's strategy of always selecting the latest version this never @@ -2009,13 +2009,13 @@ index d1e75b13..e236c005 100644 pv = { path = "../pv", features = ["uvsecret", "request"] } -- -2.53.0 +2.55.0 From b6009c80b112ad85ca2aa649126b913af5af253c Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 29 Nov 2023 17:06:50 +0100 -Subject: [PATCH 09/22] rust: Use default panic behaviour +Subject: [PATCH 09/31] rust: Use default panic behaviour Reviewed-by: Marc Hartmayer Signed-off-by: Steffen Eiden @@ -2034,13 +2034,13 @@ index 65a70a97..7ba1faa1 100644 lto = true -panic = "abort" -- -2.53.0 +2.55.0 From c4e48d060b7d92d7c6cd150728ecb55b301afa62 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Thu, 30 Nov 2023 16:02:16 +0100 -Subject: [PATCH 10/22] rust/pv: Update mockito to version 1 +Subject: [PATCH 10/31] rust/pv: Update mockito to version 1 Signed-off-by: Steffen Eiden (cherry picked from commit 21662d38e68b58bad033cdb1fca99987dd07cf78) @@ -2732,13 +2732,13 @@ index 1c0d2b56..5ca2e712 100644 .with_body_from_file(res_path) .create() -- -2.53.0 +2.55.0 From 66783f1901dcaca6f567ad13b05acc7dbe412ff0 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 20 Dec 2023 13:31:18 +0100 -Subject: [PATCH 11/22] rust/Makefile: Fix CC/AR variables for TEST_TARGETS +Subject: [PATCH 11/31] rust/Makefile: Fix CC/AR variables for TEST_TARGETS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -2766,13 +2766,13 @@ index e4e98859..fa3cf04d 100644 $(PV_TARGETS): .check-dep-pvtools $(PV_TARGETS) $(CARGO_TARGETS): .check-cargo .no-cross-compile -- -2.53.0 +2.55.0 From d54a8aa4d7b77338fd5511d895eadbb074b6024a Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Fri, 15 Dec 2023 11:30:14 +0100 -Subject: [PATCH 12/22] rust/pv: Provide access for SecretList members +Subject: [PATCH 12/31] rust/pv: Provide access for SecretList members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -3044,13 +3044,13 @@ index 6943bd31..72a05b27 100644 fn dump_secret_entry() { const EXP: &[u8] = &[ -- -2.53.0 +2.55.0 From e75bbd754e5912d34c0aedfe35ccedd54ca850be Mon Sep 17 00:00:00 2001 From: Harald Freudenberger Date: Fri, 1 Dec 2023 12:10:20 +0100 -Subject: [PATCH 13/22] rust/pvapconfig: Introduce new tool pvapconfig +Subject: [PATCH 13/31] rust/pvapconfig: Introduce new tool pvapconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -5715,13 +5715,13 @@ index 00000000..2f98bd5e + } +} -- -2.53.0 +2.55.0 From 6b69de3c519971a88c5953075586b322e1efdc3e Mon Sep 17 00:00:00 2001 From: Joern Siglen Date: Wed, 25 Oct 2023 15:01:11 +0200 -Subject: [PATCH 14/22] dbginfo.sh: enhance ethtool collection for ROCE +Subject: [PATCH 14/31] dbginfo.sh: enhance ethtool collection for ROCE (RHEL-24110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -5752,13 +5752,13 @@ index 9226a8bb..f70cf6ee 100755 else pr_skip "ethtool: no devices" -- -2.53.0 +2.55.0 From 90943f11e0feef6bc6cde3bf0b80ad0a21c55d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Hor=C3=A1k?= Date: Wed, 10 Apr 2024 11:27:58 +0200 -Subject: [PATCH 15/22] rust/pv: Support `Armonk` in IBM signing key subject +Subject: [PATCH 15/31] rust/pv: Support `Armonk` in IBM signing key subject (RHEL-30398) New IBM signing keys will have Armonk as locality in the subject. @@ -6064,13 +6064,13 @@ index 5ca2e712..e4c60c57 100644 verifier.verify(&hkd_exp), Err(Error::HkdVerify(AfterValidity)) -- -2.53.0 +2.55.0 From e1423607a66ee37f8ae581fbf5fa013f5ab80ae8 Mon Sep 17 00:00:00 2001 From: Marc Hartmayer Date: Thu, 14 Mar 2024 16:05:09 +0000 -Subject: [PATCH 16/22] genprotimg: support `Armonk` in IBM signing key subject +Subject: [PATCH 16/31] genprotimg: support `Armonk` in IBM signing key subject (RHEL-30398) New IBM signing certificates will have 'Armonk' as locality in the @@ -6396,13 +6396,13 @@ index fdf66de2..e45e57df 100644 WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(BIO, BIO_free_all) WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(BN_CTX, BN_CTX_free) -- -2.53.0 +2.55.0 From 1605e9c0033e245f8a6690e2ce95a27e383722df Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Tue, 12 Mar 2024 10:14:43 +0100 -Subject: [PATCH 17/22] libpv: Support `Armonk` in IBM signing key subject +Subject: [PATCH 17/31] libpv: Support `Armonk` in IBM signing key subject (RHEL-30398) New IBM signing keys will have Armonk as locality in the subject. @@ -6645,13 +6645,13 @@ index c8bb8cc3..f4774fcb 100644 for (int i = 0; i < sk_X509_CRL_num(ret); i++) { X509_CRL *crl = sk_X509_CRL_value(ret, i); -- -2.53.0 +2.55.0 From 3bd5cce64692d4b630b313cf465a55595971bed4 Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Wed, 20 Mar 2024 15:36:52 +0100 -Subject: [PATCH 18/22] pvattest: Fix root-ca parsing (RHEL-30398) +Subject: [PATCH 18/31] pvattest: Fix root-ca parsing (RHEL-30398) The parser setup falsely set the argument type as filename array, but code expected a single filename. Fixed by setting up the parser @@ -6691,13 +6691,13 @@ index fe5662f9..5924ddc8 100644 /* NOTE REQUIRED */ -- -2.53.0 +2.55.0 From a1b83b44def87915acaf0d9668bdf96824d0e781 Mon Sep 17 00:00:00 2001 From: Ingo Franzki Date: Thu, 3 Apr 2025 11:26:47 +0200 -Subject: [PATCH 19/22] zkey: Adjust CCA host library version detection for +Subject: [PATCH 19/31] zkey: Adjust CCA host library version detection for newer CCA versions (RHEL-105487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -6748,13 +6748,13 @@ index 71c08ad1..9d79c4b3 100644 warnx("CCA formware version is invalid: %s", version_data); return -EINVAL; -- -2.53.0 +2.55.0 From 8b98ad7e7e273b2327ad50bdf31935d4a439a618 Mon Sep 17 00:00:00 2001 From: Ingo Franzki Date: Thu, 11 Sep 2025 08:37:38 +0200 -Subject: [PATCH 20/22] libekmfweb: Fix generation of certificate or CSR with +Subject: [PATCH 20/31] libekmfweb: Fix generation of certificate or CSR with RSA-PSS instead of RSA (RHEL-114130) Currently a certificate or certificate signing request generated by the @@ -6816,13 +6816,13 @@ index ee25fc08..3a822e78 100644 if (rc != 0) { pr_verbose(verbose, "SK_OPENSSL_generate_ss_cert failed " -- -2.53.0 +2.55.0 From ad8dcda916c7c7ec4294fff11e1858e3cbd9ec2f Mon Sep 17 00:00:00 2001 From: Eduard Shishkin Date: Mon, 19 Feb 2024 17:25:04 +0100 -Subject: [PATCH 21/22] dasdfmt: Change mode default (RHEL-121638) +Subject: [PATCH 21/31] dasdfmt: Change mode default (RHEL-121638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -6946,13 +6946,13 @@ index 2065e12e..a49e41c0 100644 /* -- -2.53.0 +2.55.0 From e75eaad672b9352f1ceb0e90e525b7c988ed55ea Mon Sep 17 00:00:00 2001 From: Aswin Karuvally Date: Sun, 1 Feb 2026 18:39:38 +0530 -Subject: [PATCH 22/22] hsci: Automatically set appropriate MTU for HSCI +Subject: [PATCH 22/31] hsci: Automatically set appropriate MTU for HSCI (RHEL-153548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -6994,5 +6994,2016 @@ index 5540e82f..8f884c9a 100755 ip link set dev $hscibp up >/dev/null 2>&1 if [ $? -ne 0 ]; then -- -2.53.0 +2.55.0 + + +From d412d1e612a3befae886fa5739d1d4b8bddd40b7 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Mon, 29 Jun 2026 16:13:50 +0200 +Subject: [PATCH 23/31] zkey: Check values before constructing a command to + execute (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Check the values that are incorporated into commands that might get +executed to not contain any characters that could be used for +shell-injection attacks. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 278f4f6fd57b6cf43a50b8d65bd1e2900b57fe81) +--- + zkey/keystore.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 57 insertions(+) + +diff --git a/zkey/keystore.c b/zkey/keystore.c +index c0a7037b..f3e67816 100644 +--- a/zkey/keystore.c ++++ b/zkey/keystore.c +@@ -10,6 +10,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -4193,6 +4194,32 @@ static int _keystore_execute_cmd(const char *cmd, + } + + ++/** ++ * Check if all characters of the string are within [A-Za-z0-9._/:-], and thus ++ * are safe to be used in generated commands that might get executed. ++ * ++ * @param[in] str the string to check ++ * ++ * @returns true if the string is valid, false otherwise ++ */ ++static bool is_valid_characters_for_command(const char *str) ++{ ++ const char *chars = "._/:-"; ++ size_t i; ++ ++ for (i = 0; i < strlen(str); i++) { ++ if (isalnum(str[i])) ++ continue; ++ if (strchr(chars, str[i]) != NULL) ++ continue; ++ ++ warnx("'%s' contains invalid characters", str); ++ return false; ++ } ++ ++ return true; ++} ++ + struct crypt_info { + bool execute; + bool batch_mode; +@@ -4251,9 +4278,21 @@ static int _keystore_process_cryptsetup(struct keystore *keystore, + int rc = 0; + char *cmd; + ++ if (!is_valid_characters_for_command(cipher_spec)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(key_file_name)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(volume)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(dmname)) ++ return -EINVAL; ++ + sprintf(temp, "--sector-size %lu ", sector_size); + + if (info->keyfile) { ++ if (!is_valid_characters_for_command(info->keyfile)) ++ return -EINVAL; ++ + util_asprintf(&keyfile_opt, "--key-file '%s' ", info->keyfile); + if (info->keyfile_offset > 0) + util_asprintf(&offset_opt, "--keyfile-offset %lu ", +@@ -4262,6 +4301,9 @@ static int _keystore_process_cryptsetup(struct keystore *keystore, + util_asprintf(&size_opt, "--keyfile-size %lu ", + info->keyfile_size); + } else if (passphrase_file != NULL) { ++ if (!is_valid_characters_for_command(passphrase_file)) ++ return -EINVAL; ++ + util_asprintf(&keyfile_opt, "--key-file '%s' ", + passphrase_file); + } +@@ -4397,6 +4439,15 @@ static int _keystore_process_crypttab(struct keystore *UNUSED(keystore), + { + char temp[1000]; + ++ if (!is_valid_characters_for_command(cipher_spec)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(key_file_name)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(volume)) ++ return -EINVAL; ++ if (!is_valid_characters_for_command(dmname)) ++ return -EINVAL; ++ + if (strcasecmp(volume_type, VOLUME_TYPE_PLAIN) == 0) { + sprintf(temp, ",sector-size=%lu", sector_size); + printf("%s\t%s\t%s\tplain,cipher=%s,size=%lu%s\n", +@@ -4404,6 +4455,9 @@ static int _keystore_process_crypttab(struct keystore *UNUSED(keystore), + key_file_size * 8, sector_size > 0 ? temp : ""); + } else if (strcasecmp(volume_type, VOLUME_TYPE_LUKS2) == 0) { + if (info->keyfile != NULL) { ++ if (!is_valid_characters_for_command(info->keyfile)) ++ return -EINVAL; ++ + printf("%s\t%s\t%s\tluks", dmname, volume, + info->keyfile); + if (info->keyfile_offset > 0) +@@ -4412,6 +4466,9 @@ static int _keystore_process_crypttab(struct keystore *UNUSED(keystore), + if (info->keyfile_size > 0) + printf(",keyfile-size=%lu", info->keyfile_size); + } else if (passphrase_file != NULL) { ++ if (!is_valid_characters_for_command(passphrase_file)) ++ return -EINVAL; ++ + printf("%s\t%s\t%s\tluks", dmname, volume, + passphrase_file); + } else { +-- +2.55.0 + + +From c17eded89a7195859ebec082d8a176f5cbf52ca0 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Tue, 30 Jun 2026 09:46:27 +0200 +Subject: [PATCH 24/31] zkey: Protect from symlink-following attacks + (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Files in the zkey repository can be created by any member of the +'zkeyadm' group as well as 'root'. Such files are owned by the creator +and the 'zkeyadm' group, and allow read and write for the owner user +and owner group. + +When creating or writing files inside the zkey repository, make sure +that the file is not a sysmlink. That way, only files within the zkey +repository are set to be read/write for the owner user and members of +the 'zkeyadm' group. Make sure to open such files with the 'O_NOFOLLOW' +flag, and use 'lstat()' to check for files and directories. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 94292dac547eb81b2c07367cc309c38ec75cf1d3) +--- + zkey/keystore.c | 30 ++++++++++++++++++++---------- + zkey/kms.c | 43 ++++++++++++++++++++++++++++++++----------- + zkey/pkey.c | 2 +- + zkey/plugin-utils.c | 25 ++++++++++++++++++------- + zkey/properties.c | 3 ++- + zkey/utils.c | 34 ++++++++++++++++++++++++++++++++-- + zkey/utils.h | 2 ++ + 7 files changed, 107 insertions(+), 32 deletions(-) + +diff --git a/zkey/keystore.c b/zkey/keystore.c +index f3e67816..af549f5c 100644 +--- a/zkey/keystore.c ++++ b/zkey/keystore.c +@@ -257,21 +257,30 @@ static void _keystore_free_key_filenames(struct key_filenames *names) + static int _keystore_set_file_permission(struct keystore *keystore, + const char *filename) + { +- int rc; ++ int fd, rc = 0; + +- if (chmod(filename, keystore->mode) != 0) { ++ fd = open(filename, O_RDONLY | O_NOFOLLOW); ++ if (fd < 0) { + rc = -errno; +- warnx("chmod failed on file '%s': %s", filename, strerror(-rc)); ++ warnx("Failed to open '%s': %s", filename, strerror(-rc)); + return rc; + } + +- if (chown(filename, geteuid(), keystore->owner) != 0) { ++ if (fchmod(fd, keystore->mode) != 0) { ++ rc = -errno; ++ warnx("chmod failed on file '%s': %s", filename, strerror(-rc)); ++ goto out; ++ } ++ ++ if (fchown(fd, geteuid(), keystore->owner) != 0) { + rc = -errno; + warnx("chown failed on file '%s': %s", filename, strerror(-rc)); +- return rc; ++ goto out; + } + +- return 0; ++out: ++ close(fd); ++ return rc; + } + + /** +@@ -1359,8 +1368,8 @@ static int _keystore_lock_repository(struct keystore *keystore) + util_asprintf(&lock_file_name, "%s/%s", keystore->directory, + LOCK_FILE_NAME); + +- if (stat(lock_file_name, &sb) == 0) { +- keystore->lock_fd = open(lock_file_name, O_RDONLY); ++ if (lstat(lock_file_name, &sb) == 0) { ++ keystore->lock_fd = open(lock_file_name, O_RDONLY | O_NOFOLLOW); + if (keystore->lock_fd == -1) { + rc = -errno; + warnx("Failed to open lock file '%s': %s", +@@ -1369,7 +1378,8 @@ static int _keystore_lock_repository(struct keystore *keystore) + goto out; + } + } else { +- keystore->lock_fd = open(lock_file_name, O_CREAT | O_RDONLY, ++ keystore->lock_fd = open(lock_file_name, ++ O_CREAT | O_RDONLY | O_NOFOLLOW, + keystore->mode); + if (keystore->lock_fd == -1) { + rc = -errno; +@@ -1444,7 +1454,7 @@ struct keystore *keystore_new(const char *directory, + + util_assert(directory != NULL, "Internal error: directory is NULL"); + +- if (stat(directory, &sb) != 0) { ++ if (lstat(directory, &sb) != 0) { + warnx("Can not access '%s': %s", directory, strerror(errno)); + return NULL; + } +diff --git a/zkey/kms.c b/zkey/kms.c +index 2e33b222..b7daa429 100644 +--- a/zkey/kms.c ++++ b/zkey/kms.c +@@ -24,6 +24,7 @@ + #include + #include + #include ++#include + + #include "lib/util_base.h" + #include "lib/util_libc.h" +@@ -382,7 +383,7 @@ static int _save_kms_properties(const struct keystore *keystore, + struct properties *kms_props, bool verbose) + { + char *filename = NULL; +- int rc; ++ int fd = -1, rc = 0; + + util_assert(keystore != NULL, "Internal error: keystore is NULL"); + util_assert(kms_props != NULL, "Internal error: kms_props is NULL"); +@@ -398,19 +399,28 @@ static int _save_kms_properties(const struct keystore *keystore, + goto out; + } + +- if (chmod(filename, keystore->mode) != 0) { ++ fd = open(filename, O_RDONLY | O_NOFOLLOW); ++ if (fd < 0) { + rc = -errno; +- warnx("chmod failed on file '%s': %s", filename, strerror(-rc)); ++ warnx("Failed to open '%s': %s", filename, strerror(-rc)); + return rc; + } + +- if (chown(filename, geteuid(), keystore->owner) != 0) { ++ if (fchmod(fd, keystore->mode) != 0) { + rc = -errno; +- warnx("chown failed on file '%s': %s", filename, strerror(-rc)); +- return rc; ++ warnx("chmod failed on file '%s': %s", filename, strerror(-rc)); ++ goto out; + } + ++ if (fchown(fd, geteuid(), keystore->owner) != 0) { ++ rc = -errno; ++ warnx("chown failed on file '%s': %s", filename, strerror(-rc)); ++ goto out; ++ } + out: ++ ++ if (fd >= 0) ++ close(fd); + if (filename != NULL) + free(filename); + +@@ -609,7 +619,7 @@ int bind_kms_plugin(struct keystore *keystore, const char *plugin, + char *plugin_name = NULL; + void *plugin_lib = NULL; + bool created = false; +- int rc; ++ int rc, fd; + + util_assert(keystore != NULL, "Internal error: keystore is NULL"); + util_assert(plugin != NULL, "Internal error: plugin is NULL"); +@@ -649,20 +659,31 @@ int bind_kms_plugin(struct keystore *keystore, const char *plugin, + } + created = true; + +- if (chmod(config_dir, keystore->mode) != 0) { ++ fd = open(config_dir, O_RDONLY | O_NOFOLLOW); ++ if (fd < 0) { ++ rc = -errno; ++ warnx("Failed to open '%s': %s", config_dir, strerror(-rc)); ++ goto out; ++ } ++ ++ if (fchmod(fd, keystore->mode) != 0) { + rc = -errno; + warnx("chmod failed on directory '%s': %s", config_dir, + strerror(-rc)); +- return rc; ++ close(fd); ++ goto out; + } + +- if (chown(config_dir, geteuid(), keystore->owner) != 0) { ++ if (fchown(fd, geteuid(), keystore->owner) != 0) { + rc = -errno; + warnx("chown failed on directory '%s': %s", config_dir, + strerror(-rc)); +- return rc; ++ close(fd); ++ goto out; + } + ++ close(fd); ++ + if (funcs->kms_bind != NULL) { + rc = funcs->kms_bind(config_dir); + if (rc != 0) { +diff --git a/zkey/pkey.c b/zkey/pkey.c +index 25820887..4c96ebc2 100644 +--- a/zkey/pkey.c ++++ b/zkey/pkey.c +@@ -153,7 +153,7 @@ int write_secure_key(const char *keyfile, const u8 *secure_key, + util_assert(secure_key_size > 0, + "Internal error: secure_key_size is zero"); + +- fp = fopen(keyfile, "w"); ++ fp = fopen_nofollow(keyfile, "w"); + if (fp == NULL) { + warnx("File '%s': %s", keyfile, strerror(errno)); + return -EIO; +diff --git a/zkey/plugin-utils.c b/zkey/plugin-utils.c +index e0d8d0f1..246aa258 100644 +--- a/zkey/plugin-utils.c ++++ b/zkey/plugin-utils.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -61,7 +62,7 @@ int plugin_init(struct plugin_data *pd, const char *plugin_name, + + pr_verbose(pd, "Plugin initializing, config_path: '%s'", config_path); + +- if (stat(config_path, &sb) != 0) { ++ if (lstat(config_path, &sb) != 0) { + rc = -errno; + warnx("Can not access '%s': %s", config_path, strerror(-rc)); + goto error; +@@ -219,23 +220,33 @@ out: + */ + int plugin_set_file_permission(struct plugin_data *pd, const char *filename) + { +- int rc; ++ int fd, rc = 0; + +- if (chmod(filename, pd->config_path_mode) != 0) { ++ fd = open(filename, O_RDONLY | O_NOFOLLOW); ++ if (fd < 0) { + rc = -errno; +- plugin_set_error(pd, "chmod failed on file '%s': %s", filename, ++ plugin_set_error(pd, "Failed to open '%s': %s", filename, + strerror(-rc)); + return rc; + } + +- if (chown(filename, geteuid(), pd->config_path_owner) != 0) { ++ if (fchmod(fd, pd->config_path_mode) != 0) { ++ rc = -errno; ++ plugin_set_error(pd, "chmod failed on file '%s': %s", filename, ++ strerror(-rc)); ++ goto out; ++ } ++ ++ if (fchown(fd, geteuid(), pd->config_path_owner) != 0) { + rc = -errno; + plugin_set_error(pd, "chown failed on file '%s': %s", filename, + strerror(-rc)); +- return rc; ++ goto out; + } + +- return 0; ++out: ++ close(fd); ++ return rc; + } + + /** +diff --git a/zkey/properties.c b/zkey/properties.c +index 42183ee9..56de60f9 100644 +--- a/zkey/properties.c ++++ b/zkey/properties.c +@@ -22,6 +22,7 @@ + #include "lib/util_panic.h" + + #include "properties.h" ++#include "utils.h" + + struct properties { + struct util_list list; +@@ -312,7 +313,7 @@ int properties_save(struct properties *properties, const char *filename, + util_assert(properties != NULL, "Internal error: properties is NULL"); + util_assert(filename != NULL, "Internal error: filename is NULL"); + +- fp = fopen(filename, "w"); ++ fp = fopen_nofollow(filename, "w"); + if (fp == NULL) + return -EIO; + +diff --git a/zkey/utils.c b/zkey/utils.c +index 8ae93340..521e05e9 100644 +--- a/zkey/utils.c ++++ b/zkey/utils.c +@@ -18,6 +18,7 @@ + #include + #include + #include ++#include + + #include "lib/util_path.h" + #include "lib/util_file.h" +@@ -1205,7 +1206,7 @@ int copy_file(const char *in_file_name, const char *out_file_name, + goto out; + } + +- fp_out = fopen(out_file_name, "w"); ++ fp_out = fopen_nofollow(out_file_name, "w"); + if (fp_out == NULL) { + rc = -errno; + warnx("Failed to open '%s': %s", out_file_name, strerror(-rc)); +@@ -1346,7 +1347,7 @@ int store_passphrase_from_base64(const char *b64_string, const char *filename, + return -ENOMEM; + } + +- fp = fopen(filename, "w"); ++ fp = fopen_nofollow(filename, "w"); + if (fp == NULL) { + pr_verbose(verbose, "Open of file '%s' failed: %s", filename, + strerror(errno)); +@@ -1373,3 +1374,32 @@ out: + return rc; + } + ++FILE *fopen_nofollow(const char *path, const char *mode) ++{ ++ int flags = O_NOFOLLOW; ++ int fd; ++ FILE *fp; ++ ++ /* Determine flags based on mode */ ++ if (mode[0] == 'r') ++ flags |= (mode[1] == '+') ? O_RDWR : O_RDONLY; ++ else if (mode[0] == 'w') ++ flags |= O_CREAT | O_TRUNC | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else if (mode[0] == 'a') ++ flags |= O_CREAT | O_APPEND | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else ++ return NULL; ++ ++ fd = open(path, flags, 0600); ++ if (fd < 0) ++ return NULL; ++ ++ fp = fdopen(fd, mode); ++ if (fp == NULL) { ++ close(fd); ++ return NULL; ++ } ++ return fp; ++} +diff --git a/zkey/utils.h b/zkey/utils.h +index 9334ea88..a15e499c 100644 +--- a/zkey/utils.h ++++ b/zkey/utils.h +@@ -84,4 +84,6 @@ char *read_passphrase_as_base64(const char *filename, bool verbose); + int store_passphrase_from_base64(const char *hex_string, const char *filename, + bool verbose); + ++FILE *fopen_nofollow(const char *path, const char *mode); ++ + #endif +-- +2.55.0 + + +From 7bf3db98e0c8107e195b1b18433cded4073ffe37 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Tue, 30 Jun 2026 09:50:45 +0200 +Subject: [PATCH 25/31] libseckey: Protect from symlink-following attacks + (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When creating or writing files, make sure that the file is not a +sysmlink. Such files created by libseckey are typically stored inside +the zkey repository and the owner and mode of them are changed to. +allow read/write for the owner user and the 'zkeyadm' group. It +would allow a symlink-following attack if the file being created are +symlinks. Make sure to open such files with the 'O_NOFOLLOW' flag. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 226728731c556891f67b1a3d21b6fb8faa2f909b) +--- + libseckey/sk_utilities.c | 39 +++++++++++++++++++++++++++++++++++---- + 1 file changed, 35 insertions(+), 4 deletions(-) + +diff --git a/libseckey/sk_utilities.c b/libseckey/sk_utilities.c +index e1c7f28c..771b1077 100644 +--- a/libseckey/sk_utilities.c ++++ b/libseckey/sk_utilities.c +@@ -10,10 +10,12 @@ + #include + #include + #include ++#include + #include + #include + #include + #include ++#include + + #include + #include +@@ -811,6 +813,35 @@ out: + return rc; + } + ++static inline FILE *fopen_nofollow(const char *path, const char *mode) ++{ ++ int flags = O_NOFOLLOW; ++ int fd; ++ FILE *fp; ++ ++ /* Determine flags based on mode */ ++ if (mode[0] == 'r') ++ flags |= (mode[1] == '+') ? O_RDWR : O_RDONLY; ++ else if (mode[0] == 'w') ++ flags |= O_CREAT | O_TRUNC | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else if (mode[0] == 'a') ++ flags |= O_CREAT | O_APPEND | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else ++ return NULL; ++ ++ fd = open(path, flags, 0600); ++ if (fd < 0) ++ return NULL; ++ ++ fp = fdopen(fd, mode); ++ if (fp == NULL) { ++ close(fd); ++ return NULL; ++ } ++ return fp; ++} + + /** + * Reads a X.509 certificate from the specified PEM file. +@@ -863,7 +894,7 @@ int SK_UTIL_write_x509_certificate(const char *pem_filename, X509 *cert) + if (pem_filename == NULL || cert == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +@@ -898,7 +929,7 @@ int SK_UTIL_write_x509_request(const char *pem_filename, X509_REQ *req, + if (pem_filename == NULL || req == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +@@ -982,7 +1013,7 @@ int SK_UTIL_write_key_blob(const char *filename, unsigned char *key_blob, + if (filename == NULL || key_blob == NULL || key_blob_len == 0) + return -EINVAL; + +- fp = fopen(filename, "w"); ++ fp = fopen_nofollow(filename, "w"); + if (fp == NULL) + return -errno; + +@@ -1047,7 +1078,7 @@ int SK_UTIL_write_public_key(const char *pem_filename, EVP_PKEY *pkey) + if (pem_filename == NULL || pkey == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +-- +2.55.0 + + +From f6a934acc8738a9cfe64283ad55ee10da1f73803 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Tue, 30 Jun 2026 09:51:34 +0200 +Subject: [PATCH 26/31] libekmfweb: Protect from symlink-following attacks + (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When creating or writing files, make sure that the file is not a +sysmlink. Such files created by libekmfweb are typically stored inside +the zkey repository and the owner and mode of them are changed to. +allow read/write for the owner user and the 'zkeyadm' group. It +would allow a symlink-following attack if the file being created are +symlinks. Make sure to open such files with the 'O_NOFOLLOW' flag. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit a3a2d70603811e3703d8e831b07487dd9e0f1c3e) +--- + libekmfweb/ekmfweb.c | 8 ++++---- + libekmfweb/utilities.c | 40 ++++++++++++++++++++++++++++++++++++---- + libekmfweb/utilities.h | 2 ++ + 3 files changed, 42 insertions(+), 8 deletions(-) + +diff --git a/libekmfweb/ekmfweb.c b/libekmfweb/ekmfweb.c +index 3a822e78..8ce97533 100644 +--- a/libekmfweb/ekmfweb.c ++++ b/libekmfweb/ekmfweb.c +@@ -233,7 +233,7 @@ static int _ekmf_extract_pubkey(const char *cert, const char *pub_key_pem, + goto out; + } + +- fp = fopen(pub_key_pem, "w"); ++ fp = fopen_nofollow(pub_key_pem, "w"); + if (fp == NULL) { + rc = -errno; + pr_verbose(verbose, "File '%s': %s", pub_key_pem, +@@ -458,7 +458,7 @@ retry: + out); + + if (server_cert_pem != NULL) { +- fp = fopen(server_cert_pem, "w"); ++ fp = fopen_nofollow(server_cert_pem, "w"); + if (fp == NULL) { + rc = -errno; + pr_verbose(verbose, "File '%s': %s", server_cert_pem, +@@ -495,7 +495,7 @@ retry: + */ + if (ci->num_of_certs > 1 && ca_bundle_pem != NULL && + do_verify == 0) { +- fp = fopen(ca_bundle_pem, "w"); ++ fp = fopen_nofollow(ca_bundle_pem, "w"); + if (fp == NULL) { + rc = -errno; + pr_verbose(verbose, "File '%s': %s", +@@ -1415,7 +1415,7 @@ int ekmf_login(const struct ekmf_config *config, CURL **curl_handle, + while (*tok == ' ') + tok++; + +- fp = fopen(config->login_token, "w"); ++ fp = fopen_nofollow(config->login_token, "w"); + if (fp == NULL) { + rc = -errno; + pr_verbose(verbose, "Failed to open file %s: '%s'", +diff --git a/libekmfweb/utilities.c b/libekmfweb/utilities.c +index 766e833c..d196d5c3 100644 +--- a/libekmfweb/utilities.c ++++ b/libekmfweb/utilities.c +@@ -9,9 +9,11 @@ + #include + #include + #include ++#include + #include + #include + #include ++#include + + #include + #include +@@ -1930,7 +1932,7 @@ int write_key_blob(const char *filename, unsigned char *key_blob, + if (filename == NULL || key_blob == NULL || key_blob_len == 0) + return -EINVAL; + +- fp = fopen(filename, "w"); ++ fp = fopen_nofollow(filename, "w"); + if (fp == NULL) + return -errno; + +@@ -2051,7 +2053,7 @@ int write_x509_certificate(const char *pem_filename, X509 *cert) + if (pem_filename == NULL || cert == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +@@ -2085,7 +2087,7 @@ int write_x509_request(const char *pem_filename, X509_REQ *req, bool new_hdr) + if (pem_filename == NULL || req == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +@@ -2153,7 +2155,7 @@ int write_public_key(const char *pem_filename, EVP_PKEY *pkey) + if (pem_filename == NULL || pkey == NULL) + return -EINVAL; + +- fp = fopen(pem_filename, "w"); ++ fp = fopen_nofollow(pem_filename, "w"); + if (fp == NULL) + return -errno; + +@@ -2605,3 +2607,33 @@ int json_object_object_add_ex(struct json_object *obj, const char *const key, + return 0; + } + #endif ++ ++FILE *fopen_nofollow(const char *path, const char *mode) ++{ ++ int flags = O_NOFOLLOW; ++ int fd; ++ FILE *fp; ++ ++ /* Determine flags based on mode */ ++ if (mode[0] == 'r') ++ flags |= (mode[1] == '+') ? O_RDWR : O_RDONLY; ++ else if (mode[0] == 'w') ++ flags |= O_CREAT | O_TRUNC | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else if (mode[0] == 'a') ++ flags |= O_CREAT | O_APPEND | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else ++ return NULL; ++ ++ fd = open(path, flags, 0600); ++ if (fd < 0) ++ return NULL; ++ ++ fp = fdopen(fd, mode); ++ if (fp == NULL) { ++ close(fd); ++ return NULL; ++ } ++ return fp; ++} +diff --git a/libekmfweb/utilities.h b/libekmfweb/utilities.h +index f460449b..19120238 100644 +--- a/libekmfweb/utilities.h ++++ b/libekmfweb/utilities.h +@@ -124,4 +124,6 @@ int json_object_object_add_ex(struct json_object *obj, const char *const key, + const unsigned int opts); + #endif + ++FILE *fopen_nofollow(const char *path, const char *mode); ++ + #endif +-- +2.55.0 + + +From aadf42aa974c87e9cb2d173aa18f7e5b889e2558 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Tue, 30 Jun 2026 09:52:15 +0200 +Subject: [PATCH 27/31] libkmipclient: Protect from symlink-following attacks + (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When creating or writing files, make sure that the file is not a +sysmlink. Such files created by libkmipclient are typically stored inside +the zkey repository and the owner and mode of them are changed to. +allow read/write for the owner user and the 'zkeyadm' group. It +would allow a symlink-following attack if the file being created are +symlinks. Make sure to open such files with the 'O_NOFOLLOW' flag. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit f0bf1985c3cf08810924cf040f9b42fb7fbe6d37) +--- + libkmipclient/kmip.c | 6 +++--- + libkmipclient/utils.c | 31 +++++++++++++++++++++++++++++++ + libkmipclient/utils.h | 2 ++ + 3 files changed, 36 insertions(+), 3 deletions(-) + +diff --git a/libkmipclient/kmip.c b/libkmipclient/kmip.c +index b313f1f8..e0779c70 100644 +--- a/libkmipclient/kmip.c ++++ b/libkmipclient/kmip.c +@@ -1490,7 +1490,7 @@ retry: + } + + if (i == 0 && server_cert_pem != NULL) { +- fp = fopen(server_cert_pem, "w"); ++ fp = fopen_nofollow(server_cert_pem, "w"); + if (fp == NULL) { + rc = -errno; + kmip_debug(debug, "Failed to open %s for write", +@@ -1508,7 +1508,7 @@ retry: + fp = NULL; + + if (server_pubkey_pem != NULL) { +- fp = fopen(server_pubkey_pem, "w"); ++ fp = fopen_nofollow(server_pubkey_pem, "w"); + if (fp == NULL) { + rc = -errno; + kmip_debug(debug, "Failed to open %s " +@@ -1534,7 +1534,7 @@ retry: + + if (i > 0 && cert_chain_pem != NULL) { + if (fp == NULL) +- fp = fopen(cert_chain_pem, "w"); ++ fp = fopen_nofollow(cert_chain_pem, "w"); + if (fp == NULL) { + rc = -errno; + kmip_debug(debug, "Failed to open %s for write", +diff --git a/libkmipclient/utils.c b/libkmipclient/utils.c +index ef70c453..0baf586c 100644 +--- a/libkmipclient/utils.c ++++ b/libkmipclient/utils.c +@@ -12,9 +12,11 @@ + + #include + #include ++#include + #include + #include + #include ++#include + + #include "utils.h" + #include "names.h" +@@ -726,3 +728,32 @@ enum kmip_tag kmip_find_v1_attribute_name_tag(struct kmip_node *parent) + return 0; + } + ++FILE *fopen_nofollow(const char *path, const char *mode) ++{ ++ int flags = O_NOFOLLOW; ++ int fd; ++ FILE *fp; ++ ++ /* Determine flags based on mode */ ++ if (mode[0] == 'r') ++ flags |= (mode[1] == '+') ? O_RDWR : O_RDONLY; ++ else if (mode[0] == 'w') ++ flags |= O_CREAT | O_TRUNC | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else if (mode[0] == 'a') ++ flags |= O_CREAT | O_APPEND | ++ ((mode[1] == '+') ? O_RDWR : O_WRONLY); ++ else ++ return NULL; ++ ++ fd = open(path, flags, 0600); ++ if (fd < 0) ++ return NULL; ++ ++ fp = fdopen(fd, mode); ++ if (fp == NULL) { ++ close(fd); ++ return NULL; ++ } ++ return fp; ++} +diff --git a/libkmipclient/utils.h b/libkmipclient/utils.h +index 89734feb..a8ef8c5c 100644 +--- a/libkmipclient/utils.h ++++ b/libkmipclient/utils.h +@@ -58,4 +58,6 @@ void kmip_node_dump(struct kmip_node *node, bool debug); + + enum kmip_tag kmip_find_v1_attribute_name_tag(struct kmip_node *parent); + ++FILE *fopen_nofollow(const char *path, const char *mode); ++ + #endif +-- +2.55.0 + + +From d5d9fb427bd1deee93ad651991d7b7416a09e855 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Tue, 30 Jun 2026 10:19:17 +0200 +Subject: [PATCH 28/31] libkmipclient: Limit the nesting level of KMIP + STRUCTURE recursion (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +KMIP STRUCTURE elements can be nested, which causes a recursion of +functions kmip_decode_ttlv(), kmip_decode_xml(), and kmip_decode_json(). +A malformed KMIP response may thus cause stack exhaustion. + +Limit the KMIP STRUCTURE nesting level to 32 levels. This is more than +enough for currently defined KMIP responses. The practically used +nesting level is 8 or 9, dependent on the type of KMIP response. + +Assisted-by: IBM Bob:2.0.0 +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 5fdeaab3d09c71c899ba13b29629b3110ea0443e) +--- + libkmipclient/https.c | 8 +++++--- + libkmipclient/json.c | 11 +++++++++-- + libkmipclient/kmip.h | 12 ++++++++---- + libkmipclient/tls.c | 3 ++- + libkmipclient/ttlv.c | 11 +++++++++-- + libkmipclient/xml.c | 12 ++++++++++-- + 6 files changed, 43 insertions(+), 14 deletions(-) + +diff --git a/libkmipclient/https.c b/libkmipclient/https.c +index 2cea1276..c01e13f1 100644 +--- a/libkmipclient/https.c ++++ b/libkmipclient/https.c +@@ -735,7 +735,8 @@ int kmip_connection_https_perform(struct kmip_connection *conn, + switch (conn->config.encoding) { + case KMIP_ENCODING_TTLV: + rc = kmip_decode_ttlv(write_cb.ttlv.resp_mem_bio, NULL, +- response, debug); ++ response, KMIP_DECODE_MAX_NESTING_LEVEL, ++ debug); + if (rc != 0) { + kmip_debug(debug, "kmip_decode_ttlv failed"); + goto out; +@@ -750,7 +751,7 @@ int kmip_connection_https_perform(struct kmip_connection *conn, + } + + rc = kmip_decode_json(write_cb.json.resp_obj, NULL, response, +- debug); ++ KMIP_DECODE_MAX_NESTING_LEVEL, debug); + if (rc != 0) { + kmip_debug(debug, "kmip_decode_json failed"); + goto out; +@@ -768,7 +769,8 @@ int kmip_connection_https_perform(struct kmip_connection *conn, + + rc = kmip_decode_xml(xmlDocGetRootElement( + write_cb.xml.ctx->myDoc), +- NULL, response, debug); ++ NULL, response, ++ KMIP_DECODE_MAX_NESTING_LEVEL, debug); + if (rc != 0) { + kmip_debug(debug, "kmip_decode_xml failed"); + goto out; +diff --git a/libkmipclient/json.c b/libkmipclient/json.c +index 78337192..53beb236 100644 +--- a/libkmipclient/json.c ++++ b/libkmipclient/json.c +@@ -27,12 +27,16 @@ + * @param parent the parent node or NULL if no parent exists. + * @param node On return: the decoded node. The newly allocated + * node has a reference count of 1. ++ * @param max_nesting_level the maximum nesting levels of structures within the ++ * KMIP node. If the nesting level is reached, E2BIG ++ * is returned. + * @param debug if true, debug messages are printed + * + * @returns 0 in case of success, or a negative errno value + */ + int kmip_decode_json(const json_object *obj, struct kmip_node *parent, +- struct kmip_node **node, bool debug) ++ struct kmip_node **node, size_t max_nesting_level, ++ bool debug) + { + json_object *tag_obj, *type_obj, *value_obj, *name_obj; + enum kmip_tag tag, v1_attr_tag = 0; +@@ -42,6 +46,9 @@ int kmip_decode_json(const json_object *obj, struct kmip_node *parent, + int rc, num, i; + int64_t int64; + ++ if (max_nesting_level == 0) ++ return -E2BIG; ++ + if (obj == NULL || node == NULL) + return -EINVAL; + +@@ -138,7 +145,7 @@ int kmip_decode_json(const json_object *obj, struct kmip_node *parent, + for (i = 0; i < num; i++) { + rc = kmip_decode_json( + json_object_array_get_idx(value_obj, i), +- n, &e, debug); ++ n, &e, max_nesting_level - 1, debug); + if (rc != 0) { + kmip_debug(debug, "Failed to parse " + "array element %d", i); +diff --git a/libkmipclient/kmip.h b/libkmipclient/kmip.h +index e9185776..993bbb8e 100644 +--- a/libkmipclient/kmip.h ++++ b/libkmipclient/kmip.h +@@ -93,19 +93,23 @@ int kmip_connection_https_perform(struct kmip_connection *connection, + bool debug); + void kmip_connection_https_term(struct kmip_connection *connection); + +-/* KIMP decoding and encoding internal functions */ ++/* KIMP decoding and encoding internal functions and definitions */ ++#define KMIP_DECODE_MAX_NESTING_LEVEL 32 ++ + int kmip_decode_ttlv(BIO *bio, size_t *size, struct kmip_node **node, +- bool debug); ++ size_t max_nesting_level, bool debug); + int kmip_encode_ttlv(struct kmip_node *node, BIO *bio, size_t *size, + bool debug); + + int kmip_decode_json(const json_object *obj, struct kmip_node *parent, +- struct kmip_node **node, bool debug); ++ struct kmip_node **node, size_t max_nesting_level, ++ bool debug); + int kmip_encode_json(const struct kmip_node *node, json_object **obj, + bool debug); + + int kmip_decode_xml(const xmlNode *xml, struct kmip_node *parent, +- struct kmip_node **node, bool debug); ++ struct kmip_node **node, size_t max_nesting_level, ++ bool debug); + int kmip_encode_xml(const struct kmip_node *node, xmlNode **xml, bool debug); + + #endif +diff --git a/libkmipclient/tls.c b/libkmipclient/tls.c +index 4bd84e88..19664172 100644 +--- a/libkmipclient/tls.c ++++ b/libkmipclient/tls.c +@@ -488,7 +488,8 @@ int kmip_connection_tls_perform(struct kmip_connection *conn, + kmip_debug(debug, "%lu bytes sent", size); + + /* receive the response */ +- rc = kmip_decode_ttlv(conn->plain_tls.bio, NULL, response, debug); ++ rc = kmip_decode_ttlv(conn->plain_tls.bio, NULL, response, ++ KMIP_DECODE_MAX_NESTING_LEVEL, debug); + if (rc != 0 || *response == NULL) { + kmip_debug(debug, "kmip_decode_ttlv failed"); + goto out; +diff --git a/libkmipclient/ttlv.c b/libkmipclient/ttlv.c +index 9c6702b1..fe9815de 100644 +--- a/libkmipclient/ttlv.c ++++ b/libkmipclient/ttlv.c +@@ -28,12 +28,15 @@ + * as many bytes as needed. + * @param node On return: the decoded node. The newly allocated + * node has a reference count of 1. ++ * @param max_nesting_level the maximum nesting levels of structures within the ++ * KMIP node. If the nesting level is reached, E2BIG ++ * is returned. + * @param debug if true, debug messages are printed + * + * @returns 0 in case of success, or a negative errno value + */ + int kmip_decode_ttlv(BIO *bio, size_t *size, struct kmip_node **node, +- bool debug) ++ size_t max_nesting_level, bool debug) + { + unsigned char padding[KMIP_TTLV_BLOCK_LENGTH]; + unsigned char ttlv[KMIP_TTLV_HEADER_LENGTH]; +@@ -44,6 +47,9 @@ int kmip_decode_ttlv(BIO *bio, size_t *size, struct kmip_node **node, + uint64_t int64; + int rc; + ++ if (max_nesting_level == 0) ++ return -E2BIG; ++ + if (bio == NULL || node == NULL) + return -EINVAL; + +@@ -167,7 +173,8 @@ int kmip_decode_ttlv(BIO *bio, size_t *size, struct kmip_node **node, + switch (n->type) { + case KMIP_TYPE_STRUCTURE: + while (value_len > 0) { +- rc = kmip_decode_ttlv(bio, &value_len, &e, debug); ++ rc = kmip_decode_ttlv(bio, &value_len, &e, ++ max_nesting_level - 1, debug); + if (rc != 0) { + kmip_debug(debug, "kmip_decode_ttlv failed: " + "rc: %d", rc); +diff --git a/libkmipclient/xml.c b/libkmipclient/xml.c +index 658bfdb1..c7e2e208 100644 +--- a/libkmipclient/xml.c ++++ b/libkmipclient/xml.c +@@ -28,12 +28,16 @@ + * @param parent the parent node or NULL if no parent exists. + * @param node On return: the decoded node.The newly allocated + * node has a reference count of 1. ++ * @param max_nesting_level the maximum nesting levels of structures within the ++ * KMIP node. If the nesting level is reached, E2BIG ++ * is returned. + * @param debug if true, debug messages are printed + * + * @returns 0 in case of success, or a negative errno value + */ + int kmip_decode_xml(const xmlNode *xml, struct kmip_node *parent, +- struct kmip_node **node, bool debug) ++ struct kmip_node **node, size_t max_nesting_level, ++ bool debug) + { + char *tag_attr = NULL, *name_attr = NULL, *type_attr = NULL; + enum kmip_tag tag, v1_attr_tag = 0; +@@ -44,6 +48,9 @@ int kmip_decode_xml(const xmlNode *xml, struct kmip_node *parent, + int64_t int64; + int rc = 0, i; + ++ if (max_nesting_level == 0) ++ return -E2BIG; ++ + if (xml == NULL || node == NULL) + return -EINVAL; + +@@ -122,7 +129,8 @@ int kmip_decode_xml(const xmlNode *xml, struct kmip_node *parent, + if (child->type != XML_ELEMENT_NODE) + continue; + +- rc = kmip_decode_xml(child, n, &e, debug); ++ rc = kmip_decode_xml(child, n, &e, ++ max_nesting_level - 1, debug); + if (rc != 0) { + kmip_debug(debug, "Failed to parse child " + "element %d", i); +-- +2.55.0 + + +From eabaa760c295b840602a8e6402254487904a4217 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Fri, 31 Jul 2026 10:48:35 +0200 +Subject: [PATCH 29/31] zkey: Harden KMS config directory handling + (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When the zkey repository is bound to a KMS, a KMS config directory is +created inside the zkey repository. The path of this config directory is +stored in the kms.conf properties file. + +The properties file could be manipulated by a malicious user (which must +be a member of the 'zkeyadm' user group) and the config directory path +could be changed to point to a path outside of the zkey repository. + +Whenever using the config directory, make sure that it is inside the +zkey repository and fail early if not. + +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 0eef7846bd9d29546877edd1368b5b0d7162132a) +--- + zkey/kms.c | 48 +++++++++++++++++++++++++++++++++++++++++++++++- + zkey/kms.h | 3 ++- + zkey/zkey.c | 2 +- + 3 files changed, 50 insertions(+), 3 deletions(-) + +diff --git a/zkey/kms.c b/zkey/kms.c +index b7daa429..678403b3 100644 +--- a/zkey/kms.c ++++ b/zkey/kms.c +@@ -480,15 +480,54 @@ out: + return rc; + } + ++/* ++ * Returns true if the specified path is within the base path. ++ * It canonicalizes both, the base path and the path, and compares that ++ * the path starts with the base path followed by a '/'. ++ */ ++static bool is_path_within_base_path(const char *base_path, const char *path) ++{ ++ char *real_base_path, *real_path; ++ size_t base_len; ++ bool ret = false; ++ ++ real_base_path = canonicalize_file_name(base_path); ++ real_path = canonicalize_file_name(path); ++ ++ if (real_base_path == NULL || real_path == NULL) { ++ warnx("Failed to resolve path for safety check: %s", ++ strerror(errno)); ++ goto out; ++ } ++ ++ /* Require path to be strictly inside the base path */ ++ base_len = strlen(real_base_path); ++ if (strncmp(real_path, real_base_path, base_len) != 0 || ++ real_path[base_len] != '/') { ++ warnx("'%s' is not inside '%s'", path, base_path); ++ goto out; ++ } ++ ++ ret = true; ++ ++out: ++ free(real_base_path); ++ free(real_path); ++ ++ return ret; ++} ++ + /** + * Initializes the KMS plugin. + * + * @param[in] kms_info The KMS Plugin info ++ * @param[in] keystore the keystore to bind to the plugin + * @param[in] verbose if true, verbose messages are printed + * + * @returns 0 for success or a negative errno in case of an error. + */ +-int init_kms_plugin(struct kms_info *kms_info, bool verbose) ++int init_kms_plugin(struct kms_info *kms_info, struct keystore *keystore, ++ bool verbose) + { + char *config_path = NULL; + char **apqn_list = NULL; +@@ -496,6 +535,7 @@ int init_kms_plugin(struct kms_info *kms_info, bool verbose) + int i, rc = 0; + + util_assert(kms_info != NULL, "Internal error: kms_info is NULL"); ++ util_assert(keystore != NULL, "Internal error: keystore is NULL"); + + config_path = properties_get(kms_info->props, + KMS_CONFIG_PROP_KMS_CONFIG); +@@ -505,6 +545,12 @@ int init_kms_plugin(struct kms_info *kms_info, bool verbose) + goto out; + } + ++ if (!is_path_within_base_path(keystore->directory, config_path)) { ++ warnx("Invalid KMS config directory: '%s'", config_path); ++ rc = -EIO; ++ goto out; ++ } ++ + if (kms_info->funcs->kms_initialize != NULL) { + kms_info->handle = kms_info->funcs->kms_initialize(config_path, + verbose); +diff --git a/zkey/kms.h b/zkey/kms.h +index 42aefe9e..6a26d3c3 100644 +--- a/zkey/kms.h ++++ b/zkey/kms.h +@@ -33,7 +33,8 @@ int list_kms_plugins(bool verbose); + + int check_for_kms_plugin(struct kms_info *kms_info, bool verbose); + +-int init_kms_plugin(struct kms_info *kms_info, bool verbose); ++int init_kms_plugin(struct kms_info *kms_info, struct keystore *keystore, ++ bool verbose); + + void free_kms_plugin(struct kms_info *kms_info); + +diff --git a/zkey/zkey.c b/zkey/zkey.c +index 843e5544..3340398b 100644 +--- a/zkey/zkey.c ++++ b/zkey/zkey.c +@@ -3287,7 +3287,7 @@ int main(int argc, char *argv[]) + } + + if (g.kms_info.plugin_lib != NULL) { +- rc = init_kms_plugin(&g.kms_info, g.verbose); ++ rc = init_kms_plugin(&g.kms_info, g.keystore, g.verbose); + if (rc != 0) { + rc = EXIT_FAILURE; + goto out; +-- +2.55.0 + + +From 2ea41d26c5eae4275b0ca0b025a3c0af891b4ea9 Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Mon, 3 Aug 2026 10:08:19 +0200 +Subject: [PATCH 30/31] zkey: Harden 'zkey kms unbind' command (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The 'zkey kms unbind' command removes the KMS config directory and all +files contained. Make sure that when removing the KMS config directory +that it is inside the zkey repository to not follow symlinks when the +KMS config directory itself or any files or directories inside that +directory are symlinks. Open the directory with O_NOFOLLOW to protect +from symlink traversal. Symlinks to files or directories within the +directory are detected via fstatat() with AT_SYMLINK_NOFOLLOW and cause +the removal to fail. + +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit 1b90d15f1057c02bf581707253294d2d2bd7a8c4) +--- + zkey/kms.c | 85 ++++++++++++++++++++++++++++++++---------------------- + 1 file changed, 50 insertions(+), 35 deletions(-) + +diff --git a/zkey/kms.c b/zkey/kms.c +index 678403b3..e5f3d2a8 100644 +--- a/zkey/kms.c ++++ b/zkey/kms.c +@@ -773,35 +773,48 @@ out: + /** + * Removes a directory and all its contents. + */ +-static int remove_directory_recursively(const char *directory) ++static int remove_directory_recursively(int parent_fd, const char *name) + { +- char *filename = NULL; + struct dirent *de; ++ struct stat st; ++ int rc = 0, dirfd; + DIR *dirp; +- int rc = 0; + +- dirp = opendir(directory); ++ dirfd = openat(parent_fd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (dirfd < 0) { ++ rc = -errno; ++ warnx("Failed to open directory '%s': %s", name, strerror(-rc)); ++ return rc; ++ } ++ ++ dirp = fdopendir(dirfd); + if (dirp == NULL) { + rc = -errno; +- warnx("Failed to open directory '%s'", directory); ++ close(dirfd); ++ warnx("Failed to open directory '%s': %s", name, strerror(-rc)); + return rc; + } + + while ((de = readdir(dirp))) { +- util_asprintf(&filename, "%s/%s", directory, de->d_name); +- if (de->d_type == DT_DIR) { +- if (strcmp(de->d_name, ".") != 0 && +- strcmp(de->d_name, "..") != 0) +- rc = remove_directory_recursively(filename); ++ if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) ++ continue; ++ ++ if (fstatat(dirfd, de->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) { ++ rc = -errno; ++ warnx("Failed to stat '%s': %s", de->d_name, ++ strerror(-rc)); ++ break; ++ } ++ ++ if (S_ISDIR(st.st_mode)) { ++ rc = remove_directory_recursively(dirfd, de->d_name); + } else { +- rc = remove(filename); +- if (rc != 0) { ++ if (unlinkat(dirfd, de->d_name, 0) != 0) { + rc = -errno; +- warnx("Failed to remove '%s': %s", filename, ++ warnx("Failed to remove '%s': %s", de->d_name, + strerror(-rc)); + } + } +- free(filename); + + if (rc != 0) + break; +@@ -810,9 +823,9 @@ static int remove_directory_recursively(const char *directory) + if (rc != 0) + goto out; + +- if (rmdir(directory) != 0) { ++ if (unlinkat(parent_fd, name, AT_REMOVEDIR) != 0) { + rc = -errno; +- warnx("Failed to remove '%s': %s", filename, strerror(-rc)); ++ warnx("Failed to remove '%s': %s", name, strerror(-rc)); + goto out; + } + +@@ -836,9 +849,7 @@ out: + int unbind_kms_plugin(struct kms_info *kms_info, struct keystore *keystore, + bool UNUSED(verbose)) + { +- char *config_dir = NULL; +- char *filename = NULL; +- int rc; ++ int rc, dirfd = -1; + + util_assert(kms_info != NULL, "Internal error: kms_info is NULL"); + util_assert(keystore != NULL, "Internal error: keystore is NULL"); +@@ -861,30 +872,34 @@ int unbind_kms_plugin(struct kms_info *kms_info, struct keystore *keystore, + } + } + +- config_dir = properties_get(kms_info->props, +- KMS_CONFIG_PROP_KMS_CONFIG); +- if (config_dir != NULL) { +- rc = remove_directory_recursively(config_dir); +- if (rc != 0) { +- warnx("Failed to remove the KMS plugin's config " +- "directory: %s", strerror(-rc)); +- goto out; +- } ++ dirfd = open(keystore->directory, ++ O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (dirfd < 0) { ++ rc = -errno; ++ warnx("Failed to open directory '%s': %s", ++ keystore->directory, strerror(-rc)); ++ goto out; + } + +- util_asprintf(&filename, "%s/%s", keystore->directory, KMS_CONFIG_FILE); +- rc = remove(filename); ++ rc = remove_directory_recursively(dirfd, kms_info->plugin_name); + if (rc != 0) { ++ warnx("Failed to remove the KMS plugin's config " ++ "directory '%s/%s': %s", keystore->directory, ++ kms_info->plugin_name, strerror(-rc)); ++ goto out; ++ } ++ ++ if (unlinkat(dirfd, KMS_CONFIG_FILE, 0) != 0) { + rc = -errno; +- warnx("Failed to remove '%s': %s", filename, strerror(-rc)); ++ warnx("Failed to remove '%s/%s': %s", keystore->directory, ++ KMS_CONFIG_FILE, strerror(-rc)); + goto out; + } + + out: +- if (config_dir != NULL) +- free(config_dir); +- if (filename != NULL) +- free(filename); ++ if (dirfd >= 0) ++ close(dirfd); ++ + return rc; + } + +-- +2.55.0 + + +From 41cc0dab4a6b38405ae99f79ef8ea006d793cafc Mon Sep 17 00:00:00 2001 +From: Ingo Franzki +Date: Mon, 3 Aug 2026 11:29:18 +0200 +Subject: [PATCH 31/31] zkey/ekmfweb, zkey/kmip: Harden KMS plugin file + handling (RHEL-243143) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Update the plugin file handling helpers to always operate relative +to the plugin config directory and thus avoid following symlinks. + +Open the config directory at plugin initialization time (with +O_DIRECTORY and O_NOFOLLOW) and then perform the file operations +relative to that directory handle, i.e. via openat(..., O_NOFOLLOW), +unlinkat(), and renameat(). + +For file paths read from a properties file, make sure that they are +inside the plugin configuration directory and fail early if not. + +Signed-off-by: Ingo Franzki +Reviewed-by: Finn Callies +Signed-off-by: Jan Höppner +(cherry picked from commit bc81c3221872e465bbed29ba9092f822b514ae2c) +--- + zkey/ekmfweb/zkey-ekmfweb.c | 35 +++++++----- + zkey/kmip/zkey-kmip.c | 47 ++++++++++------ + zkey/plugin-utils.c | 108 ++++++++++++++++++++++++++++++++---- + zkey/plugin-utils.h | 13 +++++ + 4 files changed, 163 insertions(+), 40 deletions(-) + +diff --git a/zkey/ekmfweb/zkey-ekmfweb.c b/zkey/ekmfweb/zkey-ekmfweb.c +index 7154ecce..569ba118 100644 +--- a/zkey/ekmfweb/zkey-ekmfweb.c ++++ b/zkey/ekmfweb/zkey-ekmfweb.c +@@ -218,6 +218,10 @@ static int _get_ekmf_config(struct plugin_handle *ph) + + ph->ekmf_config.identity_secure_key = properties_get(ph->pd.properties, + EKMFWEB_CONFIG_IDENTITY_KEY); ++ if (ph->ekmf_config.identity_secure_key != NULL && ++ !plugin_path_is_within_config(&ph->pd, ++ ph->ekmf_config.identity_secure_key)) ++ return -EINVAL; + + ph->ekmf_config.base_url = properties_get(ph->pd.properties, + EKMFWEB_CONFIG_URL); +@@ -305,7 +309,7 @@ static void _remove_login_token_if_error(struct plugin_handle *ph, int error) + switch (error) { + case -EACCES: + case -EPERM: +- remove(ph->ekmf_config.login_token); ++ plugin_remove_config_file(&ph->pd, ph->ekmf_config.login_token); + FREE_AND_SET_NULL(ph->ekmf_config.login_token); + break; + default: +@@ -1712,7 +1716,7 @@ static int _get_ekmfweb_settings(struct plugin_handle *ph) + _check_config_complete(ph); + + if (ph->ekmf_config.login_token != NULL) { +- remove(ph->ekmf_config.login_token); ++ plugin_remove_config_file(&ph->pd, ph->ekmf_config.login_token); + FREE_AND_SET_NULL(ph->ekmf_config.login_token); + } + rc = plugin_set_or_remove_property(&ph->pd, EKMFWEB_CONFIG_LOGIN_TOKEN, +@@ -1743,7 +1747,8 @@ static int _get_ekmfweb_settings(struct plugin_handle *ph) + goto out; + + if (ph->ekmf_config.ekmf_server_pubkey != NULL) +- remove(ph->ekmf_config.ekmf_server_pubkey); ++ plugin_remove_config_file(&ph->pd, ++ ph->ekmf_config.ekmf_server_pubkey); + FREE_AND_SET_NULL(ph->ekmf_config.ekmf_server_pubkey); + + util_asprintf((char **)&ph->ekmf_config.ekmf_server_pubkey, +@@ -2048,7 +2053,7 @@ static int _configure_connection(struct plugin_handle *ph, + if (rc != 0) + goto out; + } else { +- remove(server_cert_file); ++ plugin_remove_config_file(&ph->pd, server_cert_file); + } + rc = plugin_set_or_remove_property(&ph->pd, EKMFWEB_CONFIG_SERVER_CERT, + tls_trust_server_cert ? +@@ -2067,7 +2072,7 @@ static int _configure_connection(struct plugin_handle *ph, + if (rc != 0) + goto out; + } else { +- remove(server_pubkey_file); ++ plugin_remove_config_file(&ph->pd, server_pubkey_file); + } + rc = plugin_set_or_remove_property(&ph->pd, + EKMFWEB_CONFIG_SERVER_PUBKEY, +@@ -2078,13 +2083,13 @@ static int _configure_connection(struct plugin_handle *ph, + + out: + if (server_cert_temp != NULL) { +- remove(server_cert_temp); ++ plugin_remove_config_file(&ph->pd, server_cert_temp); + free(server_cert_temp); + } + if (server_cert_file != NULL) + free(server_cert_file); + if (server_pubkey_temp != NULL) { +- remove(server_pubkey_temp); ++ plugin_remove_config_file(&ph->pd, server_pubkey_temp); + free(server_pubkey_temp); + } + if (server_pubkey_file != NULL) +@@ -2350,7 +2355,7 @@ static int _generate_identity_key(struct plugin_handle *ph) + reenc_file = properties_get(ph->pd.properties, + EKMFWEB_CONFIG_IDENTITY_KEY_REENC); + if (reenc_file != NULL) { +- remove(reenc_file); ++ plugin_remove_config_file(&ph->pd, reenc_file); + free(reenc_file); + properties_remove(ph->pd.properties, + EKMFWEB_CONFIG_IDENTITY_KEY_REENC); +@@ -3460,7 +3465,7 @@ int kms_login(const kms_handle_t handle) + if (rc == 0 && valid) + return 0; + +- remove(ph->ekmf_config.login_token); ++ plugin_remove_config_file(&ph->pd, ph->ekmf_config.login_token); + FREE_AND_SET_NULL(ph->ekmf_config.login_token); + + rc = plugin_set_or_remove_property(&ph->pd, +@@ -3635,16 +3640,20 @@ int kms_reenciper(const kms_handle_t handle, enum kms_reencipher_mode mode, + + printf("Completing re-enciphering of identity key.\n"); + +- rc = remove(ph->ekmf_config.identity_secure_key); ++ rc = plugin_remove_config_file(&ph->pd, ++ ph->ekmf_config.identity_secure_key); + if (rc != 0) { +- rc = -errno; + _set_error(ph, "Failed to remove file '%s': %s", + ph->ekmf_config.identity_secure_key, + strerror(-rc)); + goto out; + } + +- rc = rename(reenc_file, ph->ekmf_config.identity_secure_key); ++ rc = renameat(ph->pd.config_path_fd, ++ plugin_basename(reenc_file), ++ ph->pd.config_path_fd, ++ plugin_basename( ++ ph->ekmf_config.identity_secure_key)); + if (rc != 0) { + rc = -errno; + _set_error(ph, "Failed to rename file '%s' to '%s': %s", +@@ -3824,7 +3833,7 @@ int kms_reenciper(const kms_handle_t handle, enum kms_reencipher_mode mode, + + out: + if (rc != 0 && reenc_file != NULL) +- remove(reenc_file); ++ plugin_remove_config_file(&ph->pd, reenc_file); + if (reenc_file != NULL) + free(reenc_file); + +diff --git a/zkey/kmip/zkey-kmip.c b/zkey/kmip/zkey-kmip.c +index e7b7c739..c5dde348 100644 +--- a/zkey/kmip/zkey-kmip.c ++++ b/zkey/kmip/zkey-kmip.c +@@ -799,6 +799,12 @@ kms_handle_t kms_initialize(const char *config_path, bool verbose) + + ph->identity_secure_key = properties_get(ph->pd.properties, + KMIP_CONFIG_IDENTITY_KEY); ++ if (ph->identity_secure_key != NULL && ++ !plugin_path_is_within_config(&ph->pd, ph->identity_secure_key)) { ++ rc = -EINVAL; ++ goto error; ++ } ++ + ph->server = properties_get(ph->pd.properties, KMIP_CONFIG_SERVER); + + tmp = properties_get(ph->pd.properties, KMIP_CONFIG_PROFILE); +@@ -1797,7 +1803,7 @@ static int _generate_identity_key(struct plugin_handle *ph, + reenc_file = properties_get(ph->pd.properties, + KMIP_CONFIG_IDENTITY_KEY_REENC); + if (reenc_file != NULL) { +- remove(reenc_file); ++ plugin_remove_config_file(&ph->pd, reenc_file); + free(reenc_file); + properties_remove(ph->pd.properties, + KMIP_CONFIG_IDENTITY_KEY_REENC); +@@ -1806,7 +1812,7 @@ static int _generate_identity_key(struct plugin_handle *ph, + client_cert = properties_get(ph->pd.properties, + KMIP_CONFIG_CLIENT_CERTIFICATE); + if (client_cert != NULL) { +- remove(client_cert); ++ plugin_remove_config_file(&ph->pd, client_cert); + free(client_cert); + properties_remove(ph->pd.properties, + KMIP_CONFIG_CLIENT_CERTIFICATE); +@@ -2994,7 +3000,7 @@ static int _configure_connection(struct plugin_handle *ph, + if (rc != 0) + goto out; + } else { +- remove(server_cert_file); ++ plugin_remove_config_file(&ph->pd, server_cert_file); + } + rc = plugin_set_or_remove_property(&ph->pd, KMIP_CONFIG_SERVER_CERT, + tls_trust_server_cert ? +@@ -3013,7 +3019,7 @@ static int _configure_connection(struct plugin_handle *ph, + if (rc != 0) + goto out; + } else { +- remove(server_pubkey_file); ++ plugin_remove_config_file(&ph->pd, server_pubkey_file); + } + rc = plugin_set_or_remove_property(&ph->pd, + KMIP_CONFIG_SERVER_PUBKEY, +@@ -3025,13 +3031,13 @@ static int _configure_connection(struct plugin_handle *ph, + /* Remove any wrapping key properties from previous configuration */ + file_name = properties_get(ph->pd.properties, KMIP_CONFIG_WRAPPING_KEY); + if (file_name != NULL) { +- remove(file_name); ++ plugin_remove_config_file(&ph->pd, file_name); + free(file_name); + } + file_name = properties_get(ph->pd.properties, + KMIP_CONFIG_WRAPPING_KEY_REENC); + if (file_name != NULL) { +- remove(file_name); ++ plugin_remove_config_file(&ph->pd, file_name); + free(file_name); + } + rc = plugin_set_or_remove_property(&ph->pd, KMIP_CONFIG_WRAPPING_KEY, +@@ -3066,13 +3072,13 @@ static int _configure_connection(struct plugin_handle *ph, + + out: + if (server_cert_temp != NULL) { +- remove(server_cert_temp); ++ plugin_remove_config_file(&ph->pd, server_cert_temp); + free(server_cert_temp); + } + if (server_cert_file != NULL) + free(server_cert_file); + if (server_pubkey_temp != NULL) { +- remove(server_pubkey_temp); ++ plugin_remove_config_file(&ph->pd, server_pubkey_temp); + free(server_pubkey_temp); + } + if (server_pubkey_file != NULL) +@@ -3961,7 +3967,7 @@ static int _generate_wrapping_key(struct plugin_handle *ph, + reenc_file = properties_get(ph->pd.properties, + KMIP_CONFIG_WRAPPING_KEY_REENC); + if (reenc_file != NULL) { +- remove(reenc_file); ++ plugin_remove_config_file(&ph->pd, reenc_file); + free(reenc_file); + properties_remove(ph->pd.properties, + KMIP_CONFIG_WRAPPING_KEY_REENC); +@@ -3997,7 +4003,7 @@ static int _generate_wrapping_key(struct plugin_handle *ph, + + out: + if (wrapping_key_file_tmp != NULL) { +- remove(wrapping_key_file_tmp); ++ plugin_remove_config_file(&ph->pd, wrapping_key_file_tmp); + free(wrapping_key_file_tmp); + } + if (wrapping_key_file != NULL) +@@ -4360,15 +4366,15 @@ static int _complete_reencipher(struct plugin_handle *ph, + if (key_file == NULL || reenc_file == NULL) + goto out; + +- rc = remove(key_file); ++ rc = plugin_remove_config_file(&ph->pd, key_file); + if (rc != 0) { +- rc = -errno; + _set_error(ph, "Failed to remove file '%s': %s", +- key_file, strerror(-rc)); ++ key_file, strerror(-rc)); + goto out; + } + +- rc = rename(reenc_file, key_file); ++ rc = renameat(ph->pd.config_path_fd, plugin_basename(reenc_file), ++ ph->pd.config_path_fd, plugin_basename(key_file)); + if (rc != 0) { + rc = -errno; + _set_error(ph, "Failed to rename file '%s' to '%s': %s", +@@ -4604,9 +4610,18 @@ int kms_reenciper(const kms_handle_t handle, enum kms_reencipher_mode mode, + KMIP_CONFIG_IDENTITY_KEY); + if (ident_key_file == NULL) + goto out; ++ if (!plugin_path_is_within_config(&ph->pd, ident_key_file)) { ++ rc = -EINVAL; ++ goto out; ++ } + + wrap_key_file = properties_get(ph->pd.properties, + KMIP_CONFIG_WRAPPING_KEY); ++ if (wrap_key_file != NULL && ++ !plugin_path_is_within_config(&ph->pd, wrap_key_file)) { ++ rc = -EINVAL; ++ goto out; ++ } + if (mode == KMS_REENC_MODE_STAGED) { + util_asprintf(&ident_reenc_file, "%s/%s", + ph->pd.config_path, +@@ -4709,14 +4724,14 @@ int kms_reenciper(const kms_handle_t handle, enum kms_reencipher_mode mode, + + out: + if (rc != 0 && ident_reenc_file != NULL) +- remove(ident_reenc_file); ++ plugin_remove_config_file(&ph->pd, ident_reenc_file); + if (ident_reenc_file != NULL) + free(ident_reenc_file); + if (ident_key_file != NULL) + free(ident_key_file); + + if (rc != 0 && wrap_reenc_file != NULL) +- remove(wrap_reenc_file); ++ plugin_remove_config_file(&ph->pd, wrap_reenc_file); + if (wrap_reenc_file != NULL) + free(wrap_reenc_file); + if (wrap_key_file != NULL) +diff --git a/zkey/plugin-utils.c b/zkey/plugin-utils.c +index 246aa258..cdd211ee 100644 +--- a/zkey/plugin-utils.c ++++ b/zkey/plugin-utils.c +@@ -54,6 +54,7 @@ int plugin_init(struct plugin_data *pd, const char *plugin_name, + util_assert(config_file != NULL, "Internal error: config_file is NULL"); + + memset(pd, 0, sizeof(struct plugin_data)); ++ pd->config_path_fd = -1; + + pd->plugin_name = util_strdup(plugin_name); + pd->config_path = util_strdup(config_path); +@@ -90,6 +91,15 @@ int plugin_init(struct plugin_data *pd, const char *plugin_name, + S_IRGRP | S_IWGRP | + S_IROTH); + ++ pd->config_path_fd = open(config_path, ++ O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (pd->config_path_fd < 0) { ++ rc = -errno; ++ warnx("Failed to open directory '%s': %s", config_path, ++ strerror(-rc)); ++ goto error; ++ } ++ + pd->properties = properties_new(); + rc = plugin_load_config(pd); + if (rc != 0 && rc != -EIO) { +@@ -115,6 +125,10 @@ void plugin_term(struct plugin_data *pd) + + pr_verbose(pd, "Plugin terminated"); + ++ if (pd->config_path_fd >= 0) { ++ close(pd->config_path_fd); ++ pd->config_path_fd = -1; ++ } + if (pd->plugin_name != NULL) + free((void *)pd->plugin_name); + if (pd->config_path != NULL) +@@ -222,7 +236,8 @@ int plugin_set_file_permission(struct plugin_data *pd, const char *filename) + { + int fd, rc = 0; + +- fd = open(filename, O_RDONLY | O_NOFOLLOW); ++ fd = openat(pd->config_path_fd, plugin_basename(filename), ++ O_RDONLY | O_NOFOLLOW); + if (fd < 0) { + rc = -errno; + plugin_set_error(pd, "Failed to open '%s': %s", filename, +@@ -316,6 +331,8 @@ out: + * file (if existent), and then renaming the temporary file to the active file. + * The active file permissions are also set to the permissions and the group of + * configuration directory. ++ * All operations are descriptor-relative to config_path_fd so that no ++ * intermediate symlink can redirect them outside the config directory. + * + * @param pd the plugin data + * @param temp_file the name of the temporary file +@@ -326,20 +343,22 @@ out: + int plugin_activate_temp_file(struct plugin_data *pd, const char *temp_file, + const char *active_file) + { ++ const char *active_base, *temp_base; + int rc; + +- if (util_path_exists(active_file)) { +- rc = remove(active_file); +- if (rc != 0) { +- rc = -errno; +- plugin_set_error(pd, "remove failed on file '%s': %s", +- active_file, strerror(-rc)); +- return rc; +- } ++ active_base = plugin_basename(active_file); ++ temp_base = plugin_basename(temp_file); ++ ++ if (unlinkat(pd->config_path_fd, active_base, 0) != 0 && ++ errno != ENOENT) { ++ rc = -errno; ++ plugin_set_error(pd, "remove failed on file '%s': %s", ++ active_file, strerror(-rc)); ++ return rc; + } + +- rc = rename(temp_file, active_file); +- if (rc != 0) { ++ if (renameat(pd->config_path_fd, temp_base, ++ pd->config_path_fd, active_base) != 0) { + rc = -errno; + plugin_set_error(pd, "rename failed on file '%s': %s", + temp_file, strerror(-rc)); +@@ -349,6 +368,73 @@ int plugin_activate_temp_file(struct plugin_data *pd, const char *temp_file, + return plugin_set_file_permission(pd, active_file); + } + ++/** ++ * Removes a file that resides directly inside the plugin config directory. ++ * Uses unlinkat(config_path_fd) so no intermediate symlink can redirect the ++ * operation outside the config directory. Only the basename of 'filename' ++ * is used; any directory prefix is ignored. ++ * ++ * @param pd the plugin data ++ * @param filename full path of the file to remove (only basename used) ++ * ++ * @returns 0 on success, or a negative errno value on failure ++ */ ++int plugin_remove_config_file(struct plugin_data *pd, const char *filename) ++{ ++ int rc = 0; ++ ++ if (filename == NULL) ++ return 0; ++ ++ if (unlinkat(pd->config_path_fd, plugin_basename(filename), 0) != 0) ++ rc = -errno; ++ ++ return rc; ++} ++ ++/** ++ * Checks whether 'path' resolves to a location strictly inside the plugin's ++ * config directory. Both the config directory and 'path' are canonicalized ++ * via canonicalize_file_name() so that symlinks and '..' components in ++ * 'path' cannot escape the config directory. ++ * ++ * @param pd the plugin data ++ * @param path the path to validate ++ * ++ * @returns true if path is inside the config directory, false otherwise. ++ * On failure (path cannot be resolved) false is returned and the ++ * plugin error message is set. ++ */ ++bool plugin_path_is_within_config(struct plugin_data *pd, const char *path) ++{ ++ char *real_config, *real_path; ++ size_t config_len; ++ bool ok = false; ++ ++ real_config = canonicalize_file_name(pd->config_path); ++ real_path = canonicalize_file_name(path); ++ ++ if (real_config == NULL || real_path == NULL) { ++ plugin_set_error(pd, "Failed to resolve path '%s': %s", ++ path, strerror(errno)); ++ goto out; ++ } ++ ++ config_len = strlen(real_config); ++ if (strncmp(real_path, real_config, config_len) == 0 && ++ real_path[config_len] == '/') ++ ok = true; ++ else ++ plugin_set_error(pd, ++ "'%s' is not inside the plugin config " ++ "directory '%s'", path, pd->config_path); ++ ++out: ++ free(real_config); ++ free(real_path); ++ return ok; ++} ++ + /** + * Check if the certificate is a self signed certificate, and if it is expired + * or not yet valid. +diff --git a/zkey/plugin-utils.h b/zkey/plugin-utils.h +index 1454971e..af501be3 100644 +--- a/zkey/plugin-utils.h ++++ b/zkey/plugin-utils.h +@@ -15,9 +15,11 @@ + #include + #include + #include ++#include + #include + #include + #include ++#include + + #include "kms-plugin.h" + #include "cca.h" +@@ -25,6 +27,7 @@ + struct plugin_data { + const char *plugin_name; + const char *config_path; ++ int config_path_fd; + mode_t config_path_mode; + gid_t config_path_owner; + const char *config_file; +@@ -42,6 +45,14 @@ struct plugin_data { + } \ + } while (0) + ++static inline const char *plugin_basename(const char *path) ++{ ++ const char *base; ++ ++ base = strrchr(path, '/'); ++ return base != NULL ? base + 1 : path; ++} ++ + int plugin_init(struct plugin_data *pd, const char *plugin_name, + const char *config_path, const char *config_file, + bool verbose); +@@ -53,6 +64,8 @@ void plugin_set_error(struct plugin_data *pd, const char *fmt, ...); + int plugin_load_config(struct plugin_data *pd); + int plugin_save_config(struct plugin_data *pd); + int plugin_set_file_permission(struct plugin_data *pd, const char *filename); ++int plugin_remove_config_file(struct plugin_data *pd, const char *filename); ++bool plugin_path_is_within_config(struct plugin_data *pd, const char *path); + + bool plugin_check_property(struct plugin_data *pd, const char *name); + int plugin_set_or_remove_property(struct plugin_data *pd, const char *name, +-- +2.55.0 diff --git a/s390utils.spec b/s390utils.spec index 3c38e26..53a07c2 100644 --- a/s390utils.spec +++ b/s390utils.spec @@ -18,7 +18,7 @@ Name: s390utils Summary: Utilities and daemons for IBM z Systems Version: 2.29.0 -Release: 3%{?dist}.5 +Release: 3%{?dist}.6 Epoch: 2 License: MIT #URL: http://www.ibm.com/developerworks/linux/linux390/s390-tools.html @@ -1174,6 +1174,10 @@ User-space development files for the s390/s390x architecture. %changelog +* Thu Aug 27 2026 Dan Horák - 2:2.29.0-3.6 +- zkey: Security fixes for zkey and friends (RHEL-243143) +- Resolves: RHEL-243143 + * Wed Apr 22 2026 Dan Horák - 2:2.29.0-3.5 - hsci: Automatically set appropriate MTU for HSCI (RHEL-153548) - Resolves: RHEL-153548