Metadata Signing

Note

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).

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().

Note

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”, “key”: “public.key”}

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

This 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.

Deprecated since version 3.10: The path to the public key is no longer required to be present in the output of a signing script. It is recommended to pass the value of the public key during the object creation process instead.

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(
        AsciiArmoredDetachedSigningService,
        on_delete=models.SET_NULL,
        null=True
    )
    

    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.",
        view_name="signing-services-detail",
        queryset=models.AsciiArmoredDetachedSigningService.objects.all(),
        many=False,
        required=False,
        allow_null=True
    )
    
  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("."):
        try:
            signature = metadata_signing_service.sign(metadata.filepath)
        except RuntimeError:
            raise
        add_to_repository(metadata, signature)