Skip to content

Commit 4b024e5

Browse files
committed
Update signing key fields to include a version prefix
1 parent ba800da commit 4b024e5

File tree

8 files changed

+127
-31
lines changed

8 files changed

+127
-31
lines changed

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: 5 additions & 5 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.")
@@ -57,7 +57,7 @@ async def asign(
5757
Args:
5858
filename: The absolute path to the package to be signed.
5959
env_vars: (optional) Dict of env_vars to be passed to the signing script.
60-
pubkey_fingerprint: The V4 fingerprint that correlates with the private key to use.
60+
pubkey_fingerprint: The raw fingerprint that correlates with the private key to use.
6161
"""
6262
if not pubkey_fingerprint:
6363
raise ValueError("A pubkey_fingerprint must be provided.")
@@ -107,16 +107,16 @@ class RpmPackageSigningResult(BaseModel):
107107
original_package_sha256 (String):
108108
The sha256 digest of the original package artifact.
109109
package_signing_fingerprint (String):
110-
The fingerprint used to sign the package. This value is copied from the repo's
111-
package_signing_fingerprint field.
110+
The prefixed fingerprint used to sign the package (e.g. 'v4:<hex>').
111+
This value is copied from the repo's package_signing_fingerprint field.
112112
113113
Relations:
114114
result_package (ForeignKey):
115115
The resulting package that was signed by @package_signing_fingerprint.
116116
"""
117117

118118
original_package_sha256 = models.TextField(max_length=64)
119-
package_signing_fingerprint = models.TextField(max_length=40)
119+
package_signing_fingerprint = models.TextField()
120120
result_package = models.ForeignKey(Content, on_delete=models.CASCADE)
121121

122122
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: 8 additions & 3 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}")
@@ -88,7 +92,8 @@ def _sign_file(package_file, signing_service, signing_fingerprint):
8892

8993
async def _asign_file(package_file, signing_service, signing_fingerprint):
9094
"""Sign a package asynchronously and return the local path of the signed file."""
91-
result = await signing_service.asign(package_file.name, pubkey_fingerprint=signing_fingerprint)
95+
raw_fingerprint = parse_signing_fingerprint(signing_fingerprint)
96+
result = await signing_service.asign(package_file.name, pubkey_fingerprint=raw_fingerprint)
9297
signed_package_path = Path(result["rpm_package"])
9398
if not signed_package_path.exists():
9499
raise Exception(f"Signing script did not create the signed package: {result}")

pulp_rpm/tests/functional/api/test_package_signing.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,18 @@ def test_sign_package_on_upload(
9494
# Upload Package to Repository
9595
# The same file is uploaded, but signed with different keys each time
9696
for fingerprint in fingerprint_set:
97+
prefixed = f"v4:{fingerprint}"
9798
repository = rpm_repository_factory(
9899
package_signing_service=rpm_package_signing_service.pulp_href,
99-
package_signing_fingerprint=fingerprint,
100+
package_signing_fingerprint=prefixed,
100101
)
101102
upload_response = rpm_package_api.create(
102103
file=str(file_to_upload.absolute()),
103104
repository=repository.pulp_href,
104105
)
105106
package_href = monitor_task(upload_response.task).created_resources[2]
106107
package = rpm_package_api.read(package_href)
107-
assert package.signing_keys == [fingerprint]
108+
assert package.signing_keys == [prefixed]
108109

109110
# Verify that the final served package is signed
110111
publication = rpm_publication_factory(repository=repository.pulp_href)
@@ -121,7 +122,7 @@ def test_sign_package_on_upload(
121122
repository = rpm_repository_api.read(repository.pulp_href)
122123
assert (
123124
rpm_package_api.list(
124-
repository_version=repository.latest_version_href, signing_key=fingerprint
125+
repository_version=repository.latest_version_href, signing_key=prefixed
125126
).count
126127
== 1
127128
)
@@ -224,9 +225,10 @@ def test_sign_chunked_package_on_upload(
224225
# Upload Package to Repository
225226
# The same file is uploaded, but signed with different keys each time
226227
for fingerprint in fingerprint_set:
228+
prefixed = f"v4:{fingerprint}"
227229
repository = rpm_repository_factory(
228230
package_signing_service=rpm_package_signing_service.pulp_href,
229-
package_signing_fingerprint=fingerprint,
231+
package_signing_fingerprint=prefixed,
230232
)
231233
file_chunks_data = pulpcore_chunked_file_factory(file_to_upload)
232234
size = file_chunks_data["size"]
@@ -240,7 +242,7 @@ def test_sign_chunked_package_on_upload(
240242
)
241243
package_href = monitor_task(upload_response.task).created_resources[2]
242244
package = rpm_package_api.read(package_href)
243-
assert package.signing_keys == [fingerprint]
245+
assert package.signing_keys == [prefixed]
244246

245247
# Verify that the final served package is signed
246248
publication = rpm_publication_factory(repository=repository.pulp_href)
@@ -271,6 +273,7 @@ def test_signed_repo_modify(
271273
"""Ensure packages added via modify are signed before distribution."""
272274

273275
gpg, fingerprint, _ = signing_gpg_metadata
276+
prefixed = f"v4:{fingerprint}"
274277

275278
rpm_tool = RpmTool(tmp_path)
276279
rpm_tool.import_pubkey_string(gpg.export_keys(fingerprint))
@@ -283,7 +286,7 @@ def test_signed_repo_modify(
283286

284287
repository = rpm_repository_factory(
285288
package_signing_service=rpm_package_signing_service.pulp_href,
286-
package_signing_fingerprint=fingerprint,
289+
package_signing_fingerprint=prefixed,
287290
)
288291

289292
created_package = rpm_package_factory(url=RPM_UNSIGNED_URL)
@@ -299,7 +302,7 @@ def test_signed_repo_modify(
299302
repository_version=repository.latest_version_href
300303
).results[0]
301304
assert signed_package.pulp_href != created_package.pulp_href
302-
assert signed_package.signing_keys == [fingerprint]
305+
assert signed_package.signing_keys == [prefixed]
303306
assert sorted(task_result.created_resources) == sorted(
304307
[repository.latest_version_href, signed_package.pulp_href, signed_package.artifact]
305308
)
@@ -341,14 +344,15 @@ def test_already_signed_package(
341344
"""Don't sign a package if it's already signed with our key."""
342345

343346
_, fingerprint, _ = signing_gpg_metadata
347+
prefixed = f"v4:{fingerprint}"
344348

345349
repo_one = rpm_repository_factory(
346350
package_signing_service=rpm_package_signing_service.pulp_href,
347-
package_signing_fingerprint=fingerprint,
351+
package_signing_fingerprint=prefixed,
348352
)
349353
repo_two = rpm_repository_factory(
350354
package_signing_service=rpm_package_signing_service.pulp_href,
351-
package_signing_fingerprint=fingerprint,
355+
package_signing_fingerprint=prefixed,
352356
)
353357

354358
created_package = rpm_package_factory(url=RPM_UNSIGNED_URL)
@@ -365,7 +369,7 @@ def test_already_signed_package(
365369
repository_version=repo_one.latest_version_href
366370
).results
367371
signed_package_href = repo_one_packages[0].pulp_href
368-
assert repo_one_packages[0].signing_keys == [fingerprint]
372+
assert repo_one_packages[0].signing_keys == [prefixed]
369373
assert len(repo_one_packages) == 1
370374
assert sorted(task_result.created_resources) == sorted(
371375
[signed_package_href, repo_one_packages[0].artifact, repo_one.latest_version_href]
@@ -401,7 +405,7 @@ def test_signed_repo_rejects_on_demand_content(
401405
_, fingerprint, _ = signing_gpg_metadata
402406
destination_repo = rpm_repository_factory(
403407
package_signing_service=rpm_package_signing_service.pulp_href,
404-
package_signing_fingerprint=fingerprint,
408+
package_signing_fingerprint=f"v4:{fingerprint}",
405409
)
406410

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

pulp_rpm/tests/unit/test_signing.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
11
from types import SimpleNamespace
22

3+
import pytest
34
import subprocess
45

6+
from pulp_rpm.app.shared_utils import (
7+
SIGNING_FINGERPRINT_RE,
8+
normalize_signing_fingerprint,
9+
parse_signing_fingerprint,
10+
)
511
from pulp_rpm.app.tasks.signing import _verify_package_fingerprint
612

713

14+
@pytest.mark.parametrize(
15+
"value",
16+
[
17+
"v3:0123456789ABCDEF0123456789ABCDEF",
18+
"v4:0123456789ABCDEF0123456789ABCDEF01234567",
19+
"v5:" + "A" * 64,
20+
"v6:" + "B" * 64,
21+
"keyid:0123456789ABCDEF",
22+
],
23+
)
24+
def test_signing_fingerprint_re_valid(value):
25+
assert SIGNING_FINGERPRINT_RE.match(value)
26+
27+
28+
@pytest.mark.parametrize(
29+
"value",
30+
[
31+
"0123456789ABCDEF0123456789ABCDEF01234567", # no prefix
32+
"v4:ghijkl", # non-hex
33+
"v4:0123", # too short
34+
"keyid:0123", # too short
35+
"keyid:0123456789ABCDEF0", # too long
36+
"v4:abcdef1234567890abcdef1234567890abcdef12", # lowercase
37+
],
38+
)
39+
def test_signing_fingerprint_re_invalid(value):
40+
assert not SIGNING_FINGERPRINT_RE.match(value)
41+
42+
43+
def test_normalize_signing_fingerprint():
44+
assert (
45+
normalize_signing_fingerprint("v4:abcdef1234567890abcdef1234567890abcdef12")
46+
== "v4:ABCDEF1234567890ABCDEF1234567890ABCDEF12"
47+
)
48+
assert normalize_signing_fingerprint("keyid:abcdef1234567890") == "keyid:ABCDEF1234567890"
49+
50+
51+
def test_parse_signing_fingerprint():
52+
assert parse_signing_fingerprint("v4:ABCDEF12") == "ABCDEF12"
53+
assert parse_signing_fingerprint("keyid:0123456789ABCDEF") == "0123456789ABCDEF"
54+
55+
856
def test_verify_package_fingerprint_accepts_rpm_fingerprint(monkeypatch, tmp_path):
957
rpm_output = """\
1058
Header OpenPGP V4 signature, key fingerprint: c6e7f081cf80e13146676e88829b606631645531: OK
@@ -18,7 +66,7 @@ def fake_run(*args, **kwargs):
1866

1967
monkeypatch.setattr(subprocess, "run", fake_run)
2068
package = SimpleNamespace(name=str(tmp_path / "example.rpm"))
21-
assert _verify_package_fingerprint(package, "c6e7f081cf80e13146676e88829b606631645531")
69+
assert _verify_package_fingerprint(package, "v4:C6E7F081CF80E13146676E88829B606631645531")
2270

2371

2472
def test_verify_package_fingerprint_accepts_long_key_id(monkeypatch, tmp_path):
@@ -35,4 +83,4 @@ def fake_run(*args, **kwargs):
3583

3684
monkeypatch.setattr(subprocess, "run", fake_run)
3785
package = SimpleNamespace(name=str(tmp_path / "example.rpm"))
38-
assert _verify_package_fingerprint(package, "999f7cbf38ab71f4")
86+
assert _verify_package_fingerprint(package, "keyid:999F7CBF38AB71F4")

0 commit comments

Comments
 (0)