From 3723dbc38d244f5e4e422456afd33198e143f486 Mon Sep 17 00:00:00 2001 From: shridhargadekar Date: Tue, 19 May 2026 13:44:27 +0530 Subject: [PATCH 1/2] Add TLS/LDAPS utilities for certificate management - Add TLSUtils class in sssd_test_framework/utils/tls.py - trust_ca_certificate(): Install CA cert to system trust store - trust_ca_certificate_file(): Install CA cert from file - configure_ldap_tls(): Configure LDAP client TLS settings - disable_certificate_verification(): Disable cert verification (testing only) - Add export_root_ca_certificate() method to ADHost - Exports AD root CA certificate in PEM format - Used for LDAPS connections on port 636 - Add tls utility to Client role - Accessible via client.tls in tests This enables LDAPS testing by allowing tests to trust AD's CA certificate: ca_cert = ad.host.export_root_ca_certificate() client.tls.trust_ca_certificate(ca_cert, 'ad-root-ca') client.adcli.join(domain=domain, args=['--use-ldaps'])" Signed-off-by: shridhargadekar --- sssd_test_framework/hosts/ad.py | 37 +++++++++ sssd_test_framework/roles/client.py | 6 ++ sssd_test_framework/utils/tls.py | 123 ++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 sssd_test_framework/utils/tls.py diff --git a/sssd_test_framework/hosts/ad.py b/sssd_test_framework/hosts/ad.py index d603795c..398daf5e 100644 --- a/sssd_test_framework/hosts/ad.py +++ b/sssd_test_framework/hosts/ad.py @@ -100,6 +100,43 @@ def naming_context(self) -> str: return self.__naming_context + def export_root_ca_certificate(self) -> str: + """ + Export the AD root CA certificate in PEM format. + + This method retrieves the most recent root CA certificate from the AD + domain controller's local machine root certificate store and exports it + in PEM format. The certificate is used for LDAPS (LDAP over SSL/TLS) + connections. + + :return: PEM-formatted root CA certificate content. + :rtype: str + :raises RuntimeError: If certificate cannot be exported. + """ + result = self.conn.run( + """ + $cert = Get-ChildItem Cert:\\LocalMachine\\Root | + Sort-Object NotBefore -Descending | + Select-Object -First 1 + + if (-not $cert) { + throw "No root CA certificate found" + } + + $pem = "-----BEGIN CERTIFICATE-----`r`n" + + [Convert]::ToBase64String($cert.RawData, "InsertLineBreaks") + + "`r`n-----END CERTIFICATE-----" + + Write-Output $pem + """, + raise_on_error=False, + ) + + if result.rc != 0: + raise RuntimeError(f"Failed to export root CA certificate: {result.stderr}") + + return result.stdout.strip() + def disconnect(self) -> None: return diff --git a/sssd_test_framework/roles/client.py b/sssd_test_framework/roles/client.py index d79bc581..30bae366 100644 --- a/sssd_test_framework/roles/client.py +++ b/sssd_test_framework/roles/client.py @@ -25,6 +25,7 @@ from ..utils.sss_override import SSSOverrideUtils from ..utils.sssctl import SSSCTLUtils from ..utils.sssd import SSSDUtils +from ..utils.tls import TLSUtils from ..utils.vfido import Vfido from .base import BaseLinuxRole @@ -118,6 +119,11 @@ def __init__(self, *args, **kwargs) -> None: Managing virtual passkey device and service """ + self.tls: TLSUtils = TLSUtils(self.host) + """ + TLS and certificate management. + """ + def setup(self) -> None: """ Called before execution of each test. diff --git a/sssd_test_framework/utils/tls.py b/sssd_test_framework/utils/tls.py new file mode 100644 index 00000000..0c24adea --- /dev/null +++ b/sssd_test_framework/utils/tls.py @@ -0,0 +1,123 @@ +"""TLS and certificate management utilities.""" + +from __future__ import annotations + +from pytest_mh import MultihostHost, MultihostUtility +from pytest_mh.conn import ProcessResult + +__all__ = [ + "TLSUtils", +] + + +class TLSUtils(MultihostUtility[MultihostHost]): + """ + Interface for TLS/SSL certificate operations. + + .. code-block:: python + :caption: Example usage + + @pytest.mark.topology(KnownTopology.AD) + def test_ldaps(client: Client, ad: AD): + # Export and trust AD's root CA certificate + ca_cert = ad.host.export_root_ca_certificate() + client.tls.trust_ca_certificate(ca_cert, "ad-root-ca") + + # Now LDAPS operations work + r = client.adcli.join( + domain=ad.host.domain, + login_user=ad.host.adminuser, + password=ad.host.adminpw, + args=["--use-ldaps"] + ) + assert r.rc == 0 + """ + + def trust_ca_certificate( + self, + certificate_content: str, + certificate_name: str | None = None, + ) -> ProcessResult: + """ + Trust a CA certificate by installing it to system trust store. + + :param certificate_content: PEM-formatted certificate content. + :type certificate_content: str + :param certificate_name: Optional certificate filename (without extension). + :type certificate_name: str | None + :return: Result of update-ca-trust command. + :rtype: ProcessResult + """ + if certificate_name is None: + certificate_name = "custom-ca" + + cert_path = f"/etc/pki/ca-trust/source/anchors/{certificate_name}.crt" + + # Write certificate to trust anchors + self.host.conn.run( + f"cat > {cert_path}", + input=certificate_content, + ) + + # Update system trust store + return self.host.conn.run("update-ca-trust") + + def trust_ca_certificate_file( + self, + certificate_path: str, + certificate_name: str | None = None, + ) -> ProcessResult: + """ + Trust a CA certificate from file path. + + :param certificate_path: Path to certificate file on local machine. + :type certificate_path: str + :param certificate_name: Optional certificate filename (without extension). + :type certificate_name: str | None + :return: Result of update-ca-trust command. + :rtype: ProcessResult + """ + with open(certificate_path) as f: + cert_content = f.read() + + return self.trust_ca_certificate(cert_content, certificate_name) + + def configure_ldap_tls( + self, + *, + tls_reqcert: str = "demand", + tls_cacertdir: str | None = None, + tls_cacert: str | None = None, + ) -> None: + """ + Configure LDAP client TLS settings in /etc/openldap/ldap.conf. + + :param tls_reqcert: Certificate verification level (never, allow, try, demand, hard). + :type tls_reqcert: str + :param tls_cacertdir: Path to CA certificate directory. + :type tls_cacertdir: str | None + :param tls_cacert: Path to specific CA certificate file. + :type tls_cacert: str | None + """ + config_lines = [f"TLS_REQCERT {tls_reqcert}"] + + if tls_cacertdir: + config_lines.append(f"TLS_CACERTDIR {tls_cacertdir}") + + if tls_cacert: + config_lines.append(f"TLS_CACERT {tls_cacert}") + + config_content = "\n".join(config_lines) + "\n" + self.host.conn.run( + "cat > /etc/openldap/ldap.conf", + input=config_content, + ) + + def disable_certificate_verification(self) -> None: + """ + Disable TLS certificate verification (for testing only). + + .. warning:: + This is insecure and should only be used for development/testing. + """ + self.configure_ldap_tls(tls_reqcert="never") From a10817b2898e162ae54bf4bf68544efaff62d1ca Mon Sep 17 00:00:00 2001 From: shridhargadekar Date: Tue, 19 May 2026 15:03:00 +0530 Subject: [PATCH 2/2] Fix TLSUtils review issues: multi-distro support, shell quoting, non-destructive config Addresses review feedback: 1. Add multi-distribution support: - Detect RHEL/Fedora vs Debian/Ubuntu CA trust systems - RHEL: /etc/pki/ca-trust/source/anchors + update-ca-trust - Debian: /usr/local/share/ca-certificates + update-ca-certificates - Auto-detect /etc/openldap/ldap.conf vs /etc/ldap/ldap.conf 2. Add proper shell quoting: - Import shlex module - Use shlex.quote() for all file paths - Prevents issues with spaces/special characters in certificate names 3. Make LDAP config non-destructive: - Use grep + sed to update existing TLS_REQCERT lines - Append new lines only if they don't exist - Preserves all other LDAP configuration settings - Creates config file with touch if it doesn't exist Changes: - Added _detect_ca_trust_system() helper method - Updated trust_ca_certificate() to use detected paths and shlex.quote() - Rewrote configure_ldap_tls() to use sed for in-place updates - Updated docstrings to reflect multi-distro support --- sssd_test_framework/utils/tls.py | 92 ++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/sssd_test_framework/utils/tls.py b/sssd_test_framework/utils/tls.py index 0c24adea..5bea6364 100644 --- a/sssd_test_framework/utils/tls.py +++ b/sssd_test_framework/utils/tls.py @@ -2,6 +2,8 @@ from __future__ import annotations +import shlex + from pytest_mh import MultihostHost, MultihostUtility from pytest_mh.conn import ProcessResult @@ -33,6 +35,38 @@ def test_ldaps(client: Client, ad: AD): assert r.rc == 0 """ + def _detect_ca_trust_system(self) -> tuple[str, str, str]: + """ + Detect the CA trust system (RHEL vs Debian-based). + + :return: Tuple of (cert_dir, update_command, config_file) + :rtype: tuple[str, str, str] + """ + # Check for RHEL/Fedora CA trust system + result = self.host.conn.run("test -d /etc/pki/ca-trust/source/anchors", raise_on_error=False) + if result.rc == 0: + return ( + "/etc/pki/ca-trust/source/anchors", + "update-ca-trust", + "/etc/openldap/ldap.conf", + ) + + # Check for Debian/Ubuntu CA trust system + result = self.host.conn.run("test -d /usr/local/share/ca-certificates", raise_on_error=False) + if result.rc == 0: + return ( + "/usr/local/share/ca-certificates", + "update-ca-certificates", + "/etc/ldap/ldap.conf", + ) + + # Fallback to RHEL paths + return ( + "/etc/pki/ca-trust/source/anchors", + "update-ca-trust", + "/etc/openldap/ldap.conf", + ) + def trust_ca_certificate( self, certificate_content: str, @@ -41,26 +75,35 @@ def trust_ca_certificate( """ Trust a CA certificate by installing it to system trust store. + Automatically detects the distribution and uses appropriate paths: + - RHEL/Fedora: /etc/pki/ca-trust/source/anchors/ + update-ca-trust + - Debian/Ubuntu: /usr/local/share/ca-certificates/ + update-ca-certificates + :param certificate_content: PEM-formatted certificate content. :type certificate_content: str :param certificate_name: Optional certificate filename (without extension). :type certificate_name: str | None - :return: Result of update-ca-trust command. + :return: Result of update-ca-trust/update-ca-certificates command. :rtype: ProcessResult """ if certificate_name is None: certificate_name = "custom-ca" - cert_path = f"/etc/pki/ca-trust/source/anchors/{certificate_name}.crt" + # Detect CA trust system + cert_dir, update_cmd, _ = self._detect_ca_trust_system() + + # Build certificate path with proper quoting + cert_path = f"{cert_dir}/{certificate_name}.crt" + quoted_path = shlex.quote(cert_path) # Write certificate to trust anchors self.host.conn.run( - f"cat > {cert_path}", + f"cat > {quoted_path}", input=certificate_content, ) # Update system trust store - return self.host.conn.run("update-ca-trust") + return self.host.conn.run(update_cmd) def trust_ca_certificate_file( self, @@ -74,7 +117,7 @@ def trust_ca_certificate_file( :type certificate_path: str :param certificate_name: Optional certificate filename (without extension). :type certificate_name: str | None - :return: Result of update-ca-trust command. + :return: Result of update-ca-trust/update-ca-certificates command. :rtype: ProcessResult """ with open(certificate_path) as f: @@ -90,7 +133,10 @@ def configure_ldap_tls( tls_cacert: str | None = None, ) -> None: """ - Configure LDAP client TLS settings in /etc/openldap/ldap.conf. + Configure LDAP client TLS settings in /etc/openldap/ldap.conf or /etc/ldap/ldap.conf. + + This method non-destructively updates or appends TLS settings to the LDAP + configuration file. Existing settings are preserved. :param tls_reqcert: Certificate verification level (never, allow, try, demand, hard). :type tls_reqcert: str @@ -99,20 +145,36 @@ def configure_ldap_tls( :param tls_cacert: Path to specific CA certificate file. :type tls_cacert: str | None """ - config_lines = [f"TLS_REQCERT {tls_reqcert}"] - - if tls_cacertdir: - config_lines.append(f"TLS_CACERTDIR {tls_cacertdir}") + # Detect distribution-specific LDAP config path + _, _, ldap_conf = self._detect_ca_trust_system() + quoted_conf = shlex.quote(ldap_conf) - if tls_cacert: - config_lines.append(f"TLS_CACERT {tls_cacert}") + # Create config file if it doesn't exist + self.host.conn.run(f"touch {quoted_conf}") - config_content = "\n".join(config_lines) + "\n" + # Update or append TLS_REQCERT self.host.conn.run( - "cat > /etc/openldap/ldap.conf", - input=config_content, + f"grep -q '^TLS_REQCERT' {quoted_conf} && " + f"sed -i 's/^TLS_REQCERT.*/TLS_REQCERT {tls_reqcert}/' {quoted_conf} || " + f"echo 'TLS_REQCERT {tls_reqcert}' >> {quoted_conf}" ) + # Update or append TLS_CACERTDIR if specified + if tls_cacertdir: + self.host.conn.run( + f"grep -q '^TLS_CACERTDIR' {quoted_conf} && " + f"sed -i 's|^TLS_CACERTDIR.*|TLS_CACERTDIR {tls_cacertdir}|' {quoted_conf} || " + f"echo 'TLS_CACERTDIR {tls_cacertdir}' >> {quoted_conf}" + ) + + # Update or append TLS_CACERT if specified + if tls_cacert: + self.host.conn.run( + f"grep -q '^TLS_CACERT' {quoted_conf} && " + f"sed -i 's|^TLS_CACERT.*|TLS_CACERT {tls_cacert}|' {quoted_conf} || " + f"echo 'TLS_CACERT {tls_cacert}' >> {quoted_conf}" + ) + def disable_certificate_verification(self) -> None: """ Disable TLS certificate verification (for testing only).