Skip to content

Commit 701c4dc

Browse files
committed
Update signing key fields to include a version prefix
Assisted By: Claude Opus 4.6
1 parent 0136f85 commit 701c4dc

File tree

10 files changed

+138
-32
lines changed

10 files changed

+138
-32
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Signing fingerprints now use a versioned prefix format (e.g. ``v4:<hex>``, ``keyid:<hex>``) for
2+
``package_signing_fingerprint`` on repositories and ``signing_keys`` on packages. The repository
3+
serializer validates the format on input.

pulp_rpm/app/migrations/0070_add_rpm_signing_keys.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@
55

66

77
class Migration(migrations.Migration):
8-
98
dependencies = [
10-
('rpm', '0069_DATA_fix_signing_fingerprint'),
9+
("rpm", "0069_DATA_fix_signing_fingerprint"),
1110
]
1211

1312
operations = [
1413
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),
14+
model_name="package",
15+
name="signing_keys",
16+
field=django.contrib.postgres.fields.ArrayField(
17+
base_field=models.TextField(), default=None, null=True, size=None
18+
),
19+
),
20+
migrations.AlterField(
21+
model_name="rpmpackagesigningresult",
22+
name="package_signing_fingerprint",
23+
field=models.TextField(),
24+
),
25+
migrations.AlterField(
26+
model_name="rpmrepository",
27+
name="package_signing_fingerprint",
28+
field=models.TextField(null=True),
1829
),
1930
]

pulp_rpm/app/models/content.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def sign(
3737
Args:
3838
filename: The absolute path to the package to be signed.
3939
env_vars: (optional) Dict of env_vars to be passed to the signing script.
40-
pubkey_fingerprint: The V4 fingerprint that correlates with the private key to use.
40+
pubkey_fingerprint: The raw fingerprint that correlates with the private key to use.
4141
"""
4242
if not pubkey_fingerprint:
4343
raise ValueError("A pubkey_fingerprint must be provided.")
@@ -87,16 +87,16 @@ class RpmPackageSigningResult(BaseModel):
8787
original_package_sha256 (String):
8888
The sha256 digest of the original package artifact.
8989
package_signing_fingerprint (String):
90-
The fingerprint used to sign the package. This value is copied from the repo's
91-
package_signing_fingerprint field.
90+
The prefixed fingerprint used to sign the package (e.g. 'v4:<hex>').
91+
This value is copied from the repo's package_signing_fingerprint field.
9292
9393
Relations:
9494
result_package (ForeignKey):
9595
The resulting package that was signed by @package_signing_fingerprint.
9696
"""
9797

9898
original_package_sha256 = models.TextField(max_length=64)
99-
package_signing_fingerprint = models.TextField(max_length=40)
99+
package_signing_fingerprint = models.TextField()
100100
result_package = models.ForeignKey(Content, on_delete=models.CASCADE)
101101

102102
class Meta:

pulp_rpm/app/models/repository.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
208208
package_signing_service (RpmPackageSigningService):
209209
Signing service to be used on package signing operations related to this repository.
210210
package_signing_fingerprint (String):
211-
The V4 fingerprint (160 bits) to be used by @package_signing_service.
211+
The fingerprint to be used by @package_signing_service.
212+
Format: 'v<N>:<hex-fingerprint>' or 'keyid:<16-hex>'.
212213
repo_config (JSON): repo configuration that will be served by distribution
213214
compression_type(pulp_rpm.app.constants.COMPRESSION_TYPES):
214215
Compression type to use for metadata files.
@@ -238,7 +239,7 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
238239
package_signing_service = models.ForeignKey(
239240
RpmPackageSigningService, on_delete=models.SET_NULL, null=True
240241
)
241-
package_signing_fingerprint = models.TextField(null=True, max_length=40)
242+
package_signing_fingerprint = models.TextField(null=True)
242243
last_sync_details = models.JSONField(default=dict)
243244
retain_package_versions = models.PositiveIntegerField(default=0)
244245

pulp_rpm/app/serializers/repository.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
UlnRemote,
4343
)
4444
from pulp_rpm.app.schema import COPY_CONFIG_SCHEMA
45+
from pulp_rpm.app.shared_utils import SIGNING_FINGERPRINT_RE, normalize_signing_fingerprint
4546
from urllib.parse import urlparse
4647
from textwrap import dedent
4748

@@ -89,10 +90,11 @@ class RpmRepositorySerializer(RepositorySerializer):
8990
)
9091
package_signing_fingerprint = serializers.CharField(
9192
help_text=_(
92-
"The pubkey V4 fingerprint (160 bits) to be passed to the package signing service."
93-
"The signing service will use that on signing operations related to this repository."
93+
"The pubkey fingerprint to be passed to the package signing service. "
94+
"Format: 'v<N>:<hex-fingerprint>' or 'keyid:<16-hex-char>'. "
95+
"Example: 'v4:ABCDEF1234567890ABCDEF1234567890ABCDEF12'."
9496
),
95-
max_length=40,
97+
max_length=68,
9698
required=False,
9799
allow_null=True,
98100
default=None,
@@ -194,6 +196,16 @@ def to_representation(self, instance):
194196
data[field] = None
195197
return data
196198

199+
def validate_package_signing_fingerprint(self, value):
200+
if value is None:
201+
return value
202+
value = normalize_signing_fingerprint(value)
203+
if not SIGNING_FINGERPRINT_RE.match(value):
204+
raise serializers.ValidationError(
205+
_("Invalid format. Expected 'v<N>:<hex-fingerprint>' or 'keyid:<16-hex>'.")
206+
)
207+
return value
208+
197209
def validate(self, data):
198210
"""Validate data."""
199211
if checksum_type := data.get("checksum_type"):

pulp_rpm/app/shared_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import shutil
23
import subprocess
34
import tempfile
@@ -13,6 +14,20 @@
1314
from pulpcore.plugin.exceptions import InvalidSignatureError
1415
from pulp_rpm.app.rpm_version import RpmVersion
1516

17+
# Matches versioned fingerprints (v3/v4/v5/v6) and legacy 16-char key IDs.
18+
SIGNING_FINGERPRINT_RE = re.compile(r"^(v\d:[0-9A-F]{32,64}|keyid:[0-9A-F]{16})$")
19+
20+
21+
def normalize_signing_fingerprint(value):
22+
"""Uppercase the hex portion of a prefixed signing fingerprint."""
23+
prefix, sep, hex_part = value.partition(":")
24+
return f"{prefix}:{hex_part.upper()}" if sep else value
25+
26+
27+
def parse_signing_fingerprint(value):
28+
"""Extract the raw hex value from a prefixed signing fingerprint."""
29+
return value.split(":", 1)[1]
30+
1631

1732
def annotate_with_age(qs):
1833
"""Provide an "age" score for each Package object in the queryset.

pulp_rpm/app/tasks/signing.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pulp_rpm.app.models.content import RpmPackageSigningResult, RpmPackageSigningService
2323
from pulp_rpm.app.models.package import Package
2424
from pulp_rpm.app.models.repository import RpmRepository
25+
from pulp_rpm.app.shared_utils import parse_signing_fingerprint
2526

2627

2728
log = logging.getLogger(__name__)
@@ -55,14 +56,16 @@ def _verify_package_fingerprint(path, signing_fingerprint):
5556
f"{completed_process.stderr}."
5657
)
5758

59+
raw_fingerprint = parse_signing_fingerprint(signing_fingerprint)
60+
5861
# check for `key ID` followed by a string of hex digits
5962
key_ids = re.findall(r"key ID ([0-9A-Fa-f]+)", completed_process.stdout, re.IGNORECASE)
6063
# check for `key fingerprint:` followed by a string of hex digits
6164
fingerprints = re.findall(
6265
r"key fingerprint:\s*([0-9A-Fa-f]+)", completed_process.stdout, re.IGNORECASE
6366
)
6467
for candidate in key_ids + fingerprints:
65-
if signing_fingerprint.lower().endswith(candidate.lower()):
68+
if raw_fingerprint.lower().endswith(candidate.lower()):
6669
return True
6770

6871
return False
@@ -79,7 +82,8 @@ def _update_signing_keys(package_file, keys):
7982

8083
def _sign_file(package_file, signing_service, signing_fingerprint):
8184
"""Sign a package and return the local path of the signed file."""
82-
result = signing_service.sign(package_file.name, pubkey_fingerprint=signing_fingerprint)
85+
raw_fingerprint = parse_signing_fingerprint(signing_fingerprint)
86+
result = signing_service.sign(package_file.name, pubkey_fingerprint=raw_fingerprint)
8387
signed_package_path = Path(result["rpm_package"])
8488
if not signed_package_path.exists():
8589
raise Exception(f"Signing script did not create the signed package: {result}")

pulp_rpm/tests/functional/api/test_package_signing.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from pulpcore.exceptions.validation import InvalidSignatureError
1010

1111
from pulp_rpm.app.shared_utils import RpmTool
12-
from pulp_rpm.tests.functional.constants import RPM_PACKAGE_FILENAME, RPM_UNSIGNED_URL
12+
from pulp_rpm.tests.functional.constants import (
13+
RPM_PACKAGE_FILENAME,
14+
RPM_PACKAGE_FILENAME2,
15+
RPM_UNSIGNED_URL,
16+
RPM_UNSIGNED_URL2,
17+
)
1318
from pulp_rpm.tests.functional.utils import get_package_repo_path
1419

1520

@@ -94,17 +99,18 @@ def test_sign_package_on_upload(
9499
# Upload Package to Repository
95100
# The same file is uploaded, but signed with different keys each time
96101
for fingerprint in fingerprint_set:
102+
prefixed = f"v4:{fingerprint}"
97103
repository = rpm_repository_factory(
98104
package_signing_service=rpm_package_signing_service.pulp_href,
99-
package_signing_fingerprint=fingerprint,
105+
package_signing_fingerprint=prefixed,
100106
)
101107
upload_response = rpm_package_api.create(
102108
file=str(file_to_upload.absolute()),
103109
repository=repository.pulp_href,
104110
)
105111
package_href = monitor_task(upload_response.task).created_resources[2]
106112
package = rpm_package_api.read(package_href)
107-
assert package.signing_keys == [fingerprint]
113+
assert package.signing_keys == [prefixed]
108114

109115
# Verify that the final served package is signed
110116
publication = rpm_publication_factory(repository=repository.pulp_href)
@@ -121,7 +127,7 @@ def test_sign_package_on_upload(
121127
repository = rpm_repository_api.read(repository.pulp_href)
122128
assert (
123129
rpm_package_api.list(
124-
repository_version=repository.latest_version_href, signing_key=fingerprint
130+
repository_version=repository.latest_version_href, signing_key=prefixed
125131
).count
126132
== 1
127133
)
@@ -216,17 +222,18 @@ def test_sign_chunked_package_on_upload(
216222
rpm_tool.import_pubkey_string(gpg_a.pubkey)
217223
rpm_tool.import_pubkey_string(gpg_b.pubkey)
218224

219-
file_to_upload = tmp_path / RPM_PACKAGE_FILENAME
220-
file_to_upload.write_bytes(requests.get(RPM_UNSIGNED_URL).content)
225+
file_to_upload = tmp_path / RPM_PACKAGE_FILENAME2
226+
file_to_upload.write_bytes(requests.get(RPM_UNSIGNED_URL2).content)
221227
with pytest.raises(InvalidSignatureError, match="The package is not signed: .*"):
222228
rpm_tool.verify_signature(file_to_upload)
223229

224230
# Upload Package to Repository
225231
# The same file is uploaded, but signed with different keys each time
226232
for fingerprint in fingerprint_set:
233+
prefixed = f"v4:{fingerprint}"
227234
repository = rpm_repository_factory(
228235
package_signing_service=rpm_package_signing_service.pulp_href,
229-
package_signing_fingerprint=fingerprint,
236+
package_signing_fingerprint=prefixed,
230237
)
231238
file_chunks_data = pulpcore_chunked_file_factory(file_to_upload)
232239
size = file_chunks_data["size"]
@@ -240,7 +247,7 @@ def test_sign_chunked_package_on_upload(
240247
)
241248
package_href = monitor_task(upload_response.task).created_resources[2]
242249
package = rpm_package_api.read(package_href)
243-
assert package.signing_keys == [fingerprint]
250+
assert package.signing_keys == [prefixed]
244251

245252
# Verify that the final served package is signed
246253
publication = rpm_publication_factory(repository=repository.pulp_href)
@@ -271,6 +278,7 @@ def test_signed_repo_modify(
271278
"""Ensure packages added via modify are signed before distribution."""
272279

273280
gpg, fingerprint, _ = signing_gpg_metadata
281+
prefixed = f"v4:{fingerprint}"
274282

275283
rpm_tool = RpmTool(tmp_path)
276284
rpm_tool.import_pubkey_string(gpg.export_keys(fingerprint))
@@ -283,7 +291,7 @@ def test_signed_repo_modify(
283291

284292
repository = rpm_repository_factory(
285293
package_signing_service=rpm_package_signing_service.pulp_href,
286-
package_signing_fingerprint=fingerprint,
294+
package_signing_fingerprint=prefixed,
287295
)
288296

289297
created_package = rpm_package_factory(url=RPM_UNSIGNED_URL)
@@ -299,7 +307,7 @@ def test_signed_repo_modify(
299307
repository_version=repository.latest_version_href
300308
).results[0]
301309
assert signed_package.pulp_href != created_package.pulp_href
302-
assert signed_package.signing_keys == [fingerprint]
310+
assert signed_package.signing_keys == [prefixed]
303311
assert sorted(task_result.created_resources) == sorted(
304312
[repository.latest_version_href, signed_package.pulp_href, signed_package.artifact]
305313
)
@@ -341,14 +349,15 @@ def test_already_signed_package(
341349
"""Don't sign a package if it's already signed with our key."""
342350

343351
_, fingerprint, _ = signing_gpg_metadata
352+
prefixed = f"v4:{fingerprint}"
344353

345354
repo_one = rpm_repository_factory(
346355
package_signing_service=rpm_package_signing_service.pulp_href,
347-
package_signing_fingerprint=fingerprint,
356+
package_signing_fingerprint=prefixed,
348357
)
349358
repo_two = rpm_repository_factory(
350359
package_signing_service=rpm_package_signing_service.pulp_href,
351-
package_signing_fingerprint=fingerprint,
360+
package_signing_fingerprint=prefixed,
352361
)
353362

354363
created_package = rpm_package_factory(url=RPM_UNSIGNED_URL)
@@ -365,7 +374,7 @@ def test_already_signed_package(
365374
repository_version=repo_one.latest_version_href
366375
).results
367376
signed_package_href = repo_one_packages[0].pulp_href
368-
assert repo_one_packages[0].signing_keys == [fingerprint]
377+
assert repo_one_packages[0].signing_keys == [prefixed]
369378
assert len(repo_one_packages) == 1
370379
assert sorted(task_result.created_resources) == sorted(
371380
[signed_package_href, repo_one_packages[0].artifact, repo_one.latest_version_href]
@@ -401,7 +410,7 @@ def test_signed_repo_rejects_on_demand_content(
401410
_, fingerprint, _ = signing_gpg_metadata
402411
destination_repo = rpm_repository_factory(
403412
package_signing_service=rpm_package_signing_service.pulp_href,
404-
package_signing_fingerprint=fingerprint,
413+
package_signing_fingerprint=f"v4:{fingerprint}",
405414
)
406415

407416
packages = rpm_package_api.list(repository_version=source_repo.latest_version_href).results

pulp_rpm/tests/functional/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@
365365
RPM_UNSIGNED_URL = urljoin(RPM_UNSIGNED_FIXTURE_URL, RPM_PACKAGE_FILENAME)
366366
"""The path to a single unsigned RPM package."""
367367

368+
RPM_UNSIGNED_URL2 = urljoin(RPM_UNSIGNED_FIXTURE_URL, RPM_PACKAGE_FILENAME2)
369+
"""The path to a second single unsigned RPM package."""
370+
368371
RPM_UPDATED_UPDATEINFO_FIXTURE_URL = urljoin(PULP_FIXTURES_BASE_URL, "rpm-updated-updateinfo/")
369372
"""The URL to a repository containing UpdateRecords (Advisory) with the same IDs
370373
as the ones in the standard repositories, but with different metadata.

0 commit comments

Comments
 (0)