diff --git a/0024-multisig-Do-not-parse-OpenPGP-keys.patch b/0024-multisig-Do-not-parse-OpenPGP-keys.patch new file mode 100644 index 0000000..bb1486a --- /dev/null +++ b/0024-multisig-Do-not-parse-OpenPGP-keys.patch @@ -0,0 +1,242 @@ +From 3fd00a0bf41ac2c9342e0ba1d8550ee1ac5f2604 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +Date: Fri, 12 Sep 2025 18:12:38 +0200 +Subject: [PATCH] multisig: Do not parse OpenPGP keys +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +If a key in OpenPGP version 6 format format was defined for +a repository, the key was always omitted and never imported to pqrpm +key store. As a result, packages signed with that key could not be +verified: + + [...] + Importing GPG key 0xC9D3B13C: + Userid : "rsa4k_user" + Fingerprint: CE77 8FBE 1E8D 4DD2 7691 FB33 CA58 1189 C9D3 B13C + From : /root/repo/rsa4k.cert + Key imported successfully + foo 16 MB/s | 16 kB 00:00 + Import of key(s) didn't help, wrong key(s)? + Public key for foo-1.0-1.noarch.rpm is not installed. Failing package is: foo-1.0-1.noarch + GPG Keys are configured as: file:///root/repo/rsa4k.cert, file:///root/repo/mldsa87.cert + Error: GPG check FAILED + +The cause was that multisig plugin called dnf.crypto.retrieve() to +obtain a list of OpenPGP objects from the gpgkey URLs. That DNF +function uses dnf.crypto.rawkey2infos() to parse the OpenPGP packets +with a gpgme library. But library does not support OpenPGPv6. As +a result, multisig got an empty list of OpenPGP objects and dis not +import any key in OpenPGPv6 format into pqrm key store. + +Considering that the only available OpenPGPv6 parser is rpm-sequoia +library wrapped by pqrpm's librpmio library, and this plugin cannot +load them into its name space not to clash with the system librpmio +library, an ideal fix would have to develop standalone executable on +top of them and parse it output by this plugin. And considering that +best code is no code, + +I removed printing any details about the imported keys from this +plugin. The only details about the key this plugin now prints is URL +or local path to the key file: + + Importing GPG keys from: /root/repos/opgp6/mldsa65.cert + Is this ok [y/N]: y + Key imported successfully + +Because the user ID and key ID were also required for DNSSEC +validation, I had to remove that code. This patch also removes all +code that became unreachable because of this fix. + +Another downside is that now the plugin cannot does not check whether +a key has already been imported, resulting always asking a using for +importing the keys associated with a repository the unverifiable +package comes from. + +Resolve: https://issues.redhat.com/browse/RHEL-114424 +Signed-off-by: Petr Písař +--- + plugins/multisig.py | 115 +++++++++++++++----------------------------- + 1 file changed, 40 insertions(+), 75 deletions(-) + +diff --git a/plugins/multisig.py b/plugins/multisig.py +index 8735a26..f29e41f 100644 +--- a/plugins/multisig.py ++++ b/plugins/multisig.py +@@ -1,16 +1,26 @@ + from __future__ import print_function, absolute_import, unicode_literals + import dnf +-import dnf.crypto +-import dnf.dnssec + import dnf.exceptions + from dnf.i18n import ucd + import dnf.rpm.transaction + import dnf.transaction ++import dnf.util + from dnfpluginscore import _, logger + import os + import subprocess + import sys + ++class MultiSigKey(object): ++ def __init__(self, data, url): ++ self.id_ = None ++ self.fingerprint = None ++ self.timestamp = None ++ self.raw_key = data ++ self.url = url ++ self.userid = None ++ self.short_id = None ++ self.rpm_id = None ++ + class MultiSig(dnf.Plugin): + """ + This plugin verifies signatures of RPM packages by executing an +@@ -168,33 +178,6 @@ class MultiSig(dnf.Plugin): + logger.debug(_("Multisig: verification result: {} (code={})").format(msg, result)) + return result, msg + +- def keyInstalled(self, fingerprint): +- ''' +- Return if the GPG key described by the given fingerprint is installed +- in the multisig keyring. +- +- Return values: +- - True key is installed +- - False otherwise +- Trows: If rpmkeys program could not been executed. +- +- No effort is made to handle duplicates. +- ''' +- # XXX: rpmkeys expects lowercase +- # +- logger.debug(_("Multisig: Checking a presence of key={}").format(fingerprint)) +- args = (self.rpmkeys_executable, +- '--root', self.base.conf.installroot, +- '--list', fingerprint.lower()) +- p = subprocess.run( +- args=args, +- executable=self.rpmkeys_executable, +- cwd='/', +- stdin=subprocess.DEVNULL, +- stdout=subprocess.DEVNULL, +- stderr=subprocess.DEVNULL) +- return p.returncode == 0 +- + def importKey(self, key): + ''' + Import given Key object into the multisig keyring. +@@ -232,6 +215,28 @@ class MultiSig(dnf.Plugin): + returncode, stdout, stderr)) + return returncode == 0 + ++ def retrieve(self, keyurl, repo=None): ++ """Retrieve a content of a key file specified by the URL using ++ repository's proxy configuration. ++ ++ :param keyurl URL of the key file ++ :param repo repository object ++ :returns: list of MultiSigKey objects populated from the key file ++ """ ++ if keyurl.startswith('http:'): ++ logger.warning(_("retrieving repo key for %s unencrypted from %s"), repo.id, keyurl) ++ with dnf.util._urlopen(keyurl, repo=repo) as handle: ++ # This is a place for parsing the key file and populating key ID etc. ++ keyinfos = [MultiSigKey(handle.read(), keyurl)] ++ return keyinfos ++ ++ def log_key_import(self, keyinfo): ++ """Print and log details about keys to be imported. ++ """ ++ msg = (_('Importing GPG keys from: %s') % ++ (keyinfo.url.replace("file://", ""))) ++ logger.critical("%s", msg) ++ + def _get_key_for_package(self, po, askcb=None, fullaskcb=None): + """Retrieve a key for a package. If needed, use the given + callback to prompt whether the key should be imported. +@@ -264,53 +269,17 @@ class MultiSig(dnf.Plugin): + user_cb_fail = False + self._repo_set_imported_gpg_keys.append(repo.id) + for keyurl in keyurls: +- keys = dnf.crypto.retrieve(keyurl, repo) ++ keys = self.retrieve(keyurl, repo) + + for info in keys: +- # Check if key is already installed +- if self.keyInstalled(info.fingerprint): +- msg = _('GPG key at %s (0x%s) is already installed') +- logger.info(msg, keyurl, info.short_id) +- continue +- +- # DNS Extension: create a key object, pass it to the verification class +- # and print its result as an advice to the user. +- if self.base.conf.gpgkey_dns_verification: +- dns_input_key = dnf.dnssec.KeyInfo.from_rpm_key_object(info.userid, +- info.raw_key) +- dns_result = dnf.dnssec.DNSSECKeyVerification.verify(dns_input_key) +- logger.info(dnf.dnssec.nice_user_msg(dns_input_key, dns_result)) +- + # Try installing/updating GPG key + info.url = keyurl +- if self.base.conf.gpgkey_dns_verification: +- dnf.crypto.log_dns_key_import(info, dns_result) +- else: +- dnf.crypto.log_key_import(info) ++ self.log_key_import(info) + rc = False + if self.base.conf.assumeno: + rc = False + elif self.base.conf.assumeyes: +- # DNS Extension: We assume, that the key is trusted in case it is valid, +- # its existence is explicitly denied or in case the domain is not signed +- # and therefore there is no way to know for sure (this is mainly for +- # backward compatibility) +- # FAQ: +- # * What is PROVEN_NONEXISTENCE? +- # In DNSSEC, your domain does not need to be signed, but this state +- # (not signed) has to be proven by the upper domain. e.g. when example.com. +- # is not signed, com. servers have to sign the message, that example.com. +- # does not have any signing key (KSK to be more precise). +- if self.base.conf.gpgkey_dns_verification: +- if dns_result in (dnf.dnssec.Validity.VALID, +- dnf.dnssec.Validity.PROVEN_NONEXISTENCE): +- rc = True +- logger.info(dnf.dnssec.any_msg(_("The key has been approved."))) +- else: +- rc = False +- logger.info(dnf.dnssec.any_msg(_("The key has been rejected."))) +- else: +- rc = True ++ rc = True + + # grab the .sig/.asc for the keyurl, if it exists if it + # does check the signature on the key if it is signed by +@@ -318,11 +287,8 @@ class MultiSig(dnf.Plugin): + # rc = True else ask as normal. + + elif fullaskcb: +- rc = fullaskcb({"po": po, "userid": info.userid, +- "hexkeyid": info.short_id, +- "keyurl": keyurl, +- "fingerprint": info.fingerprint, +- "timestamp": info.timestamp}) ++ rc = fullaskcb({"po": po, ++ "keyurl": keyurl}) + elif askcb: + rc = askcb(po, info.userid, info.short_id) + +@@ -331,8 +297,7 @@ class MultiSig(dnf.Plugin): + continue + + # Import the key +- # XXX: raw_key of second info erroneously contains first and +- # second key. Probably a bug in key parser. ++ # XXX: raw_key contains all keys found in the key file. + #logger.debug(_("Multisig: Importing a key: {}").format(info.raw_key)) + result = self.importKey(info) + if result == False: +-- +2.51.0 + diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec index 4c11c3b..b37a196 100644 --- a/dnf-plugins-core.spec +++ b/dnf-plugins-core.spec @@ -34,7 +34,7 @@ Name: dnf-plugins-core Version: 4.3.0 -Release: 22%{?dist} +Release: 23%{?dist} Summary: Core Plugins for DNF License: GPLv2+ URL: https://github.com/rpm-software-management/dnf-plugins-core @@ -59,6 +59,7 @@ Patch20: 0020-needs-restarting-Get-boot-time-from-systemd-UnitsLoa.patch Patch21: 0021-dnf-copr-enable-on-Asahi-Fedora-Linux-Remix-guesses.patch Patch22: 0022-reposync-Avoid-multiple-downloads-of-duplicate-packa.patch Patch23: 0023-multisig-A-new-plugin-for-verifying-extraordinary-RP.patch +Patch24: 0024-multisig-Do-not-parse-OpenPGP-keys.patch BuildArch: noarch BuildRequires: cmake @@ -825,6 +826,9 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/ %endif %changelog +* Mon Sep 15 2025 Petr Pisar - 4.3.0-23 +- Fix importing OpenPGPv6 keys (RHEL-114424) + * Wed Jun 25 2025 Petr Pisar - 4.3.0-22 - Add multisig plugin (RHEL-100157)