async def verify_image_integrity(self, image_name: ImageName, **kwargs) -> Dict: data = await self._verify_image_config(image_name) # Note: We do not need to reconcile manifest layer ids here, as "we" derived them in # :func:docker_sign_verify.manifests.DeviceMapperRepositoryManifest.get_layers. # Reconcile manifest layers and image layers (in order)... uncompressed_layer_files = [] try: for i, layer in enumerate(data.manifest_layers): # Retrieve the repository image layer and verify the digest ... uncompressed_layer_files.append(tempfile.NamedTemporaryFile()) data_compressed = await self.get_image_layer_to_disk( image_name, layer, uncompressed_layer_files[i] ) must_be_equal( data.image_layers[i], data_compressed.digest, f"Repository layer[{i}] digest mismatch", ) except Exception: # for file in uncompressed_layer_files: # file.close() raise LOGGER.debug("Integrity check passed.") return { "compressed_layer_files": "TODO", "image_config": data.image_config, "manifest": data.manifest, "uncompressed_layer_files": uncompressed_layer_files, }
async def verify_image_integrity(self, image_name: ImageName, **kwargs) -> Dict: data = await self._verify_image_config(image_name) # Reconcile manifest layers and image layers (in order)... uncompressed_layer_files = [] for i, layer in enumerate(data["manifest_layers"]): # Retrieve the archive image layer and verify the digest ... uncompressed_layer_files.append(tempfile.NamedTemporaryFile()) data_uncompressed = await self.get_image_layer_to_disk( image_name, layer, uncompressed_layer_files[i]) must_be_equal( data["image_layers"][i], data_uncompressed["digest"], "Archive layer[{0}] digest mismatch".format(i), ) LOGGER.debug("Integrity check passed.") return { "compressed_layer_files": "TODO", "image_config": data["image_config"], "manifest": data["manifest"], "uncompressed_layer_files": uncompressed_layer_files, }
async def verify_image_integrity( self, image_name: ImageName, **kwargs) -> ImageSourceVerifyImageIntegrity: data = await self._verify_image_config(image_name, **kwargs) # Reconcile manifest layers and image layers (in order)... uncompressed_layer_files = [] try: for i, layer in enumerate(data.manifest_layers): # Retrieve the archive image layer and verify the digest ... uncompressed_layer_files.append( await aiotempfile(prefix="tmp-uncompressed")) data_uncompressed = await self.get_image_layer_to_disk( image_name, layer, uncompressed_layer_files[i]) must_be_equal( data.image_layers[i], data_uncompressed.digest, f"Archive layer[{i}] digest mismatch", ) except Exception: for file in uncompressed_layer_files: file.close() raise LOGGER.debug("Integrity check passed.") return ImageSourceVerifyImageIntegrity( compressed_layer_files=[], # TODO: Implement this image_config=data.image_config, manifest=data.manifest, uncompressed_layer_files=uncompressed_layer_files, )
def test_must_be_equal_msg(expected: Any = "bar", actual: Any = "foo"): """Test that an custom error message can be used.""" message = "custom message here" with pytest.raises(RuntimeError) as exc_info: must_be_equal(expected, actual, message) assert str(expected) in str(exc_info.value) assert str(actual) in str(exc_info.value) assert message in str(exc_info.value)
def test_must_be_equal(expected: Any, actual: Any, result: bool): """Test that equality can be determined.""" if not result: with pytest.raises(RuntimeError) as exc_info: must_be_equal(expected, actual) assert str(expected) in str(exc_info.value) assert str(actual) in str(exc_info.value) assert "does not match" in str(exc_info.value) else: must_be_equal(expected, actual)
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 verify_signatures( self, *, signer_kwargs: Dict[str, Dict] = None) -> ImageConfigVerifySignatures: """ Verifies the PEM encoded signature values in the image configuration. Args: signer_kwargs: Parameters to be passed to the underlying Signer instance. Returns: NamedTuple: signature_data: List as defined by :func:~docker_sign_verify.ImageConfig.get_signature_list. results: Signer-specific result value. """ signatures = self.get_signature_list() if not signatures: raise NoSignatureError() # Assumptions: # * The signature list is ordered. # * The first entry in the list *must* be a (co-)signature, as there is nothing older to endorse. # * Normalization during canonicalization ensures a consistent empty set. results = [] for i, signature_entry in enumerate(signatures): _temp = self.clone() # (Co-)signature if signature_entry.digest == signatures[0].digest: _temp.clear_signature_list() # Endorsement else: _temp.set_signature_list(signatures[:i]) digest = _temp.get_digest_canonical() must_be_equal( signature_entry.digest, digest, "Image config canonical digest mismatch", error_type=DigestMismatchError, ) signer = Signer.for_signature(signature_entry.signature, signer_kwargs=signer_kwargs) result = await signer.verify(digest.encode("utf-8"), signature_entry.signature) results.append(result) return ImageConfigVerifySignatures(results=results, signatures=signatures)
async def verify_signatures(self) -> ImageConfigVerifySignatures: """ Verifies the PEM encoded signature values in the image configuration. Returns: dict: signature_data: List as defined by :func:~docker_sign_verify.ImageConfig.get_signature_list. results: Signer-specific result value. """ signatures = self.get_signature_list() if not signatures: raise NoSignatureError() # Assumptions: # * The signature list is ordered. # * The first entry in the list *must* be a (co-)signature, as there is nothing older to endorse. # * Normalization during canonicalization ensures a consistent empty set. results = [] for i, signature in enumerate(signatures): _temp = deepcopy(self) # (Co-)signature if signature["digest"] == signatures[0]["digest"]: _temp.clear_signature_list() # Endorsement else: _temp.set_signature_list(signatures[:i]) digest = _temp.get_digest_canonical() must_be_equal( signature["digest"], digest, "Image config canonical digest mismatch", error_type=DigestMismatchError, ) signer = Signer.for_signature(signature["signature"]) result = await signer.verify(digest.encode("utf-8"), signature["signature"]) results.append(result) return {"signatures": signatures, "results": results}
async def verify_image_integrity( self, image_name: ImageName, **kwargs) -> ImageSourceVerifyImageIntegrity: data = await self._verify_image_config(image_name, **kwargs) # Reconcile manifest layers and image layers (in order)... compressed_layer_files = [] uncompressed_layer_files = [] try: for i, layer in enumerate(data.manifest_layers): # Retrieve the registry image layer and verify the digest ... compressed_layer_files.append( await aiotempfile(prefix="tmp-compressed")) data_compressed = await self.get_image_layer_to_disk( image_name, layer, compressed_layer_files[i], **kwargs) must_be_equal( layer, data_compressed.digest, f"Registry layer[{i}] digest mismatch", ) must_be_equal( os.path.getsize(compressed_layer_files[i].name), data_compressed.size, f"Registry layer[{i}] size mismatch", ) # Decompress (convert) the registry image layer into the image layer # and verify the digest ... uncompressed_layer_files.append( await aiotempfile(prefix="tmp-uncompressed")) data_uncompressed = await gunzip( compressed_layer_files[i].name, uncompressed_layer_files[i]) must_be_equal( data.image_layers[i], data_uncompressed.digest, f"Image layer[{i}] digest mismatch", ) except Exception: for file in compressed_layer_files + uncompressed_layer_files: file.close() raise LOGGER.debug("Integrity check passed.") return ImageSourceVerifyImageIntegrity( compressed_layer_files=compressed_layer_files, image_config=data.image_config, manifest=data.manifest, uncompressed_layer_files=uncompressed_layer_files, )
async def verify_image_integrity( self, image_name: ImageName, **kwargs) -> ImageSourceVerifyImageIntegrity: data = await self._verify_image_config(image_name, **kwargs) # Reconcile manifest layers and image layers (in order)... compressed_layer_files = [] uncompressed_layer_files = [] for i, layer in enumerate(data["manifest_layers"]): # Retrieve the registry image layer and verify the digest ... compressed_layer_files.append(await aiotempfile()) data_compressed = await self.get_image_layer_to_disk( image_name, layer, compressed_layer_files[i], **kwargs) must_be_equal( layer, data_compressed["digest"], "Registry layer[{0}] digest mismatch".format(i), ) must_be_equal( os.path.getsize(compressed_layer_files[i].name), data_compressed["size"], "Registry layer[{0}] size mismatch".format(i), ) # Decompress (convert) the registry image layer into the image layer # and verify the digest ... uncompressed_layer_files.append(await aiotempfile()) data_uncompressed = await gunzip(compressed_layer_files[i].name, uncompressed_layer_files[i]) must_be_equal( data["image_layers"][i], data_uncompressed["digest"], "Image layer[{0}] digest mismatch".format(i), ) LOGGER.debug("Integrity check passed.") return { "compressed_layer_files": compressed_layer_files, "image_config": data["image_config"], "manifest": data["manifest"], "uncompressed_layer_files": uncompressed_layer_files, }