Skip to content

Commit

Permalink
Add support for signing packages on upload
Browse files Browse the repository at this point in the history
What this does:
- Create RpmPackageSigningService
- Create RpmTool utility
- Add fields to Repository model
- Add branch on Package upload to sign the package with the associated
  SigningService and fingerprint.
- Add docs
- Use presence of SigningService at Repo as signing trigger

Closes #2986
  • Loading branch information
pedro-psb committed Jun 13, 2024
1 parent b7a9e20 commit cab0c83
Show file tree
Hide file tree
Showing 19 changed files with 952 additions and 36 deletions.
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
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

0 comments on commit cab0c83

Please sign in to comment.