2026-07-14 11:27:49 +02:00
|
|
|
"""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(
|
2026-07-22 17:47:32 +02:00
|
|
|
f"Image Checksum is not defined. Try to download image signature from {checksum_url}."
|
2026-07-14 11:27:49 +02:00
|
|
|
)
|
|
|
|
|
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(
|
2026-07-22 17:47:32 +02:00
|
|
|
"Proceeding to signature verification, which ensures the file comes from a trusted source."
|
2026-07-14 11:27:49 +02:00
|
|
|
)
|
|
|
|
|
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.")
|