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

Add support for Rpm Package signing #3425

Merged
merged 1 commit into from
Jun 13, 2024
Merged
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/2986.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added (tech preview) support for signing RPM packages when uploading to a Repository.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include functest_requirements.txt
include LICENSE
include pulp_rpm/app/schema/*
include pulp_rpm/tests/functional/sign-metadata.sh
include pulp_rpm/tests/sample-rpm-0-0.x86_64.rpm
include pyproject.toml
include requirements.txt
include test_requirements.txt
Expand Down
47 changes: 47 additions & 0 deletions pulp_rpm/app/migrations/0062_rpmpackagesigningservice_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.10 on 2024-04-25 16:39

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("rpm", "0061_fix_modulemd_defaults_digest"),
]

operations = [
migrations.CreateModel(
name="RpmPackageSigningService",
fields=[
(
"signingservice_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="core.signingservice",
),
),
],
options={
"abstract": False,
},
bases=("core.signingservice",),
),
migrations.AddField(
model_name="rpmrepository",
name="package_signing_fingerprint",
field=models.TextField(max_length=40, null=True),
),
migrations.AddField(
model_name="rpmrepository",
name="package_signing_service",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="rpm.rpmpackagesigningservice",
),
),
]
1 change: 1 addition & 0 deletions pulp_rpm/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
UpdateReference,
)
from .comps import PackageCategory, PackageEnvironment, PackageGroup, PackageLangpacks # noqa
from .content import RpmPackageSigningService # noqa
from .custom_metadata import RepoMetadataFile # noqa
from .distribution import Addon, Checksum, DistributionTree, Image, Variant # noqa
from .modulemd import Modulemd, ModulemdDefaults, ModulemdObsolete # noqa
Expand Down
72 changes: 72 additions & 0 deletions pulp_rpm/app/models/content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import tempfile
Copy link
Member

Choose a reason for hiding this comment

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

hmm, maybe having this in modes/repository.py or models/signing_service.py will be better?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've followed pulpcore, which defines the base SigningService under content. But I agree, models/signing_service.py feels better (and its what pulp_deb does also).

from pathlib import Path

from django.conf import settings
from pulpcore.plugin.exceptions import PulpException
from pulpcore.plugin.models import SigningService
from typing import Optional

from pulp_rpm.app.shared_utils import RpmTool


class RpmPackageSigningService(SigningService):
"""
A model used for signing RPM packages.
The pubkey_fingerprint should be passed explicitly in the sign method.
"""

def _env_variables(self, env_vars=None):
# Prevent the signing service pubkey to be used for signing a package.
# The pubkey should be provided explicitly.
_env_vars = {"PULP_SIGNING_KEY_FINGERPRINT": None}
if env_vars:
_env_vars.update(env_vars)
return super()._env_variables(_env_vars)

def sign(
self,
filename: str,
env_vars: Optional[dict] = None,
pubkey_fingerprint: Optional[str] = None,
):
"""
Sign a package @filename using @pubkey_figerprint.
Args:
filename: The absolute path to the package to be signed.
env_vars: (optional) Dict of env_vars to be passed to the signing script.
pubkey_fingerprint: The V4 fingerprint that correlates with the private key to use.
"""
if not pubkey_fingerprint:
raise ValueError("A pubkey_fingerprint must be provided.")
_env_vars = env_vars or {}
_env_vars["PULP_SIGNING_KEY_FINGERPRINT"] = pubkey_fingerprint
return super().sign(filename, _env_vars)

def validate(self):
"""
Validate a signing service for a Rpm Package signature.
Specifically, it validates that self.signing_script can sign an rpm package with
the sample key self.pubkey and that the self.sign() method returns:
```json
{"rpm_package": "<path/to/package.rpm>"}
```
See [RpmTool.verify_signature][] for the signature verificaton method used.
"""
with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_directory_name:
# get and sign sample rpm
temp_file = RpmTool.get_empty_rpm(temp_directory_name)
return_value = self.sign(temp_file, pubkey_fingerprint=self.pubkey_fingerprint)
try:
return_value["rpm_package"]
except KeyError:
raise PulpException(f"Malformed output from signing script: {return_value}")

# verify with rpm tool
rpm_tool = RpmTool(root=Path(temp_directory_name))
rpm_tool.import_pubkey_string(self.public_key)
rpm_tool.verify_signature(temp_file)
33 changes: 20 additions & 13 deletions pulp_rpm/app/models/repository.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import re
import os
import re
import textwrap

from gettext import gettext as _
from logging import getLogger

from aiohttp.web_response import Response
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from pulpcore.plugin.download import DownloaderFactory
from pulpcore.plugin.models import (
AutoAddObjPermsMixin,
Artifact,
AsciiArmoredDetachedSigningService,
AutoAddObjPermsMixin,
Content,
ContentArtifact,
Distribution,
Publication,
Remote,
RemoteArtifact,
Repository,
RepositoryContent,
RepositoryVersion,
Publication,
PublishedMetadata,
Distribution,
)
from pulpcore.plugin.repo_version_utils import (
remove_duplicates,
Expand All @@ -32,22 +31,22 @@
)

from pulp_rpm.app.constants import CHECKSUM_CHOICES, COMPRESSION_CHOICES
from pulp_rpm.app.downloaders import RpmDownloader, RpmFileDownloader, UlnDownloader
from pulp_rpm.app.exceptions import DistributionTreeConflict
from pulp_rpm.app.models import (
DistributionTree,
Modulemd,
ModulemdDefaults,
ModulemdObsolete,
Package,
PackageCategory,
PackageGroup,
PackageEnvironment,
PackageGroup,
PackageLangpacks,
RepoMetadataFile,
Modulemd,
ModulemdDefaults,
ModulemdObsolete,
RpmPackageSigningService,
UpdateRecord,
)

from pulp_rpm.app.downloaders import RpmDownloader, RpmFileDownloader, UlnDownloader
from pulp_rpm.app.exceptions import DistributionTreeConflict
from pulp_rpm.app.shared_utils import urlpath_sanitize

log = getLogger(__name__)
Expand Down Expand Up @@ -202,6 +201,10 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
The name of a checksum type to use for metadata when generating metadata.
package_checksum_type (String):
The name of a default checksum type to use for packages when generating metadata.
package_signing_service (RpmPackageSigningService):
Signing service to be used on package signing operations related to this repository.
package_signing_fingerprint (String):
The V4 fingerprint (160 bits) to be used by @package_signing_service.
repo_config (JSON): repo configuration that will be served by distribution
compression_type(pulp_rpm.app.constants.COMPRESSION_TYPES):
Compression type to use for metadata files.
Expand All @@ -226,6 +229,10 @@ class RpmRepository(Repository, AutoAddObjPermsMixin):
metadata_signing_service = models.ForeignKey(
AsciiArmoredDetachedSigningService, on_delete=models.SET_NULL, null=True
)
package_signing_service = models.ForeignKey(
RpmPackageSigningService, on_delete=models.SET_NULL, null=True
)
package_signing_fingerprint = models.TextField(null=True, max_length=40)
original_checksum_types = models.JSONField(default=dict)
last_sync_details = models.JSONField(default=dict)
retain_package_versions = models.PositiveIntegerField(default=0)
Expand Down
41 changes: 35 additions & 6 deletions pulp_rpm/app/serializers/package.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import logging
import traceback

from gettext import gettext as _

from rest_framework import serializers
from rest_framework.exceptions import NotAcceptable

from pulpcore.plugin.serializers import (
ContentChecksumSerializer,
SingleArtifactContentUploadSerializer,
)
from pulpcore.plugin.util import get_domain_pk
from rest_framework import serializers
from rest_framework.exceptions import NotAcceptable

from pulp_rpm.app.models import Package
from pulp_rpm.app.shared_utils import read_crpackage_from_artifact, format_nvra

from pulp_rpm.app.shared_utils import format_nvra, read_crpackage_from_artifact

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -230,6 +227,7 @@ class PackageSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSe

def __init__(self, *args, **kwargs):
"""Initializer for RpmPackageSerializer."""

super().__init__(*args, **kwargs)
if "relative_path" in self.fields:
self.fields["relative_path"].required = False
Expand Down Expand Up @@ -322,6 +320,37 @@ class Meta:
)
model = Package

def validate(self, data):
validated_data = super().validate(data)
sign_package = self.context.get("sign_package", None)
# choose branch, if not set externally
if sign_package is None:
sign_package = bool(
validated_data.get("repository")
and validated_data["repository"].package_signing_service
)
self.context["sign_package"] = sign_package

# normal branch
if sign_package is False:
return validated_data

# signing branch
if not validated_data["repository"].package_signing_fingerprint:
raise serializers.ValidationError(
_(
"To sign a package on upload, the associated Repository must set both"
"'package_signing_service' and 'package_signing_fingerprint'."
)
)

if not validated_data.get("file"):
raise serializers.ValidationError(
_("To sign a package on upload, a file must be provided.")
)

return validated_data


class MinimalPackageSerializer(PackageSerializer):
"""
Expand Down
29 changes: 28 additions & 1 deletion pulp_rpm/app/serializers/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
SKIP_TYPES,
SYNC_POLICY_CHOICES,
)
from pulp_rpm.app.models import RpmDistribution, RpmPublication, RpmRemote, RpmRepository, UlnRemote
from pulp_rpm.app.models import (
RpmDistribution,
RpmPackageSigningService,
RpmPublication,
RpmRemote,
RpmRepository,
UlnRemote,
)
from pulp_rpm.app.schema import COPY_CONFIG_SCHEMA
from urllib.parse import urlparse

Expand All @@ -52,6 +59,24 @@ class RpmRepositorySerializer(RepositorySerializer):
required=False,
allow_null=True,
)
package_signing_service = RelatedField(
help_text="A reference to an associated package signing service.",
view_name="signing-services-detail",
queryset=RpmPackageSigningService.objects.all(),
many=False,
required=False,
allow_null=True,
)
package_signing_fingerprint = serializers.CharField(
help_text=_(
"The pubkey V4 fingerprint (160 bits) to be passed to the package signing service."
"The signing service will use that on signing operations related to this repository."
),
max_length=40,
required=False,
allow_blank=True,
default="",
)
retain_package_versions = serializers.IntegerField(
help_text=_(
"The number of versions of each package to keep in the repository; "
Expand Down Expand Up @@ -220,6 +245,8 @@ class Meta:
fields = RepositorySerializer.Meta.fields + (
"autopublish",
"metadata_signing_service",
"package_signing_service",
"package_signing_fingerprint",
"retain_package_versions",
"checksum_type",
"metadata_checksum_type",
Expand Down
Loading
Loading