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