From 9c2f3d33ac9ecf0bc5bf1093278386b4f6bfd50b Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 29 Jun 2025 15:04:43 +1000 Subject: [PATCH 1/5] Add U2F login support --- .../lib/applications/application_menu.py | 11 +- archinstall/lib/args.py | 6 + archinstall/lib/authentication/__init__.py | 0 .../authentication/authentication_handler.py | 166 ++++++++++++++++++ .../lib/authentication/authentication_menu.py | 112 ++++++++++++ archinstall/lib/disk/encryption_menu.py | 2 +- archinstall/lib/disk/fido.py | 42 ++++- archinstall/lib/global_menu.py | 30 ++++ archinstall/lib/models/authentication.py | 77 ++++++++ archinstall/scripts/guided.py | 16 +- tests/data/test_config.json | 6 + tests/test_args.py | 7 + 12 files changed, 460 insertions(+), 15 deletions(-) create mode 100644 archinstall/lib/authentication/__init__.py create mode 100644 archinstall/lib/authentication/authentication_handler.py create mode 100644 archinstall/lib/authentication/authentication_menu.py create mode 100644 archinstall/lib/models/authentication.py diff --git a/archinstall/lib/applications/application_menu.py b/archinstall/lib/applications/application_menu.py index 7c07b84c87..20ab93e126 100644 --- a/archinstall/lib/applications/application_menu.py +++ b/archinstall/lib/applications/application_menu.py @@ -84,9 +84,14 @@ def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfigur allow_skip=True, ).run() - enabled = result.item() == MenuItem.yes() - - return BluetoothConfiguration(enabled) + match result.type_: + case ResultType.Selection: + enabled = result.item() == MenuItem.yes() + return BluetoothConfiguration(enabled) + case ResultType.Skip: + return preset + case _: + raise ValueError('Unhandled result type') def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None: diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 5e2d9448a5..85d8914a3a 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -14,6 +14,7 @@ from archinstall.lib.crypt import decrypt from archinstall.lib.models.application import ApplicationConfiguration +from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration from archinstall.lib.models.locale import LocaleConfiguration @@ -64,6 +65,7 @@ class ArchConfig: bootloader: Bootloader = field(default=Bootloader.get_default()) uki: bool = False app_config: ApplicationConfiguration | None = None + auth_config: AuthenticationConfiguration | None = None hostname: str = 'archlinux' kernels: list[str] = field(default_factory=lambda: ['linux']) ntp: bool = True @@ -107,6 +109,7 @@ def safe_json(self) -> dict[str, Any]: 'custom_commands': self.custom_commands, 'bootloader': self.bootloader.json(), 'app_config': self.app_config.json() if self.app_config else None, + 'auth_config': self.auth_config.json() if self.auth_config else None, } if self.locale_config: @@ -193,6 +196,9 @@ def from_config(cls, args_config: dict[str, Any]) -> 'ArchConfig': if audio_config_args is not None or app_config_args is not None: arch_config.app_config = ApplicationConfiguration.parse_arg(app_config_args, audio_config_args) + if auth_config_args := args_config.get('auth_config', None): + arch_config.auth_config = AuthenticationConfiguration.parse_arg(auth_config_args) + if hostname := args_config.get('hostname', ''): arch_config.hostname = hostname diff --git a/archinstall/lib/authentication/__init__.py b/archinstall/lib/authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py new file mode 100644 index 0000000000..5f3e082f62 --- /dev/null +++ b/archinstall/lib/authentication/authentication_handler.py @@ -0,0 +1,166 @@ +import getpass +from pathlib import Path +from typing import TYPE_CHECKING + +from archinstall.lib.general import SysCommandWorker +from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod +from archinstall.lib.models.profile_model import ProfileConfiguration +from archinstall.lib.models.users import User +from archinstall.lib.output import debug +from archinstall.lib.translationhandler import tr +from archinstall.tui.curses_menu import Tui + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + + +class AuthenticationHandler: + def __init__(self) -> None: + self._u2f_auth_file = Path('/etc/u2f_mappings') + + def setup_auth( + self, + install_session: 'Installer', + auth_config: AuthenticationConfiguration, + users: list['User'] | None = None, + profile_config: ProfileConfiguration | None = None, + ) -> None: + if auth_config.u2f_config: + self._setup_u2f_login(install_session, auth_config.u2f_config, users, profile_config) + + def _setup_u2f_login( + self, + install_session: 'Installer', + u2f_config: U2FLoginConfiguration, + users: list[User], + profile_config: ProfileConfiguration | None = None, + ) -> None: + self._configure_u2f_mapping(install_session, u2f_config, users) + self._update_pam_config(install_session, u2f_config, profile_config) + + def _update_pam_config( + self, + install_session: 'Installer', + u2f_config: U2FLoginConfiguration, + profile_config: ProfileConfiguration | None = None, + ) -> None: + match u2f_config.u2f_login_method: + case U2FLoginMethod.Passwordless: + config_entry = f'auth sufficient pam_u2f.so authfile={self._u2f_auth_file} cue' + case U2FLoginMethod.SecondFactor: + config_entry = f'auth required pam_u2f.so authfile={self._u2f_auth_file} cue' + case _: + raise ValueError(f'Unknown U2F login method: {u2f_config.u2f_login_method}') + + debug(f'U2F PAM configuration: {config_entry}') + debug(f'Passwordless sudo enabled: {u2f_config.passwordless_sudo}') + + sudo_config = install_session.target / 'etc/pam.d/sudo' + sys_login = install_session.target / 'etc/pam.d/system-login' + + if u2f_config.passwordless_sudo: + self._add_u2f_entry(sudo_config, config_entry) + + self._add_u2f_entry(sys_login, config_entry) + + # if profile_config and profile_config.profile: + # if profile_config.greeter is not None: + # self._setup_greeter_config( + # install_session, + # config_entry, + # profile_config.greeter, + # ) + # + # if profile_config.profile.is_desktop_profile(): + # desktop_profile: DesktopProfile = profile_config.profile + # else: + # self._add_u2f_entry(sys_login, config_entry) + # else: + # self._add_u2f_entry(sys_login, config_entry) + + # def _setup_greeter_config( + # self, + # install_session: 'Installer', + # config_entry: str, + # greeter_type: GreeterType, + # ) -> None: + # match greeter_type: + # case GreeterType.Lightdm: + # pass + # case GreeterType.LightdmSlick: + # pass + # case GreeterType.Sddm: + # sddm_config = install_session.target / 'etc/pam.d/sddm' + # self._add_u2f_entry(sys_login, config_entry) + # case GreeterType.Gdm: + # sddm_config = install_session.target / 'etc/pam.d/gdm-password' + # self._add_u2f_entry(sys_login, config_entry) + # case GreeterType.Ly: + # pass + # case GreeterType.CosmicSession: + # pass + + def _add_u2f_entry(self, file: Path, entry: str) -> None: + if not file.exists(): + debug(f'File does not exist: {file}') + return None + + content = file.read_text().splitlines() + + # remove any existing u2f auth entry + content = [line for line in content if 'pam_u2f.so' not in line] + + # add the u2f auth entry as the first one after comments + for i, line in enumerate(content): + if not line.startswith('#'): + content.insert(i, entry) + break + else: + content.append(entry) + + file.write_text('\n'.join(content) + '\n') + + def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User]) -> None: + debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}') + + Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')) + + # https://developers.yubico.com/pam-u2f/ + u2f_auth_file = install_session.target / self._u2f_auth_file + u2f_auth_file.touch() + existing_keys = u2f_auth_file.read_text() + + registered_keys: list[str] = [] + + for user in users: + Tui.print('') + Tui.print(tr('Setting up U2F device for user: {}').format(user.username)) + Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it')) + + worker = SysCommandWorker(f'pamu2fcfg -u {user.username}', peek_output=True) + pin_inputted = False + + while worker.is_alive(): + if pin_inputted is False: + debug(worker._trace_log) + if bytes('enter pin for', 'UTF-8') in worker._trace_log.lower(): + worker.write(bytes(getpass.getpass(''), 'UTF-8')) + pin_inputted = True + + output = worker.decode().strip().splitlines() + debug(f'Output from pamu2fcfg: {output}') + + key = output[-1].strip() + registered_keys.append(key) + + all_keys = '\n'.join(registered_keys) + + if existing_keys: + existing_keys += f'\n{all_keys}' + else: + existing_keys = all_keys + + u2f_auth_file.write_text(existing_keys) + + +auth_handler = AuthenticationHandler() diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py new file mode 100644 index 0000000000..298b0a4ab6 --- /dev/null +++ b/archinstall/lib/authentication/authentication_menu.py @@ -0,0 +1,112 @@ +from typing import override + +from archinstall.lib.disk.fido import Fido2 +from archinstall.lib.menu.abstract_menu import AbstractSubMenu +from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod +from archinstall.lib.translationhandler import tr +from archinstall.tui.curses_menu import SelectMenu +from archinstall.tui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.result import ResultType +from archinstall.tui.types import Alignment, FrameProperties, Orientation + + +class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): + def __init__(self, preset: AuthenticationConfiguration | None = None): + if preset: + self._auth_config = preset + else: + self._auth_config = AuthenticationConfiguration() + + menu_optioons = self._define_menu_options() + self._item_group = MenuItemGroup(menu_optioons, checkmarks=True) + + super().__init__( + self._item_group, + config=self._auth_config, + allow_reset=True, + ) + + @override + def run(self, additional_title: str | None = None) -> AuthenticationConfiguration: + super().run(additional_title=additional_title) + return self._auth_config + + def _define_menu_options(self) -> list[MenuItem]: + return [ + MenuItem( + text=tr('U2F login setup'), + action=setup_u2f_login, + value=self._auth_config.u2f_config, + preview_action=self._prev_u2f_login, + dependencies=[self._depends_on_u2f], + key='u2f_config', + ), + ] + + def _depends_on_u2f(self) -> bool: + devices = Fido2.get_fido2_devices() + if not devices: + return False + return True + + def _prev_u2f_login(self, item: MenuItem) -> str | None: + if item.value is not None: + u2f_config: U2FLoginConfiguration = item.value + + login_method = u2f_config.u2f_login_method.display_value() if u2f_config.u2f_login_method else tr('Not set') + output = tr('U2F login method: ') + login_method + + output += '\n' + output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled')) + + return output + return None + + +def setup_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None: + items = [] + for method in U2FLoginMethod: + items.append(MenuItem(method.display_value(), value=method)) + + group = MenuItemGroup(items) + + if preset is not None: + group.set_selected_by_value(preset.u2f_login_method) + + result = SelectMenu[U2FLoginMethod]( + group, + alignment=Alignment.CENTER, + frame=FrameProperties.min(tr('U2F Login Method')), + allow_skip=True, + allow_reset=True, + ).run() + + match result.type_: + case ResultType.Selection: + u2f_method = result.get_value() + + group = MenuItemGroup.yes_no() + group.focus_item = MenuItem.no() + header = tr('Enable passwordless sudo?') + + result = SelectMenu[bool]( + group, + header=header, + alignment=Alignment.CENTER, + columns=2, + orientation=Orientation.HORIZONTAL, + allow_skip=True, + ).run() + + passwordless_sudo = result.item() == MenuItem.yes() + + return U2FLoginConfiguration( + u2f_login_method=u2f_method, + passwordless_sudo=passwordless_sudo, + ) + case ResultType.Skip: + return preset + case ResultType.Reset: + return None + case _: + raise ValueError('Unhandled result type') diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index 738fc019a6..34551c9a0d 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -268,7 +268,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None: header = tr('Select a FIDO2 device to use for HSM') + '\n' try: - fido_devices = Fido2.get_fido2_devices() + fido_devices = Fido2.get_cryptenroll_devices() except ValueError: return None diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 7ce900d3f3..d2c3a44f36 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -13,11 +13,39 @@ class Fido2: - _loaded: bool = False - _fido2_devices: ClassVar[list[Fido2Device]] = [] + _loaded_cryptsetup: bool = False + _loaded_u2f: bool = False + _cryptenroll_devices: ClassVar[list[Fido2Device]] = [] + _u2f_devices: ClassVar[list[Fido2Device]] = [] @classmethod - def get_fido2_devices(cls, reload: bool = False) -> list[Fido2Device]: + def get_fido2_devices(cls) -> list[Fido2Device]: + """ + fido2-tool output example: + + /dev/hidraw4: vendor=0x1050, product=0x0407 (Yubico YubiKey OTP+FIDO+CCID) + """ + + if not cls._loaded_u2f: + cls._loaded_u2f = True + try: + ret = SysCommand('fido2-token -L').decode() + except SysCallError as e: + error(f'failed to read fido2 devices: {e}') + return [] + + fido_devices = clear_vt100_escape_codes_from_str(ret) + + for line in fido_devices.split('\r\n'): + path, details = line.replace(',', '').split(':', maxsplit=1) + vendor, product, manufacturer = details.strip().split(' ', maxsplit=2) + + cls._u2f_devices.append(Fido2Device(Path(path.strip()), manufacturer.strip(), product.strip().split('=')[1])) + + return cls._u2f_devices + + @classmethod + def get_cryptenroll_devices(cls, reload: bool = False) -> list[Fido2Device]: """ Uses systemd-cryptenroll to list the FIDO2 devices connected that supports FIDO2. @@ -38,7 +66,7 @@ def get_fido2_devices(cls, reload: bool = False) -> list[Fido2Device]: # to prevent continuous reloading which will slow # down moving the cursor in the menu - if not cls._loaded or reload: + if not cls._loaded_cryptsetup or reload: try: ret = SysCommand('systemd-cryptenroll --fido2-device=list').decode() except SysCallError: @@ -65,10 +93,10 @@ def get_fido2_devices(cls, reload: bool = False) -> list[Fido2Device]: Fido2Device(Path(path), manufacturer, product), ) - cls._loaded = True - cls._fido2_devices = devices + cls._loaded_cryptsetup = True + cls._cryptenroll_devices = devices - return cls._fido2_devices + return cls._cryptenroll_devices @classmethod def fido2_enroll( diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 96f2ba703d..99082246fa 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -4,12 +4,14 @@ from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.models.application import ApplicationConfiguration +from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification from archinstall.lib.packages import list_available_packages from archinstall.tui.menu_item import MenuItem, MenuItemGroup from .applications.application_menu import ApplicationMenu from .args import ArchConfig +from .authentication.authentication_menu import AuthenticationMenu from .configuration import save_config from .hardware import SysInfo from .interactions.general_conf import ( @@ -114,6 +116,13 @@ def _get_menu_options(self) -> list[MenuItem]: preview_action=self._prev_root_pwd, key='root_enc_password', ), + MenuItem( + text=tr('Authentication'), + action=self._select_authentication, + value=[], + preview_action=self._prev_authentication, + key='auth_config', + ), MenuItem( text=tr('User account'), action=self._create_user_account, @@ -257,6 +266,10 @@ def _select_applications(self, preset: ApplicationConfiguration | None) -> Appli app_config = ApplicationMenu(preset).run() return app_config + def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None: + auth_config = AuthenticationMenu(preset).run() + return auth_config + def _update_lang_text(self) -> None: """ The options for the global menu are generated with a static text; @@ -296,6 +309,23 @@ def _prev_additional_pkgs(self, item: MenuItem) -> str | None: return output return None + def _prev_authentication(self, item: MenuItem) -> str | None: + if item.value: + auth_config: AuthenticationConfiguration = item.value + output = '' + + if auth_config.u2f_config: + u2f_config = auth_config.u2f_config + login_method = u2f_config.u2f_login_method.display_value() if u2f_config.u2f_login_method else tr('Not set') + output = tr('U2F login method: ') + login_method + + output += '\n' + output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled')) + + return output + + return None + def _prev_applications(self, item: MenuItem) -> str | None: if item.value: app_config: ApplicationConfiguration = item.value diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py new file mode 100644 index 0000000000..a126b75647 --- /dev/null +++ b/archinstall/lib/models/authentication.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Any, NotRequired, TypedDict + +from archinstall.lib.translationhandler import tr + + +class AuthConfigSerialization(TypedDict): + u2f_login_method: NotRequired[str] + enable_sudo: bool + + +class AuthenticationSerialization(TypedDict): + auth_config: NotRequired[AuthConfigSerialization] + + +class U2FLoginMethod(Enum): + Passwordless = 'passwordless' + SecondFactor = 'second_factor' + + def display_value(self) -> str: + match self: + case U2FLoginMethod.Passwordless: + return tr('Passwordless login') + case U2FLoginMethod.SecondFactor: + return tr('Second factor login') + case _: + raise ValueError(f'Unknown type: {self}') + + +@dataclass +class U2FLoginConfiguration: + u2f_login_method: U2FLoginMethod | None = None + passwordless_sudo: bool = False + + def json(self) -> AuthConfigSerialization: + config: AuthConfigSerialization = { + 'u2f_login_method': self.u2f_login_method.value if self.u2f_login_method else None, + 'passwordless_sudo': self.passwordless_sudo, + } + return config + + def parse_arg(args: dict[str, Any]) -> 'U2FLoginConfiguration': + u2f_config = U2FLoginConfiguration() + u2f_login_method = args.get('u2f_login_method') + + if u2f_login_method is None: + return None + + u2f_config.u2f_login_method = U2FLoginMethod(u2f_login_method) + + if passwordless_sudo := args.get('passwordless_sudo') is not None: + u2f_config.passwordless_sudo = passwordless_sudo + + return u2f_config + + +@dataclass +class AuthenticationConfiguration: + u2f_config: U2FLoginConfiguration | None = None + + @staticmethod + def parse_arg(args: dict[str, Any]) -> 'AuthenticationConfiguration': + auth_config = AuthenticationConfiguration() + + if (u2f_config := args.get('u2f_config')) is not None: + auth_config.u2f_config = U2FLoginConfiguration.parse_arg(u2f_config) + + return auth_config + + def json(self) -> AuthenticationSerialization: + config: AuthenticationSerialization = {} + + if self.u2f_config: + config['u2f_config'] = self.u2f_config.json() + + return config diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index e2266e892b..d74df63053 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -4,8 +4,8 @@ from archinstall import SysInfo from archinstall.lib.applications.application_handler import application_handler from archinstall.lib.args import arch_config_handler +from archinstall.lib.authentication.authentication_handler import auth_handler from archinstall.lib.configuration import ConfigurationOutput -from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands @@ -71,6 +71,11 @@ def perform_installation(mountpoint: Path) -> None: disk_config, kernels=config.kernels, ) as installation: + # if profile_config := config.profile_config: + # profile_handler.install_profile_config(installation, profile_config) + + # exit(0) + # Mount all the drives to the desired mountpoint if disk_config.config_type != DiskLayoutType.Pre_mount: installation.mount_ordered_layout() @@ -116,6 +121,9 @@ def perform_installation(mountpoint: Path) -> None: if users := config.users: installation.create_users(users) + if config.auth_config: + auth_handler.setup_auth(installation, config.auth_config, config.users, config.profile_config) + if config.packages and config.packages[0] != '': installation.add_additional_packages(config.packages) @@ -198,9 +206,9 @@ def guided() -> None: if aborted: return guided() - if arch_config_handler.config.disk_config: - fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) - fs_handler.perform_filesystem_operations() + # if arch_config_handler.config.disk_config: + # fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) + # fs_handler.perform_filesystem_operations() perform_installation(arch_config_handler.args.mountpoint) diff --git a/tests/data/test_config.json b/tests/data/test_config.json index f469df954d..9b0c218d32 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -9,6 +9,12 @@ "audio": "pipewire" } }, + "auth_config": { + "u2f_config": { + "passwordless_sudo": true, + "u2f_login_method": "passwordless" + } + }, "audio_config": { "audio": "pipewire" }, diff --git a/tests/test_args.py b/tests/test_args.py index 2df7e4ad2a..5a6d61f0bb 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -7,6 +7,7 @@ from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments from archinstall.lib.hardware import GfxDriver from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration +from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType from archinstall.lib.models.locale import LocaleConfiguration @@ -132,6 +133,12 @@ def test_config_file_parsing( bluetooth_config=BluetoothConfiguration(enabled=True), audio_config=AudioConfiguration(audio=Audio.PIPEWIRE), ), + auth_config=AuthenticationConfiguration( + u2f_config=U2FLoginConfiguration( + u2f_login_method=U2FLoginMethod.Passwordless, + passwordless_sudo=True, + ), + ), locale_config=LocaleConfiguration( kb_layout='us', sys_lang='en_US', From b96201db224b77dc48c1d19431348a0643b418d7 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 29 Jun 2025 18:43:39 +1000 Subject: [PATCH 2/5] Update --- .../authentication/authentication_handler.py | 9 ++++---- .../lib/authentication/authentication_menu.py | 6 ++--- archinstall/lib/global_menu.py | 2 +- archinstall/lib/models/authentication.py | 23 ++++++++++--------- archinstall/scripts/guided.py | 10 +++++--- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py index 5f3e082f62..1c8ca771ac 100644 --- a/archinstall/lib/authentication/authentication_handler.py +++ b/archinstall/lib/authentication/authentication_handler.py @@ -16,7 +16,7 @@ class AuthenticationHandler: def __init__(self) -> None: - self._u2f_auth_file = Path('/etc/u2f_mappings') + self._u2f_auth_file = Path('etc/u2f_mappings') def setup_auth( self, @@ -25,7 +25,7 @@ def setup_auth( users: list['User'] | None = None, profile_config: ProfileConfiguration | None = None, ) -> None: - if auth_config.u2f_config: + if auth_config.u2f_config and users is not None: self._setup_u2f_login(install_session, auth_config.u2f_config, users, profile_config) def _setup_u2f_login( @@ -123,6 +123,8 @@ def _add_u2f_entry(self, file: Path, entry: str) -> None: def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User]) -> None: debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}') + install_session.pacman.strap('pam-u2f') + Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')) # https://developers.yubico.com/pam-u2f/ @@ -137,12 +139,11 @@ def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLo Tui.print(tr('Setting up U2F device for user: {}').format(user.username)) Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it')) - worker = SysCommandWorker(f'pamu2fcfg -u {user.username}', peek_output=True) + worker = SysCommandWorker(f'arch-chroot {install_session.target} pamu2fcfg -u {user.username}', peek_output=True) pin_inputted = False while worker.is_alive(): if pin_inputted is False: - debug(worker._trace_log) if bytes('enter pin for', 'UTF-8') in worker._trace_log.lower(): worker.write(bytes(getpass.getpass(''), 'UTF-8')) pin_inputted = True diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 298b0a4ab6..e8852bbb74 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -53,7 +53,7 @@ def _prev_u2f_login(self, item: MenuItem) -> str | None: if item.value is not None: u2f_config: U2FLoginConfiguration = item.value - login_method = u2f_config.u2f_login_method.display_value() if u2f_config.u2f_login_method else tr('Not set') + login_method = u2f_config.u2f_login_method.display_value() output = tr('U2F login method: ') + login_method output += '\n' @@ -89,7 +89,7 @@ def setup_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | No group.focus_item = MenuItem.no() header = tr('Enable passwordless sudo?') - result = SelectMenu[bool]( + result_sudo = SelectMenu[bool]( group, header=header, alignment=Alignment.CENTER, @@ -98,7 +98,7 @@ def setup_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | No allow_skip=True, ).run() - passwordless_sudo = result.item() == MenuItem.yes() + passwordless_sudo = result_sudo.item() == MenuItem.yes() return U2FLoginConfiguration( u2f_login_method=u2f_method, diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 99082246fa..f0df3ef4c5 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -316,7 +316,7 @@ def _prev_authentication(self, item: MenuItem) -> str | None: if auth_config.u2f_config: u2f_config = auth_config.u2f_config - login_method = u2f_config.u2f_login_method.display_value() if u2f_config.u2f_login_method else tr('Not set') + login_method = u2f_config.u2f_login_method.display_value() output = tr('U2F login method: ') + login_method output += '\n' diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py index a126b75647..1f702866a4 100644 --- a/archinstall/lib/models/authentication.py +++ b/archinstall/lib/models/authentication.py @@ -5,13 +5,13 @@ from archinstall.lib.translationhandler import tr -class AuthConfigSerialization(TypedDict): - u2f_login_method: NotRequired[str] - enable_sudo: bool +class U2FLoginConfigSerialization(TypedDict): + u2f_login_method: str + passwordless_sudo: bool class AuthenticationSerialization(TypedDict): - auth_config: NotRequired[AuthConfigSerialization] + u2f_config: NotRequired[U2FLoginConfigSerialization] class U2FLoginMethod(Enum): @@ -30,23 +30,24 @@ def display_value(self) -> str: @dataclass class U2FLoginConfiguration: - u2f_login_method: U2FLoginMethod | None = None + u2f_login_method: U2FLoginMethod passwordless_sudo: bool = False - def json(self) -> AuthConfigSerialization: - config: AuthConfigSerialization = { - 'u2f_login_method': self.u2f_login_method.value if self.u2f_login_method else None, + def json(self) -> U2FLoginConfigSerialization: + return { + 'u2f_login_method': self.u2f_login_method.value, 'passwordless_sudo': self.passwordless_sudo, } - return config - def parse_arg(args: dict[str, Any]) -> 'U2FLoginConfiguration': - u2f_config = U2FLoginConfiguration() + @staticmethod + def parse_arg(args: dict[str, Any]) -> 'U2FLoginConfiguration' | None: u2f_login_method = args.get('u2f_login_method') if u2f_login_method is None: return None + u2f_config = U2FLoginConfiguration(u2f_login_method=U2FLoginMethod(u2f_login_method)) + u2f_config.u2f_login_method = U2FLoginMethod(u2f_login_method) if passwordless_sudo := args.get('passwordless_sudo') is not None: diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index d74df63053..9759d1a498 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -6,6 +6,7 @@ from archinstall.lib.args import arch_config_handler from archinstall.lib.authentication.authentication_handler import auth_handler from archinstall.lib.configuration import ConfigurationOutput +from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands @@ -74,6 +75,9 @@ def perform_installation(mountpoint: Path) -> None: # if profile_config := config.profile_config: # profile_handler.install_profile_config(installation, profile_config) + # if config.auth_config: + # auth_handler.setup_auth(installation, config.auth_config, config.users, config.profile_config) + # exit(0) # Mount all the drives to the desired mountpoint @@ -206,9 +210,9 @@ def guided() -> None: if aborted: return guided() - # if arch_config_handler.config.disk_config: - # fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) - # fs_handler.perform_filesystem_operations() + if arch_config_handler.config.disk_config: + fs_handler = FilesystemHandler(arch_config_handler.config.disk_config) + fs_handler.perform_filesystem_operations() perform_installation(arch_config_handler.args.mountpoint) From 263ea2d6dd38fbc59c72f54eb318679cd066a67e Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 29 Jun 2025 18:47:23 +1000 Subject: [PATCH 3/5] Update --- archinstall/lib/models/authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py index 1f702866a4..d260ddac80 100644 --- a/archinstall/lib/models/authentication.py +++ b/archinstall/lib/models/authentication.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, NotRequired, TypedDict +from typing import Any, NotRequired, Optional, TypedDict from archinstall.lib.translationhandler import tr @@ -40,7 +40,7 @@ def json(self) -> U2FLoginConfigSerialization: } @staticmethod - def parse_arg(args: dict[str, Any]) -> 'U2FLoginConfiguration' | None: + def parse_arg(args: dict[str, Any]) -> Optional['U2FLoginConfiguration']: u2f_login_method = args.get('u2f_login_method') if u2f_login_method is None: From a046f53737717995bac16be803f077855404df69 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Sun, 29 Jun 2025 18:50:02 +1000 Subject: [PATCH 4/5] Update --- .../authentication/authentication_handler.py | 45 +------------------ archinstall/scripts/guided.py | 10 +---- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py index 1c8ca771ac..f38bce5aec 100644 --- a/archinstall/lib/authentication/authentication_handler.py +++ b/archinstall/lib/authentication/authentication_handler.py @@ -4,7 +4,6 @@ from archinstall.lib.general import SysCommandWorker from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod -from archinstall.lib.models.profile_model import ProfileConfiguration from archinstall.lib.models.users import User from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr @@ -23,26 +22,23 @@ def setup_auth( install_session: 'Installer', auth_config: AuthenticationConfiguration, users: list['User'] | None = None, - profile_config: ProfileConfiguration | None = None, ) -> None: if auth_config.u2f_config and users is not None: - self._setup_u2f_login(install_session, auth_config.u2f_config, users, profile_config) + self._setup_u2f_login(install_session, auth_config.u2f_config, users) def _setup_u2f_login( self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], - profile_config: ProfileConfiguration | None = None, ) -> None: self._configure_u2f_mapping(install_session, u2f_config, users) - self._update_pam_config(install_session, u2f_config, profile_config) + self._update_pam_config(install_session, u2f_config) def _update_pam_config( self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, - profile_config: ProfileConfiguration | None = None, ) -> None: match u2f_config.u2f_login_method: case U2FLoginMethod.Passwordless: @@ -63,43 +59,6 @@ def _update_pam_config( self._add_u2f_entry(sys_login, config_entry) - # if profile_config and profile_config.profile: - # if profile_config.greeter is not None: - # self._setup_greeter_config( - # install_session, - # config_entry, - # profile_config.greeter, - # ) - # - # if profile_config.profile.is_desktop_profile(): - # desktop_profile: DesktopProfile = profile_config.profile - # else: - # self._add_u2f_entry(sys_login, config_entry) - # else: - # self._add_u2f_entry(sys_login, config_entry) - - # def _setup_greeter_config( - # self, - # install_session: 'Installer', - # config_entry: str, - # greeter_type: GreeterType, - # ) -> None: - # match greeter_type: - # case GreeterType.Lightdm: - # pass - # case GreeterType.LightdmSlick: - # pass - # case GreeterType.Sddm: - # sddm_config = install_session.target / 'etc/pam.d/sddm' - # self._add_u2f_entry(sys_login, config_entry) - # case GreeterType.Gdm: - # sddm_config = install_session.target / 'etc/pam.d/gdm-password' - # self._add_u2f_entry(sys_login, config_entry) - # case GreeterType.Ly: - # pass - # case GreeterType.CosmicSession: - # pass - def _add_u2f_entry(self, file: Path, entry: str) -> None: if not file.exists(): debug(f'File does not exist: {file}') diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 9759d1a498..e8ff25b82d 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -72,14 +72,6 @@ def perform_installation(mountpoint: Path) -> None: disk_config, kernels=config.kernels, ) as installation: - # if profile_config := config.profile_config: - # profile_handler.install_profile_config(installation, profile_config) - - # if config.auth_config: - # auth_handler.setup_auth(installation, config.auth_config, config.users, config.profile_config) - - # exit(0) - # Mount all the drives to the desired mountpoint if disk_config.config_type != DiskLayoutType.Pre_mount: installation.mount_ordered_layout() @@ -126,7 +118,7 @@ def perform_installation(mountpoint: Path) -> None: installation.create_users(users) if config.auth_config: - auth_handler.setup_auth(installation, config.auth_config, config.users, config.profile_config) + auth_handler.setup_auth(installation, config.auth_config, config.users) if config.packages and config.packages[0] != '': installation.add_additional_packages(config.packages) From 7ff4fedac29502446956a0f9e4e225aff6babee3 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 30 Jun 2025 21:47:43 +1000 Subject: [PATCH 5/5] Update --- .../authentication/authentication_handler.py | 34 +++++++++++-------- archinstall/lib/disk/fido.py | 2 +- archinstall/lib/installer.py | 4 +-- archinstall/scripts/guided.py | 4 +-- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py index f38bce5aec..ab0b0ebfaf 100644 --- a/archinstall/lib/authentication/authentication_handler.py +++ b/archinstall/lib/authentication/authentication_handler.py @@ -21,18 +21,14 @@ def setup_auth( self, install_session: 'Installer', auth_config: AuthenticationConfiguration, - users: list['User'] | None = None, + users: list['User'], + hostname: str, ) -> None: if auth_config.u2f_config and users is not None: - self._setup_u2f_login(install_session, auth_config.u2f_config, users) + self._setup_u2f_login(install_session, auth_config.u2f_config, users, hostname) - def _setup_u2f_login( - self, - install_session: 'Installer', - u2f_config: U2FLoginConfiguration, - users: list[User], - ) -> None: - self._configure_u2f_mapping(install_session, u2f_config, users) + def _setup_u2f_login(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None: + self._configure_u2f_mapping(install_session, u2f_config, users, hostname) self._update_pam_config(install_session, u2f_config) def _update_pam_config( @@ -42,9 +38,9 @@ def _update_pam_config( ) -> None: match u2f_config.u2f_login_method: case U2FLoginMethod.Passwordless: - config_entry = f'auth sufficient pam_u2f.so authfile={self._u2f_auth_file} cue' + config_entry = 'auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue' case U2FLoginMethod.SecondFactor: - config_entry = f'auth required pam_u2f.so authfile={self._u2f_auth_file} cue' + config_entry = 'auth required pam_u2f.so authfile=/etc/u2f_mappings cue' case _: raise ValueError(f'Unknown U2F login method: {u2f_config.u2f_login_method}') @@ -79,7 +75,13 @@ def _add_u2f_entry(self, file: Path, entry: str) -> None: file.write_text('\n'.join(content) + '\n') - def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User]) -> None: + def _configure_u2f_mapping( + self, + install_session: 'Installer', + u2f_config: U2FLoginConfiguration, + users: list[User], + hostname: str, + ) -> None: debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}') install_session.pacman.strap('pam-u2f') @@ -87,7 +89,7 @@ def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLo Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}')) # https://developers.yubico.com/pam-u2f/ - u2f_auth_file = install_session.target / self._u2f_auth_file + u2f_auth_file = install_session.target / 'etc/u2f_mappings' u2f_auth_file.touch() existing_keys = u2f_auth_file.read_text() @@ -98,7 +100,11 @@ def _configure_u2f_mapping(self, install_session: 'Installer', u2f_config: U2FLo Tui.print(tr('Setting up U2F device for user: {}').format(user.username)) Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it')) - worker = SysCommandWorker(f'arch-chroot {install_session.target} pamu2fcfg -u {user.username}', peek_output=True) + cmd = ' '.join(['arch-chroot', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']) + + debug(f'Enrolling U2F device: {cmd}') + + worker = SysCommandWorker(cmd, peek_output=True) pin_inputted = False while worker.is_alive(): diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index d2c3a44f36..b6676db41d 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -38,7 +38,7 @@ def get_fido2_devices(cls) -> list[Fido2Device]: for line in fido_devices.split('\r\n'): path, details = line.replace(',', '').split(':', maxsplit=1) - vendor, product, manufacturer = details.strip().split(' ', maxsplit=2) + _, product, manufacturer = details.strip().split(' ', maxsplit=2) cls._u2f_devices.append(Fido2Device(Path(path.strip()), manufacturer.strip(), product.strip().split('=')[1])) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ac861f022c..652482ed49 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1506,7 +1506,7 @@ def _add_efistub_bootloader( parent_dev_path = device_handler.get_parent_device_path(boot_partition.safe_dev_path) - cmd_template = ( + cmd_template = [ 'efibootmgr', '--create', '--disk', @@ -1520,7 +1520,7 @@ def _add_efistub_bootloader( '--unicode', *cmdline, '--verbose', - ) + ] for kernel in self.kernels: # Setup the firmware entry diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index e8ff25b82d..2c2173a760 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -117,8 +117,8 @@ def perform_installation(mountpoint: Path) -> None: if users := config.users: installation.create_users(users) - if config.auth_config: - auth_handler.setup_auth(installation, config.auth_config, config.users) + if config.auth_config and config.users: + auth_handler.setup_auth(installation, config.auth_config, config.users, config.hostname) if config.packages and config.packages[0] != '': installation.add_additional_packages(config.packages)