|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import TYPE_CHECKING |
| 4 | + |
| 5 | +from pytest_mh import MultihostHost, MultihostUtility |
| 6 | +from pytest_mh.cli import CLIBuilder, CLIBuilderArgs |
| 7 | +from pytest_mh.utils.fs import LinuxFileSystem |
| 8 | +from pytest_mh.utils.services import SystemdServices |
| 9 | + |
| 10 | +if TYPE_CHECKING: |
| 11 | + from ..roles.client import Client |
| 12 | + |
| 13 | +__all__ = [ |
| 14 | + "SmartCardUtils", |
| 15 | +] |
| 16 | + |
| 17 | + |
| 18 | +class SmartCardUtils(MultihostUtility[MultihostHost]): |
| 19 | + """ |
| 20 | + Utility class for managing smart card operations using SoftHSM and PKCS#11. |
| 21 | + """ |
| 22 | + |
| 23 | + SOFTHSM2_CONF_PATH = "/opt/test_ca/softhsm2.conf" |
| 24 | + TOKEN_STORAGE_PATH = "/opt/test_ca/tokens" |
| 25 | + OPENSC_CACHE_PATHS = [ |
| 26 | + "$HOME/.cache/opensc/", |
| 27 | + "/run/sssd/.cache/opensc/", |
| 28 | + ] |
| 29 | + |
| 30 | + def __init__(self, host: MultihostHost, fs: LinuxFileSystem, svc: SystemdServices) -> None: |
| 31 | + """ |
| 32 | + :param host: Multihost object. |
| 33 | + :type host: MultihostHost |
| 34 | + :param fs: Filesystem utility object. |
| 35 | + :type fs: LinuxFileSystem |
| 36 | + :param svc: Systemd svc utility object. |
| 37 | + :type svc: SystemdServices |
| 38 | + """ |
| 39 | + super().__init__(host) |
| 40 | + |
| 41 | + self.cli: CLIBuilder = host.cli |
| 42 | + """CLI builder utility for command construction.""" |
| 43 | + |
| 44 | + self.fs: LinuxFileSystem = fs |
| 45 | + """Filesystem utility used to handle file operations.""" |
| 46 | + |
| 47 | + self.svc: SystemdServices = svc |
| 48 | + """Systemd utility to manage and interact with svc.""" |
| 49 | + |
| 50 | + def initialize_card(self, label: str = "sc_test", so_pin: str = "12345678", user_pin: str = "123456") -> None: |
| 51 | + """ |
| 52 | + Initializes a SoftHSM token with the given label and PINs. |
| 53 | +
|
| 54 | + Cleans cache directories and prepares the token directory. |
| 55 | +
|
| 56 | + :param label: Token label, defaults to "sc_test" |
| 57 | + :type label: str, optional |
| 58 | + :param so_pin: Security Officer PIN, defaults to "12345678" |
| 59 | + :type so_pin: str, optional |
| 60 | + :param user_pin: User PIN, defaults to "123456" |
| 61 | + :type user_pin: str, optional |
| 62 | + """ |
| 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) |
| 68 | + |
| 69 | + args: CLIBuilderArgs = { |
| 70 | + "label": (self.cli.option.VALUE, label), |
| 71 | + "free": (self.cli.option.SWITCH, True), |
| 72 | + "so-pin": (self.cli.option.VALUE, so_pin), |
| 73 | + "pin": (self.cli.option.VALUE, user_pin), |
| 74 | + } |
| 75 | + self.host.conn.run( |
| 76 | + self.cli.command("softhsm2-util --init-token", args), env={"SOFTHSM2_CONF": self.SOFTHSM2_CONF_PATH} |
| 77 | + ) |
| 78 | + |
| 79 | + def add_cert( |
| 80 | + self, |
| 81 | + cert_path: str, |
| 82 | + cert_id: str = "01", |
| 83 | + pin: str = "123456", |
| 84 | + private: bool | None = False, |
| 85 | + ) -> None: |
| 86 | + """ |
| 87 | + Adds a certificate or private key to the smart card. |
| 88 | +
|
| 89 | + :param cert_path: Path to the certificate or key file. |
| 90 | + :type cert_path: str |
| 91 | + :param cert_id: Object ID, defaults to "01" |
| 92 | + :type cert_id: str, optional |
| 93 | + :param pin: User PIN, defaults to "123456" |
| 94 | + :type pin: str, optional |
| 95 | + :param private: Whether the object is a private key. Defaults to False. |
| 96 | + :type private: bool, optional |
| 97 | + """ |
| 98 | + obj_type = "privkey" if private else "cert" |
| 99 | + args: CLIBuilderArgs = { |
| 100 | + "module": (self.cli.option.VALUE, "/usr/lib64/pkcs11/libsofthsm2.so"), |
| 101 | + "login": (self.cli.option.SWITCH, True), |
| 102 | + "pin": (self.cli.option.VALUE, pin), |
| 103 | + "write-object": (self.cli.option.VALUE, cert_path), |
| 104 | + "type": (self.cli.option.VALUE, obj_type), |
| 105 | + "id": (self.cli.option.VALUE, cert_id), |
| 106 | + } |
| 107 | + self.host.conn.run(self.cli.command("pkcs11-tool", args), env={"SOFTHSM2_CONF": self.SOFTHSM2_CONF_PATH}) |
| 108 | + |
| 109 | + def add_key(self, key_path: str, key_id: str = "01", pin: str = "123456") -> None: |
| 110 | + """ |
| 111 | + Adds a private key to the smart card. |
| 112 | +
|
| 113 | + :param key_path: Path to the private key. |
| 114 | + :type key_path: str |
| 115 | + :param key_id: Key ID, defaults to "01" |
| 116 | + :type key_id: str, optional |
| 117 | + :param pin: User PIN, defaults to "123456" |
| 118 | + :type pin: str, optional |
| 119 | + """ |
| 120 | + self.add_cert(cert_path=key_path, cert_id=key_id, pin=pin, private=True) |
| 121 | + |
| 122 | + def generate_cert( |
| 123 | + self, |
| 124 | + key_path: str = "/tmp/selfsigned.key", |
| 125 | + cert_path: str = "/tmp/selfsigned.crt", |
| 126 | + subj: str = "/CN=Test Cert", |
| 127 | + ) -> tuple[str, str]: |
| 128 | + """ |
| 129 | + Generates a self-signed certificate and private key. |
| 130 | +
|
| 131 | + :param key_path: Output path for the private key, defaults to "/tmp/selfsigned.key" |
| 132 | + :type key_path: str, optional |
| 133 | + :param cert_path: Output path for the certificate, defaults to "/tmp/selfsigned.crt" |
| 134 | + :type cert_path: str, optional |
| 135 | + :param subj: Subject for the certificate, defaults to "/CN=Test Cert" |
| 136 | + :type subj: str, optional |
| 137 | + :return: Tuple of (key_path, cert_path) |
| 138 | + :rtype: tuple |
| 139 | + """ |
| 140 | + args: CLIBuilderArgs = { |
| 141 | + "x509": (self.cli.option.SWITCH, True), |
| 142 | + "nodes": (self.cli.option.SWITCH, True), |
| 143 | + "sha256": (self.cli.option.SWITCH, True), |
| 144 | + "days": (self.cli.option.VALUE, "365"), |
| 145 | + "newkey": (self.cli.option.VALUE, "rsa:2048"), |
| 146 | + "keyout": (self.cli.option.VALUE, key_path), |
| 147 | + "out": (self.cli.option.VALUE, cert_path), |
| 148 | + "subj": (self.cli.option.VALUE, subj), |
| 149 | + } |
| 150 | + self.host.conn.run(self.cli.command("openssl req", args)) |
| 151 | + return key_path, cert_path |
| 152 | + |
| 153 | + def insert_card(self) -> None: |
| 154 | + """ |
| 155 | + Simulates card insertion by starting the smart card service. |
| 156 | + """ |
| 157 | + self.svc.start("virt_cacard.service") |
| 158 | + |
| 159 | + def remove_card(self) -> None: |
| 160 | + """ |
| 161 | + Simulates card removal by stopping the smart card service. |
| 162 | + """ |
| 163 | + self.svc.stop("virt_cacard.service") |
| 164 | + |
| 165 | + def setup_local_card(self, client: Client, username: str) -> None: |
| 166 | + """ |
| 167 | + Setup local system for smart card authentication. |
| 168 | +
|
| 169 | + .. code-block:: python |
| 170 | + :caption: Example usage |
| 171 | +
|
| 172 | + @pytest.mark.topology(KnownTopology.Client) |
| 173 | + def test_example(client: Client): |
| 174 | + client.smartcard.setup_local_card(client, 'localuser1') |
| 175 | +
|
| 176 | + result = client.host.conn.run("su - localuser1 -c 'su - localuser1 -c whoami'", input="123456") |
| 177 | + assert "PIN" in result.stderr |
| 178 | + assert "localuser1" in result.stdout |
| 179 | + """ |
| 180 | + client.host.fs.rm("/etc/sssd/pki/sssd_auth_ca_db.pem") |
| 181 | + key, cert = self.generate_cert() |
| 182 | + self.initialize_card() |
| 183 | + self.add_key(key) |
| 184 | + self.add_cert(cert) |
| 185 | + client.authselect.select("sssd", ["with-smartcard"]) |
| 186 | + self.svc.restart("virt_cacard.service") |
| 187 | + client.sssd.common.local() |
| 188 | + client.sssd.dom("local")["local_auth_policy"] = "only" |
| 189 | + client.sssd.section(f"certmap/local/{username}")["matchrule"] = "<SUBJECT>.*CN=Test Cert.*" |
| 190 | + client.sssd.pam["pam_cert_auth"] = "True" |
| 191 | + |
| 192 | + data = client.host.fs.read(cert) |
| 193 | + client.host.fs.append("/etc/sssd/pki/sssd_auth_ca_db.pem", data) |
| 194 | + client.sssd.start() |
0 commit comments