Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion sssd_test_framework/utils/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -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]):
"""
Expand Down
53 changes: 41 additions & 12 deletions sssd_test_framework/utils/smartcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,34 @@ 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
:param so_pin: Security Officer PIN, defaults to "12345678"
: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),
Expand All @@ -82,6 +92,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:
"""
Expand All @@ -95,7 +106,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"
Expand All @@ -107,11 +125,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.

Expand All @@ -121,10 +148,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,
Expand Down
33 changes: 33 additions & 0 deletions sssd_test_framework/utils/sssd.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ..roles.base import BaseRole
from ..roles.kdc import KDC
from .authselect import AuthselectUtils
from .smartcard import SmartCardUtils


__all__ = [
Expand Down Expand Up @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side note. You may want a smartcard_with_virtcacard or similar to handle the setup since it's fairly standard (I think).

"""
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()
Loading