Skip to content

Commit

Permalink
Add support for signing package 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 pulp#2986
  • Loading branch information
pedro-psb committed May 22, 2024
1 parent 7cf796d commit ce969a3
Show file tree
Hide file tree
Showing 18 changed files with 964 additions and 49 deletions.
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
43 changes: 29 additions & 14 deletions pulp_rpm/app/serializers/repository.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
from gettext import gettext as _

import logging
from gettext import gettext as _

from django.conf import settings
from jsonschema import Draft7Validator
from rest_framework import serializers

from pulpcore.plugin.util import get_domain
from pulpcore.plugin.models import (
AsciiArmoredDetachedSigningService,
Remote,
Publication,
)
from pulpcore.plugin.models import AsciiArmoredDetachedSigningService, Publication, Remote
from pulpcore.plugin.serializers import (
RelatedField,
DetailRelatedField,
DistributionSerializer,
PublicationSerializer,
RelatedField,
RemoteSerializer,
RepositorySerializer,
RepositorySyncURLSerializer,
ValidateFieldsMixin,
)
from pulpcore.plugin.util import get_domain
from rest_framework import serializers

from pulp_rpm.app.constants import (
ALLOWED_CHECKSUM_ERROR_MSG,
ALLOWED_PUBLISH_CHECKSUMS,
ALLOWED_PUBLISH_CHECKSUM_ERROR_MSG,
ALLOWED_PUBLISH_CHECKSUMS,
CHECKSUM_CHOICES,
COMPRESSION_CHOICES,
SKIP_TYPES,
SYNC_POLICY_CHOICES,
COMPRESSION_CHOICES,
)
from pulp_rpm.app.models import (
RpmDistribution,
RpmPackageSigningService,
RpmPublication,
RpmRemote,
RpmRepository,
RpmPublication,
UlnRemote,
)
from pulp_rpm.app.schema import COPY_CONFIG_SCHEMA
Expand All @@ -63,6 +58,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_pubkey = 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 @@ -231,6 +244,8 @@ class Meta:
fields = RepositorySerializer.Meta.fields + (
"autopublish",
"metadata_signing_service",
"package_signing_service",
"package_signing_pubkey",
"retain_package_versions",
"checksum_type",
"metadata_checksum_type",
Expand Down
Loading

0 comments on commit ce969a3

Please sign in to comment.