fix(icu): update documentation and fixes

This commit is contained in:
Alexis Métaireau 2025-02-04 16:18:18 +01:00
parent 3b858dac27
commit 858d31458b
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
5 changed files with 65 additions and 25 deletions

View file

@ -3,7 +3,7 @@ import logging
import platform import platform
import shutil import shutil
import subprocess import subprocess
from typing import List, Tuple from typing import List, Optional, Tuple
from . import errors from . import errors
from .util import get_resource_path, get_subprocess_startupinfo from .util import get_resource_path, get_subprocess_startupinfo
@ -192,10 +192,14 @@ def container_pull(image: str) -> bool:
return process.returncode == 0 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 Returns a image hash from a local image name
""" """
cmd = [get_runtime_name(), "image", "inspect", image, "-f", "{{.Digest}}"] cmd = [get_runtime_name(), "image", "inspect", image, "-f", "{{.Digest}}"]
try:
result = subprocess.run(cmd, capture_output=True, check=True) result = subprocess.run(cmd, capture_output=True, check=True)
except subprocess.CalledProcessError as e:
return None
else:
return result.stdout.strip().decode().strip("sha256:") return result.stdout.strip().decode().strip("sha256:")

View file

@ -8,7 +8,7 @@ from ..util import get_resource_path
from . import attestations, errors, log, registry, signatures from . import attestations, errors, log, registry, signatures
DEFAULT_REPOSITORY = "freedomofpress/dangerzone" 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") PUBKEY_DEFAULT_LOCATION = get_resource_path("freedomofpress-dangerzone-pub.key")
@ -24,14 +24,18 @@ def main(debug: bool) -> None:
@main.command() @main.command()
@click.argument("image") @click.argument("image", default=DEFAULT_IMAGE_NAME)
@click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION) @click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION)
def upgrade(image: str, pubkey: str) -> None: def upgrade(image: str, pubkey: str) -> None:
"""Upgrade the image to the latest signed version.""" """Upgrade the image to the latest signed version."""
manifest_hash = registry.get_manifest_hash(image) manifest_hash = registry.get_manifest_hash(image)
try: try:
is_upgraded = signatures.upgrade_container_image(image, manifest_hash, pubkey) is_upgraded = signatures.upgrade_container_image(image, manifest_hash, pubkey)
if is_upgraded:
click.echo(f"✅ The local image {image} has been 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: except errors.ImageAlreadyUpToDate as e:
click.echo(f"{e}") click.echo(f"{e}")
raise click.Abort() raise click.Abort()
@ -56,15 +60,15 @@ def load_archive(image_filename: str, pubkey: str) -> None:
@main.command() @main.command()
@click.argument("image") @click.argument("image")
@click.option("--destination", default="dangerzone-airgapped.tar") @click.option("--output", default="dangerzone-airgapped.tar")
def prepare_archive(image: str, destination: str) -> None: def prepare_archive(image: str, output: str) -> None:
"""Prepare an archive to upgrade the dangerzone image on an airgapped environment.""" """Prepare an archive to upgrade the dangerzone image on an airgapped environment."""
signatures.prepare_airgapped_archive(image, destination) signatures.prepare_airgapped_archive(image, output)
click.echo(f"✅ Archive {destination} created") click.echo(f"✅ Archive {output} created")
@main.command() @main.command()
@click.argument("image") @click.argument("image", default=DEFAULT_IMAGE_NAME)
@click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION) @click.option("--pubkey", default=PUBKEY_DEFAULT_LOCATION)
def verify_local(image: str, pubkey: str) -> None: def verify_local(image: str, pubkey: str) -> None:
""" """
@ -85,6 +89,7 @@ def verify_local(image: str, pubkey: str) -> None:
@main.command() @main.command()
@click.argument("image") @click.argument("image")
def list_remote_tags(image: str) -> None: def list_remote_tags(image: str) -> None:
"""List the tags available for a given image."""
click.echo(f"Existing tags for {image}") click.echo(f"Existing tags for {image}")
for tag in registry.list_tags(image): for tag in registry.list_tags(image):
click.echo(tag) click.echo(tag)
@ -93,6 +98,7 @@ def list_remote_tags(image: str) -> None:
@main.command() @main.command()
@click.argument("image") @click.argument("image")
def get_manifest(image: str) -> None: def get_manifest(image: str) -> None:
"""Retrieves a remove manifest for a given image and displays it."""
click.echo(registry.get_manifest(image)) click.echo(registry.get_manifest(image))

View file

@ -6,6 +6,10 @@ class ImageAlreadyUpToDate(UpdaterError):
pass pass
class ImageNotFound(UpdaterError):
pass
class SignatureError(UpdaterError): class SignatureError(UpdaterError):
pass pass

View file

@ -64,8 +64,13 @@ def verify_signature(signature: dict, image_hash: str, pubkey: str) -> bool:
signature_bundle = signature_to_bundle(signature) signature_bundle = signature_to_bundle(signature)
payload_bytes = b64decode(signature_bundle["Payload"]) payload_bytes = b64decode(signature_bundle["Payload"])
if json.loads(payload_bytes)["critical"]["type"] != f"sha256:{image_hash}": payload_hash = json.loads(payload_bytes)["critical"]["image"][
raise errors.SignatureMismatch("The signature does not match the image hash") "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 ( with (
NamedTemporaryFile(mode="w") as signature_file, NamedTemporaryFile(mode="w") as signature_file,
@ -220,7 +225,7 @@ def convert_oci_images_signatures(
"Payload": payload_b64, "Payload": payload_b64,
"Cert": None, "Cert": None,
"Chain": None, "Chain": None,
"rekorBundle": bundle, "Bundle": bundle,
"RFC3161Timestamp": None, "RFC3161Timestamp": None,
} }
@ -311,7 +316,11 @@ def verify_local_image(image: str, pubkey: str) -> bool:
Verifies that a local image has a valid signature Verifies that a local image has a valid signature
""" """
log.info(f"Verifying local image {image} against pubkey {pubkey}") log.info(f"Verifying local image {image} against pubkey {pubkey}")
try:
image_hash = runtime.get_local_image_hash(image) 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}") log.debug(f"Image hash: {image_hash}")
signatures = load_signatures(image_hash, pubkey) signatures = load_signatures(image_hash, pubkey)
if len(signatures) < 1: if len(signatures) < 1:

View file

@ -1,19 +1,19 @@
# Independent Container Updates # Independent Container Updates
Since version 0.9.0, Dangerzone is able to ship container images independently 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 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`). 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: To verify the attestations against our expectations, use the following command:
```bash ```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: 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. 🎉 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. 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: On the machine on which you prepare the packages:
```bash ```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: On the airgapped machine, copy the file and run the following command:
```bash ```bash
dangerzone-image load-archive dangerzone-image load-archive dz-fa94872.tar
``` ```