Only in /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2: .distro diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/.gitlab-ci.yml /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/.gitlab-ci.yml --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/.gitlab-ci.yml 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/.gitlab-ci.yml 2026-08-30 19:41:17.000000000 +0000 @@ -2,5 +2,5 @@ variables: - GIT_SUBMODULE_STRATEGY: normal + GIT_SUBMODULE_STRATEGY: none workflow: @@ -21,4 +21,6 @@ .testscript: &testscript + - git submodule sync + - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/" - git submodule update --init tests/test-data - make unit_tests diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/README.md /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/README.md --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/README.md 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/README.md 2026-08-30 19:41:17.000000000 +0000 @@ -171,4 +171,29 @@ | `-r`, `--recovery-key FILE` | Add a recovery key passphrase to the root volume | | `--recovery-key-type {binary,text,both}` | Recovery key format (default: `binary`) | +| `--volume-digest-key FILE` | PEM private key to sign the `x-cvmutils` structure (adds `x-cvmutils-sig` to LUKS tokens) | + +### LUKS Token Annotations + +After sealing with `systemd-cryptenroll`, the deploy phase annotates each newly created LUKS2 token with the following custom fields: + +| Field | Type | Description | +|-------|------|-------------| +| `x-cvmutils` | object | Structure containing `timestamp` (Unix integer), `version` (string), and `volume-digest` (HMAC-SHA256 of `cryptsetup:root:` keyed with the LUKS master key, hex-encoded) | +| `x-cvmutils-sig` | string | RSA/EC signature over the canonical JSON serialisation of `x-cvmutils` (keys sorted, no spaces), signed with the key from `--volume-digest-key`, base64-encoded (only present when `--volume-digest-key` is specified) | + +The volume digest and its signature can be used to verify the integrity and origin of the LUKS volume. To verify the signature on a running system: + +```bash +TOKEN_ID=0 # adjust to the relevant token ID +python3 -c " +import sys, json, base64, subprocess +t = json.load(subprocess.Popen(['cryptsetup', 'token', 'export', '--token-id', '$TOKEN_ID', '/dev/sda3'], stdout=subprocess.PIPE).stdout) +data = json.dumps(t['x-cvmutils'], sort_keys=True, separators=(',', ':')).encode() +sig = base64.b64decode(t['x-cvmutils-sig']) +open('/tmp/cvmutils.sig', 'wb').write(sig) +open('/tmp/cvmutils.dat', 'wb').write(data) +" +openssl dgst -sha256 -verify public.pem -signature /tmp/cvmutils.sig /tmp/cvmutils.dat +``` ### Makeverity Options (Experimental) diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/cvmencryptimage.py /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/cvmencryptimage.py --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/cvmencryptimage.py 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/cvmencryptimage.py 2026-08-30 19:41:17.000000000 +0000 @@ -19,8 +19,9 @@ from cvmutils.efi import bootchains_from_shim_fallback from cvmutils.log import Log +from cvmutils.luks import LUKS from cvmutils.partitions import Partitions from cvmutils.pcr import PCR from cvmutils.sb import SecureBoot -from cvmutils.tools import run_command +from cvmutils.tools import run_command, get_version LUKSADD_PARAMS = ["-q", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000"] @@ -360,4 +361,7 @@ log.info("Sealing root volume key with systemd-cryptenroll") + luks = LUKS(pt.get_path('root'), lukspw) + token_ids_before = luks.get_token_ids() + for pcrs in pcrs_list_unique: run_command(["systemd-cryptenroll", pt.get_path('root'), "--tpm2-device-key=" + args.srkpub, @@ -365,4 +369,14 @@ "--unlock-key-file=" + tempdir + '/lukspw']) + token_ids_after = luks.get_token_ids() + new_token_ids = token_ids_after - token_ids_before + if new_token_ids: + cvmutils_data = { + "timestamp": int(time.time()), + "version": get_version(), + "volume-digest": luks.get_volume_digest(), + } + luks.annotate_tokens(new_token_ids, cvmutils_data, args.volume_digest_key) + # Remove cleartext password run_command(["cryptsetup", "luksRemoveKey", "--key-file", "-", pt.get_path('root')], input=lukspw) @@ -448,4 +462,5 @@ parser.add_argument('-r', '--recovery-key', help='Recovery key file (deploy only, adds an additional passphrase to root volume)') parser.add_argument('--recovery-key-type', choices=['binary', 'text', 'both'], default='binary', help='Recovery key type') + parser.add_argument('--volume-digest-key', help='PEM private key to sign the x-cvmutils structure (deploy only, adds x-cvmutils-sig to LUKS tokens)') parser.add_argument('--noswtpm', help='DEPRECATED', action="store_true") parser.add_argument('--pcr4',help='Expected PCR4 sha256 value for root volume key sealing (deploy only, sha256 or "auto")') diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/efi.py /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/efi.py --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/efi.py 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/efi.py 2026-08-30 19:41:17.000000000 +0000 @@ -11,15 +11,100 @@ from cvmutils.log import Log +def find_files_ending_with(directory: str, suffix: str) -> list: + """ + Find all files in a directory that end with a given suffix (case-insensitive). + + Args: + directory: Directory to search in + suffix: Suffix to match (e.g., ".efi", ".addon.efi") + + Returns: + Sorted list of matching files with full paths + """ + if not os.path.exists(directory): + return [] + + suffix_lower = suffix.lower() + result = [] + + try: + for entry in os.listdir(directory): + full_path = os.path.join(directory, entry) + if os.path.isfile(full_path) and entry.lower().endswith(suffix_lower): + result.append(full_path) + except (PermissionError, OSError): + return [] + + return sorted(result) + +def case_insensitive_exists(base_path: str, relative_path: str) -> str: + """ + Check if a path exists in a case-insensitive manner on the ESP. + + Args: + base_path: The ESP mount point (checked as-is) + relative_path: The path relative to ESP (checked case-insensitively) + + Returns: + The actual full path if found, None otherwise. + """ + # Check base path exists as-is + if not os.path.exists(base_path): + return None + + # Start from base path + current_path = base_path + + # Split relative path and traverse case-insensitively + parts = relative_path.strip('/').split('/') + for part in parts: + if not part: + continue + + # Try to find matching entry in current directory + try: + found = False + for entry in os.listdir(current_path): + if entry.lower() == part.lower(): + current_path = os.path.join(current_path, entry) + found = True + break + + if not found: + return None + except (PermissionError, OSError): + return None + + return current_path if os.path.exists(current_path) else None + def get_addons(path: str, ukipath: str) -> list: """ Find all global and UKI specific addons on the ESP + + Args: + path: ESP mount point + ukipath: Full path to UKI or relative path from ESP """ result = [] - for addonpath in [path + "/loader/addons/", ukipath + ".extra.d/"]: - if os.path.exists(addonpath): - for addon in sorted(glob.glob(addonpath + "*.addon.efi")): - result.append(addon) + + # Check global addons + actual_path = case_insensitive_exists(path, "/loader/addons") + if actual_path: + result.extend(find_files_ending_with(actual_path, ".addon.efi")) + + # Check UKI-specific addons + # If ukipath is a full path, extract the relative part + if ukipath.startswith(path): + uki_relpath = ukipath[len(path):] + else: + uki_relpath = ukipath + + actual_path = case_insensitive_exists(path, uki_relpath + ".extra.d") + if actual_path: + result.extend(find_files_ending_with(actual_path, ".addon.efi")) + return result +# pylint: disable=too-many-branches def parse_one_bootline(path: str, vendor: str, bootline: str) -> list: """ @@ -35,12 +120,14 @@ if vendor == 'Linux': ukipath = "/EFI/Linux/" + re.sub("\\\\", "/", bootline.split(',')[0].split(' ')[0]) - if not os.path.exists(f"{path}/{ukipath}"): + actual_uki = case_insensitive_exists(path, ukipath) + if not actual_uki: log.error(f"{ukipath} from shim fallback doesn't exist!") return None - return [{"shim": None, "bootloader": None, "uki": f"{path}/{ukipath}", "addons": get_addons(path, ukipath)}] + return [{"shim": None, "bootloader": None, "uki": actual_uki, "addons": get_addons(path, ukipath)}] - shimpath = f"{path}/EFI/{vendor}/{bootline.split(',')[0].split(' ')[0]}" - if not os.path.exists(shimpath): - log.error(f"Shim {shimpath} from shim fallback doesn't exist!") + shimrelpath = "/EFI/" + vendor + "/" + bootline.split(',')[0].split(' ')[0] + actual_shim = case_insensitive_exists(path, shimrelpath) + if not actual_shim: + log.error(f"{shimrelpath} from shim fallback doesn't exist!") return None @@ -48,11 +135,12 @@ ukipath = bootline.split(',')[2].split(' ')[0] if ukipath.lower().endswith(".efi"): - ukipath = re.sub("\\\\", "/", ukipath) - if not os.path.exists(f"{path}/{ukipath}"): - log.warning(f"UKI {ukipath} from shim fallback doesn't exist!") + ukirelpath = "/" + re.sub("\\\\", "/", ukipath) + actual_uki = case_insensitive_exists(path, ukirelpath) + if not actual_uki: + log.warning(f"UKI {ukirelpath} from shim fallback doesn't exist!") ukipath = None else: - log.info(f"Using UKI {ukipath} from shim fallback for PCR prediction") - ukis.append(f"{path}/{ukipath}") + log.info(f"Using UKI {ukirelpath} from shim fallback for PCR prediction") + ukis.append(actual_uki) else: log.warning("BOOTX64.CSV does not set UKI to boot") @@ -61,11 +149,15 @@ bootloaderpath = None if not ukis: - bootloaderpath = f"{path}/EFI/{vendor}/grubx64.efi" - if not os.path.exists(bootloaderpath): + grub_relpath = f"/EFI/{vendor}/grubx64.efi" + bootloaderpath = case_insensitive_exists(path, grub_relpath) + if not bootloaderpath: log.warning("No UKI set in BOOTX64.CSV and grubx64.efi is missing, skipping boot option") return None - ukis = sorted(glob.glob(f"{path}/EFI/Linux/*.efi")) - if ukis == []: + # Look for UKIs case-insensitively + linux_dir = case_insensitive_exists(path, "/EFI/Linux") + if linux_dir: + ukis = find_files_ending_with(linux_dir, ".efi") + if not ukis: log.warning("No UKIs found in /EFI/Linux, skipping boot option") return None @@ -74,5 +166,5 @@ for ukipath in ukis: - result.append({"shim": shimpath, "bootloader": bootloaderpath, "uki": ukipath, "addons": get_addons(path, ukipath)}) + result.append({"shim": actual_shim, "bootloader": bootloaderpath, "uki": ukipath, "addons": get_addons(path, ukipath)}) return result @@ -213,41 +305,35 @@ result = [] - shimname = 'shim' - if os.path.exists(f"{path}/EFI/redhat"): - vendor='redhat' - elif os.path.exists(f"{path}/EFI/fedora"): - vendor='fedora' - elif os.path.exists(f"{path}/EFI/azurelinux"): - vendor='azurelinux' - elif os.path.exists(f"{path}/EFI/rocky"): - vendor='rocky' - else: - log.warning("No EFI vendor dir, assuming legacy Mariner 2 layout") - vendor='BOOT' - shimname='boot' - - if os.path.exists(f"{path}/EFI/{vendor}/BOOTX64.CSV"): - fallback = f"{path}/EFI/{vendor}/BOOTX64.CSV" - elif os.path.exists(f"{path}/EFI/BOOT/BOOTX64.CSV"): - fallback = f"{path}/EFI/BOOT/BOOTX64.CSV" - elif os.path.exists(f"{path}/EFI/Linux/BOOTX64.CSV"): - # Direct UKI boot without shim - vendor='Linux' - fallback = f"{path}/EFI/Linux/BOOTX64.CSV" - else: - fallback = None - - if fallback: - # BOOTX64.CSV may contain several entries - with open(f"{path}/EFI/{vendor}/BOOTX64.CSV", encoding='utf-16-le') as f: - for bootline in f.readlines(): - bootchain = parse_one_bootline(path, vendor, bootline) - if bootchain: - result += bootchain - else: - # No BOOTX64.CSV, let's hope there's a second stage bootloader - bootchain = parse_one_bootline(path, vendor, f"{shimname}x64.efi,,,") - if bootchain: - result += bootchain + efi_path = case_insensitive_exists(path, "/EFI") + if not efi_path: + raise RuntimeError(f"No EFI directory found at {path}/EFI") + + # Search for BOOTX64.CSV files in all EFI subdirectories + csv_found = False + for vendor in sorted(os.listdir(efi_path)): + vendor_path = os.path.join(efi_path, vendor) + if not os.path.isdir(vendor_path): + continue + + csv_path = case_insensitive_exists(path, f"/EFI/{vendor}/BOOTX64.CSV") + if csv_path: + csv_found = True + log.info(f"Found BOOTX64.CSV in {vendor} directory") + with open(csv_path, encoding='utf-16-le') as f: + for bootline in f.readlines(): + bootchain = parse_one_bootline(path, vendor, bootline) + if bootchain: + result += bootchain + + # If no BOOTX64.CSV found, check for fallback bootloader + if not csv_found: + fallback_bootloader = case_insensitive_exists(path, "/EFI/BOOT/BOOTX64.EFI") + if fallback_bootloader: + log.info("No BOOTX64.CSV found, using fallback /EFI/BOOT/BOOTX64.EFI") + bootchain = parse_one_bootline(path, "BOOT", "BOOTX64.EFI,,,") + if bootchain: + result += bootchain + else: + raise RuntimeError("No BOOTX64.CSV found and no fallback bootloader at /EFI/BOOT/BOOTX64.EFI") if not result: Only in /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils: luks.py diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/tools.py /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/tools.py --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/cvmutils/tools.py 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/cvmutils/tools.py 2026-08-30 19:41:17.000000000 +0000 @@ -6,6 +6,16 @@ import subprocess import sys +from importlib.metadata import version as pkg_version, PackageNotFoundError from cvmutils.log import Log +CVMUTILS_VERSION = "0.3.2" + +def get_version(): + """ Get cvmutils version """ + try: + return pkg_version("cvmutils") + except PackageNotFoundError: + return CVMUTILS_VERSION + # pylint: disable=redefined-builtin, too-many-arguments, too-many-positional-arguments def run_command(cmdargs, sysexit=False, canfail=False, input=None, text=True, output=None): diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/man/cvm-encrypt-image.1 /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/man/cvm-encrypt-image.1 --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/man/cvm-encrypt-image.1 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/man/cvm-encrypt-image.1 2026-08-30 19:41:17.000000000 +0000 @@ -1,4 +1,4 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH CVM-ENCRYPT-IMAGE "1" "May 2026" "cvm-encrypt-image 0.3.2" "User Commands" +.TH CVM-ENCRYPT-IMAGE "1" "June 2026" "cvm-encrypt-image 0.3.2" "User Commands" .SH NAME cvm-encrypt-image \- manual page for cvm-encrypt-image 0.3.2 @@ -12,4 +12,5 @@ [\-r RECOVERY_KEY] [\-\-recovery\-key\-type {binary,text,both}] +[\-\-volume\-digest\-key VOLUME_DIGEST_KEY] [\-\-noswtpm] [\-\-pcr4 PCR4] [\-\-pcr7 PCR7] [\-\-nosecureboot] @@ -76,4 +77,8 @@ Recovery key type .TP +\fB\-\-volume\-digest\-key\fR VOLUME_DIGEST_KEY +PEM private key to sign the x\-cvmutils structure +(deploy only, adds x\-cvmutils\-sig to LUKS tokens) +.TP \fB\-\-noswtpm\fR DEPRECATED diff -U2 -r /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/man/cvm-reseal.1 /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/man/cvm-reseal.1 --- /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/man/cvm-reseal.1 2026-05-29 15:19:04.000000000 +0000 +++ /var/lib/copr-rpmbuild/results/python-cvmutils/srpm-unpacked/cvmutils-0.3.2.tar.gz-extract/cvmutils-0.3.2/man/cvm-reseal.1 2026-08-30 19:41:17.000000000 +0000 @@ -1,4 +1,4 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH CVM-RESEAL "1" "May 2026" "cvm-reseal 0.3.2" "User Commands" +.TH CVM-RESEAL "1" "June 2026" "cvm-reseal 0.3.2" "User Commands" .SH NAME cvm-reseal \- manual page for cvm-reseal 0.3.2 Only in /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/tests: test-data Only in /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/tests: test_pcr_predictor.py Only in /var/lib/copr-rpmbuild/results/python-cvmutils/upstream-unpacked/Source0/cvmutils-0.3.2/tests: test_reseal.py