Added a HSM menu entry (#1196)

* Added a HSM menu entry, but also a safety check to make sure a FIDO device is connected

* flake8 complaints

* Adding FIDO lookup using cryptenroll listing

* Added systemd-cryptenroll --fido2-device=list

* Removed old _select_hsm call

* Fixed flake8 complaints

* Added support for locking and unlocking with a HSM

* Removed hardcoded paths in favor of PR merge

* Removed hardcoded paths in favor of PR merge

* Fixed mypy complaint

* Flake8 issue

* Added sd-encrypt for HSM and revert back to encrypt when HSM is not used (stability reason)

* Added /etc/vconsole.conf and tweaked fido2_enroll() to use the proper paths

* Spelling error

* Using UUID instead of PARTUUID when using HSM. I can't figure out how to get sd-encrypt to use PARTUUID instead. Added a Partition().part_uuid function. Actually renamed .uuid to .part_uuid and created a .uuid instead.

* Adding missing package libfido2 and removed tpm2-device=auto as it overrides everything and forces password prompt to be used over FIDO2, no matter the order of the options.

* Added some notes to clarify some choices.

* Had to move libfido2 package install to later in the chain, as there's not even a base during mounting :P
This commit is contained in:
Anton Hvornum 2022-05-18 11:28:59 +02:00 committed by GitHub
parent 561ea7e8f5
commit 493cccc18f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 241 additions and 26 deletions

View File

@ -1,7 +1,7 @@
[flake8] [flake8]
count = True count = True
# Several of the following could be autofixed or improved by running the code through psf/black # Several of the following could be autofixed or improved by running the code through psf/black
ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293 ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293,W503
max-complexity = 40 max-complexity = 40
max-line-length = 236 max-line-length = 236
show-source = True show-source = True

View File

@ -45,6 +45,11 @@ from .lib.menu.selection_menu import (
from .lib.translation import Translation, DeferredTranslation from .lib.translation import Translation, DeferredTranslation
from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony
from .lib.configuration import * from .lib.configuration import *
from .lib.udev import udevadm_info
from .lib.hsm import (
get_fido2_devices,
fido2_enroll
)
parser = ArgumentParser() parser = ArgumentParser()
__version__ = "2.4.2" __version__ = "2.4.2"

View File

@ -1,12 +1,23 @@
import json import json
import logging import logging
from pathlib import Path import pathlib
from typing import Optional, Dict from typing import Optional, Dict
from .storage import storage from .storage import storage
from .general import JSON, UNSAFE_JSON from .general import JSON, UNSAFE_JSON
from .output import log 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: class ConfigurationOutput:
def __init__(self, config: Dict): def __init__(self, config: Dict):
@ -21,7 +32,7 @@ class ConfigurationOutput:
self._user_credentials = {} self._user_credentials = {}
self._disk_layout = None self._disk_layout = None
self._user_config = {} 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_config_file = 'user_configuration.json'
self._user_creds_file = "user_credentials.json" self._user_creds_file = "user_credentials.json"
self._disk_layout_file = "user_disk_layout.json" self._disk_layout_file = "user_disk_layout.json"
@ -84,7 +95,7 @@ class ConfigurationOutput:
print() 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()): if (not dest_path.exists()) or not (dest_path.is_dir()):
log( log(
'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()), '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 False
return True 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): if self._is_valid_path(dest_path):
with open(dest_path / self._user_config_file, 'w') as config_file: with open(dest_path / self._user_config_file, 'w') as config_file:
config_file.write(self.user_config_to_json()) 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 self._is_valid_path(dest_path):
if user_creds := self.user_credentials_to_json(): if user_creds := self.user_credentials_to_json():
target = dest_path / self._user_creds_file target = dest_path / self._user_creds_file
with open(target, 'w') as config_file: with open(target, 'w') as config_file:
config_file.write(user_creds) 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 self._is_valid_path(dest_path):
if disk_layout := self.disk_layout_to_json(): if disk_layout := self.disk_layout_to_json():
target = dest_path / self._disk_layout_file target = dest_path / self._disk_layout_file
with target.open('w') as config_file: with target.open('w') as config_file:
config_file.write(disk_layout) config_file.write(disk_layout)
def save(self, dest_path :Path = None): def save(self, dest_path :pathlib.Path = None):
if not dest_path: if not dest_path:
dest_path = self._default_save_path dest_path = self._default_save_path

View File

@ -275,7 +275,7 @@ class BlockDevice:
count = 0 count = 0
while count < 5: while count < 5:
for partition_uuid, partition in self.partitions.items(): for partition_uuid, partition in self.partitions.items():
if partition.uuid.lower() == uuid.lower(): if partition.part_uuid.lower() == uuid.lower():
return partition return partition
else: else:
log(f"uuid {uuid} not found. Waiting for {count +1} time",level=logging.DEBUG) log(f"uuid {uuid} not found. Waiting for {count +1} time",level=logging.DEBUG)

View File

@ -150,7 +150,7 @@ class Filesystem:
if partition.get('boot', False): if partition.get('boot', False):
log(f"Marking partition {partition['device_instance']} as bootable.") 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 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: 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) 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 self.mode == MBR:
if len(self.blockdevice.partitions) > 3: if len(self.blockdevice.partitions) > 3:
@ -210,7 +210,7 @@ class Filesystem:
count = 0 count = 0
while count < 10: while count < 10:
new_uuid = None 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: if len(new_uuid_set) > 0:
new_uuid = new_uuid_set.pop() new_uuid = new_uuid_set.pop()
@ -236,7 +236,7 @@ class Filesystem:
# TODO: This should never be able to happen # 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"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"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}") raise DiskError(f"Could not add partition using: {parted_string}")
def set_name(self, partition: int, name: str) -> bool: def set_name(self, partition: int, name: str) -> bool:

View File

@ -184,7 +184,7 @@ class Partition:
return device['pttype'] return device['pttype']
@property @property
def uuid(self) -> Optional[str]: def part_uuid(self) -> Optional[str]:
""" """
Returns the PARTUUID as returned by lsblk. Returns the PARTUUID as returned by lsblk.
This is more reliable than relying on /dev/disk/by-partuuid as 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)) 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 partuuid = self._safe_uuid
if partuuid: if partuuid:
return partuuid return partuuid
@ -216,6 +236,28 @@ class Partition:
log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG) 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: try:
return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip() return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip()
except SysCallError as error: except SysCallError as error:

View File

@ -135,6 +135,8 @@ class JsonEncoder:
return obj.isoformat() return obj.isoformat()
elif isinstance(obj, (list, set, tuple)): elif isinstance(obj, (list, set, tuple)):
return [json.loads(json.dumps(item, cls=JSON)) for item in obj] return [json.loads(json.dumps(item, cls=JSON)) for item in obj]
elif isinstance(obj, (pathlib.Path)):
return str(obj)
else: else:
return obj return obj

View File

@ -0,0 +1,4 @@
from .fido import (
get_fido2_devices,
fido2_enroll
)

View File

@ -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

View File

@ -23,6 +23,7 @@ from .profiles import Profile
from .disk.btrfs import manage_btrfs_subvolumes from .disk.btrfs import manage_btrfs_subvolumes
from .disk.partition import get_mount_fs_type from .disk.partition import get_mount_fs_type
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
from .hsm import fido2_enroll
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -126,7 +127,9 @@ class Installer:
self.MODULES = [] self.MODULES = []
self.BINARIES = [] self.BINARIES = []
self.FILES = [] 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.KERNEL_PARAMS = []
self._zram_enabled = False self._zram_enabled = False
@ -241,10 +244,10 @@ class Installer:
# open the luks device and all associate stuff # open the luks device and all associate stuff
if not (password := partition.get('!password', None)): if not (password := partition.get('!password', None)):
raise RequirementError(f"Missing partition {partition['device_instance'].path} encryption password in layout: {partition}") 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" loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
else: else:
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}" 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 # 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: 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): 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 # this way all the requesrs will be to the dm_crypt device and not to the physical partition
partition['device_instance'] = unlocked_device 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 # we manage the btrfs partitions
for partition in [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]: for partition in [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]:
if partition.get('filesystem',{}).get('mount_options',[]): if partition.get('filesystem',{}).get('mount_options',[]):
@ -609,6 +616,15 @@ class Installer:
mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n") mkinit.write(f"MODULES=({' '.join(self.MODULES)})\n")
mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n")
mkinit.write(f"FILES=({' '.join(self.FILES)})\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") mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n")
return SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}').exit_code == 0 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') self.HOOKS.remove('fsck')
if self.detect_encryption(partition): if self.detect_encryption(partition):
if 'encrypt' not in self.HOOKS: if storage['arguments']['HSM']:
self.HOOKS.insert(self.HOOKS.index('filesystems'), 'encrypt') # 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(): if not has_uefi():
self.base_packages.append('grub') self.base_packages.append('grub')
@ -700,6 +723,14 @@ class Installer:
# TODO: Use python functions for this # TODO: Use python functions for this
SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') 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.mkinitcpio('-P')
self.helper_flags['base'] = True self.helper_flags['base'] = True
@ -814,11 +845,23 @@ class Installer:
if real_device := self.detect_encryption(root_partition): if real_device := self.detect_encryption(root_partition):
# TODO: We need to detect if the encrypted device is a whole disk encryption, # 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) # 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) log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}/{real_device.part_uuid}'.", level=logging.DEBUG)
entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev {options_entry}')
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: else:
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) 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.uuid} {options_entry}') entry.write(f'options root=PARTUUID={root_partition.part_uuid} {options_entry}')
self.helper_flags['bootloader'] = "systemd" self.helper_flags['bootloader'] = "systemd"
@ -903,11 +946,11 @@ class Installer:
if real_device := self.detect_encryption(root_partition): if real_device := self.detect_encryption(root_partition):
# TODO: We need to detect if the encrypted device is a whole disk encryption, # 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) # 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) 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.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') 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: else:
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) 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.uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}') 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') SysCommand(f'efibootmgr --disk {boot_partition.path[:-1]} --part {boot_partition.path[-1]} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose')

View File

@ -85,6 +85,12 @@ class GlobalMenu(GeneralMenu):
lambda x: self._select_encrypted_password(), lambda x: self._select_encrypted_password(),
display_func=lambda x: secret(x) if x else 'None', display_func=lambda x: secret(x) if x else 'None',
dependencies=['harddrives']) 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'] = \ self._menu_options['swap'] = \
Selector( Selector(
_('Swap'), _('Swap'),

View File

@ -2,12 +2,14 @@ from __future__ import annotations
import logging import logging
import sys import sys
import pathlib
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
from .menu import Menu, MenuSelectionType from .menu import Menu, MenuSelectionType
from ..locale_helpers import set_keyboard_language from ..locale_helpers import set_keyboard_language
from ..output import log from ..output import log
from ..translation import Translation from ..translation import Translation
from ..hsm.fido import get_fido2_devices
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -466,3 +468,25 @@ class GeneralMenu:
return language return language
return preset_value 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

View File

@ -0,0 +1 @@
from .udevadm import udevadm_info

View File

@ -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

View File

@ -1,8 +1,15 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0.1\n"
msgid "[!] A log file has been created here: {} {}" msgid "[!] A log file has been created here: {} {}"
msgstr "" msgstr ""

View File

@ -57,6 +57,10 @@ def ask_user_questions():
# Get disk encryption password (or skip if blank) # Get disk encryption password (or skip if blank)
global_menu.enable('!encryption-password') global_menu.enable('!encryption-password')
if archinstall.arguments.get('advanced', False) or archinstall.arguments.get('HSM', None):
# Enables the use of HSM
global_menu.enable('HSM')
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB) # Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
global_menu.enable('bootloader') global_menu.enable('bootloader')
@ -130,6 +134,7 @@ def perform_installation(mountpoint):
Only requirement is that the block devices are Only requirement is that the block devices are
formatted and setup prior to entering this function. formatted and setup prior to entering this function.
""" """
with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', ['linux'])) as installation: with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', ['linux'])) as installation:
# Mount all the drives to the desired mountpoint # Mount all the drives to the desired mountpoint
# This *can* be done outside of the installation, but the installer can deal with it. # This *can* be done outside of the installation, but the installer can deal with it.
@ -301,5 +306,6 @@ if archinstall.arguments.get('dry_run'):
if not archinstall.arguments.get('silent'): if not archinstall.arguments.get('silent'):
input(str(_('Press Enter to continue.'))) input(str(_('Press Enter to continue.')))
archinstall.configuration_sanity_check()
perform_filesystem_operations() perform_filesystem_operations()
perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt')) perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt'))