mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
213 lines
6.7 KiB
Python
Executable file
213 lines
6.7 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import hashlib
|
|
import logging
|
|
import pathlib
|
|
import platform
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DIFFOCI_VERSION = "v0.1.5"
|
|
# https://github.com/reproducible-containers/diffoci/releases/download/v0.1.5/SHA256SUMS
|
|
DIFFOCI_CHECKSUMS = """
|
|
ae171821b18c3b9e5cd1953323e79fe5ec1e972e9586474b18227b2cd052e695 diffoci-v0.1.5.darwin-amd64
|
|
fadabdac9be45fb3dfe2a53986422e53dcc6e1fdc8062713c5760e8959a37c2b diffoci-v0.1.5.darwin-arm64
|
|
01d25fe690196945a6bd510d30559338aa489c034d3a1b895a0d82a4b860698f diffoci-v0.1.5.linux-amd64
|
|
5cbc5d13b51183e2988ee0f406d428eb846d51b7c2c12ae17d0775371f43103e diffoci-v0.1.5.linux-arm-v7
|
|
2d067bd1af8a26b2c206c6bf2bde9bcb21062ddb5dc575e110e0e1a93d0d065f diffoci-v0.1.5.linux-arm64
|
|
0923f0c01f270c596fea9f84e529af958d6caba3fa0f6bf4f03df2a12f23b3fc diffoci-v0.1.5.linux-ppc64le
|
|
5821cbc299a90caa167c3a91465292907077ca1123375f88165a842b8970e710 diffoci-v0.1.5.linux-riscv64
|
|
917d7f23d2bd8fcc755cb2f722fc50ffd83389e04838c3b6e9c3463ea96a9be1 diffoci-v0.1.5.linux-s390x
|
|
"""
|
|
DIFFOCI_URL = "https://github.com/reproducible-containers/diffoci/releases/download/{version}/diffoci-{version}.{arch}"
|
|
|
|
DIFFOCI_PATH = (
|
|
pathlib.Path.home() / ".local" / "share" / "dangerzone-dev" / "helpers" / "diffoci"
|
|
)
|
|
IMAGE_NAME = "dangerzone.rocks/dangerzone"
|
|
|
|
|
|
def run(*args):
|
|
"""Simple function that runs a command, validates it, and returns the output"""
|
|
logger.debug(f"Running command: {' '.join(args)}")
|
|
return subprocess.run(
|
|
args,
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
).stdout
|
|
|
|
|
|
def git_commit_get():
|
|
return run("git", "rev-parse", "--short", "HEAD").decode().strip()
|
|
|
|
|
|
def git_determine_tag():
|
|
return run("git", "describe", "--long", "--first-parent").decode().strip()[1:]
|
|
|
|
|
|
def git_verify(commit, source):
|
|
if not commit in source:
|
|
raise RuntimeError(
|
|
f"Image '{source}' does not seem to be built from commit '{commit}'"
|
|
)
|
|
|
|
|
|
def get_platform_arch():
|
|
system = platform.system().lower()
|
|
arch = platform.machine().lower()
|
|
if arch == "x86_64":
|
|
arch = "amd64"
|
|
return f"{system}-{arch}"
|
|
|
|
|
|
def parse_checksums():
|
|
lines = [
|
|
line.replace(f"diffoci-{DIFFOCI_VERSION}.", "").split(" ")
|
|
for line in DIFFOCI_CHECKSUMS.split("\n")
|
|
if line
|
|
]
|
|
return {arch: checksum for checksum, arch in lines}
|
|
|
|
|
|
def diffoci_hash_matches(diffoci):
|
|
"""Check if the hash of the downloaded diffoci bin matches the expected one."""
|
|
arch = get_platform_arch()
|
|
expected_checksum = parse_checksums().get(arch)
|
|
m = hashlib.sha256()
|
|
m.update(diffoci)
|
|
diffoci_checksum = m.hexdigest()
|
|
return diffoci_checksum == expected_checksum
|
|
|
|
|
|
def diffoci_is_installed():
|
|
"""Determine if diffoci has been installed.
|
|
|
|
Determine if diffoci has been installed, by checking if the binary exists, and if
|
|
its hash is the expected one. If the binary exists but the hash is different, then
|
|
this is a sign that we need to update the local diffoci binary.
|
|
"""
|
|
if not DIFFOCI_PATH.exists():
|
|
return False
|
|
return diffoci_hash_matches(DIFFOCI_PATH.open("rb").read())
|
|
|
|
|
|
def diffoci_download():
|
|
"""Download the diffoci tool, based on a URL and its checksum."""
|
|
download_url = DIFFOCI_URL.format(version=DIFFOCI_VERSION, arch=get_platform_arch())
|
|
logger.info(f"Downloading diffoci helper from {download_url}")
|
|
with urllib.request.urlopen(download_url) as f:
|
|
diffoci_bin = f.read()
|
|
|
|
if not diffoci_hash_matches(diffoci_bin):
|
|
raise ValueError(
|
|
"Unexpected checksum for downloaded diffoci binary:"
|
|
f" {diffoci_checksum} !={DIFFOCI_CHECKSUM}"
|
|
)
|
|
|
|
DIFFOCI_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
DIFFOCI_PATH.open("wb+").write(diffoci_bin)
|
|
DIFFOCI_PATH.chmod(DIFFOCI_PATH.stat().st_mode | stat.S_IEXEC)
|
|
|
|
|
|
def diffoci_diff(source, local_target):
|
|
"""Diff the source image against the recently built target image using diffoci."""
|
|
target = f"podman://{local_target}"
|
|
try:
|
|
return run(
|
|
str(DIFFOCI_PATH),
|
|
"diff",
|
|
source,
|
|
target,
|
|
"--semantic",
|
|
"--verbose",
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
error = e.stdout.decode()
|
|
raise RuntimeError(
|
|
f"Could not rebuild an identical image to {source}. Diffoci report:\n{error}"
|
|
)
|
|
|
|
|
|
def build_image(tag, use_cache=False):
|
|
"""Build the Dangerzone container image with a special tag."""
|
|
run(
|
|
"python3",
|
|
"./install/common/build-image.py",
|
|
"--no-save",
|
|
"--use-cache",
|
|
str(use_cache),
|
|
"--tag",
|
|
tag,
|
|
)
|
|
|
|
|
|
def parse_args():
|
|
image_tag = git_determine_tag()
|
|
# TODO: Remove the local "podman://" prefix once we have started pushing images to a
|
|
# remote.
|
|
default_image_name = f"podman://{IMAGE_NAME}:{image_tag}"
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog=sys.argv[0],
|
|
description="Dev script for verifying container image reproducibility",
|
|
)
|
|
parser.add_argument(
|
|
"--source",
|
|
default=default_image_name,
|
|
help=(
|
|
"The name of the image that you want to reproduce. If the image resides in"
|
|
" the local Docker / Podman engine, you can prefix it with podman:// or"
|
|
f" docker:// accordingly (default: {default_image_name})"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--use-cache",
|
|
default=False,
|
|
action="store_true",
|
|
help="Whether to reuse the build cache (off by default for better reproducibility)",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
args = parse_args()
|
|
|
|
logger.info(f"Ensuring that current Git commit matches image '{args.source}'")
|
|
commit = git_commit_get()
|
|
git_verify(commit, args.source)
|
|
|
|
if not diffoci_is_installed():
|
|
diffoci_download()
|
|
|
|
tag = f"reproduce-{commit}"
|
|
target = f"{IMAGE_NAME}:{tag}"
|
|
logger.info(f"Building container image and tagging it as '{target}'")
|
|
build_image(tag, args.use_cache)
|
|
|
|
logger.info(
|
|
f"Ensuring that source image '{args.source}' is semantically identical with"
|
|
f" built image '{target}'"
|
|
)
|
|
try:
|
|
diffoci_diff(args.source, target)
|
|
except subprocess.CalledProcessError as e:
|
|
raise RuntimeError(
|
|
f"Could not reproduce image {args.source} for commit {commit}"
|
|
)
|
|
breakpoint()
|
|
|
|
logger.info(f"Successfully reproduced image '{args.source}' from commit '{commit}'")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|