async def sign_image(
        self,
        signer: Signer,
        src_image_name: ImageName,
        dest_image_source: ImageSource,
        dest_image_name: ImageName,
        signature_type: SignatureTypes = SignatureTypes.SIGN,
        **kwargs,
    ) -> ImageSourceSignImage:
        LOGGER.debug(
            "%s: %s ...",
            "Endorsing"
            if signature_type == SignatureTypes.ENDORSE else "Signing"
            if signature_type == SignatureTypes.SIGN else "Resigning",
            src_image_name.resolve_name(),
        )

        dest_image_name = dest_image_name.clone()
        if dest_image_name.resolve_digest():
            dest_image_name.digest = None
            LOGGER.warning(
                "It is not possible to store a signed image to a predetermined digest! Adjusted destination: %s",
                dest_image_name.resolve_name(),
            )

        # Generate a signed image configuration ...
        data = await self._sign_image_config(signer, src_image_name,
                                             signature_type, **kwargs)
        LOGGER.debug("    Signature:\n%s", data.signature_value)
        image_config = data.image_config
        config_digest = image_config.get_digest()
        LOGGER.debug("    config digest (signed): %s", config_digest)

        # Generate a new registry manifest ...
        manifest = data.verify_image_data.manifest.clone()
        manifest.set_config_digest(config_digest,
                                   len(image_config.get_bytes()))
        data = ImageSourceSignImage(
            image_config=data.image_config,
            manifest_signed=manifest,
            signature_value=data.signature_value,
            verify_image_data=data.verify_image_data,
        )

        await dest_image_source.put_image(
            self,
            dest_image_name,
            manifest,
            image_config,
            data.verify_image_data.compressed_layer_files,
            **kwargs,
        )

        dest_image_name.digest = manifest.get_digest()

        if not self.dry_run:
            LOGGER.debug("Created new image: %s",
                         dest_image_name.resolve_name())

        return data
Example #2
0
    async def _verify_image_config(
        self, image_name: ImageName, **kwargs
    ) -> ImageSourceVerifyImageConfig:
        """
        Verifies the integration of an image configuration against metadata contained within a manifest.

        Args:
            image_name: The image name for which to retrieve the configuration.

        Returns:
            NamedTuple:
                image_config: The image configuration.
                image_layers: The listing of image layer identifiers.
                manifest: The image-source specific manifest.
                manifest_layers: The listing of manifest layer identifiers.
        """

        # Retrieve the image configuration digest and layers identifiers from the manifest ...
        LOGGER.debug("Verifying Integrity: %s ...", image_name.resolve_name())
        manifest = await self.get_manifest(image_name, **kwargs)
        LOGGER.debug("    manifest digest: %s", xellipsis(manifest.get_digest()))
        config_digest = manifest.get_config_digest(image_name)
        LOGGER.debug("    config digest: %s", xellipsis(config_digest))
        manifest_layers = manifest.get_layers(image_name)
        LOGGER.debug("    manifest layers:")
        for layer in manifest_layers:
            LOGGER.debug("        %s", xellipsis(layer))

        # Retrieve the image configuration ...
        image_config = await self.get_image_config(image_name, **kwargs)
        config_digest_canonical = image_config.get_digest_canonical()
        LOGGER.debug(
            "    config digest (canonical): %s", xellipsis(config_digest_canonical)
        )
        must_be_equal(
            config_digest,
            image_config.get_digest(),
            "Image config digest mismatch",
        )

        # Retrieve the image layers from the image configuration ...
        image_layers = image_config.get_image_layers()
        LOGGER.debug("    image layers:")
        for layer in image_layers:
            LOGGER.debug("        %s", xellipsis(layer))

        # Quick check: Ensure that the layer counts are consistent
        must_be_equal(len(manifest_layers), len(image_layers), "Layer count mismatch")

        return ImageSourceVerifyImageConfig(
            image_config=image_config,
            image_layers=image_layers,
            manifest=manifest,
            manifest_layers=manifest_layers,
        )
    async def put_image_config(self, image_name: ImageName,
                               image_config: ImageConfig, **kwargs):
        image_name = image_name.clone()
        if image_name.resolve_digest():
            image_name.digest = None
            LOGGER.debug(
                "It is not possible to store an image configuration to a non-deterministic digest!"
                " Adjusted destination: %s",
                image_name.resolve_name(),
            )

        image_config_digest = image_config.get_digest()
        name = f"{image_config_digest.sha256}.json"
        if not await self._file_exists(name):
            with open(self.archive, "rb+") as file_out:
                tar_add_file(file_out, name, image_config.get_bytes())
Example #4
0
    async def verify_image_signatures(
        self, image_name: ImageName, **kwargs
    ) -> ImageSourceVerifyImageSignatures:
        """
        Verifies that signatures contained within the image source data format are valid (that the image has not been
        modified since they were created)

        Args:
            image_name: The image name.

        Returns:
            NamedTuple:
                compressed_layer_files: The list of compressed layer files on disk (optional).
                image config: The image configuration.
                manifest: The image source-specific manifest file (archive, registry, repository).
                signatures: as defined by :func:~docker_sign_verify.ImageConfig.verify_signatures.
                uncompressed_layer_files: The list of uncompressed layer files on disk.
        """

        # Verify image integrity (we use the verified values from this point on)
        data = await self.verify_image_integrity(image_name, **kwargs)

        # Verify image signatures ...
        try:
            LOGGER.debug("Verifying Signature(s): %s ...", image_name.resolve_name())
            LOGGER.debug(
                "    config digest (signed): %s",
                xellipsis(data.image_config.get_digest()),
            )
            signatures = await data.image_config.verify_signatures(
                signer_kwargs=self.signer_kwargs
            )
            data = ImageSourceVerifyImageSignatures(
                compressed_layer_files=data.compressed_layer_files,
                image_config=data.image_config,
                manifest=data.manifest,
                signatures=signatures,
                uncompressed_layer_files=data.uncompressed_layer_files,
            )

            # List the image signatures ...
            LOGGER.debug("    signatures:")
            for result in data.signatures.results:
                if not hasattr(result, "valid"):
                    raise UnsupportedSignatureTypeError(
                        f"Unsupported signature type: {type(result)}!"
                    )

                if hasattr(result, "signer_short") and hasattr(result, "signer_long"):
                    if not result.valid:
                        raise SignatureMismatchError(
                            f"Verification failed for signature; {result.signer_short}"
                        )

                    for line in result.signer_long.splitlines():
                        LOGGER.debug(line)
                # Try to be friendly ...
                else:
                    if not result.valid:
                        raise SignatureMismatchError(
                            f"Verification failed for signature; unknown type: {type(result)}!"
                        )
                    LOGGER.debug("        Signature of unknown type: %s", type(result))
        except Exception:
            for file in data.compressed_layer_files + data.uncompressed_layer_files:
                file.close()
            raise

        LOGGER.debug("Signature check passed.")

        return data
Example #5
0
    async def sign_image(
        self,
        signer: Signer,
        src_image_name: ImageName,
        dest_image_source: ImageSource,
        dest_image_name: ImageName,
        signature_type: SignatureTypes = SignatureTypes.SIGN,
        **kwargs,
    ):
        LOGGER.debug(
            "%s: %s ...",
            "Endorsing"
            if signature_type == SignatureTypes.ENDORSE
            else "Signing"
            if signature_type == SignatureTypes.SIGN
            else "Resigning",
            src_image_name.resolve_name(),
        )

        # Generate a signed image configuration ...
        data = await self._sign_image_config(signer, src_image_name, signature_type)
        manifest = data.verify_image_data.manifest
        LOGGER.debug("    Signature:\n%s", data.signature_value)
        image_config = data.image_config

        # Replicate all of the image layers ...
        LOGGER.debug("    Replicating image layers ...")
        repository_layers = manifest.get_layers(src_image_name)
        for i, repository_layer in enumerate(repository_layers):
            if not dest_image_source.layer_exists(dest_image_name, repository_layer):
                await dest_image_source.put_image_layer_from_disk(
                    dest_image_name,
                    data.verify_image_data.compressed_layer_files[i],
                )
        # TODO: We we need to track the layer translations here? Is this possible for DM repos?

        # Push the new image configuration ...
        config_digest_signed = image_config.get_digest()
        LOGGER.debug("    config digest (signed): %s", config_digest_signed)
        await dest_image_source.put_image_config(dest_image_name, image_config)

        # Generate a new repository manifest, and push ...
        if type(dest_image_source).__name__ == "ArchiveImageSource":
            raise NotImplementedError
        elif type(dest_image_source).__name__ == "RegistryV2ImageSource":
            raise NotImplementedError
        elif isinstance(dest_image_source, DeviceMapperRepositoryImageSource):
            manifest_signed = (
                await dest_image_source.get_manifest()
            )  # type: DeviceMapperRepositoryManifest

            if dest_image_name.tag:
                manifest_signed.override_config(config_digest_signed, dest_image_name)
            data.manifest_signed = manifest_signed
            await dest_image_source.put_manifest(manifest_signed, dest_image_name)
        else:
            raise RuntimeError(f"Unknown derived class: {type(dest_image_source)}")

        if not self.dry_run:
            LOGGER.debug("Created new image: %s", dest_image_name.resolve_name())

        return data
    async def sign_image(
        self,
        signer: Signer,
        src_image_name: ImageName,
        dest_image_source: ImageSource,
        dest_image_name: ImageName,
        signature_type: SignatureTypes = SignatureTypes.SIGN,
        **kwargs,
    ):
        LOGGER.debug(
            "%s: %s ...",
            "Endorsing"
            if signature_type == SignatureTypes.ENDORSE else "Signing"
            if signature_type == SignatureTypes.SIGN else "Resigning",
            src_image_name.resolve_name(),
        )

        # Generate a signed image configuration ...
        data = await self._sign_image_config(signer, src_image_name,
                                             signature_type)
        manifest = data["verify_image_data"]["manifest"]
        LOGGER.debug("    Signature:\n%s", data["signature_value"])
        image_config = data["image_config"]

        # Replicate all of the image layers ...
        LOGGER.debug("    Replicating image layers ...")
        archive_layers = manifest.get_layers(src_image_name)
        archive_layers_changed = archive_layers.copy()
        for i, archive_layer in enumerate(archive_layers):
            if not dest_image_source.layer_exists(dest_image_name,
                                                  archive_layer):
                # Update the layer
                digest = dest_image_source.put_image_layer_from_disk(
                    dest_image_name,
                    data["verify_image_data"]["uncompressed_layer_files"][i],
                )
                archive_layers_changed[i] = digest
        archive_layers = archive_layers_changed

        # Push the new image configuration ...
        config_digest_signed = image_config.get_digest()
        LOGGER.debug("    config digest (signed): %s", config_digest_signed)
        await dest_image_source.put_image_config(dest_image_name, image_config)

        # Generate a new archive manifest, and push ...
        if isinstance(dest_image_source, ArchiveImageSource):
            manifest_signed = (await dest_image_source.get_manifest()
                               )  # type: ArchiveManifest

            repotags = None
            if dest_image_name.tag:
                repotags = [str(dest_image_name)]
                manifest_signed.append_config(config_digest_signed,
                                              archive_layers, repotags)
            data["manifest_signed"] = manifest_signed
            # TODO: make sure to remove conflicting tags in "other" config entries
            await dest_image_source.put_manifest(manifest_signed)
            # TODO: Update foo.tar:/repositories as well
        elif type(dest_image_source).__name__ == "RegistryV2ImageSource":
            raise NotImplementedError
        elif type(dest_image_source
                  ).__name__ == "DeviceMapperRepositoryImageSource":
            raise NotImplementedError
        else:
            raise RuntimeError("Unknown derived class: {0}".format(
                type(dest_image_source)))

        if not self.dry_run:
            LOGGER.debug("Created new image: %s",
                         dest_image_name.resolve_name())

        return data
    async def verify_image_signatures(
        self, image_name: ImageName, **kwargs
    ) -> ImageSourceVerifyImageSignatures:
        """
        Verifies that signatures contained within the image source data format are valid (that the image has not been
        modified since they were created)

        Args:
            image_name: The image name.

        Returns:
            dict:
                compressed_layer_files: The list of compressed layer files on disk (optional).
                image config: The image configuration.
                manifest: The image source-specific manifest file (archive, registry, repository).
                signatures: as defined by :func:~docker_sign_verify.ImageConfig.verify_signatures.
                uncompressed_layer_files: The list of uncompressed layer files on disk.
        """

        # Verify image integrity (we use the verified values from this point on)
        integrity_data = await self.verify_image_integrity(image_name, **kwargs)

        # Verify image signatures ...
        LOGGER.debug("Verifying Signature(s): %s ...", image_name.resolve_name())
        LOGGER.debug(
            "    config digest (signed): %s",
            xellipsis(integrity_data["image_config"].get_digest()),
        )
        integrity_data = cast(ImageSourceVerifyImageSignatures, integrity_data)
        integrity_data["signatures"] = await integrity_data[
            "image_config"
        ].verify_signatures()

        # List the image signatures ...
        LOGGER.debug("    signatures:")
        for result in integrity_data["signatures"]["results"]:
            # pylint: disable=protected-access
            if isinstance(result, gnupg._parsers.Verify):
                if not result.valid:
                    raise SignatureMismatchError(
                        "Verification failed for signature with key_id '{0}': {1}".format(
                            result.key_id, result.status
                        )
                    )
                LOGGER.debug(
                    "        Signature made %s using key ID %s",
                    time.strftime(
                        "%Y-%m-%d %H:%M:%S", time.gmtime(float(result.sig_timestamp))
                    ),
                    result.key_id,
                )
                LOGGER.debug("            %s", result.username)
            elif result.get("type", None) == "pki":
                if not result["valid"]:
                    raise SignatureMismatchError(
                        "Verification failed for signature using cert: {0}".format(
                            result["keypair_path"]
                        )
                    )
                # TODO: Add better debug logging
                LOGGER.debug("        Signature made using undetailed PKI keypair.")
            else:
                LOGGER.error("Unknown Signature Type: %s", type(result))

        LOGGER.debug("Signature check passed.")

        return integrity_data