async def test_verify_signatures_manipulated_signatures( gnupg_keypair: GnuPGKeypair, image_config: ImageConfig ): """Test that signature verification detects manipulated signatures.""" signer = GPGSigner( keyid=gnupg_keypair.keyid, passphrase=gnupg_keypair.passphrase, homedir=gnupg_keypair.gnupg_home, ) # Add a single signature ... await image_config.sign(signer) response = await image_config.verify_signatures( signer_kwargs={GPGSigner.__name__: {"homedir": gnupg_keypair.gnupg_home}} ) assert response.results[0].valid # Modify the digest value of the (first) signature ... signatures = image_config.get_signature_list() temp = deepcopy(signatures) temp[0] = ImageConfigSignatureEntry( digest=FormattedSHA256.calculate(b"tampertampertamper"), signature=temp[0].signature, ) image_config.set_signature_list(temp) # An exception should be raised if digest value from the signature does not match the canonical digest of the image # configuration (without any signatures). with pytest.raises(DigestMismatchError) as exception: await image_config.verify_signatures() assert str(exception.value).startswith("Image config canonical digest mismatch:") # Restore the unmodified signature and endorse ... image_config.set_signature_list(signatures) await image_config.sign(signer, SignatureTypes.ENDORSE) response = await image_config.verify_signatures( signer_kwargs={GPGSigner.__name__: {"homedir": gnupg_keypair.gnupg_home}} ) assert response.results[0].valid # Modify the digest value of the second signature ... signatures = image_config.get_signature_list() temp = deepcopy(signatures) temp[1] = ImageConfigSignatureEntry( digest=FormattedSHA256.calculate(b"tampertampertamper"), signature=temp[1].signature, ) image_config.set_signature_list(temp) # An exception should be raised if digest value from the signature does not match the canonical digest of the image # configuration (including the first signature). with pytest.raises(DigestMismatchError) as exception: await image_config.verify_signatures() assert str(exception.value).startswith("Image config canonical digest mismatch:")
async def put_image_layer( self, image_name: ImageName, content, **kwargs) -> DockerRegistryClientAsyncPutBlobUpload: response = await self.docker_registry_client_async.post_blob( image_name, **kwargs) digest = FormattedSHA256.calculate(content) return await self.docker_registry_client_async.put_blob_upload( response["location"], digest, data=content, **kwargs)
def get_digest_canonical(self) -> FormattedSHA256: """ Retrieves the SHA256 digest value of the image configuration in canonical JSON form. Returns: The SHA256 digest value of the image configuration in canonical JSON form. """ return FormattedSHA256.calculate(self.get_bytes_canonical())
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
async def put_image_layer(self, image_name: ImageName, content, **kwargs) -> FormattedSHA256: # TODO: Do we really want to use random garbage here??? # Look into moby/.../save.go to find what to use instead. digest = FormattedSHA256.calculate("{0}{1}{2}".format( str(image_name), datetime.datetime.now(), random.randint(1, 101)).encode("utf-8")) layer = ArchiveManifest.digest_to_layer(digest) with open(self.archive, "rb+") as file_out: tar_add_file(file_out, layer, content) return digest
async def put_image_layer_from_disk(self, image_name: ImageName, file, **kwargs) -> FormattedSHA256: # TODO: Convert to async if self.dry_run: LOGGER.debug("Dry Run: skipping put_image_layer_from_disk") return FormattedSHA256("0" * 64) # TODO: Do we really want to use random garbage here??? # Look into moby/.../save.go to find what to use instead. digest = FormattedSHA256.calculate("{0}{1}{2}".format( str(image_name), datetime.datetime.now(), random.randint(1, 101)).encode("utf-8")) layer = ArchiveManifest.digest_to_layer(digest) with open(self.archive, "rb+") as file_out: tar_mkdir(file_out, os.path.dirname(layer)) file_out.seek(0) tar(file_out, layer, file) return digest
def get_combined_layerid(parent: FormattedSHA256, layer: FormattedSHA256) -> FormattedSHA256: """ Retrieves the layer identifier for a given parent-layer combination. Args: parent: The parent layer identifier. layer: The layer identifier. Returns: The corresponding layer identifier. """ result = layer if parent: # Note: The image layer is the digest value of the formatted string. result = FormattedSHA256.calculate("{0} {1}".format( parent, layer).encode("utf-8")) return result
async def put_image_layer_from_disk(self, image_name: ImageName, file, **kwargs) -> FormattedSHA256: # pylint: disable=protected-access file_is_async = kwargs.get("file_is_async", True) if file_is_async: file = ( file._file ) # DUCK PUNCH: Unwrap the file handle from the asynchronous object # TODO: Do we really want to use random garbage here??? # Look into moby/.../save.go to find what to use instead. digest = FormattedSHA256.calculate("{0}{1}{2}".format( str(image_name), datetime.datetime.now(), random.randint(1, 101)).encode("utf-8")) layer = ArchiveManifest.digest_to_layer(digest) with open(self.archive, "rb+") as file_out: tar_mkdir(file_out, str(Path(layer).parent)) file_out.seek(0) tar_add_file_from_disk(file_out, layer, file) return digest
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_calculate(): """Test that a formatted SHA256 can be calculated.""" assert ( FormattedSHA256.calculate(b"test data") == "sha256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9" )
def test_get_digest(json_bytes_data: TypingJsonBytesData): """Test raw image json_bytes retrieval.""" digest = json_bytes_data["json_bytes"].get_digest() assert digest == FormattedSHA256.calculate(json_bytes_data["bytes"])
def test_get_digest(manifest_data: TypingManifestData): """Test raw image manifest retrieval.""" digest = manifest_data["manifest"].get_digest() assert digest == FormattedSHA256.calculate(manifest_data["bytes"])