Skip to content

Commit ba800da

Browse files
pulpbotdralley
authored andcommitted
Add field to track package signing keys as labels
Assisted By: Claude Sonnet 4.6
1 parent 21ace23 commit ba800da

File tree

9 files changed

+120
-24
lines changed

9 files changed

+120
-24
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a new field `signing_keys` on packages that tracks the fingerprints of keys Pulp has used to sign the package.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.27 on 2026-02-25 14:00
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('rpm', '0069_DATA_fix_signing_fingerprint'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='package',
16+
name='signing_keys',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=None, null=True, size=None),
18+
),
19+
]

pulp_rpm/app/models/package.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from django.conf import settings
66
from django.db import models
7+
from django.contrib.postgres.fields import ArrayField
78

89
from pulpcore.plugin.models import Content
910
from pulpcore.plugin.util import get_domain_pk
@@ -126,6 +127,9 @@ class Package(Content):
126127
time_file (BigInteger):
127128
The mtime of the package file in seconds since the epoch; this is the 'file' time
128129
attribute in the primary XML.
130+
131+
signing_keys (ArrayField):
132+
List of signing key fingerprints used to sign the package.
129133
"""
130134

131135
PROTECTED_FROM_RECLAIM = False
@@ -205,6 +209,7 @@ class Package(Content):
205209

206210
# not part of createrepo_c metadata
207211
is_modular = models.BooleanField(default=False)
212+
signing_keys = ArrayField(models.TextField(), default=None, null=True)
208213

209214
# createrepo_c treats 'nosrc' arch (opensuse specific use) as 'src' so it can seem that two
210215
# packages are the same when they are not. By adding 'location_href' here we can recognize this.

pulp_rpm/app/serializers/package.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ class PackageSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSe
248248
read_only=True,
249249
)
250250

251+
signing_keys = serializers.ListField(
252+
child=serializers.CharField(),
253+
help_text=_("List of signing key fingerprints used to sign this package. "),
254+
allow_null=True,
255+
required=False,
256+
read_only=True,
257+
)
258+
251259
def __init__(self, *args, **kwargs):
252260
"""Initializer for RpmPackageSerializer."""
253261

@@ -286,6 +294,12 @@ def deferred_validate(self, data):
286294
data["relative_path"] = filename
287295
new_pkg["location_href"] = filename
288296
data.update(new_pkg)
297+
298+
if signing_key := self.context.get("signing_key"):
299+
data["signing_keys"] = [signing_key]
300+
else:
301+
data["signing_keys"] = None
302+
289303
return data
290304

291305
def retrieve(self, validated_data):
@@ -335,6 +349,7 @@ class Meta:
335349
"size_package",
336350
"time_build",
337351
"time_file",
352+
"signing_keys",
338353
)
339354
)
340355
model = Package

pulp_rpm/app/tasks/signing.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ def _save_upload(uploadobj, final_package):
4141
final_package.flush()
4242

4343

44-
def _verify_package_fingerprint(package_file, signing_fingerprint):
45-
"""Verify if the packge_file is signed with signing_fingerprint or not."""
44+
def _verify_package_fingerprint(path, signing_fingerprint):
45+
"""Verify if the package at path is signed with signing_fingerprint or not."""
4646
completed_process = subprocess.run(
47-
("rpm", "-Kv", package_file.name),
47+
("rpm", "-Kv", path),
4848
stdout=subprocess.PIPE,
4949
stderr=subprocess.PIPE,
5050
text=True,
@@ -68,26 +68,40 @@ def _verify_package_fingerprint(package_file, signing_fingerprint):
6868
return False
6969

7070

71-
def _create_signed_artifact(signed_package_path, result):
72-
if not signed_package_path.exists():
73-
raise Exception(f"Signing script did not create the signed package: {result}")
74-
artifact = Artifact.init_and_validate(str(signed_package_path))
75-
artifact.save()
76-
resource = CreatedResource(content_object=artifact)
77-
resource.save()
78-
return artifact
71+
def _update_signing_keys(package_file, keys):
72+
"""Return a filtered list of signing keys verified against the package file.
73+
74+
Verifies each key in keys against the package file and removes any that are not
75+
present on the package.
76+
"""
77+
return [key for key in (keys or []) if _verify_package_fingerprint(package_file, key)]
7978

8079

8180
def _sign_file(package_file, signing_service, signing_fingerprint):
81+
"""Sign a package and return the local path of the signed file."""
8282
result = signing_service.sign(package_file.name, pubkey_fingerprint=signing_fingerprint)
8383
signed_package_path = Path(result["rpm_package"])
84-
return _create_signed_artifact(signed_package_path, result)
84+
if not signed_package_path.exists():
85+
raise Exception(f"Signing script did not create the signed package: {result}")
86+
return signed_package_path
8587

8688

8789
async def _asign_file(package_file, signing_service, signing_fingerprint):
90+
"""Sign a package asynchronously and return the local path of the signed file."""
8891
result = await signing_service.asign(package_file.name, pubkey_fingerprint=signing_fingerprint)
8992
signed_package_path = Path(result["rpm_package"])
90-
return await asyncio.to_thread(_create_signed_artifact, signed_package_path, result)
93+
if not signed_package_path.exists():
94+
raise Exception(f"Signing script did not create the signed package: {result}")
95+
return signed_package_path
96+
97+
98+
def _save_artifact(artifact_path):
99+
"""Save an artifact."""
100+
artifact = Artifact.init_and_validate(str(artifact_path))
101+
artifact.save()
102+
resource = CreatedResource(content_object=artifact)
103+
resource.save()
104+
return artifact
91105

92106

93107
def _sign_package(package, signing_service, signing_fingerprint):
@@ -108,7 +122,7 @@ def _sign_package(package, signing_service, signing_fingerprint):
108122
_save_file(artifact_file, final_package)
109123

110124
# check if the package is already signed with our fingerprint
111-
if _verify_package_fingerprint(final_package, signing_fingerprint):
125+
if _verify_package_fingerprint(final_package.name, signing_fingerprint):
112126
return None
113127

114128
# check if the package has been signed in the past with our fingerprint and replace
@@ -121,12 +135,19 @@ def _sign_package(package, signing_service, signing_fingerprint):
121135

122136
# create a new signed version of the package
123137
log.info(f"Signing package {package.filename}.")
124-
artifact = _sign_file(final_package, signing_service, signing_fingerprint)
138+
signed_package_path = _sign_file(final_package, signing_service, signing_fingerprint)
139+
# Compute signing keys while the signed file is still on the local filesystem.
140+
signing_keys = _update_signing_keys(
141+
str(signed_package_path),
142+
(package.signing_keys or []) + [signing_fingerprint],
143+
)
144+
artifact = _save_artifact(signed_package_path)
125145
signed_package = package
126146
signed_package.pk = None
127147
signed_package.pulp_id = None
128148
signed_package.pkgId = artifact.sha256
129149
signed_package.checksum_type = CHECKSUM_TYPES.SHA256
150+
signed_package.signing_keys = signing_keys
130151
signed_package.save()
131152
ContentArtifact.objects.create(
132153
artifact=artifact,
@@ -167,7 +188,10 @@ def sign_and_create(
167188
uploaded_package = Upload.objects.get(pk=temporary_file_pk)
168189
_save_upload(uploaded_package, final_package)
169190

170-
artifact = _sign_file(final_package, package_signing_service, signing_fingerprint)
191+
signed_package_path = _sign_file(
192+
final_package, package_signing_service, signing_fingerprint
193+
)
194+
artifact = _save_artifact(signed_package_path)
171195
uploaded_package.delete()
172196

173197
# Create Package content
@@ -179,6 +203,12 @@ def sign_and_create(
179203
# request data like we do for a file. Instead, we'll delete it here.
180204
if "upload" in data:
181205
del data["upload"]
206+
207+
# set the signing key in the context so that it gets added to the created package's
208+
# signing_keys field. if this package is being created then it won't have been previously
209+
# signed by Pulp.
210+
context["signing_key"] = signing_fingerprint
211+
182212
general_create(app_label, serializer_name, data=data, context=context, *args, **kwargs)
183213

184214

pulp_rpm/app/tasks/synchronizing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,8 @@ def score_grouping(items):
14091409
pkg, string_cache=string_cache, tuple_cache=tuple_cache
14101410
)
14111411
)
1412+
# TODO: set signing_keys when we support package signing during sync
1413+
package.signing_keys = None
14121414
base_url = pkg.location_base or self.remote_url
14131415
url = urlpath_sanitize(base_url, package.location_href)
14141416
last_seen_package_name = pkg.name

pulp_rpm/app/viewsets/package.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class PackageFilter(ContentFilter):
2929

3030
sha256 = CharFilter(field_name="_artifacts__sha256")
3131
filename = CharFilter(field_name="content_artifact__relative_path")
32+
signing_key = CharFilter(method="filter_signing_key")
33+
34+
def filter_signing_key(self, queryset, name, value):
35+
"""Filter packages that have been signed with a given key fingerprint."""
36+
return queryset.filter(signing_keys__contains=[value])
3237

3338
class Meta:
3439
model = Package

pulp_rpm/tests/functional/api/test_package_signing.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def test_sign_package_on_upload(
6666
signing_gpg_extra,
6767
rpm_package_signing_service,
6868
rpm_package_api,
69+
rpm_repository_api,
6970
rpm_repository_factory,
7071
rpm_publication_factory,
7172
rpm_package_factory,
@@ -102,17 +103,29 @@ def test_sign_package_on_upload(
102103
repository=repository.pulp_href,
103104
)
104105
package_href = monitor_task(upload_response.task).created_resources[2]
105-
pkg_location_href = rpm_package_api.read(package_href).location_href
106+
package = rpm_package_api.read(package_href)
107+
assert package.signing_keys == [fingerprint]
106108

107109
# Verify that the final served package is signed
108110
publication = rpm_publication_factory(repository=repository.pulp_href)
109111
distribution = rpm_distribution_factory(publication=publication.pulp_href)
110112
downloaded_package = tmp_path / "package.rpm"
111113
downloaded_package.write_bytes(
112-
download_content_unit(distribution.base_path, get_package_repo_path(pkg_location_href))
114+
download_content_unit(
115+
distribution.base_path, get_package_repo_path(package.location_href)
116+
)
113117
)
114118
assert rpm_tool.verify_signature(downloaded_package)
115119

120+
# Verify signing_key filter
121+
repository = rpm_repository_api.read(repository.pulp_href)
122+
assert (
123+
rpm_package_api.list(
124+
repository_version=repository.latest_version_href, signing_key=fingerprint
125+
).count
126+
== 1
127+
)
128+
116129

117130
@pytest.fixture
118131
def pulpcore_chunked_file_factory(tmp_path):
@@ -226,22 +239,25 @@ def test_sign_chunked_package_on_upload(
226239
repository=repository.pulp_href,
227240
)
228241
package_href = monitor_task(upload_response.task).created_resources[2]
229-
pkg_location_href = rpm_package_api.read(package_href).location_href
242+
package = rpm_package_api.read(package_href)
243+
assert package.signing_keys == [fingerprint]
230244

231245
# Verify that the final served package is signed
232246
publication = rpm_publication_factory(repository=repository.pulp_href)
233247
distribution = rpm_distribution_factory(publication=publication.pulp_href)
234248
downloaded_package = tmp_path / "package.rpm"
235249
downloaded_package.write_bytes(
236-
download_content_unit(distribution.base_path, get_package_repo_path(pkg_location_href))
250+
download_content_unit(
251+
distribution.base_path, get_package_repo_path(package.location_href)
252+
)
237253
)
238254
assert rpm_tool.verify_signature(downloaded_package)
239255

240256

241257
def test_signed_repo_modify(
242258
tmp_path,
259+
delete_orphans_pre,
243260
monitor_task,
244-
pulpcore_bindings,
245261
download_content_unit,
246262
signing_gpg_metadata,
247263
rpm_package_signing_service,
@@ -253,7 +269,6 @@ def test_signed_repo_modify(
253269
rpm_distribution_factory,
254270
):
255271
"""Ensure packages added via modify are signed before distribution."""
256-
monitor_task(pulpcore_bindings.OrphansCleanupApi.cleanup({"orphan_protection_time": 0}).task)
257272

258273
gpg, fingerprint, _ = signing_gpg_metadata
259274

@@ -272,6 +287,7 @@ def test_signed_repo_modify(
272287
)
273288

274289
created_package = rpm_package_factory(url=RPM_UNSIGNED_URL)
290+
assert created_package.signing_keys is None
275291
package_href = created_package.pulp_href
276292
modify_response = rpm_repository_api.modify(
277293
repository.pulp_href, {"add_content_units": [package_href]}
@@ -282,6 +298,8 @@ def test_signed_repo_modify(
282298
signed_package = rpm_package_api.list(
283299
repository_version=repository.latest_version_href
284300
).results[0]
301+
assert signed_package.pulp_href != created_package.pulp_href
302+
assert signed_package.signing_keys == [fingerprint]
285303
assert sorted(task_result.created_resources) == sorted(
286304
[repository.latest_version_href, signed_package.pulp_href, signed_package.artifact]
287305
)
@@ -311,8 +329,8 @@ def test_signed_repo_modify(
311329

312330

313331
def test_already_signed_package(
332+
delete_orphans_pre,
314333
monitor_task,
315-
pulpcore_bindings,
316334
signing_gpg_metadata,
317335
rpm_package_signing_service,
318336
rpm_repository_factory,
@@ -321,7 +339,6 @@ def test_already_signed_package(
321339
rpm_package_api,
322340
):
323341
"""Don't sign a package if it's already signed with our key."""
324-
monitor_task(pulpcore_bindings.OrphansCleanupApi.cleanup({"orphan_protection_time": 0}).task)
325342

326343
_, fingerprint, _ = signing_gpg_metadata
327344

@@ -348,6 +365,7 @@ def test_already_signed_package(
348365
repository_version=repo_one.latest_version_href
349366
).results
350367
signed_package_href = repo_one_packages[0].pulp_href
368+
assert repo_one_packages[0].signing_keys == [fingerprint]
351369
assert len(repo_one_packages) == 1
352370
assert sorted(task_result.created_resources) == sorted(
353371
[signed_package_href, repo_one_packages[0].artifact, repo_one.latest_version_href]

pulp_rpm/tests/functional/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
"time_file": 1627056000,
308308
"url": "http://bobloblaw.com",
309309
"version": "2.3.4",
310+
"signing_keys": None,
310311
}
311312

312313

0 commit comments

Comments
 (0)