Example #1
0
def to_image_name(context, param, value: str) -> ImageName:
    """Converts an docker image name to an ImageName."""
    if isinstance(value, str):
        result = ImageName.parse(value)
    else:
        result = [ImageName.parse(v) for v in value]
    return result
def test_get_tag(archive_repositories: ArchiveRepositories,
                 image_name: ImageName):
    """Test repository tag retrieval."""
    tag = archive_repositories.get_tag(image_name)
    assert tag
    assert FormattedSHA256(tag) == image_name.digest
    assert not archive_repositories.get_tag(ImageName("does_not_exist"))
    assert not archive_repositories.get_tag(ImageName("does_not", tag="exist"))
def test_get_manifest(archive_changeset: ArchiveChangeset,
                      image_name: ImageName):
    """Test manifest retrieval."""
    manifest = archive_changeset.get_manifest(image_name)
    if image_name.resolve_digest():
        assert manifest.get_config_digest() == image_name.resolve_digest()
    if image_name.tag:
        assert ArchiveChangeset.get_repository_tag(
            image_name) in manifest.get_tags()
    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())
def test_get_config_digest(registry_v2_manifest: RegistryV2Manifest):
    """Test image configuration digest retrieval."""
    formattedsha256 = FormattedSHA256(
        "8f1196ff19e7b5c5861de192ae77e8d7a692fcbca2dd3174d324980f72ab49bf")
    assert registry_v2_manifest.get_config_digest() == formattedsha256
    assert (registry_v2_manifest.get_config_digest(
        ImageName.parse("ignored")) == formattedsha256)
Example #6
0
def test___init__(image_data: TypingGetTestData):
    """Test that image name can be instantiated."""
    assert ImageName(
        digest=image_data["digest"],
        endpoint=image_data["endpoint"],
        image=image_data["image"],
        tag=image_data["tag"],
    )
    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
def known_good_image_local(request,
                           pytest_registry: str) -> TypingKnownGoodImage:
    """Provides 'known good' metadata for a local image that can be modified."""
    request.param["image"] = request.param["image"].format(pytest_registry)
    manifest_digest = request.param["digests"][
        DockerMediaTypes.DISTRIBUTION_MANIFEST_V2]
    request.param["image_name"] = ImageName.parse(
        f"{request.param['image']}:{request.param['tag']}@{manifest_digest}")
    return request.param
def test_get_layers(registry_v2_manifest: RegistryV2Manifest):
    """Test manifest layer retrieval."""
    layers = [
        "sha256:6c8c72249e560701aa1da4cd40192274a8c0419ddb8e4a553aa02b5a1acdb863",
        "sha256:1403b179e2c9df4f57e9ea94e32882739c6b3d75ed756d4e67fcc424288c29cc",
    ]
    assert registry_v2_manifest.get_layers() == layers
    assert registry_v2_manifest.get_layers(
        ImageName.parse("ignored")) == layers
def known_good_image_remote(request) -> TypingKnownGoodImage:
    """Provides 'known good' metadata for a remote image that is readonly."""
    request.param["image"] = request.param["image"].format(
        request.param["original_endpoint"])
    manifest_digest = request.param["digests"][
        DockerMediaTypes.DISTRIBUTION_MANIFEST_V2]
    request.param["image_name"] = ImageName.parse(
        f"{request.param['image']}:{request.param['tag']}@{manifest_digest}")
    return request.param
Example #11
0
def test_parse(image_data: TypingGetTestData):
    """Test initialization via parsed strings."""
    image_name = ImageName.parse(image_data["string"])
    assert image_name.digest == image_data["digest"]
    if image_data["digest"]:
        assert isinstance(image_name.digest, FormattedSHA256)
    assert image_name.endpoint == image_data["endpoint"]
    if image_data["endpoint"]:
        assert ImageName.DEFAULT_ENDPOINT not in image_name.endpoint
    assert image_name.image == image_data["image"]
    assert ImageName.DEFAULT_NAMESPACE not in image_name.image
    assert image_name.tag == image_data["tag"]
    if image_data["tag"]:
        assert ImageName.DEFAULT_TAG not in image_name.tag

    with pytest.raises(ValueError) as exception:
        ImageName.parse("a:b:c:d")
    assert str(exception.value).startswith("Unable to parse string:")
def test_get_config_digest(
    devicemapper_repository_manifest: DeviceMapperRepositoryManifest,
    img_name: str,
    config_digest: str,
):
    """Test image configuration digest retrieval."""
    image_name = ImageName.parse(img_name)
    assert (devicemapper_repository_manifest.get_config_digest(image_name) ==
            config_digest)
Example #13
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,
        )
def test_set_tag(archive_repositories: ArchiveRepositories,
                 image_name: ImageName, name: str):
    """Test repository tag assignment."""
    tag = archive_repositories.get_tag(image_name)
    assert tag
    assert FormattedSHA256(tag) == image_name.digest

    digest = FormattedSHA256.calculate(name.encode("utf-8"))
    name = ImageName.parse(name)
    archive_repositories.set_tag(name, digest)
    assert FormattedSHA256(archive_repositories.get_tag(name)) == digest
def test_override_config(
    devicemapper_repository_manifest: DeviceMapperRepositoryManifest,
    formattedsha256: FormattedSHA256,
    img_name: str,
):
    """Test overriding manifest values."""
    image_name = ImageName.parse(img_name)
    devicemapper_repository_manifest.override_config(formattedsha256,
                                                     image_name)
    assert (devicemapper_repository_manifest.get_config_digest(image_name) ==
            formattedsha256)
    def get_tag(self, image_name: ImageName):
        """
        Retrieves a repository tag for a given image.

        Args:
            image_name: The image for which to assign the tag

        Returns:
            The repository tag, or None.
        """
        image = ArchiveChangeset.normalize_tags([image_name.image])[0]
        return self.get_json().get(image, {}).get(image_name.resolve_tag(),
                                                  None)
Example #17
0
    def _get_repository_key(image_name: ImageName) -> str:
        """
        Retrieves the repository key for a given image name.

        Args:
            image_name: Image name for which to retrieve the key.

        Returns:
            The corresponding repository key.
        """
        key_name = image_name.clone()
        key_name.tag = None
        return str(key_name)
Example #18
0
def test_append_config(
    archive_manifest: ArchiveManifest,
    sha256_archive_layer: str,
    formattedsha256: FormattedSHA256,
    repotag,
):
    """Test appending configurations."""
    layers = [formattedsha256]
    archive_manifest.append_config(formattedsha256, layers, repotag)
    image_name = ImageName("ignored", digest=formattedsha256)
    config = archive_manifest.get_config(image_name)
    assert config["Config"] == "{0}.json".format(formattedsha256.sha256)
    assert config["Layers"] == [sha256_archive_layer]
    assert config.get("RepoTags", None) == repotag
    def set_tag(self, image_name: ImageName, digests: FormattedSHA256):
        """
        Assigns a repository tag.

        Args:
            image_name: The image for which to assign the tag
            digests: The value to be assigned to the tag
        """
        _json = self.get_json()
        image = ArchiveChangeset.normalize_tags([image_name.image])[0]
        if not image in _json:
            _json[image] = {}
        _json[image][image_name.resolve_tag()] = digests.sha256
        self._set_json(_json)
Example #20
0
def test_parse_string(image_data: TypingGetTestData):
    """Test string parsing for complex image names."""
    result = ImageName._parse_string(image_data["string"])
    assert result["digest"] == image_data["digest"]
    if image_data["digest"]:
        assert isinstance(result["digest"], FormattedSHA256)
    assert result["endpoint"] == image_data["endpoint"]
    if image_data["endpoint"]:
        assert ImageName.DEFAULT_ENDPOINT not in str(result["endpoint"])
    assert result["image"] == image_data["image"]
    assert ImageName.DEFAULT_NAMESPACE not in str(result["image"])
    assert result["tag"] == image_data["tag"]
    if image_data["tag"]:
        assert ImageName.DEFAULT_TAG not in str(result["tag"])
Example #21
0
async def test_get_manifest_list(
    docker_registry_secure: DockerRegistrySecure,
    image: TypingGetTestData,
    registry_v2_image_source: RegistryV2ImageSource,
    **kwargs,
):
    """Test manifest retrieval."""
    if "tag_resolves_to_manifest_list" not in image:
        pytest.skip(
            f"Image {image['image']} does not reference a manifest list.")

    image_name = ImageName(
        image["image"],
        digest=image["digests"][
            DockerMediaTypes.DISTRIBUTION_MANIFEST_LIST_V2],
        endpoint=docker_registry_secure.endpoint,
        tag=image["tag"],
    )

    LOGGER.debug("Retrieving manifest list for: %s ...", image_name)
    manifest = await registry_v2_image_source.get_manifest(
        image_name, **kwargs)
    assert isinstance(manifest, RegistryV2Manifest)
    assert manifest.get_digest() == image_name.resolve_digest()
def test_remove_tags(archive_changeset: ArchiveChangeset):
    """Test repository tag removal"""
    image_name = ImageName(
        "base",
        digest=FormattedSHA256(
            "adecf4209bb9dd67d96393774cbd7f8bd2bad3596da42cde33daa0c41b14ac62"
        ),
        tag="7.2",
    )
    tag = ArchiveChangeset.get_repository_tag(image_name)
    manifest = archive_changeset.get_manifest(image_name)
    assert tag in manifest.get_tags()

    archive_changeset.remove_tags(["dummy"])
    manifest = archive_changeset.get_manifest(image_name)
    assert tag in manifest.get_tags()

    archive_changeset.remove_tags([tag])
    manifest = archive_changeset.get_manifest(image_name)
    assert not manifest.get_tags()
Example #23
0
async def replicate_manifest_lists(
        docker_registry_secure: DockerRegistrySecure):
    """Replicates manifests lists to the secure docker registry for testing."""
    # pylint: disable=protected-access
    LOGGER.debug("Replicating manifest lists into %s ...",
                 docker_registry_secure.service_name)
    async with DockerRegistryClientAsync() as docker_registry_client_async:
        for image in get_test_data_registryv2():
            if "tag_resolves_to_manifest_list" not in image:
                continue

            digest = image["digests"][
                DockerMediaTypes.DISTRIBUTION_MANIFEST_LIST_V2]
            image_name = ImageName(image["image"],
                                   digest=digest,
                                   tag=image["tag"])
            LOGGER.debug("- %s", image_name)

            scope = DockerAuthentication.SCOPE_REPOSITORY_PULL_PATTERN.format(
                image_name.image)
            auth_header_src = await docker_registry_client_async._get_request_headers(
                image_name, scope=scope)
            if not auth_header_src:
                LOGGER.warning(
                    "Unable to retrieve authentication headers for: %s",
                    image_name)

            pdrf_image_name = PDRFImageName(
                image_name.resolve_image(),
                digest=image_name.resolve_digest(),
                endpoint=image_name.resolve_endpoint(),
                tag=image_name.resolve_tag(),
            )
            try:
                replicate_manifest_list(
                    pdrf_image_name,
                    docker_registry_secure.endpoint,
                    auth_header_dest=docker_registry_secure.auth_header,
                    auth_header_src=auth_header_src,
                    ssl_context_dest=docker_registry_secure.ssl_context,
                )
            except Exception as exception:  # pylint: disable=broad-except
                LOGGER.warning(
                    "Unable to replicate manifest list '%s': %s",
                    image_name,
                    exception,
                    exc_info=True,
                )
Example #24
0
def test_get_layers(archive_manifest: ArchiveManifest, image_name: ImageName):
    """Test manifest layer retrieval."""
    if (image_name.resolve_digest(
    ) == "sha256:a51f3f9281a1a3d89dce25fec8acffbe9f59ddb67d98e04245c4c886e32d3782"
        ):
        assert archive_manifest.get_layers(image_name) == [
            "sha256:137120c8596a15ab42c39c0c5cf83ef864b6b65b5516887c895915e87292bd07",
            "sha256:755520f73bc74ae73b12f53229e401e8d4c584b74f5704d2d36ba7c45e2657cf",
            "sha256:13fb089903a5e0e9b00d78ba48496da528ce8d81e08a1042ebeced8c35d714cb",
            "sha256:f86d68f70ca006025a7f7013f69898f78d1d9272c4d3909e3ec4c7f9958da20e",
            "sha256:7b4a4edd704242cec1710679a088be8aabff25c3a79f4eecbe8d11d57c53a20b",
            "sha256:ef4724d42630f3022ef67c3f6749e85a13e81b8efcf98fbd517476499f10e5ab",
        ]
    else:
        assert archive_manifest.get_layers(image_name) == [
            "sha256:2c2e149ae9a88ae6bee1583459b2d3e5e317877b795c08781fab36eab4b4329f",
            "sha256:83419ef1d0ad0520c9fc44da4345637e5c05e34fa564ddf2fb6d6f94a6b2d205",
            "sha256:c2d494c64683fb7edac60aaffc570c514c1c80c797aafcf25b8a9438690da4df",
            "sha256:d3b1a8ce509767258045f6cc050dfc8cff27f66f9fa8c61c9dc46733e492e0af",
            "sha256:0a0084e273d71f3b39100a5c209a3208fafdb90e3dd038cded5e2a54265d23fa",
        ]
    def get_manifest(self, image_name: ImageName) -> "ArchiveManifest":
        """
        Retrieves the archive manifest for a given image name from the archive changeset.

        Args:
            image_name: The image name.

        Returns:
            The corresponding archive manifest.
        """
        if image_name.digest:
            for manifest in self.get_manifests():
                if manifest.get_config_digest() == image_name.resolve_digest():
                    return manifest
        else:
            tag = ArchiveChangeset.get_repository_tag(image_name)
            for manifest in self.get_manifests():
                tags = manifest.get_tags()
                if tags and tag in manifest.get_tags():
                    return manifest
        raise RuntimeError(
            f"Unable to locate configuration in archive manifest for: {image_name.resolve_name()}"
        )
Example #26
0
def get_test_data() -> Generator[TypingGetTestData, None, None]:
    """Dynamically initializes test data."""
    for endpoint in ["endpoint.io", "endpoint:port", None]:
        for image in [
                "image", "ns0/image", "ns0/ns1/image", "ns0/ns1/ns2/image"
        ]:
            for tag in ["tag", None]:
                for digest in [FormattedSHA256.calculate(b""), None]:
                    # Construct a complex string ...
                    string = image
                    if tag:
                        string = f"{string}:{tag}"
                    if digest:
                        string = f"{string}@{digest}"
                    if endpoint:
                        string = f"{endpoint}/{string}"
                    yield {
                        "digest": digest,
                        "endpoint": endpoint,
                        "image": image,
                        "object": ImageName.parse(string),
                        "string": string,
                        "tag": tag,
                    }
def test_get_config_digest(archive_manifest: ArchiveManifest,
                           image_name: ImageName):
    """Test configuration digest retrieval."""
    assert archive_manifest.get_config_digest(
        image_name) == image_name.resolve_digest()
def image_name() -> ImageName:
    """Provides a 'known good' image name."""
    yield ImageName.parse(
        "ignored@sha256:a51f3f9281a1a3d89dce25fec8acffbe9f59ddb67d98e04245c4c886e32d3782"
    )
Example #29
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
Example #30
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