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

Closes #2986
  • Loading branch information
pedro-psb committed May 22, 2024
1 parent 7cf796d commit a1b4c25
Show file tree
Hide file tree
Showing 19 changed files with 965 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGES/2986.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added supported for signing RPM Packages on upload.
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_pubkey",
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_pubkey (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_pubkey = 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
54 changes: 48 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 @@ -227,9 +224,19 @@ class PackageSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSe
),
read_only=True,
)
sign_package = serializers.BooleanField(
help_text=_(
"Sign the package using the SigningService associated with the "
"Repository this is being upload to."
),
default=False,
required=False,
write_only=True,
)

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 @@ -318,10 +325,45 @@ class Meta:
"size_package",
"time_build",
"time_file",
"sign_package",
)
)
model = Package

def validate(self, data):
validated_data = super().validate(data)
sign_package = validated_data.get("sign_package")
temp_uploaded_file = validated_data.get("file")
associated_repo = validated_data.get("repository")
if sign_package is True:
if not temp_uploaded_file:
raise serializers.ValidationError(
_("To sign a package on upload, a file must be provided.")
)
if not associated_repo:
raise serializers.ValidationError(
_("To sign a package on upload, you should provide a Repository.")
)
signing_service_pk = associated_repo.package_signing_service.pk
signing_fingerprint = associated_repo.package_signing_pubkey
if not signing_service_pk and not signing_fingerprint:
raise serializers.ValidationError(
_(
"To sign a package on upload, the related Repository should have"
"both 'package_signing_service' and 'package_signing_pubkey' set."
)
)
validated_data["signing_service_pk"] = signing_service_pk
validated_data["signing_fingerprint"] = signing_fingerprint
return validated_data

def create(self, validated_data):
# clean api-only option before creating model
validated_data.pop("sign_package", None)
validated_data.pop("signing_service_pk", None)
validated_data.pop("signing_fingerprint", None)
return super().create(validated_data)


class MinimalPackageSerializer(PackageSerializer):
"""
Expand Down
Loading

0 comments on commit a1b4c25

Please sign in to comment.