Metadata Signing


Content Signing is in tech-preview and may change in backwards incompatible ways in future releases.

Plugin writers wishing to enable users to sign metadata need to add a new field metadata_signing_service to their implementation of a repository and/or publication. This field should be exposed to users who consume content via REST API. The users may afterwards specify which signing service will be used to sign the metadata when creating a publication.

Every signing service will always be an instance of a subclass of the SigningService model. Plugin writers may either use the existing AsciiArmoredDetachedSigningService, or use that as a reference for writing their own signing service model.

The SigningService base class already provides the fully implemented sign() method, the signature of the validate() method (which must be implemented by each subclass), and the save() method (which calls the validate() method, but is otherwise fully implemented).


The sign() function will be calling the provided script to give the administrator the freedom to define, how the signature is obtained. It is their responsibility to setup the software or hardware facilities for signing and make the script use them. The plugin writer however should provide a reasonably easy default script based on e.g. a simple call to gpg.

In order to sign metadata, plugin writers are required to call the sign() method of the signing service being used. This method invokes the signing script (or other executable) which is provided by the administrator who instantiates a concrete signing service. Instantiating/creating a concrete signing service will ultimately call the save() method, which will in turn call validate(). As a result, it is up to the validate() method to ensure the signing service script provided by the administrator actually provides any signatures, signature files, and return values, as required by the individual SigningService subclass.

This is why implementing a signing service model other than AsciiArmoredDetachedSigningService simply requires inheriting from SiginingService and then implementing validate().


The existing AsciiArmoredDetachedSigningService requires a signing script that creates a detached ascii-armored signature file, and prints valid JSON in the following format to stdout:

{“file”: “filename”, “signature”: “filename.asc”}

Here “filename” is a path to the original file that was signed (passed to the signing script by the sign() method), and “filename.asc” is a path to the signature file created by the script.

The script may read the fingerprint of the key it should use for signing, from the PULP_SIGNING_KEY_FINGERPRINT environment variable. It is possible to pass a dictionary of environment variables to the signing script if need be.

The json is converted to a python dict and returned by the sign() method. If an error occurs, a runtime error is raised instead. All of this is enforced by the validate() method at the time of instantiation.

For more information see the corresponding workflow documentation.

The following procedure may be taken into account for the plugin writers:

  1. Let us assume that a file repository contains the field metadata_signing_service:

    metadata_signing_service = models.ForeignKey(

    In the serializer, there is also added a corresponding field that serializes metadata_signing_service, like so:

    metadata_signing_service = serializers.HyperlinkedRelatedField(
        help_text="A reference to an associated signing service.",
  2. Retrieve a desired signing script via the field metadata_signing_service stored in the repository:

    metadata_signing_service = FileRepository.objects.get(name='foo').metadata_signing_service

    A plugin writer can create a new repository with an associated signing service in the following two ways:

    • Using Python:

      signing_service = AsciiArmoredDetachedSigningService.objects.get(name='sign-metadata')
      FileRepository.objects.create(name='foo', metadata_signing_service=signing_service)
    • Using HTTP calls:

      http POST :24817/pulp/api/v3/repositories/file/file/ name=foo metadata_signing_service=http://localhost:24817/pulp/api/v3/signing-services/5506c8ac-8eae-4f34-bb5a-3bc08f82b088/
  3. Sign a file by calling the method sign():

    with tempfile.TemporaryDirectory("."):
            signature = metadata_signing_service.sign(metadata.filepath)
        except RuntimeError:
        add_to_repository(metadata, signature)