index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/lib/configuration.py | 27 | ||||
-rw-r--r-- | archinstall/lib/disk/blockdevice.py | 2 | ||||
-rw-r--r-- | archinstall/lib/disk/filesystem.py | 8 | ||||
-rw-r--r-- | archinstall/lib/disk/partition.py | 44 | ||||
-rw-r--r-- | archinstall/lib/general.py | 2 | ||||
-rw-r--r-- | archinstall/lib/hsm/__init__.py | 4 | ||||
-rw-r--r-- | archinstall/lib/hsm/fido.py | 47 | ||||
-rw-r--r-- | archinstall/lib/installer.py | 67 | ||||
-rw-r--r-- | archinstall/lib/menu/global_menu.py | 6 | ||||
-rw-r--r-- | archinstall/lib/menu/selection_menu.py | 24 | ||||
-rw-r--r-- | archinstall/lib/udev/__init__.py | 1 | ||||
-rw-r--r-- | archinstall/lib/udev/udevadm.py | 17 |
diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index c971768f..f3fe1e1c 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -1,12 +1,23 @@ import json import logging -from pathlib import Path +import pathlib from typing import Optional, Dict from .storage import storage from .general import JSON, UNSAFE_JSON from .output import log - +from .exceptions import RequirementError +from .hsm import get_fido2_devices + +def configuration_sanity_check(): + if storage['arguments'].get('HSM'): + if not get_fido2_devices(): + raise RequirementError( + f"In order to use HSM to pair with the disk encryption," + + f" one needs to be accessible through /dev/hidraw* and support" + + f" the FIDO2 protocol. You can check this by running" + + f" 'systemd-cryptenroll --fido2-device=list'." + ) class ConfigurationOutput: def __init__(self, config: Dict): @@ -21,7 +32,7 @@ class ConfigurationOutput: self._user_credentials = {} self._disk_layout = None self._user_config = {} - self._default_save_path = Path(storage.get('LOG_PATH', '.')) + self._default_save_path = pathlib.Path(storage.get('LOG_PATH', '.')) self._user_config_file = 'user_configuration.json' self._user_creds_file = "user_credentials.json" self._disk_layout_file = "user_disk_layout.json" @@ -84,7 +95,7 @@ class ConfigurationOutput: print() - def _is_valid_path(self, dest_path :Path) -> bool: + def _is_valid_path(self, dest_path :pathlib.Path) -> bool: if (not dest_path.exists()) or not (dest_path.is_dir()): log( 'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()), @@ -93,26 +104,26 @@ class ConfigurationOutput: return False return True - def save_user_config(self, dest_path :Path = None): + def save_user_config(self, dest_path :pathlib.Path = None): if self._is_valid_path(dest_path): with open(dest_path / self._user_config_file, 'w') as config_file: config_file.write(self.user_config_to_json()) - def save_user_creds(self, dest_path :Path = None): + def save_user_creds(self, dest_path :pathlib.Path = None): if self._is_valid_path(dest_path): if user_creds := self.user_credentials_to_json(): target = dest_path / self._user_creds_file with open(target, 'w') as config_file: config_file.write(user_creds) - def save_disk_layout(self, dest_path :Path = None): + def save_disk_layout(self, dest_path :pathlib.Path = None): if self._is_valid_path(dest_path): if disk_layout := self.disk_layout_to_json(): target = dest_path / self._disk_layout_file with target.open('w') as config_file: config_file.write(disk_layout) - def save(self, dest_path :Path = None): + def save(self, dest_path :pathlib.Path = None): if not dest_path: dest_path = self._default_save_path diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py index 4978f19c..995ca355 100644 --- a/archinstall/lib/disk/blockdevice.py +++ b/archinstall/lib/disk/blockdevice.py @@ -275,7 +275,7 @@ class BlockDevice: count = 0 while count < 5: for partition_uuid, partition in self.partitions.items(): - if partition.uuid.lower() == uuid.lower(): + if partition.part_uuid.lower() == uuid.lower(): return partition else: log(f"uuid {uuid} not found. Waiting for {count +1} time",level=logging.DEBUG) diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index db97924f..31929b63 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -150,7 +150,7 @@ class Filesystem: if partition.get('boot', False): log(f"Marking partition {partition['device_instance']} as bootable.") - self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on') + self.set(self.partuuid_to_index(partition['device_instance'].part_uuid), 'boot on') prev_partition = partition @@ -193,7 +193,7 @@ class Filesystem: def add_partition(self, partition_type :str, start :str, end :str, partition_format :Optional[str] = None) -> Partition: log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO) - previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()} + previous_partition_uuids = {partition.part_uuid for partition in self.blockdevice.partitions.values()} if self.mode == MBR: if len(self.blockdevice.partitions) > 3: @@ -210,7 +210,7 @@ class Filesystem: count = 0 while count < 10: new_uuid = None - new_uuid_set = (previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()}) + new_uuid_set = (previous_partition_uuids ^ {partition.part_uuid for partition in self.blockdevice.partitions.values()}) if len(new_uuid_set) > 0: new_uuid = new_uuid_set.pop() @@ -236,7 +236,7 @@ class Filesystem: # TODO: This should never be able to happen log(f"Could not find the new PARTUUID after adding the partition.", level=logging.ERROR, fg="red") log(f"Previous partitions: {previous_partition_uuids}", level=logging.ERROR, fg="red") - log(f"New partitions: {(previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()})}", level=logging.ERROR, fg="red") + log(f"New partitions: {(previous_partition_uuids ^ {partition.part_uuid for partition in self.blockdevice.partitions.values()})}", level=logging.ERROR, fg="red") raise DiskError(f"Could not add partition using: {parted_string}") def set_name(self, partition: int, name: str) -> bool: diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index e7568258..c52ca434 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -184,7 +184,7 @@ class Partition: return device['pttype'] @property - def uuid(self) -> Optional[str]: + def part_uuid(self) -> Optional[str]: """ Returns the PARTUUID as returned by lsblk. This is more reliable than relying on /dev/disk/by-partuuid as @@ -197,6 +197,26 @@ class Partition: time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) + partuuid = self._safe_part_uuid + if partuuid: + return partuuid + + raise DiskError(f"Could not get PARTUUID for {self.path} using 'blkid -s PARTUUID -o value {self.path}'") + + @property + def uuid(self) -> Optional[str]: + """ + Returns the UUID as returned by lsblk for the **partition**. + This is more reliable than relying on /dev/disk/by-uuid as + it doesn't seam to be able to detect md raid partitions. + For bind mounts all the subvolumes share the same uuid + """ + for i in range(storage['DISK_RETRY_ATTEMPTS']): + if not self.partprobe(): + raise DiskError(f"Could not perform partprobe on {self.device_path}") + + time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i)) + partuuid = self._safe_uuid if partuuid: return partuuid @@ -217,6 +237,28 @@ class Partition: log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) try: + return SysCommand(f'blkid -s UUID -o value {self.device_path}').decode('UTF-8').strip() + except SysCallError as error: + if self.block_device.info.get('TYPE') == 'iso9660': + # Parent device is a Optical Disk (.iso dd'ed onto a device for instance) + return None + + log(f"Could not get PARTUUID of partition using 'blkid -s UUID -o value {self.device_path}': {error}") + + @property + def _safe_part_uuid(self) -> Optional[str]: + """ + A near copy of self.uuid but without any delays. + This function should only be used where uuid is not crucial. + For instance when you want to get a __repr__ of the class. + """ + if not self.partprobe(): + if self.block_device.info.get('TYPE') == 'iso9660': + return None + + log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) + + try: return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip() except SysCallError as error: if self.block_device.info.get('TYPE') == 'iso9660': diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index a4e2a365..44b78777 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -135,6 +135,8 @@ class JsonEncoder: return obj.isoformat() elif isinstance(obj, (list, set, tuple)): return [json.loads(json.dumps(item, cls=JSON)) for item in obj] + elif isinstance(obj, (pathlib.Path)): + return str(obj) else: return obj diff --git a/archinstall/lib/hsm/__init__.py b/archinstall/lib/hsm/__init__.py new file mode 100644 index 00000000..c0888b04 --- /dev/null +++ b/archinstall/lib/hsm/__init__.py @@ -0,0 +1,4 @@ +from .fido import ( + get_fido2_devices, + fido2_enroll +)
\ No newline at end of file diff --git a/archinstall/lib/hsm/fido.py b/archinstall/lib/hsm/fido.py new file mode 100644 index 00000000..69f42890 --- /dev/null +++ b/archinstall/lib/hsm/fido.py @@ -0,0 +1,47 @@ +import typing +import pathlib +from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes +from ..disk.partition import Partition + +def get_fido2_devices() -> typing.Dict[str, typing.Dict[str, str]]: + """ + Uses systemd-cryptenroll to list the FIDO2 devices + connected that supports FIDO2. + Some devices might show up in udevadm as FIDO2 compliant + when they are in fact not. + + The drawback of systemd-cryptenroll is that it uses human readable format. + That means we get this weird table like structure that is of no use. + + So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index + and we split each line based on those positions. + """ + worker = clear_vt100_escape_codes(SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8')) + + MANUFACTURER_POS = 0 + PRODUCT_POS = 0 + devices = {} + for line in worker.split('\r\n'): + if '/dev' not in line: + MANUFACTURER_POS = line.find('MANUFACTURER') + PRODUCT_POS = line.find('PRODUCT') + continue + + path = line[:MANUFACTURER_POS].rstrip() + manufacturer = line[MANUFACTURER_POS:PRODUCT_POS].rstrip() + product = line[PRODUCT_POS:] + + devices[path] = { + 'manufacturer' : manufacturer, + 'product' : product + } + + return devices + +def fido2_enroll(hsm_device_path :pathlib.Path, partition :Partition, password :str) -> bool: + worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device_path} {partition.real_device}", peak_output=True) + pw_inputted = False + while worker.is_alive(): + if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(password, 'UTF-8')) + pw_inputted = True diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index e94a00c4..292b2c8e 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -23,6 +23,7 @@ from .profiles import Profile from .disk.btrfs import manage_btrfs_subvolumes from .disk.partition import get_mount_fs_type from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError +from .hsm import fido2_enroll if TYPE_CHECKING: _: Any @@ -126,7 +127,9 @@ class Installer: self.MODULES = [] self.BINARIES = [] self.FILES = [] - self.HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"] + # systemd, sd-vconsole and sd-encrypt will be replaced by udev, keymap and encrypt + # if HSM is not used to encrypt the root volume. Check mkinitcpio() function for that override. + self.HOOKS = ["base", "systemd", "autodetect", "keyboard", "sd-vconsole", "modconf", "block", "filesystems", "fsck"] self.KERNEL_PARAMS = [] self._zram_enabled = False @@ -241,10 +244,10 @@ class Installer: # open the luks device and all associate stuff if not (password := partition.get('!password', None)): raise RequirementError(f"Missing partition {partition['device_instance'].path} encryption password in layout: {partition}") - # i change a bit the naming conventions for the loop device loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop" else: loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" + # note that we DON'T auto_unmount (i.e. close the encrypted device so it can be used with (luks_handle := luks2(partition['device_instance'], loopdev, password, auto_unmount=False)) as unlocked_device: if partition.get('generate-encryption-key-file',False) and not self._has_root(partition): @@ -252,6 +255,10 @@ class Installer: # this way all the requesrs will be to the dm_crypt device and not to the physical partition partition['device_instance'] = unlocked_device + if self._has_root(partition) and partition.get('generate-encryption-key-file', False) is False: + hsm_device_path = storage['arguments']['HSM'] + fido2_enroll(hsm_device_path, partition['device_instance'], password) + # we manage the btrfs partitions for partition in [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]: if partition.get('filesystem',{}).get('mount_options',[]): @@ -609,6 +616,15 @@ class Installer: mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n") mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") mkinit.write(f"FILES=({' '.join(self.FILES)})\n") + + if not storage['arguments']['HSM']: + # For now, if we don't use HSM we revert to the old + # way of setting up encryption hooks for mkinitcpio. + # This is purely for stability reasons, we're going away from this. + # * systemd -> udev + # * sd-vconsole -> keymap + self.HOOKS = [hook.replace('systemd', 'udev').replace('sd-vconsole', 'keymap') for hook in self.HOOKS] + mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n") return SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}').exit_code == 0 @@ -643,8 +659,15 @@ class Installer: self.HOOKS.remove('fsck') if self.detect_encryption(partition): - if 'encrypt' not in self.HOOKS: - self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') + if storage['arguments']['HSM']: + # Required bby mkinitcpio to add support for fido2-device options + self.pacstrap('libfido2') + + if 'sd-encrypt' not in self.HOOKS: + self.HOOKS.insert(self.HOOKS.index('filesystems'), 'sd-encrypt') + else: + if 'encrypt' not in self.HOOKS: + self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') if not has_uefi(): self.base_packages.append('grub') @@ -700,6 +723,14 @@ class Installer: # TODO: Use python functions for this SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') + if storage['arguments']['HSM']: + # TODO: + # A bit of a hack, but we need to get vconsole.conf in there + # before running `mkinitcpio` because it expects it in HSM mode. + if (vconsole := pathlib.Path(f"{self.target}/etc/vconsole.conf")).exists() is False: + with vconsole.open('w') as fh: + fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") + self.mkinitcpio('-P') self.helper_flags['base'] = True @@ -814,11 +845,23 @@ class Installer: if real_device := self.detect_encryption(root_partition): # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) - log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG) - entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev {options_entry}') + log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}/{real_device.part_uuid}'.", level=logging.DEBUG) + + kernel_options = f"options" + + if storage['arguments']['HSM']: + # Note: lsblk UUID must be used, not PARTUUID for sd-encrypt to work + kernel_options += f" rd.luks.name={real_device.uuid}=luksdev" + # Note: tpm2-device and fido2-device don't play along very well: + # https://github.com/archlinux/archinstall/pull/1196#issuecomment-1129715645 + kernel_options += f" rd.luks.options=fido2-device=auto,password-echo=no" + else: + kernel_options += f" cryptdevice=PARTUUID={real_device.part_uuid}:luksdev" + + entry.write(f'{kernel_options} root=/dev/mapper/luksdev {options_entry}') else: - log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) - entry.write(f'options root=PARTUUID={root_partition.uuid} {options_entry}') + log(f"Identifying root partition by PARTUUID on {root_partition}, looking for '{root_partition.part_uuid}'.", level=logging.DEBUG) + entry.write(f'options root=PARTUUID={root_partition.part_uuid} {options_entry}') self.helper_flags['bootloader'] = "systemd" @@ -903,11 +946,11 @@ class Installer: if real_device := self.detect_encryption(root_partition): # TODO: We need to detect if the encrypted device is a whole disk encryption, # or simply a partition encryption. Right now we assume it's a partition (and we always have) - log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG) - kernel_parameters.append(f'cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') + log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.part_uuid}'.", level=logging.DEBUG) + kernel_parameters.append(f'cryptdevice=PARTUUID={real_device.part_uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') else: - log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) - kernel_parameters.append(f'root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') + log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.part_uuid}'.", level=logging.DEBUG) + kernel_parameters.append(f'root=PARTUUID={root_partition.part_uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') SysCommand(f'efibootmgr --disk {boot_partition.path[:-1]} --part {boot_partition.path[-1]} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose') diff --git a/archinstall/lib/menu/global_menu.py b/archinstall/lib/menu/global_menu.py index 13d385ef..d807433c 100644 --- a/archinstall/lib/menu/global_menu.py +++ b/archinstall/lib/menu/global_menu.py @@ -85,6 +85,12 @@ class GlobalMenu(GeneralMenu): lambda x: self._select_encrypted_password(), display_func=lambda x: secret(x) if x else 'None', dependencies=['harddrives']) + self._menu_options['HSM'] = Selector( + description=_('Use HSM to unlock encrypted drive'), + func=lambda preset: self._select_hsm(preset), + dependencies=['!encryption-password'], + default=None + ) self._menu_options['swap'] = \ Selector( _('Swap'), diff --git a/archinstall/lib/menu/selection_menu.py b/archinstall/lib/menu/selection_menu.py index 35057e9c..26be4cc7 100644 --- a/archinstall/lib/menu/selection_menu.py +++ b/archinstall/lib/menu/selection_menu.py @@ -2,12 +2,14 @@ from __future__ import annotations import logging import sys +import pathlib from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from .menu import Menu, MenuSelectionType from ..locale_helpers import set_keyboard_language from ..output import log from ..translation import Translation +from ..hsm.fido import get_fido2_devices if TYPE_CHECKING: _: Any @@ -466,3 +468,25 @@ class GeneralMenu: return language return preset_value + + def _select_hsm(self, preset :Optional[pathlib.Path] = None) -> Optional[pathlib.Path]: + title = _('Select which partitions to mark for formatting:') + title += '\n' + + fido_devices = get_fido2_devices() + + indexes = [] + for index, path in enumerate(fido_devices.keys()): + title += f"{index}: {path} ({fido_devices[path]['manufacturer']} - {fido_devices[path]['product']})" + indexes.append(f"{index}|{fido_devices[path]['product']}") + + title += '\n' + + choice = Menu(title, indexes, multi=False).run() + + match choice.type_: + case MenuSelectionType.Esc: return preset + case MenuSelectionType.Selection: + return pathlib.Path(list(fido_devices.keys())[int(choice.value.split('|',1)[0])]) + + return None
\ No newline at end of file diff --git a/archinstall/lib/udev/__init__.py b/archinstall/lib/udev/__init__.py new file mode 100644 index 00000000..86c8cc29 --- /dev/null +++ b/archinstall/lib/udev/__init__.py @@ -0,0 +1 @@ +from .udevadm import udevadm_info
\ No newline at end of file diff --git a/archinstall/lib/udev/udevadm.py b/archinstall/lib/udev/udevadm.py new file mode 100644 index 00000000..84ec9cfd --- /dev/null +++ b/archinstall/lib/udev/udevadm.py @@ -0,0 +1,17 @@ +import typing +import pathlib +from ..general import SysCommand + +def udevadm_info(path :pathlib.Path) -> typing.Dict[str, str]: + if path.resolve().exists() is False: + return {} + + result = SysCommand(f"udevadm info {path.resolve()}") + data = {} + for line in result: + if b': ' in line and b'=' in line: + _, obj = line.split(b': ', 1) + key, value = obj.split(b'=', 1) + data[key.decode('UTF-8').lower()] = value.decode('UTF-8').strip() + + return data
\ No newline at end of file |