import UBI dnf-plugins-core-4.3.0-24.el9_7

This commit is contained in:
eabdullin 2025-11-11 16:13:23 +00:00
parent 41932746dd
commit 911a483847
5 changed files with 1016 additions and 3 deletions

View File

@ -0,0 +1,40 @@
From 43d07e2b385b069b1f851e5a16f1b7d8bbed1195 Mon Sep 17 00:00:00 2001
From: Marek Blaha <mblaha@redhat.com>
Date: Mon, 4 Nov 2024 10:35:08 +0100
Subject: [PATCH] reposync: Avoid multiple downloads of duplicate packages
Download each package only once if it would have been saved to the same
location. This can occur if the repository metadata contains duplicate
entries for the same package.
Resolves: https://issues.redhat.com/browse/RHEL-64320
Upstream commit: 17e36ed
---
plugins/reposync.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/plugins/reposync.py b/plugins/reposync.py
index ab513e7..19f5440 100644
--- a/plugins/reposync.py
+++ b/plugins/reposync.py
@@ -292,7 +292,15 @@ class RepoSyncCommand(dnf.cli.Command):
query.filterm(arch='src')
elif self.opts.arches:
query.filterm(arch=self.opts.arches)
- return query
+ # skip packages that would have been downloaded to the same location
+ pkglist = []
+ seen_paths = set()
+ for pkg in query:
+ download_path = self.pkg_download_path(pkg)
+ if download_path not in seen_paths:
+ pkglist.append(pkg)
+ seen_paths.add(download_path)
+ return pkglist
def download_packages(self, pkglist):
base = self.base
--
2.48.1

View File

@ -0,0 +1,617 @@
From 37c4cd7014f4b3c5db64a0a900761e53caf645be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= <ppisar@redhat.com>
Date: Tue, 25 Mar 2025 12:34:34 +0100
Subject: [PATCH] multisig: A new plugin for verifying extraordinary RPM
signatures
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This plugin executes a dedicated rpmkeys(8) tool of pqrpm for
verifying RPM packages bound into a transaction.
The dedicated tool is supposed to understand signatures in RPM version
6 format. One feature of that format is that you can have multiple
signatures on a single RPM package.
This plugin takes public keys from a default key store of pqrpm, which
is separate from the native RPM database. A reason is that the native
database might reject storing OpenPGP keys with an unsupported key
schema. A downside is that "dnf --installroot" places the keyring to
a directory which is ununowned in the installroot.
XXX: This plugin does not report which keys required for the
verification are missing (NOKEY case in _process_rpm_output()).
Implementing that would require changing too many function interfaces.
Classical single-signature verification does not do that either.
XXX: The plugin is packaged into a package which requires pqrpm which
will only be available in RHEL 9. Next RHEL version will support the
extraordinary signatures natively. Thus this patch only makes sense in
RHEL 9.
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf-plugins-core.spec | 19 ++
doc/CMakeLists.txt | 5 +
doc/conf.py | 3 +
doc/index.rst | 1 +
doc/multisig.rst | 62 +++++++
plugins/CMakeLists.txt | 3 +
plugins/multisig.py | 402 +++++++++++++++++++++++++++++++++++++++++
7 files changed, 495 insertions(+)
create mode 100644 doc/multisig.rst
create mode 100644 plugins/multisig.py
diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
index f2d1bc4..cb3b1b8 100644
--- a/dnf-plugins-core.spec
+++ b/dnf-plugins-core.spec
@@ -301,6 +301,18 @@ Obsoletes: python-dnf-plugins-extras-migrate < %{dnf_plugins_extra}
Migrate Plugin for DNF, Python 2 version. Migrates history, group and yumdb data from yum to dnf.
%endif
+%if %{with python3}
+%package -n python3-dnf-plugin-multisig
+Summary: Multisig Plugin for DNF
+Requires: pqrpm
+Requires: python3-%{name} = %{version}-%{release}
+Provides: dnf-plugin-multisig = %{version}-%{release}
+
+%description -n python3-dnf-plugin-multisig
+Multisig Plugin for DNF, Python 3 version. The plugin verifies multiple RPMv6
+signatures on RPMv4 packages by using an external rpmkeys program.
+%endif
+
%if %{with python2}
%package -n python2-dnf-plugin-post-transaction-actions
Summary: Post transaction actions Plugin for DNF
@@ -716,6 +728,13 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/
%exclude %{_mandir}/man8/dnf-migrate.*
%endif
+%if %{with python3}
+%files -n python3-dnf-plugin-multisig
+%{python3_sitelib}/dnf-plugins/multisig.*
+%{python3_sitelib}/dnf-plugins/__pycache__/multisig.*
+%{_mandir}/man8/dnf*-multisig.*
+%endif
+
%if %{with python2}
%files -n python2-dnf-plugin-post-transaction-actions
%config(noreplace) %{_sysconfdir}/dnf/plugins/post-transaction-actions.conf
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index 79472a5..297506a 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -47,6 +47,11 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-migrate.8
DESTINATION share/man/man8)
endif()
+if (${PYTHON_VERSION_MAJOR} STREQUAL "3")
+INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf4-multisig.8
+ DESTINATION share/man/man8)
+endif()
+
if (${WITHOUT_LOCAL} STREQUAL "0")
INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-local.8
DESTINATION share/man/man8)
diff --git a/doc/conf.py b/doc/conf.py
index 327ac07..2845d18 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -300,6 +300,9 @@ man_pages = [
if sys.version_info[0] < 3:
man_pages.append(('migrate', 'dnf-migrate', u'DNF migrate Plugin', AUTHORS, 8))
+if sys.version_info[0] == 3:
+ man_pages.append(('multisig', 'dnf4-multisig', u'DNF multisig Plugin', AUTHORS, 8))
+
# If true, show URL addresses after external links.
#man_show_urls = False
diff --git a/doc/index.rst b/doc/index.rst
index 251a24e..32984a5 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -37,6 +37,7 @@ This documents core plugins of DNF:
leaves
local
migrate
+ multisig
modulesync
needs_restarting
post-transaction-actions
diff --git a/doc/multisig.rst b/doc/multisig.rst
new file mode 100644
index 0000000..21b9436
--- /dev/null
+++ b/doc/multisig.rst
@@ -0,0 +1,62 @@
+===================
+DNF multisig Plugin
+===================
+
+-----------
+Description
+-----------
+
+This plugin verifies extraordinary RPMv6 signatures when installing,
+reinstalling, upgrading, or downgrading packages from a repository. If the
+verification fails, the RPM operation will be aborted.
+
+The verification is achieved by executing a dedicated rpmkeys(8) tool which is
+supposed to understand package signatures in RPM version 6 format. One feature
+of that format is that you can have multiple signatures on a single RPM
+package. If the package has no RPMv6 signature a signature in version 4 format
+will be verified instead. If there is no signature, the verification will be
+handled as failed.
+
+The dedicated rpmkeys(8) tool trusts public keys in a key store separate from
+the native RPM database because the native database might reject storing
+OpenPGP keys with an unsupported key schema which is foreseen to be used in
+RPMv6 signatures.
+
+Public keys missing from the separate key store are attempted to be imported
+from URLs listed in ``gpgkey`` configuration field of a repository the package
+belongs to. Before importing, a user is asked for a confirmation with the
+import, unless DNF was invoked with ``--assumeyes`` or ``--assumeno`` options.
+
+The key store can be inspected with ``/usr/lib/pqrpm/bin/rpmkeys -D --list``
+command. A key can be deleted from the key store with
+``/usr/lib/pqrpm/bin/rpmkeys -D --erase KEY_ID`` command. The ``KEY_ID`` is
+the first word in an output of the ``--list`` command.
+
+Users who do not wish to verify the extraordinary RPMv6 signatures should
+uninstall this plugin.
+
+-------------
+Configuration
+-------------
+
+Hard-coded path to the rpmkeys(8) tool is ``/usr/lib/pqrpm/bin/rpmkeys``.
+
+This plugin respects ``gpgkey`` and ``gpgcheck`` fields in a repository
+configuration. See dnf.conf(5) for more details.
+
+-----
+Files
+-----
+
+``/usr/lib/pqrpm/lib/sysimage/rpm``
+ A location of the key store defined by ``/usr/lib/pqrpm/bin/rpmkeys``
+ tool.
+
+--------
+See Also
+--------
+
+* :manpage:`dnf.conf(5)`
+* :manpage:`dnf(8)`
+* :manpage:`rpmkeys(8)`
+
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index d004e5e..a84a786 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -14,6 +14,9 @@ endif()
if (${PYTHON_VERSION_MAJOR} STREQUAL "2")
INSTALL (FILES migrate.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
endif()
+if (${PYTHON_VERSION_MAJOR} STREQUAL "3")
+INSTALL (FILES multisig.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
+endif()
INSTALL (FILES needs_restarting.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
INSTALL (FILES post-transaction-actions.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
INSTALL (FILES repoclosure.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
diff --git a/plugins/multisig.py b/plugins/multisig.py
new file mode 100644
index 0000000..8735a26
--- /dev/null
+++ b/plugins/multisig.py
@@ -0,0 +1,402 @@
+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
+from dnfpluginscore import _, logger
+import os
+import subprocess
+import sys
+
+class MultiSig(dnf.Plugin):
+ """
+ This plugin verifies signatures of RPM packages by executing an
+ extraordinary "rpmkeys" tool. That tool can, for example, support multiple
+ RPM v6 signatures, or signature schemata uknown to the ordinary,
+ system-wide rpmkeys tool.
+
+ This verification is perfmored in addition to the standard verification
+ performed by DNF.
+ """
+
+ name = "multisig"
+
+ def __init__(self, base, cli):
+ super(MultiSig, self).__init__(base, cli)
+ # Path to the rpmkeys executable
+ self.rpmkeys_executable = "/usr/lib/pqrpm/bin/rpmkeys"
+ # List of repositories whose keys we have tried importing so far
+ # during a run of this plugin.
+ self._repo_set_imported_gpg_keys = [];
+
+ def pre_transaction(self):
+ inbound_packages = []
+ for ts_item in self.base.transaction:
+ if ts_item.action in dnf.transaction.FORWARD_ACTIONS:
+ inbound_packages.append(ts_item.pkg);
+ self.gpgsigcheck(inbound_packages)
+
+ def _process_rpm_output(self, data):
+ # No signatures or digests = corrupt package.
+ # There is at least one line for -: and another (empty) entry after the
+ # last newline.
+ if len(data) < 3 or data[0] != b'-:' or data[-1]:
+ return 2
+ seen_sig, missing_key, not_trusted, not_signed = False, False, False, False
+ for i in data[1:-1]:
+ if b': BAD' in i:
+ return 2
+ elif i.endswith(b': NOKEY'):
+ missing_key = True
+ elif i.endswith(b': NOTTRUSTED'):
+ not_trusted = True
+ elif i.endswith(b': NOTFOUND'):
+ not_signed = True
+ elif not i.endswith(b': OK'):
+ return 2
+ if not_trusted:
+ return 3
+ elif missing_key:
+ return 1
+ elif not_signed:
+ return 4
+ # we still check return code, so this is safe
+ return 0
+
+ def _verifyPackageUsingRpmkeys(self, package, installroot):
+ # "--define=_pkgverify_level signature" enforces signature checking;
+ # "--define=_pkgverify_flags 0x0" ensures that all signatures are checked.
+ args = (self.rpmkeys_executable,
+ '--checksig', '--root', installroot, '--verbose',
+ '--define=_pkgverify_level signature', '--define=_pkgverify_flags 0x0',
+ '-')
+ env = dict(os.environ)
+ env['LC_ALL'] = 'C'
+ with subprocess.Popen(
+ args=args,
+ executable=self.rpmkeys_executable,
+ env=env,
+ stdout=subprocess.PIPE,
+ cwd='/',
+ stdin=package) as p:
+ data = p.communicate()[0]
+ returncode = p.returncode
+ if type(returncode) is not int:
+ raise AssertionError('Popen set return code to non-int')
+ # rpmkeys can return something other than 0 or 1 in the case of a
+ # fatal error (OOM, abort() called, SIGSEGV, etc)
+ if returncode >= 2 or returncode < 0:
+ return 2
+ ret = self._process_rpm_output(data.split(b'\n'))
+ if ret:
+ return ret
+ return 2 if returncode else 0
+
+ def _checkSig(self, installroot, package):
+ """Takes a transaction set and a package, check it's sigs,
+ return 0 if they are all fine
+ return 1 if the gpg key can't be found
+ return 2 if the header is in someway damaged
+ return 3 if the key is not trusted
+ return 4 if the pkg is not gpg or pgp signed"""
+
+ fdno = os.open(package, os.O_RDONLY|os.O_NOCTTY|os.O_CLOEXEC)
+ try:
+ value = self._verifyPackageUsingRpmkeys(fdno, installroot)
+ finally:
+ os.close(fdno)
+ return value
+
+ def _sig_check_pkg(self, po):
+ """Verify the GPG signature of the given package object.
+
+ :param po: the package object to verify the signature of
+ :return: (result, error_string)
+ where result is::
+
+ 0 = GPG signature verifies ok or verification is not required.
+ 1 = GPG verification failed but installation of the right GPG key
+ might help.
+ 2 = Fatal GPG verification error, give up.
+ """
+ if po._from_cmdline:
+ check = self.base.conf.localpkg_gpgcheck
+ hasgpgkey = 0
+ else:
+ repo = self.base.repos[po.repoid]
+ check = repo.gpgcheck
+ hasgpgkey = not not repo.gpgkey
+
+ localfn = os.path.basename(po.localPkg())
+ if check:
+ logger.debug(_("Multisig: verifying: {}").format(po.localPkg()))
+ sigresult = self._checkSig(self.base.conf.installroot, po.localPkg())
+ if sigresult == 0:
+ result = 0
+ msg = _('All signatures for %s successfully verified') % localfn
+
+ elif sigresult == 1:
+ if hasgpgkey:
+ result = 1
+ else:
+ result = 2
+ msg = _('Public key for %s is not installed') % localfn
+
+ elif sigresult == 2:
+ result = 2
+ msg = _('Problem opening package %s') % localfn
+
+ elif sigresult == 3:
+ if hasgpgkey:
+ result = 1
+ else:
+ result = 2
+ result = 1
+ msg = _('Public key for %s is not trusted') % localfn
+
+ elif sigresult == 4:
+ result = 2
+ msg = _('Package %s is not signed') % localfn
+
+ else:
+ result = 0
+ msg = _('Signature verification for %s is disabled') % localfn
+
+ 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
+ # <https://github.com/rpm-software-management/rpm/issues/3721>
+ 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.
+
+ Return values:
+ - True key imported successfully
+ - False otherwise
+ Trows: If rpmkeys program could not been executed.
+
+ What happens if a key's raw_string contains multiple public key
+ packets, or if the key was already in the keyring is unspecified and
+ it depends on rpmkeys behavior. Current rpmkeys implementation
+ gracefully ignores (or updates?) existing keys.
+ '''
+ args = (self.rpmkeys_executable,
+ '--root', self.base.conf.installroot,
+ '--import', '-')
+ env = dict(os.environ)
+ env['LC_ALL'] = 'C'
+ with subprocess.Popen(
+ executable=self.rpmkeys_executable,
+ args=args,
+ env=env,
+ cwd='/',
+ # XXX: rpmkeys used to fail reading from a pipe. Fix at
+ # <https://github.com/rpm-software-management/rpm/pull/3706>.
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE) as p:
+ stdout, stderr = p.communicate(input=key.raw_key)
+ returncode = p.returncode
+ if type(returncode) is not int:
+ raise AssertionError('Popen set return code to non-int')
+ logger.debug(_("Multisig: Key import result: exitcode={}, stdout={}, stderr={}").format(
+ returncode, stdout, stderr))
+ return returncode == 0
+
+ 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.
+
+ :param po: the package object to retrieve the key of
+ :param askcb: Callback function to use to ask permission to
+ import a key. The arguments *askcb* should take are the
+ package object, the userid of the key, and the keyid
+ :param fullaskcb: Callback function to use to ask permission to
+ import a key. This differs from *askcb* in that it gets
+ passed a dictionary so that we can expand the values passed.
+ :raises: :class:`dnf.exceptions.Error` if there are errors
+ retrieving the keys
+ """
+ if po._from_cmdline:
+ # raise an exception, because po.repoid is not in self.repos
+ msg = _('Unable to retrieve a key for a commandline package: %s')
+ raise ValueError(msg % po)
+
+ repo = self.base.repos[po.repoid]
+ key_installed = repo.id in self._repo_set_imported_gpg_keys
+ keyurls = [] if key_installed else repo.gpgkey
+
+ def _prov_key_data(msg):
+ msg += _('. Failing package is: %s') % (po) + '\n '
+ msg += _('GPG Keys are configured as: %s') % \
+ (', '.join(repo.gpgkey))
+ return msg
+
+ user_cb_fail = False
+ self._repo_set_imported_gpg_keys.append(repo.id)
+ for keyurl in keyurls:
+ keys = dnf.crypto.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)
+ 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
+
+ # grab the .sig/.asc for the keyurl, if it exists if it
+ # does check the signature on the key if it is signed by
+ # one of our ca-keys for this repo or the global one then
+ # 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})
+ elif askcb:
+ rc = askcb(po, info.userid, info.short_id)
+
+ if not rc:
+ user_cb_fail = True
+ continue
+
+ # Import the key
+ # XXX: raw_key of second info erroneously contains first and
+ # second key. Probably a bug in key parser.
+ #logger.debug(_("Multisig: Importing a key: {}").format(info.raw_key))
+ result = self.importKey(info)
+ if result == False:
+ msg = _('Key import failed')
+ raise dnf.exceptions.Error(_prov_key_data(msg))
+ logger.info(_('Key imported successfully'))
+ key_installed = True
+
+ if not key_installed and user_cb_fail:
+ raise dnf.exceptions.Error(_("Didn't install any keys"))
+
+ if not key_installed:
+ msg = _('The GPG keys listed for the "%s" repository are '
+ 'already installed but they are not correct for this '
+ 'package.\n'
+ 'Check that the correct key URLs are configured for '
+ 'this repository.') % repo.name
+ raise dnf.exceptions.Error(_prov_key_data(msg))
+
+ # Check if the newly installed keys helped
+ result, errmsg = self._sig_check_pkg(po)
+ if result != 0:
+ if keyurls:
+ msg = _("Import of key(s) didn't help, wrong key(s)?")
+ logger.info(msg)
+ errmsg = ucd(errmsg)
+ raise dnf.exceptions.Error(_prov_key_data(errmsg))
+
+ def gpgsigcheck(self, pkgs):
+ """Perform GPG signature verification on the given packages,
+ installing keys if possible.
+
+ :param pkgs: a list of package objects to verify the GPG
+ signatures of
+ :raises: Will raise :class:`Error` if there's a problem
+ """
+ error_messages = []
+ for po in pkgs:
+ result, errmsg = self._sig_check_pkg(po)
+
+ if result == 0:
+ # Verified ok, or verify not req'd
+ continue
+
+ elif result == 1:
+ ay = self.base.conf.assumeyes and not self.base.conf.assumeno
+ if (not sys.stdin or not sys.stdin.isatty()) and not ay:
+ raise dnf.exceptions.Error(_('Refusing to automatically import keys when running ' \
+ 'unattended.\nUse "-y" to override.'))
+
+ # the callback here expects to be able to take options which
+ # userconfirm really doesn't... so fake it
+ fn = lambda x, y, z: self.base.output.userconfirm()
+ try:
+ self._get_key_for_package(po, fn)
+ except (dnf.exceptions.Error, ValueError) as e:
+ error_messages.append(str(e))
+
+ else:
+ # Fatal error
+ error_messages.append(errmsg)
+
+ if error_messages:
+ for msg in error_messages:
+ logger.critical(msg)
+ raise dnf.exceptions.Error(_("GPG check FAILED"))
+
--
2.50.1

View File

@ -0,0 +1,242 @@
From 3fd00a0bf41ac2c9342e0ba1d8550ee1ac5f2604 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= <ppisar@redhat.com>
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ř <ppisar@redhat.com>
---
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
- # <https://github.com/rpm-software-management/rpm/issues/3721>
- 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

View File

@ -0,0 +1,78 @@
From e1aebc68eb031f3e91ed39a0b145589f1a4a1734 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= <ppisar@redhat.com>
Date: Fri, 3 Oct 2025 12:23:11 +0200
Subject: [PATCH] multisig: Rename dnf4-multisig(8) manual page to
dnf-multisig(8)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
To align with all other plugin manual pages.
Create dnf4-multisig(8) symlink for compatibility.
FILE(CREATE_LINK) is available since cmake 3.14.
Resolve: https://issues.redhat.com/browse/RHEL-117134
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
CMakeLists.txt | 2 +-
dnf-plugins-core.spec | 2 +-
doc/CMakeLists.txt | 4 ++++
doc/conf.py | 2 +-
4 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a1eea7b..86225e7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
PROJECT (dnf-plugins-core NONE)
-CMAKE_MINIMUM_REQUIRED (VERSION 2.4)
+CMAKE_MINIMUM_REQUIRED (VERSION 3.14)
if (NOT WITHOUT_LOCAL)
set (WITHOUT_LOCAL "0")
diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
index cb3b1b8..ff6beea 100644
--- a/dnf-plugins-core.spec
+++ b/dnf-plugins-core.spec
@@ -40,7 +40,7 @@ License: GPLv2+
URL: https://github.com/rpm-software-management/dnf-plugins-core
Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz
BuildArch: noarch
-BuildRequires: cmake
+BuildRequires: cmake >= 3.14
BuildRequires: gettext
# Documentation
%if %{with python3}
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index 297506a..75e74bb 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -48,6 +48,10 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-migrate.8
endif()
if (${PYTHON_VERSION_MAJOR} STREQUAL "3")
+INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-multisig.8
+ DESTINATION share/man/man8)
+FILE(CREATE_LINK dnf-multisig.8 ${CMAKE_CURRENT_BINARY_DIR}/dnf4-multisig.8
+ SYMBOLIC)
INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf4-multisig.8
DESTINATION share/man/man8)
endif()
diff --git a/doc/conf.py b/doc/conf.py
index 2845d18..225ae5f 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -301,7 +301,7 @@ if sys.version_info[0] < 3:
man_pages.append(('migrate', 'dnf-migrate', u'DNF migrate Plugin', AUTHORS, 8))
if sys.version_info[0] == 3:
- man_pages.append(('multisig', 'dnf4-multisig', u'DNF multisig Plugin', AUTHORS, 8))
+ man_pages.append(('multisig', 'dnf-multisig', u'DNF multisig Plugin', AUTHORS, 8))
# If true, show URL addresses after external links.
#man_show_urls = False
--
2.51.0

View File

@ -34,7 +34,7 @@
Name: dnf-plugins-core
Version: 4.3.0
Release: 20%{?dist}
Release: 24%{?dist}
Summary: Core Plugins for DNF
License: GPLv2+
URL: https://github.com/rpm-software-management/dnf-plugins-core
@ -57,9 +57,13 @@ Patch18: 0018-system-upgrade-change-http-to-https-in-unit-file.patch
Patch19: 0019-reposync-Respect-norepopath-with-metadata-path.patch
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
Patch25: 0025-multisig-Rename-dnf4-multisig-8-manual-page-to-dnf-m.patch
BuildArch: noarch
BuildRequires: cmake
BuildRequires: cmake >= 3.14
BuildRequires: gettext
# Documentation
%if %{with python3}
@ -320,6 +324,18 @@ Obsoletes: python-dnf-plugins-extras-migrate < %{dnf_plugins_extra}
Migrate Plugin for DNF, Python 2 version. Migrates history, group and yumdb data from yum to dnf.
%endif
%if %{with python3}
%package -n python3-dnf-plugin-multisig
Summary: Multisig Plugin for DNF
Requires: pqrpm
Requires: python3-%{name} = %{version}-%{release}
Provides: dnf-plugin-multisig = %{version}-%{release}
%description -n python3-dnf-plugin-multisig
Multisig Plugin for DNF, Python 3 version. The plugin verifies multiple RPMv6
signatures on RPMv4 packages by using an external rpmkeys program.
%endif
%if %{with python2}
%package -n python2-dnf-plugin-post-transaction-actions
Summary: Post transaction actions Plugin for DNF
@ -735,6 +751,13 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/
%exclude %{_mandir}/man8/dnf-migrate.*
%endif
%if %{with python3}
%files -n python3-dnf-plugin-multisig
%{python3_sitelib}/dnf-plugins/multisig.*
%{python3_sitelib}/dnf-plugins/__pycache__/multisig.*
%{_mandir}/man8/dnf*-multisig.*
%endif
%if %{with python2}
%files -n python2-dnf-plugin-post-transaction-actions
%config(noreplace) %{_sysconfdir}/dnf/plugins/post-transaction-actions.conf
@ -804,6 +827,19 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/
%endif
%changelog
* Fri Oct 03 2025 Petr Pisar <ppisar@redhat.com> - 4.3.0-24
- Rename dnf4-multisig(8) manual page to dnf-multisig(8) (RHEL-117134)
* Mon Sep 15 2025 Petr Pisar <ppisar@redhat.com> - 4.3.0-23
- Fix importing OpenPGPv6 keys (RHEL-114424)
* Wed Jun 25 2025 Petr Pisar <ppisar@redhat.com> - 4.3.0-22
- Add multisig plugin (RHEL-100157)
* Tue Mar 11 2025 Marek Blaha <mblaha@redhat.com> - 4.3.0-21
- reposync: Avoid multiple downloads of duplicate packages (RHEL-64320)
- Fix bogus changelog entry date
* Mon Dec 16 2024 Jan Kolarik <jkolarik@redhat.com> - 4.3.0-20
- Add forgotten changelog
@ -865,7 +901,7 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/
* Thu Jan 05 2023 Nicola Sella <nsella@redhat.com> - 4.3.0-3
- Remove requirement of python3-distro
* Wed Dec 03 2022 Nicola Sella <nsella@redhat.com> - 4.3.0-2
* Sat Dec 03 2022 Nicola Sella <nsella@redhat.com> - 4.3.0-2
- Move system-upgrade plugin to core (RhBug:2054235)
- offline-upgrade: add support for security filters (RhBug:1939975)