dnf-plugins-core/SOURCES/0024-multisig-Do-not-parse-OpenPGP-keys.patch

243 lines
10 KiB
Diff

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