From c2d37dfb049bb69d7c24ab48cc729b98905512cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Wed, 5 Feb 2025 16:54:13 +0100 Subject: [PATCH] Check signatures before invoking the container. Also, check for new container images when starting the application. This replaces the usage of `share/image-id.txt` to ensure the image is trusted. --- dangerzone/container_utils.py | 9 +--- dangerzone/isolation_provider/container.py | 59 +++++++++------------- dangerzone/updater/__init__.py | 7 +++ dangerzone/updater/cli.py | 4 +- dangerzone/updater/signatures.py | 12 +++-- 5 files changed, 42 insertions(+), 49 deletions(-) diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index a6ef543..afb6da5 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -8,7 +8,8 @@ from typing import List, Optional, Tuple from . import errors from .util import get_resource_path, get_subprocess_startupinfo -CONTAINER_NAME = "dangerzone.rocks/dangerzone" +OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone" +CONTAINER_NAME = "ghcr.io/almet/dangerzone/dangerzone" log = logging.getLogger(__name__) @@ -110,12 +111,6 @@ def delete_image_tag(tag: str) -> None: ) -def get_expected_tag() -> str: - """Get the tag of the Dangerzone image tarball from the image-id.txt file.""" - with open(get_resource_path("image-id.txt")) as f: - return f.read().strip() - - def load_image_tarball_in_memory() -> None: log.info("Installing Dangerzone container image...") p = subprocess.Popen( diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index adc5c49..bc810d4 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -5,7 +5,7 @@ import shlex import subprocess from typing import List, Tuple -from .. import container_utils, errors +from .. import container_utils, errors, updater from ..document import Document from ..util import get_resource_path, get_subprocess_startupinfo from .base import IsolationProvider, terminate_process_group @@ -78,41 +78,23 @@ class Container(IsolationProvider): @staticmethod def install() -> bool: - """Install the container image tarball, or verify that it's already installed. + """Check if an update is available and install it if necessary.""" + # XXX Do this only if users have optted in to auto-updates - Perform the following actions: - 1. Get the tags of any locally available images that match Dangerzone's image - name. - 2. Get the expected image tag from the image-id.txt file. - - If this tag is present in the local images, then we can return. - - Else, prune the older container images and continue. - 3. Load the image tarball and make sure it matches the expected tag. - """ - old_tags = container_utils.list_image_tags() - expected_tag = container_utils.get_expected_tag() - - if expected_tag not in old_tags: - # Prune older container images. - log.info( - f"Could not find a Dangerzone container image with tag '{expected_tag}'" + # # Load the image tarball into the container runtime. + update_available, image_digest = updater.is_update_available( + container_utils.CONTAINER_NAME + ) + if update_available: + updater.upgrade_container_image( + container_utils.CONTAINER_NAME, + image_digest, + updater.DEFAULT_PUBKEY_LOCATION, ) - for tag in old_tags: - container_utils.delete_image_tag(tag) - else: - return True - # Load the image tarball into the container runtime. - container_utils.load_image_tarball_in_memory() - - # Check that the container image has the expected image tag. - # See https://github.com/freedomofpress/dangerzone/issues/988 for an example - # where this was not the case. - new_tags = container_utils.list_image_tags() - if expected_tag not in new_tags: - raise errors.ImageNotPresentException( - f"Could not find expected tag '{expected_tag}' after loading the" - " container image tarball" - ) + updater.verify_local_image( + container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION + ) return True @@ -193,6 +175,13 @@ class Container(IsolationProvider): name: str, ) -> subprocess.Popen: container_runtime = container_utils.get_runtime() + + image_digest = container_utils.get_local_image_digest( + container_utils.CONTAINER_NAME + ) + updater.verify_local_image( + container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION + ) security_args = self.get_runtime_security_args() debug_args = [] if self.debug: @@ -201,9 +190,7 @@ class Container(IsolationProvider): enable_stdin = ["-i"] set_name = ["--name", name] prevent_leakage_args = ["--rm"] - image_name = [ - container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag() - ] + image_name = [container_utils.CONTAINER_NAME + "@sha256:" + image_digest] args = ( ["run"] + security_args diff --git a/dangerzone/updater/__init__.py b/dangerzone/updater/__init__.py index 3988bf1..57bfa1c 100644 --- a/dangerzone/updater/__init__.py +++ b/dangerzone/updater/__init__.py @@ -1,3 +1,10 @@ import logging log = logging.getLogger(__name__) + +from .signatures import ( + DEFAULT_PUBKEY_LOCATION, + is_update_available, + upgrade_container_image, + verify_local_image, +) diff --git a/dangerzone/updater/cli.py b/dangerzone/updater/cli.py index c243bb9..bc6d88f 100644 --- a/dangerzone/updater/cli.py +++ b/dangerzone/updater/cli.py @@ -4,13 +4,11 @@ import logging import click -from ..util import get_resource_path from . import attestations, errors, log, registry, signatures DEFAULT_REPOSITORY = "freedomofpress/dangerzone" DEFAULT_BRANCH = "main" DEFAULT_IMAGE_NAME = "ghcr.io/freedomofpress/dangerzone/dangerzone" -PUBKEY_DEFAULT_LOCATION = get_resource_path("freedomofpress-dangerzone-pub.key") @click.group() @@ -26,7 +24,7 @@ def main(debug: bool) -> None: @main.command() @click.argument("image", default=DEFAULT_IMAGE_NAME) -@click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION) +@click.option("--pubkey", default=signatures.DEFAULT_PUBKEY_LOCATION) def upgrade(image: str, pubkey: str) -> None: """Upgrade the image to the latest signed version.""" manifest_digest = registry.get_manifest_digest(image) diff --git a/dangerzone/updater/signatures.py b/dangerzone/updater/signatures.py index e60f9f9..c87acca 100644 --- a/dangerzone/updater/signatures.py +++ b/dangerzone/updater/signatures.py @@ -11,6 +11,7 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory from typing import Dict, List, Optional, Tuple from .. import container_utils as runtime +from ..util import get_resource_path from . import cosign, errors, log, registry try: @@ -24,6 +25,7 @@ def get_config_dir() -> Path: # XXX Store this somewhere else. +DEFAULT_PUBKEY_LOCATION = get_resource_path("freedomofpress-dangerzone-pub.key") SIGNATURES_PATH = get_config_dir() / "signatures" __all__ = [ "verify_signature", @@ -103,12 +105,15 @@ def verify_signature(signature: dict, image_digest: str, pubkey: str) -> bool: return False -def is_update_available(image: str) -> bool: +def is_update_available(image: str) -> (bool, Optional[str]): remote_digest = registry.get_manifest_digest(image) local_digest = runtime.get_local_image_digest(image) log.debug("Remote digest: %s", remote_digest) log.debug("Local digest: %s", local_digest) - return remote_digest != local_digest + has_update = remote_digest != local_digest + if has_update: + return True, remote_digest + return False, None def verify_signatures( @@ -124,7 +129,8 @@ def verify_signatures( def upgrade_container_image(image: str, manifest_digest: str, pubkey: str) -> bool: """Verify and upgrade the image to the latest, if signed.""" - if not is_update_available(image): + update_available, _ = is_update_available(image) + if not update_available: raise errors.ImageAlreadyUpToDate("The image is already up to date") signatures = get_remote_signatures(image, manifest_digest)