Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for public key id checking when adding packages to repos #2954

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/2258.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pulp is able to reject packages that are unsigned or incorrectly signed, if desired.
5 changes: 5 additions & 0 deletions docs/workflows/manage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Package
Within a repository version there can be only one package (RPM or SRPM) with the same NEVRA.
NEVRA stands for name, epoch, version, release, architecture.

Repositories can be set to only allow packages signed by specific key(s) to be added to them by setting the `allowed_pub_keys` field on the repo.
That field is a list of Key IDs for the acceptable signing keys.
A Key ID is the last 16 digits of the hex fingerprint of the public keys (import the key to gpg, do `gpg --list-keys`, take the last 16 digits).
For example, a repo that had set `allowed_pub_keys: ["ABCDEF0123456789", "0987654321FEDCBA"]` would only allow packages signed by those two keys to be added, and any update that contained an RPM *not* signed by one of them would fail.


Advisory
--------
Expand Down
22 changes: 22 additions & 0 deletions pulp_rpm/app/management/commands/rpm-datarepair.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.core.management import BaseCommand, CommandError

from pulp_rpm.app.models import Package # noqa
from pulp_rpm.app.shared_utils import read_crpackage_from_artifact


class Command(BaseCommand):
Expand All @@ -15,16 +16,37 @@ class Command(BaseCommand):
def add_arguments(self, parser):
"""Set up arguments."""
parser.add_argument("issue", help=_("The github issue # of the issue to be fixed."))
parser.add_argument("-y", "--yes", action='store_true', default=False,
help=_('Execute change with no "Are you sure?" prompt.'))

def handle(self, *args, **options):
"""Implement the command."""
issue = options["issue"]

if issue == "2460":
self.repair_2460()
elif issue == "2258":
self.repair_2258(options["yes"])
else:
raise CommandError(_("Unknown issue: '{}'").format(issue))

def repair_2258(self, execute=False):
"""Re-examine package header and store signing key for issue #2258."""
if not execute:
count = Package.objects.filter(signer_key_id__isnull=True).count()
print(f"This will require downloading {count} packages out of storage for examination, "
"which may be time-and-cost expensive. Are you sure you want to do that? Run "
'again with "--yes" to proceed.')
return

packages = Package.objects.filter(signer_key_id__isnull=True).iterator()
for package in packages:
sdherr marked this conversation as resolved.
Show resolved Hide resolved
_, pub_key_id = read_crpackage_from_artifact(package._artifacts.first()) # Only one.
if pub_key_id is not None:
print(f"Fixing stored signature for {package.nevra}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, it's not the whole signature just the key ID

package.signer_key_id = pub_key_id
package.save()

def repair_2460(self):
"""Perform data repair for issue #2460."""

Expand Down
23 changes: 23 additions & 0 deletions pulp_rpm/app/migrations/0048_pub_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.17 on 2023-02-02 21:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("rpm", "0047_modulemd_datefield"),
]

operations = [
migrations.AddField(
model_name="package",
name="signer_key_id",
field=models.CharField(max_length=16, null=True),
),
migrations.AddField(
model_name="rpmrepository",
name="allowed_pub_keys",
field=models.JSONField(default=list),
),
]
8 changes: 7 additions & 1 deletion pulp_rpm/app/models/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class Package(Content):
First byte of the header
rpm_header_end (BigInteger):
Last byte of the header
signer_key_id (Text):
16-digit hex key id of the public key that signed this rpm (if any)
is_modular (Bool):
Flag to identify if the package is modular

Expand Down Expand Up @@ -220,6 +222,8 @@ class Package(Content):
rpm_vendor = models.TextField()
rpm_header_start = models.BigIntegerField(null=True)
rpm_header_end = models.BigIntegerField(null=True)
# According to rfc4880 is always 8 octets, aka a 16-digit hex number
signer_key_id = models.CharField(null=True, max_length=16)

size_archive = models.BigIntegerField(null=True)
size_installed = models.BigIntegerField(null=True)
Expand Down Expand Up @@ -289,12 +293,13 @@ class ReadonlyMeta:
readonly = ["evr"]

@classmethod
def createrepo_to_dict(cls, package):
def createrepo_to_dict(cls, package, signer_key_id=None):
"""
Convert createrepo_c package object to dict for instantiating Package object.

Args:
package(createrepo_c.Package): a RPM/SRPM package to convert
signer_key_id(str): the key_id of the signer, if any

Returns:
dict: all data for RPM/SRPM content creation
Expand Down Expand Up @@ -367,6 +372,7 @@ def createrepo_to_dict(cls, package):
PULP_PACKAGE_ATTRS.TIME_FILE: getattr(package, CR_PACKAGE_ATTRS.TIME_FILE),
PULP_PACKAGE_ATTRS.URL: getattr(package, CR_PACKAGE_ATTRS.URL) or "",
PULP_PACKAGE_ATTRS.VERSION: getattr(package, CR_PACKAGE_ATTRS.VERSION),
"signer_key_id": signer_key_id,
}

def to_createrepo_c(self):
Expand Down
23 changes: 23 additions & 0 deletions pulp_rpm/app/models/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
1 or 0 corresponding to whether repo_gpgcheck should be enabled in the generated
.repo file.
sqlite_metadata (Boolean): Whether to generate sqlite metadata files on publish.
allowed_pub_keys (JSON):
A list of Public Key IDs (16-digit hex strings) if you want to require that all rpms
added to the Repo must be signed by one of them. This is a Pulp-side *data-sanity*
check, whereas gpgcheck triggers a client-side *security* restriction. You can find the
Key ID by importing the public key and then taking the last 16 digits from the hex
string in the output of `gpg --list-keys`.
"""

TYPE = "rpm"
Expand Down Expand Up @@ -234,6 +240,7 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
gpgcheck = models.IntegerField(default=0, choices=GPGCHECK_CHOICES)
repo_gpgcheck = models.IntegerField(default=0, choices=GPGCHECK_CHOICES)
sqlite_metadata = models.BooleanField(default=False)
allowed_pub_keys = models.JSONField(default=list)

def on_new_version(self, version):
"""
Expand Down Expand Up @@ -299,6 +306,7 @@ def finalize_new_version(self, new_version):
"""
Ensure there are no duplicates in a repo version and content is not broken.

If set, remove Packages that are not signed by one of the allowed_pub_keys public keys.
Remove duplicates based on repo_key_fields.
Ensure that modulemd is added with all its RPMs.
Ensure that modulemd is removed with all its RPMs.
Expand All @@ -316,6 +324,7 @@ def finalize_new_version(self, new_version):
except RepositoryVersion.DoesNotExist:
previous_version = None

self._check_signatures(new_version)
remove_duplicates(new_version)
self._resolve_distribution_trees(new_version, previous_version)

Expand Down Expand Up @@ -350,6 +359,20 @@ def finalize_new_version(self, new_version):
"{value_errors}"
).format(repo=new_version.repository.name, value_errors=str(ve))
)

def _check_signatures(self, new_version):
"""If requested, validate that the added rpms are signed appropriately."""
if self.allowed_pub_keys:
rejected_nevras = []
for package in Package.objects.filter(pk__in=new_version.added()).iterator():
if str(package.signer_key_id) not in self.allowed_pub_keys:
Copy link
Member

@ipanova ipanova Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will happen if between repo_version 1 and 2 allowed_pub_keys is changed? repo version 2 will fail to be created because existing content won't be adhering to the allowed list. should this field be immutable then? or maybe just additive i.e only extend the list of keys?
or maybe this verification should be done at sync/upload time of incoming content and not during the whole version finalization
however if we move the verification check at sync/upload time, then we cannot really say for sure what package signatures repo version X contains, it can be a mixture.

Copy link
Member

@ipanova ipanova Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another unfortunate aspect is that we store the allowed_pub-key_list on the repo, and when creating repo version 1 only key A might have been in place, however since things can change repo version 10 can have A,B,C. So we lose the track of what rules were applied when repo version 1 was created because the only data we see is field on the repo

Copy link
Contributor Author

@sdherr sdherr Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing content is not checked, only added packages. This seems like the least surprising way to handle this restriction, but if there are better options I'm all ears. Perhaps the RepositoryVersion should copy and make immutable the list of pubkeys that were allowed when it was created? I don't know what the purpose of that would be though. You can't go back and edit the content of a previous RepoVersion can you? Just create a new one. So the only thing that matters is what the pub key list is now.

Copy link
Contributor Author

@sdherr sdherr Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you're saying about moving the check to upload/sync time. Adding content to a repo and syncing both of course create a new repo version, so this check does currently run at sync/content adding time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@dralley dralley Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe that's true. I think createrepo_c re-implements a lot of information about the RPM file structure in order to avoid that dependency. If I'm wrong let me know, because if rpmlib is installed then there are a lot more options here.

It has RPM as a hard dependency. There are only certain bits of functionality that it doesn't rely on librpm for to increase flexibility, but the basic file format processing it does. If you hunt down cr_package_from_file() and trace it backwards you can see that it's relying on librpm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not as obvious to me as I thought it would be how we could take advantage of that to drop the rpmfile dependency, but if you have a suggestion feel free to mention it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same functionality should be available, though given their docs are offline it's probably hard to tell. I'm trying to prod them to fix that currently

rpm-software-management/rpm-web#34

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdherr Could you try out this implementation? rpm-software-management/createrepo_c#346 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works if python3-rpm is installed, yes, but that's not a given.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line should be

if package.signer_key_id is None or str(package.signer_key_id) not in self.allowed_pub_keys

Otherwise it will try to compare "None" which will still work as expected in practice but isn't the best code

rejected_nevras.append(package.nevra)
if rejected_nevras:
raise Exception(
f"Repo {self.name} has specified that packages must be signed by one of the "
f"following keys: \n{self.allowed_pub_keys} \nThe following packages are not "
f"signed appropriately and the update has been rejected: \n{rejected_nevras}"
)

def _apply_retention_policy(self, new_version):
"""Apply the repository's "retain_package_versions" settings to the new version.
Expand Down
9 changes: 8 additions & 1 deletion pulp_rpm/app/serializers/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ class PackageSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSe
help_text=_("Last byte of the header"),
read_only=True,
)
signer_key_id = serializers.CharField(
sdherr marked this conversation as resolved.
Show resolved Hide resolved
help_text=_("16-digit hex key id of the public key that signed this rpm (if any)"),
allow_blank=True,
required=False,
read_only=True,
)
is_modular = serializers.BooleanField(
help_text=_("Flag to identify if the package is modular"),
required=False,
Expand Down Expand Up @@ -247,7 +253,7 @@ def deferred_validate(self, data):
data = super().deferred_validate(data)
# export META from rpm and prepare dict as saveable format
try:
new_pkg = Package.createrepo_to_dict(read_crpackage_from_artifact(data["artifact"]))
new_pkg = Package.createrepo_to_dict(*read_crpackage_from_artifact(data["artifact"]))
except OSError:
log.info(traceback.format_exc())
raise NotAcceptable(detail="RPM file cannot be parsed for metadata")
Expand Down Expand Up @@ -314,6 +320,7 @@ class Meta:
"rpm_vendor",
"rpm_header_start",
"rpm_header_end",
"signer_key_id",
"is_modular",
"size_archive",
"size_installed",
Expand Down
29 changes: 29 additions & 0 deletions pulp_rpm/app/serializers/repository.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from gettext import gettext as _
import json
import re

from django.conf import settings
from jsonschema import Draft7Validator
Expand Down Expand Up @@ -105,6 +107,17 @@ class RpmRepositorySerializer(RepositorySerializer):
"DEPRECATED: An option specifying whether Pulp should generate SQLite metadata."
),
)
allowed_pub_keys = serializers.JSONField(
default=list,
required=False,
help_text=_(
"A list of Public Key IDs (16-digit hex strings) if you want to require that all rpms "
"added to the Repo must be signed by one of them. This is a Pulp-side *data-sanity* "
"check, whereas gpgcheck triggers a client-side *security* restriction. You can find "
"the Key ID by importing the public key and then taking the last 16 digits from the "
"hex string in the output of `gpg --list-keys`."
),
)

def validate(self, data):
"""Validate data."""
Expand All @@ -116,6 +129,21 @@ def validate(self, data):
):
raise serializers.ValidationError({field: _(ALLOWED_CHECKSUM_ERROR_MSG)})

signed_by = "allowed_pub_keys"
if signed_by in data:
if type(data[signed_by]) == list:
keys = data[signed_by] # framework parses empty list automatically
else:
keys = json.loads(data[signed_by])
uppercase_keys = []
for key in keys:
key = key.upper() # key ids in rpm headers are always uppercase
if not re.match("^[A-F0-9]{16}$", key):
raise serializers.ValidationError(
{field: _("Public Key IDs must be 16-digit hex strings.")}
)
uppercase_keys.append(key)
data[signed_by] = uppercase_keys
validated_data = super().validate(data)
return validated_data

Expand All @@ -129,6 +157,7 @@ class Meta:
"gpgcheck",
"repo_gpgcheck",
"sqlite_metadata",
"allowed_pub_keys",
)
model = RpmRepository

Expand Down
48 changes: 47 additions & 1 deletion pulp_rpm/app/shared_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import createrepo_c as cr
from logging import getLogger
import tempfile
import traceback
import shutil
from hashlib import sha256
from pgpy.pgp import PGPSignature
import rpmfile
Copy link
Contributor

@dralley dralley Feb 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These would be significant dependencies, I need to see if these would actually be acceptable to release as-is. Filing a createrepo_c as you've done is great, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I'm absolutely not tied to these deps in particular, these are just what I happened to get it working with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, totally reasonable. Having looked at rpmfile it looks like basically a partial reimplementation of RPM which I'm not thrilled about, I would definitely prefer to use RPM proper since createrepo_c needs it anyway.

I'll ping them to get the ball rolling a bit faster.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the feedback I got is that pulpcore and the debian plugin use python-gnupg, so it would be best if we can depend on that instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already chatted about this on Matrix, but I'll respond here too for posterity.

There is no equivalent in python-gnupg to the in-memory parsing of the signature packet that pgpy does. Nor does it even give access to the signer key id. However, all python-gnupg is is a wrapper around a subprocess call-out to gpg, so we could just cut out the middleman and do that ourselves if we don't mind the extra overhead of writing the signature to disk and calling gpg in a subprocess. That change would look something like this.

As for dropping the rpmfile dep, that's harder unless createrepo_c adds the python bindings that we're asking for. You can definitely access those headers through the rpm module, if python3-rpm is installed, which is not a given. And since that's a module that wants to be installed as on OS package instead of through pip, requiring it is harder. The best solution is if createrepo makes these headers available since that's already installed, so we'll see what they say.

Copy link
Contributor

@dralley dralley Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked over the code for rpmfile and it looks.. OK. There's nothing terribly objectionable, just that it's overly simple for the number of edge cases that I know exist, but for the sake of a small number of specific tags it should probably be fine.

Copy link
Contributor

@dralley dralley Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have an answer on the package signing questions I have by the end of the week. The people I've been trying to ask are very busy with working on DNF5 but I have a 1hr meeting with them directly soon.

We can also reference the pulp 2 code... I just don't want to express 100% confidence that it was (or remains after X years) correct.

In any case I don't want to hold up this feature any longer, so one way or another we can hopefully merge it soon. If we find issues later we can fix them so long as it's tech-preview.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdherr It looks like pgpy does not support EdDSA. I think that's probably a longer-term issue since RSA is used everywhere... it may eventually become one but I'd rather pgpy be a short-term solution anyway.


from django.conf import settings
from django.core.files.storage import default_storage as storage
from django.utils.dateparse import parse_datetime

log = getLogger(__name__)


def format_nevra(name=None, epoch=0, version=None, release=None, arch=None):
"""Generate Name-Epoch-Version-Release-Arch string."""
Expand Down Expand Up @@ -51,9 +57,10 @@ def read_crpackage_from_artifact(artifact):
cr_pkginfo = cr.package_from_rpm(
temp_file.name, changelog_limit=settings.KEEP_CHANGELOG_LIMIT
)
signer_key_id = parse_signer_id(temp_file.name)

artifact_file.close()
return cr_pkginfo
return cr_pkginfo, signer_key_id


def urlpath_sanitize(*args):
Expand Down Expand Up @@ -159,3 +166,42 @@ def parse_time(value):
int | datetime | None: formatted time value
"""
return int(value) if value.isdigit() else parse_datetime(value)


def parse_signer_id(rpm_path):
"""
Parse the key_id of the signing key from the RPM header, given a locally-available RPM.

Args:
rpm_path(str): Path to the local RPM file.
Returns:
str: 16-digit hex key_id of signing key, or None.
"""
# I have filed an Issue with createrepo_c requesting the ability to access the signature
# through the python bindings. Until that is available we'll have to read the header
# a second time with a utility that actually makes it available.
# https://github.com/rpm-software-management/createrepo_c/issues/346
# TODO: When the above is resolved re-evaluate and potentially drop extra dependencies.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdherr You can drop all of this now

signature = ""
try:
with rpmfile.open(rpm_path) as rpm:

def hdr(header):
return rpm.headers.get(header, "")

# What the `rpm -qi` command does. See "--info" definition in /usr/lib/rpm/rpmpopt-*
signature = hdr("dsaheader") or hdr("rsaheader") or hdr("siggpg") or hdr("sigpgp")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

except:
log.info(f"Could not extract signature from RPM file: {rpm_path}")
log.info(traceback.format_exc())
return None

signer_key_id = "" # If package is unsigned, store empty str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncertain about this. It makes sense from the perspective of being able to tell "have I processed it or not" but it makes it more difficult to determine whether or not any particular RPM is signed. It's a "two values of null" situation.

@ipanova @ggainey Thoughts?

if signature:
try:
signer_key_id = PGPSignature.from_blob(signature).signer
except:
signer_key_id = None # If error (or never examined), store None
log.info(f"Could not parse PGP signature for {hdr('nevra')}")
log.info(traceback.format_exc())
return signer_key_id
13 changes: 13 additions & 0 deletions pulp_rpm/app/tasks/synchronizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
from pulp_rpm.app.shared_utils import (
is_previous_version,
get_sha256,
parse_signer_id,
urlpath_sanitize,
)
from pulp_rpm.app.rpm_version import RpmVersion
Expand Down Expand Up @@ -1435,6 +1436,8 @@ def _post_save(self, batch):

When it has a treeinfo file, save a batch of Addon, Checksum, Image, Variant objects.

Examine Package headers and save signer_key_id.

Args:
batch (list of :class:`~pulpcore.plugin.stages.DeclarativeContent`): The batch of
:class:`~pulpcore.plugin.stages.DeclarativeContent` objects to be saved.
Expand Down Expand Up @@ -1492,6 +1495,7 @@ def _handle_distribution_tree(declarative_content):
update_collection_to_save = []
update_references_to_save = []
update_collection_packages_to_save = []
packages_to_update = []
seen_updaterecords = []

for declarative_content in batch:
Expand Down Expand Up @@ -1534,6 +1538,12 @@ def _handle_distribution_tree(declarative_content):
for update_reference in update_references:
update_reference.update_record = update_record
update_references_to_save.append(update_reference)
elif isinstance(declarative_content.content, Package):
artifact = declarative_content.d_artifacts[0] # Packages have 1 artifact
signer_key_id = parse_signer_id(artifact.artifact.file.path)
sdherr marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think path will work when Pulp's artifact storage is S3 / Azure. We could use .read() and save it into a temporary file, then parse that, but it would not be terribly efficient. But short term it might be the only way... what we need is a way to parse the downloaded files before they get shipped off from temporary storage to the storage backend.

That is too invasive of a change to handle in this PR though.

The question is whether we want to live with that inefficiency or disable the feature if the user is using a cloud storage backend.

if signer_key_id is not None:
declarative_content.content.signer_key_id = signer_key_id
packages_to_update.append(declarative_content.content)

if update_collection_to_save:
UpdateCollection.objects.bulk_create(update_collection_to_save, ignore_conflicts=True)
Expand All @@ -1546,6 +1556,9 @@ def _handle_distribution_tree(declarative_content):
if update_references_to_save:
UpdateReference.objects.bulk_create(update_references_to_save, ignore_conflicts=True)

if packages_to_update:
Package.objects.bulk_update(packages_to_update, ["signer_key_id"])


class RpmQueryExistingContents(Stage):
"""
Expand Down
Loading