From 858d31458bb87c222ef067e9cdc51122793929d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Tue, 4 Feb 2025 16:18:18 +0100 Subject: [PATCH] fix(icu): update documentation and fixes --- dangerzone/container_utils.py | 12 ++++--- dangerzone/updater/cli.py | 22 +++++++----- dangerzone/updater/errors.py | 4 +++ dangerzone/updater/signatures.py | 17 ++++++--- .../independent-container-updates.md | 35 ++++++++++++++----- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index ea95e1a..7c9dcb8 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -3,7 +3,7 @@ import logging import platform import shutil import subprocess -from typing import List, Tuple +from typing import List, Optional, Tuple from . import errors from .util import get_resource_path, get_subprocess_startupinfo @@ -192,10 +192,14 @@ def container_pull(image: str) -> bool: return process.returncode == 0 -def get_local_image_hash(image: str) -> str: +def get_local_image_hash(image: str) -> Optional[str]: """ Returns a image hash from a local image name """ cmd = [get_runtime_name(), "image", "inspect", image, "-f", "{{.Digest}}"] - result = subprocess.run(cmd, capture_output=True, check=True) - return result.stdout.strip().decode().strip("sha256:") + try: + result = subprocess.run(cmd, capture_output=True, check=True) + except subprocess.CalledProcessError as e: + return None + else: + return result.stdout.strip().decode().strip("sha256:") diff --git a/dangerzone/updater/cli.py b/dangerzone/updater/cli.py index c8eedae..76e466a 100644 --- a/dangerzone/updater/cli.py +++ b/dangerzone/updater/cli.py @@ -8,7 +8,7 @@ from ..util import get_resource_path from . import attestations, errors, log, registry, signatures DEFAULT_REPOSITORY = "freedomofpress/dangerzone" -DEFAULT_IMAGE_NAME = "ghcr.io/freedomofpress/dangerzone" +DEFAULT_IMAGE_NAME = "ghcr.io/freedomofpress/dangerzone/dangerzone" PUBKEY_DEFAULT_LOCATION = get_resource_path("freedomofpress-dangerzone-pub.key") @@ -24,14 +24,18 @@ def main(debug: bool) -> None: @main.command() -@click.argument("image") +@click.argument("image", default=DEFAULT_IMAGE_NAME) @click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION) def upgrade(image: str, pubkey: str) -> None: """Upgrade the image to the latest signed version.""" manifest_hash = registry.get_manifest_hash(image) try: is_upgraded = signatures.upgrade_container_image(image, manifest_hash, pubkey) - click.echo(f"✅ The local image {image} has been upgraded") + if is_upgraded: + click.echo(f"✅ The local image {image} has been upgraded") + click.echo(f"✅ The image has been signed with {pubkey}") + click.echo(f"✅ Signatures has been verified and stored locally") + except errors.ImageAlreadyUpToDate as e: click.echo(f"✅ {e}") raise click.Abort() @@ -56,15 +60,15 @@ def load_archive(image_filename: str, pubkey: str) -> None: @main.command() @click.argument("image") -@click.option("--destination", default="dangerzone-airgapped.tar") -def prepare_archive(image: str, destination: str) -> None: +@click.option("--output", default="dangerzone-airgapped.tar") +def prepare_archive(image: str, output: str) -> None: """Prepare an archive to upgrade the dangerzone image on an airgapped environment.""" - signatures.prepare_airgapped_archive(image, destination) - click.echo(f"✅ Archive {destination} created") + signatures.prepare_airgapped_archive(image, output) + click.echo(f"✅ Archive {output} created") @main.command() -@click.argument("image") +@click.argument("image", default=DEFAULT_IMAGE_NAME) @click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION) def verify_local(image: str, pubkey: str) -> None: """ @@ -85,6 +89,7 @@ def verify_local(image: str, pubkey: str) -> None: @main.command() @click.argument("image") def list_remote_tags(image: str) -> None: + """List the tags available for a given image.""" click.echo(f"Existing tags for {image}") for tag in registry.list_tags(image): click.echo(tag) @@ -93,6 +98,7 @@ def list_remote_tags(image: str) -> None: @main.command() @click.argument("image") def get_manifest(image: str) -> None: + """Retrieves a remove manifest for a given image and displays it.""" click.echo(registry.get_manifest(image)) diff --git a/dangerzone/updater/errors.py b/dangerzone/updater/errors.py index 935ce3c..d302975 100644 --- a/dangerzone/updater/errors.py +++ b/dangerzone/updater/errors.py @@ -6,6 +6,10 @@ class ImageAlreadyUpToDate(UpdaterError): pass +class ImageNotFound(UpdaterError): + pass + + class SignatureError(UpdaterError): pass diff --git a/dangerzone/updater/signatures.py b/dangerzone/updater/signatures.py index a6a9d41..e8db4cc 100644 --- a/dangerzone/updater/signatures.py +++ b/dangerzone/updater/signatures.py @@ -64,8 +64,13 @@ def verify_signature(signature: dict, image_hash: str, pubkey: str) -> bool: signature_bundle = signature_to_bundle(signature) payload_bytes = b64decode(signature_bundle["Payload"]) - if json.loads(payload_bytes)["critical"]["type"] != f"sha256:{image_hash}": - raise errors.SignatureMismatch("The signature does not match the image hash") + payload_hash = json.loads(payload_bytes)["critical"]["image"][ + "docker-manifest-digest" + ] + if payload_hash != f"sha256:{image_hash}": + raise errors.SignatureMismatch( + f"The signature does not match the image hash ({payload_hash}, {image_hash})" + ) with ( NamedTemporaryFile(mode="w") as signature_file, @@ -220,7 +225,7 @@ def convert_oci_images_signatures( "Payload": payload_b64, "Cert": None, "Chain": None, - "rekorBundle": bundle, + "Bundle": bundle, "RFC3161Timestamp": None, } @@ -311,7 +316,11 @@ def verify_local_image(image: str, pubkey: str) -> bool: Verifies that a local image has a valid signature """ log.info(f"Verifying local image {image} against pubkey {pubkey}") - image_hash = runtime.get_local_image_hash(image) + try: + image_hash = runtime.get_local_image_hash(image) + except subprocess.CalledProcessError: + raise errors.ImageNotFound(f"The image {image} does not exist locally") + log.debug(f"Image hash: {image_hash}") signatures = load_signatures(image_hash, pubkey) if len(signatures) < 1: diff --git a/docs/developer/independent-container-updates.md b/docs/developer/independent-container-updates.md index 7f5029e..51c40df 100644 --- a/docs/developer/independent-container-updates.md +++ b/docs/developer/independent-container-updates.md @@ -1,19 +1,19 @@ # Independent Container Updates Since version 0.9.0, Dangerzone is able to ship container images independently -from issuing a new release of the software. +from releases. -This is useful as images need to be kept updated with the latest security fixes. +One of the main benefits of doing so is to lower the time needed to patch security issues inside the containers. -## Nightly images and attestations +## Checking attestations -Each night, new images are built and pushed to our container registry, alongside +Each night, new images are built and pushed to the container registry, alongside with a provenance attestation, enabling anybody to ensure that the image has been originally built by Github CI runners, from a defined source repository (in our case `freedomofpress/dangerzone`). To verify the attestations against our expectations, use the following command: ```bash -poetry run ./dev_scripts/registry.py attest ghcr.io/freedomofpress/dangerzone/dangerzone:latest --repo freedomofpress/dangerzone +dangerzone-image attest-provenance ghcr.io/freedomofpress/dangerzone/dangerzone --repository freedomofpress/dangerzone ``` In case of sucess, it will report back: @@ -22,18 +22,35 @@ In case of sucess, it will report back: 🎉 The image available at `ghcr.io/freedomofpress/dangerzone/dangerzone:latest` has been built by Github runners from the `freedomofpress/dangerzone` repository. ``` -## Container updates on air-gapped environments +## Install updates + +To check if a new container image has been released, and update your local installation with it, you can use the following commands: + +```bash +./dev_scripts/dangerzone-image --debug upgrade ghcr.io/almet/dangerzone/dangerzone +``` + +## Verify local + +You can verify that the image you have locally matches the stored signatures, and that these have been signed with a trusted public key: + +```bash +dangerzone-image verify-local ghcr.io/almet/dangerzone/dangerzone +``` + +## Air-gapped environments In order to make updates on an air-gapped environment, you will need to prepare an archive for the air-gapped environment. This archive will contain all the needed material to validate that the new container image has been signed and is valid. On the machine on which you prepare the packages: ```bash -dangerzone-image prepare-archive ghcr.io/almet/dangerzone/dangerzone@sha256:fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7 +dangerzone-image prepare-archive --output dz-fa94872.tar ghcr.io/almet/dangerzone/dangerzone@sha256:fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7 ``` On the airgapped machine, copy the file and run the following command: ```bash -dangerzone-image load-archive -``` \ No newline at end of file +dangerzone-image load-archive dz-fa94872.tar +``` +