Files
linux-image-manager/lim/image/verify.py
Kevin Veen-Birkenbach ccdef065df refactor!: port shell scripts to Python package
Bash scripts were untestable and duplicated device/LUKS/mount logic;
the lim/ package centralizes it behind one subprocess wrapper and a
YAML image catalog (single point of truth).

BREAKING CHANGE: scripts/*.sh removed. Use `lim --type <cmd>`; new
types mount/umount/single-boot/raid1-boot/lock/unlock/import/export
replace direct script calls. --extra is deprecated and ignored.

- distributions.yml + lim/catalog.py hold the image catalog (PyYAML)
- pytest suite: 102 tests with mocked subprocess (tests/unit) and a
  250-line max file-length guard (tests/lint)
- ruff strict (select ALL), GitHub Actions CI, Dependabot; Travis gone
- Makefile: install (symlink ~/.local/bin/lim) and test targets
- fixes over bash: SUDO_USER-aware chown, mmcblk/nvme partition paths,
  sha512 checksum support, whole-pipeline failure detection, blkid
  UUID fallback for pre-mounted images, conditional fstab seeding for
  PARTUUID/LABEL images, clean errors for missing binaries
2026-07-14 11:37:19 +02:00

101 lines
3.7 KiB
Python

"""Image integrity (checksums) and authenticity (GPG signatures) checks."""
import hashlib
import re
from pathlib import Path
from lim import runner, ui
from lim.errors import LimError
ALGORITHM_BY_DIGEST_LENGTH = {
32: "md5",
40: "sha1",
64: "sha256",
128: "sha512",
}
def url_exists(url: str) -> bool:
return runner.succeeds(["wget", "-q", "--method=HEAD", url])
def resolve_checksum(download_url: str) -> str | None:
"""Try to fetch a published checksum next to the image download."""
for extension in ("sha1", "sha512", "md5"):
checksum_url = f"{download_url}.{extension}"
ui.info(
"Image Checksum is not defined. "
f"Try to download image signature from {checksum_url}."
)
if url_exists(checksum_url):
content = runner.output(["wget", checksum_url, "-q", "-O", "-"])
checksum = content.split()[0] if content.split() else ""
if checksum:
ui.info(f"Defined image_checksum as {checksum}")
return checksum
ui.warning(f"No checksum found under {checksum_url}.")
return None
def verify_checksum(image_path: str | Path, checksum: str | None) -> None:
"""Verify the image against an md5/sha1/sha256/sha512 hex checksum."""
if not checksum:
ui.warning("No checksum is defined. Skipping checksum verification.")
return
algorithm = ALGORITHM_BY_DIGEST_LENGTH.get(len(checksum))
if algorithm is None:
raise LimError(
f"Checksum '{checksum}' has no recognized digest length "
"(expected md5, sha1, sha256 or sha512)."
)
ui.info(f"Checking {algorithm} checksum...")
digest = hashlib.new(algorithm)
with Path(image_path).open("rb") as handle:
while chunk := handle.read(1024 * 1024):
digest.update(chunk)
if digest.hexdigest() != checksum.lower():
raise LimError("Verification failed. HINT: Force the download of the image.")
ui.info(f"{algorithm} checksum verified.")
def verify_signature(download_url: str, image_path: str | Path, image_folder: Path) -> None:
"""Best-effort GPG signature verification of the downloaded image."""
ui.info("Note: Checksums verify integrity but do not confirm authenticity.")
ui.info(
"Proceeding to signature verification, "
"which ensures the file comes from a trusted source."
)
signature_url = f"{download_url}.sig"
ui.info(f"Attempting to download the image signature from: {signature_url}")
if not url_exists(signature_url):
ui.warning(f"No signature found under {signature_url}.")
return
signature_path = image_folder / (Path(str(image_path)).name + ".sig")
if not runner.succeeds(["wget", "-q", "-O", str(signature_path), signature_url]):
ui.warning("Failed to download the signature file.")
return
ui.info("Extract the key ID from the signature file")
verification = runner.output(
["gpg", "--status-fd", "1", "--verify", str(signature_path), str(image_path)],
check=False,
)
missing_keys = re.findall(r"NO_PUBKEY (\S+)", verification)
if missing_keys:
key_id = missing_keys[-1]
if runner.succeeds(["gpg", "--list-keys", key_id]):
ui.info(f"Key {key_id} already in keyring.")
else:
ui.info("Import the public key")
runner.run(
["gpg", "--keyserver", "keyserver.ubuntu.com", "--recv-keys", key_id],
check=False,
)
ui.info("Verify the signature")
if runner.succeeds(["gpg", "--verify", str(signature_path), str(image_path)]):
ui.info("Signature verification succeeded.")
else:
ui.warning("Signature verification failed.")