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:
parent
561ea7e8f5
commit
493cccc18f
2
.flake8
2
.flake8
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .fido import (
|
||||||
|
get_fido2_devices,
|
||||||
|
fido2_enroll
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from .udevadm import udevadm_info
|
||||||
|
|
@ -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
|
||||||
Binary file not shown.
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue