Skip to content

Commit 4823cbb

Browse files
committed
Add support for multi-token management in SmartCardUtils
1 parent 8a9e9d9 commit 4823cbb

4 files changed

Lines changed: 139 additions & 11 deletions

File tree

sssd_test_framework/misc/globals.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@
33
test_venv = "/opt/test_venv"
44
test_venv_bin = f"{test_venv}/bin"
55
scauto_path = f"{test_venv_bin}/scauto"
6+
7+
USER_RESOLVABLE_ATTEMPTS: int = 15
8+
"""Maximum number of polling attempts when waiting for a user to become resolvable by SSSD."""
9+
10+
USER_RESOLVABLE_INTERVAL_S: int = 2
11+
"""Seconds to sleep between each polling attempt when waiting for a user to be resolvable."""
12+
13+
USER_RESOLVABLE_CACHE_EXPIRY_ATTEMPT: int = 3
14+
"""Attempt number at which ``sss_cache -E`` is called to flush the SSSD cache."""

sssd_test_framework/utils/authentication.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import time
56
from datetime import datetime
67
from enum import Enum
78
from typing import Any
@@ -11,7 +12,12 @@
1112
from pytest_mh.utils.fs import LinuxFileSystem
1213

1314
from ..misc.errors import ExpectScriptError
14-
from ..misc.globals import test_venv_bin
15+
from ..misc.globals import (
16+
USER_RESOLVABLE_ATTEMPTS,
17+
USER_RESOLVABLE_CACHE_EXPIRY_ATTEMPT,
18+
USER_RESOLVABLE_INTERVAL_S,
19+
test_venv_bin,
20+
)
1521
from .idp import IdpAuthenticationUtils
1622

1723
__all__ = [
@@ -844,6 +850,54 @@ def vfido_passkey(
844850
rc, _, _, _ = self.vfido_passkey_with_output(username=username, pin=pin, command=command)
845851
return rc == 0
846852

853+
def smartcard_with_su_output(self, username: str, pin: str, *, num_certs: int = 1) -> ProcessResult:
854+
"""
855+
Wait for the user to become resolvable then authenticate via ``su`` with the smart card PIN.
856+
857+
:param username: Username.
858+
:type username: str
859+
:param pin: Smart card PIN.
860+
:type pin: str
861+
:param num_certs: Number of certificates that map to the user, defaults to 1.
862+
:type num_certs: int, optional
863+
:return: Result of the ``su`` command.
864+
:rtype: ProcessResult
865+
"""
866+
for attempt in range(USER_RESOLVABLE_ATTEMPTS):
867+
time.sleep(USER_RESOLVABLE_INTERVAL_S)
868+
check = self.host.conn.run(f"getent passwd {username}", raise_on_error=False)
869+
if check.rc == 0:
870+
break
871+
if attempt == USER_RESOLVABLE_CACHE_EXPIRY_ATTEMPT:
872+
self.host.conn.run("sss_cache -E", raise_on_error=False)
873+
else:
874+
raise AssertionError(
875+
f"User '{username}' was not resolvable by SSSD after {USER_RESOLVABLE_ATTEMPTS} attempts"
876+
)
877+
878+
su_input = f"1\n{pin}" if num_certs > 1 else pin
879+
return self.host.conn.run(
880+
f"su - {username} -c 'su - {username} -c whoami'",
881+
input=su_input,
882+
raise_on_error=False,
883+
)
884+
885+
def smartcard_with_su(self, username: str, pin: str, *, num_certs: int = 1) -> bool:
886+
"""
887+
Wait for the user to become resolvable then authenticate via ``su`` with the smart card PIN.
888+
889+
:param username: Username.
890+
:type username: str
891+
:param pin: Smart card PIN.
892+
:type pin: str
893+
:param num_certs: Number of certificates that map to the user, defaults to 1.
894+
:type num_certs: int, optional
895+
:return: True if authentication was successful, False otherwise.
896+
:rtype: bool
897+
"""
898+
result = self.smartcard_with_su_output(username, pin, num_certs=num_certs)
899+
return result.rc == 0 and "PIN" in result.stderr and username in result.stdout
900+
847901

848902
class SSHAuthenticationUtils(MultihostUtility[MultihostHost]):
849903
"""

sssd_test_framework/utils/smartcard.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,34 @@ def __init__(self, host: MultihostHost, fs: LinuxFileSystem, svc: SystemdService
4747
self.svc: SystemdServices = svc
4848
"""Systemd utility to manage and interact with svc."""
4949

50-
def initialize_card(self, label: str = "sc_test", so_pin: str = "12345678", user_pin: str = "123456") -> None:
50+
def initialize_card(
51+
self,
52+
label: str = "sc_test",
53+
so_pin: str = "12345678",
54+
user_pin: str = "123456",
55+
reset: bool = True,
56+
) -> None:
5157
"""
52-
Initializes a SoftHSM token with the given label and PINs.
58+
Initialize a SoftHSM token with the given label and PINs.
5359
54-
Cleans cache directories and prepares the token directory.
60+
When *reset* is ``True`` (default), existing token storage and OpenSC
61+
caches are removed first. Pass ``False`` to add a token alongside
62+
existing ones (multi-token / multi-card setup).
5563
5664
:param label: Token label, defaults to "sc_test"
5765
:type label: str, optional
5866
:param so_pin: Security Officer PIN, defaults to "12345678"
5967
:type so_pin: str, optional
6068
:param user_pin: User PIN, defaults to "123456"
6169
:type user_pin: str, optional
70+
:param reset: Remove existing tokens before initializing, defaults to True
71+
:type reset: bool, optional
6272
"""
63-
for path in self.OPENSC_CACHE_PATHS:
64-
self.fs.rm(path)
65-
66-
self.fs.rm(self.TOKEN_STORAGE_PATH)
67-
self.fs.mkdir_p(self.TOKEN_STORAGE_PATH)
73+
if reset:
74+
for path in self.OPENSC_CACHE_PATHS:
75+
self.fs.rm(path)
76+
self.fs.rm(self.TOKEN_STORAGE_PATH)
77+
self.fs.mkdir_p(self.TOKEN_STORAGE_PATH)
6878

6979
args: CLIBuilderArgs = {
7080
"label": (self.cli.option.VALUE, label),
@@ -82,6 +92,8 @@ def add_cert(
8292
cert_id: str = "01",
8393
pin: str = "123456",
8494
private: bool | None = False,
95+
token_label: str | None = None,
96+
label: str | None = None,
8597
) -> None:
8698
"""
8799
Adds a certificate or private key to the smart card.
@@ -94,6 +106,15 @@ def add_cert(
94106
:type pin: str, optional
95107
:param private: Whether the object is a private key. Defaults to False.
96108
:type private: bool, optional
109+
:param token_label: Label of the target token. When ``None`` (the
110+
default) ``pkcs11-tool`` writes to the first available token.
111+
Set this when multiple tokens exist to target a specific one.
112+
:type token_label: str | None, optional
113+
:param label: Label for the PKCS#11 object being written. Required
114+
when ``p11_child`` accesses the token directly (i.e. without
115+
``virt_cacard``), because the response parser expects a
116+
non-empty label.
117+
:type label: str | None, optional
97118
"""
98119
obj_type = "privkey" if private else "cert"
99120
args: CLIBuilderArgs = {
@@ -104,9 +125,20 @@ def add_cert(
104125
"type": (self.cli.option.VALUE, obj_type),
105126
"id": (self.cli.option.VALUE, cert_id),
106127
}
128+
if token_label is not None:
129+
args["token-label"] = (self.cli.option.VALUE, token_label)
130+
if label is not None:
131+
args["label"] = (self.cli.option.VALUE, label)
107132
self.host.conn.run(self.cli.command("pkcs11-tool", args), env={"SOFTHSM2_CONF": self.SOFTHSM2_CONF_PATH})
108133

109-
def add_key(self, key_path: str, key_id: str = "01", pin: str = "123456") -> None:
134+
def add_key(
135+
self,
136+
key_path: str,
137+
key_id: str = "01",
138+
pin: str = "123456",
139+
token_label: str | None = None,
140+
label: str | None = None,
141+
) -> None:
110142
"""
111143
Adds a private key to the smart card.
112144
@@ -116,8 +148,12 @@ def add_key(self, key_path: str, key_id: str = "01", pin: str = "123456") -> Non
116148
:type key_id: str, optional
117149
:param pin: User PIN, defaults to "123456"
118150
:type pin: str, optional
151+
:param token_label: Label of the target token (see :meth:`add_cert`).
152+
:type token_label: str | None, optional
153+
:param label: Label for the PKCS#11 object (see :meth:`add_cert`).
154+
:type label: str | None, optional
119155
"""
120-
self.add_cert(cert_path=key_path, cert_id=key_id, pin=pin, private=True)
156+
self.add_cert(cert_path=key_path, cert_id=key_id, pin=pin, private=True, token_label=token_label, label=label)
121157

122158
def generate_cert(
123159
self,

sssd_test_framework/utils/sssd.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ..roles.base import BaseRole
2424
from ..roles.kdc import KDC
2525
from .authselect import AuthselectUtils
26+
from .smartcard import SmartCardUtils
2627

2728

2829
__all__ = [
@@ -1178,3 +1179,31 @@ def subid(self) -> None:
11781179
Configure SSSD for subid.
11791180
"""
11801181
self.sssd.authselect.select("sssd", ["with-subid"])
1182+
1183+
def smartcard_with_softhsm(self, smartcard: SmartCardUtils) -> None:
1184+
"""
1185+
Configure SSSD for smart card authentication with SoftHSM multi-token support.
1186+
1187+
:param smartcard: SmartCardUtils instance.
1188+
:type smartcard: SmartCardUtils
1189+
"""
1190+
conf = smartcard.SOFTHSM2_CONF_PATH
1191+
token_storage = smartcard.TOKEN_STORAGE_PATH
1192+
module = "/usr/lib64/pkcs11/libsofthsm2.so"
1193+
1194+
smartcard.host.conn.run(f"grep -q 'slots.removable' {conf} || echo 'slots.removable = true' >> {conf}")
1195+
smartcard.host.conn.run(f"cp {conf} /etc/softhsm2.conf")
1196+
smartcard.host.conn.run(f'echo "module: {module}" > /etc/pkcs11/modules/softhsm2.module')
1197+
smartcard.fs.mkdir_p("/etc/systemd/system/sssd.service.d")
1198+
smartcard.host.conn.run(
1199+
f'printf "[Service]\\nEnvironment=SOFTHSM2_CONF={conf}\\n" '
1200+
f"> /etc/systemd/system/sssd.service.d/softhsm.conf"
1201+
)
1202+
smartcard.host.conn.run("systemctl daemon-reload")
1203+
smartcard.host.conn.run("chmod -R o+rX /opt/test_ca/")
1204+
smartcard.host.conn.run(f"chown -R sssd:sssd {token_storage}/ && chmod -R 770 {token_storage}/")
1205+
1206+
self.sssd.authselect.select("sssd", ["with-smartcard", "with-mkhomedir"])
1207+
self.sssd.pam["pam_cert_auth"] = "True"
1208+
self.sssd.domain["local_auth_policy"] = "enable:smartcard"
1209+
self.sssd.start()

0 commit comments

Comments
 (0)