diff --git a/sssd_test_framework/utils/authentication.py b/sssd_test_framework/utils/authentication.py index bae60ef2..2482d601 100644 --- a/sssd_test_framework/utils/authentication.py +++ b/sssd_test_framework/utils/authentication.py @@ -11,7 +11,9 @@ from pytest_mh.utils.fs import LinuxFileSystem from ..misc.errors import ExpectScriptError -from ..misc.globals import test_venv_bin +from ..misc.globals import ( + test_venv_bin, +) from .idp import IdpAuthenticationUtils __all__ = [ @@ -844,6 +846,48 @@ def vfido_passkey( rc, _, _, _ = self.vfido_passkey_with_output(username=username, pin=pin, command=command) return rc == 0 + def smartcard_with_output( + self, username: str, pin: str, *, num_certs: int = 1, cert_selection: int = 1 + ) -> ProcessResult: + """ + Wait for the user to become resolvable then authenticate via ``su`` with the smart card PIN. + + :param username: Username. + :type username: str + :param pin: Smart card PIN. + :type pin: str + :param num_certs: Number of certificates that map to the user, defaults to 1. + :type num_certs: int, optional + :param cert_selection: Index of the certificate to select when multiple are present, defaults to 1. + :type cert_selection: int, optional + :return: Result of the ``su`` command. + :rtype: ProcessResult + """ + su_input = f"{cert_selection}\n{pin}" if num_certs > 1 else pin + return self.host.conn.run( + f"su - {username} -c 'su - {username} -c whoami'", + input=su_input, + raise_on_error=False, + ) + + def smartcard(self, username: str, pin: str, *, num_certs: int = 1, cert_selection: int = 1) -> bool: + """ + Wait for the user to become resolvable then authenticate via ``su`` with the smart card PIN. + + :param username: Username. + :type username: str + :param pin: Smart card PIN. + :type pin: str + :param num_certs: Number of certificates that map to the user, defaults to 1. + :type num_certs: int, optional + :param cert_selection: Index of the certificate to select when multiple are present, defaults to 1. + :type cert_selection: int, optional + :return: True if authentication was successful, False otherwise. + :rtype: bool + """ + result = self.smartcard_with_output(username, pin, num_certs=num_certs, cert_selection=cert_selection) + return result.rc == 0 and "PIN" in result.stderr and username in result.stdout + class SSHAuthenticationUtils(MultihostUtility[MultihostHost]): """ diff --git a/sssd_test_framework/utils/smartcard.py b/sssd_test_framework/utils/smartcard.py index 64748514..ae23d37d 100644 --- a/sssd_test_framework/utils/smartcard.py +++ b/sssd_test_framework/utils/smartcard.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from ..roles.client import Client + from ..roles.ipa import IPA __all__ = [ "SmartCardUtils", @@ -47,11 +48,19 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem, svc: SystemdService self.svc: SystemdServices = svc """Systemd utility to manage and interact with svc.""" - def initialize_card(self, label: str = "sc_test", so_pin: str = "12345678", user_pin: str = "123456") -> None: + def initialize_card( + self, + label: str = "sc_test", + so_pin: str = "12345678", + user_pin: str = "123456", + reset: bool = True, + ) -> None: """ - Initializes a SoftHSM token with the given label and PINs. + Initialize a SoftHSM token with the given label and PINs. - Cleans cache directories and prepares the token directory. + When *reset* is ``True`` (default), existing token storage and OpenSC + caches are removed first. Pass ``False`` to add a token alongside + existing ones (multi-token / multi-card setup). :param label: Token label, defaults to "sc_test" :type label: str, optional @@ -59,12 +68,14 @@ def initialize_card(self, label: str = "sc_test", so_pin: str = "12345678", user :type so_pin: str, optional :param user_pin: User PIN, defaults to "123456" :type user_pin: str, optional + :param reset: Remove existing tokens before initializing, defaults to True + :type reset: bool, optional """ - for path in self.OPENSC_CACHE_PATHS: - self.fs.rm(path) - - self.fs.rm(self.TOKEN_STORAGE_PATH) - self.fs.mkdir_p(self.TOKEN_STORAGE_PATH) + if reset: + for path in self.OPENSC_CACHE_PATHS: + self.fs.rm(path) + self.fs.rm(self.TOKEN_STORAGE_PATH) + self.fs.mkdir_p(self.TOKEN_STORAGE_PATH) args: CLIBuilderArgs = { "label": (self.cli.option.VALUE, label), @@ -82,6 +93,7 @@ def add_cert( cert_id: str = "01", pin: str = "123456", private: bool | None = False, + token_label: str | None = None, label: str | None = None, ) -> None: """ @@ -95,7 +107,14 @@ def add_cert( :type pin: str, optional :param private: Whether the object is a private key. Defaults to False. :type private: bool, optional - :param label: Label for the PKCS#11 object, defaults to None. + :param token_label: Label of the target token. When ``None`` (the + default) ``pkcs11-tool`` writes to the first available token. + Set this when multiple tokens exist to target a specific one. + :type token_label: str | None, optional + :param label: Label for the PKCS#11 object being written. Required + when ``p11_child`` accesses the token directly (i.e. without + ``virt_cacard``), because the response parser expects a + non-empty label. :type label: str | None, optional """ obj_type = "privkey" if private else "cert" @@ -107,11 +126,20 @@ def add_cert( "type": (self.cli.option.VALUE, obj_type), "id": (self.cli.option.VALUE, cert_id), } + if token_label is not None: + args["token-label"] = (self.cli.option.VALUE, token_label) if label is not None: args["label"] = (self.cli.option.VALUE, label) self.host.conn.run(self.cli.command("pkcs11-tool", args), env={"SOFTHSM2_CONF": self.SOFTHSM2_CONF_PATH}) - def add_key(self, key_path: str, key_id: str = "01", pin: str = "123456", label: str | None = None) -> None: + def add_key( + self, + key_path: str, + key_id: str = "01", + pin: str = "123456", + token_label: str | None = None, + label: str | None = None, + ) -> None: """ Adds a private key to the smart card. @@ -121,10 +149,12 @@ def add_key(self, key_path: str, key_id: str = "01", pin: str = "123456", label: :type key_id: str, optional :param pin: User PIN, defaults to "123456" :type pin: str, optional - :param label: Label for the PKCS#11 object, defaults to None. + :param token_label: Label of the target token (see :meth:`add_cert`). + :type token_label: str | None, optional + :param label: Label for the PKCS#11 object (see :meth:`add_cert`). :type label: str | None, optional """ - self.add_cert(cert_path=key_path, cert_id=key_id, pin=pin, private=True, label=label) + self.add_cert(cert_path=key_path, cert_id=key_id, pin=pin, private=True, token_label=token_label, label=label) def generate_cert( self, @@ -199,3 +229,56 @@ def test_example(client: Client): data = client.host.fs.read(cert) client.host.fs.append("/etc/sssd/pki/sssd_auth_ca_db.pem", data) client.sssd.start() + + def enroll_to_token( + self, + client: Client, + ipa: IPA, + username: str, + *, + token_label: str = "sc_test", + cert_id: str = "01", + pin: str = "123456", + init: bool = False, + ) -> None: + """ + Request an IPA-signed certificate for *username* and store it on *token_label*. + + When *init* is ``True``, the token is first initialized via + :meth:`initialize_card` (resetting any existing token storage). + Pass ``False`` when the card has already been initialized or when + adding a second certificate to an existing token. + + :param client: Client role object. + :type client: Client + :param ipa: IPA role object whose CA issues the certificate. + :type ipa: IPA + :param username: IPA principal to issue the certificate for. + :type username: str + :param token_label: SoftHSM token label to write the objects to, + defaults to "sc_test". + :type token_label: str, optional + :param cert_id: PKCS#11 object ID, defaults to "01". + :type cert_id: str, optional + :param pin: User PIN for the token, defaults to "123456". + :type pin: str, optional + :param init: Initialize (and reset) the token before enrolling, + defaults to False. + :type init: bool, optional + """ + if init: + self.initialize_card(label=token_label, user_pin=pin) + + cert, key, _ = ipa.ca.request(username) + cert_content = ipa.fs.read(cert) + key_content = ipa.fs.read(key) + + name_suffix = token_label + cert_path = f"/opt/test_ca/{username}_{name_suffix}.crt" + key_path = f"/opt/test_ca/{username}_{name_suffix}.key" + + client.fs.write(cert_path, cert_content) + client.fs.write(key_path, key_content) + + self.add_key(key_path, key_id=cert_id, pin=pin, token_label=token_label, label=username) + self.add_cert(cert_path, cert_id=cert_id, pin=pin, token_label=token_label, label=username) diff --git a/sssd_test_framework/utils/sssd.py b/sssd_test_framework/utils/sssd.py index 03208d54..182e7147 100644 --- a/sssd_test_framework/utils/sssd.py +++ b/sssd_test_framework/utils/sssd.py @@ -23,6 +23,7 @@ from ..roles.base import BaseRole from ..roles.kdc import KDC from .authselect import AuthselectUtils + from .smartcard import SmartCardUtils __all__ = [ @@ -1178,3 +1179,35 @@ def subid(self) -> None: Configure SSSD for subid. """ self.sssd.authselect.select("sssd", ["with-subid"]) + + def smartcard_with_softhsm(self, smartcard: SmartCardUtils) -> None: + """ + Configure SSSD for smart card authentication with SoftHSM multi-token support. + + :param smartcard: SmartCardUtils instance. + :type smartcard: SmartCardUtils + """ + conf = smartcard.SOFTHSM2_CONF_PATH + token_storage = smartcard.TOKEN_STORAGE_PATH + module = "/usr/lib64/pkcs11/libsofthsm2.so" + + softhsm_conf = smartcard.fs.read(conf) + if "slots.removable" not in softhsm_conf: + smartcard.fs.append(conf, "\nslots.removable = true", dedent=False) + smartcard.fs.copy(conf, "/etc/softhsm2.conf") + smartcard.fs.write("/etc/pkcs11/modules/softhsm2.module", f"module: {module}") + smartcard.fs.mkdir_p("/etc/systemd/system/sssd.service.d") + smartcard.fs.write( + "/etc/systemd/system/sssd.service.d/softhsm.conf", + f"[Service]\nEnvironment=SOFTHSM2_CONF={conf}", + dedent=False, + ) + smartcard.svc.reload_daemon() + smartcard.fs.chmod("o+rX", "/opt/test_ca/", args=["-R"]) + smartcard.fs.chown(f"{token_storage}/", user="sssd", group="sssd", args=["-R"]) + smartcard.fs.chmod("770", f"{token_storage}/", args=["-R"]) + + self.sssd.authselect.select("sssd", ["with-smartcard", "with-mkhomedir"]) + self.sssd.pam["pam_cert_auth"] = "True" + self.sssd.domain["local_auth_policy"] = "enable:smartcard" + self.sssd.start()