diff --git a/src/p11_child/p11_child_openssl.c b/src/p11_child/p11_child_openssl.c index 9478d7450d8..2e78643be07 100644 --- a/src/p11_child/p11_child_openssl.c +++ b/src/p11_child/p11_child_openssl.c @@ -38,6 +38,9 @@ #include "util/crypto/sss_crypto.h" #include "p11_child/p11_child.h" +/* Timeout in seconds for soft_ocsp OCSP requests when no explicit timeout is set. */ +#define SOFT_OCSP_DEFAULT_TIMEOUT 10 + struct p11_ctx { X509_STORE *x509_store; const char *ca_db; @@ -381,8 +384,18 @@ static errno_t do_ocsp(struct p11_ctx *p11_ctx, X509 *cert) OCSP_request_add1_nonce(ocsp_req, NULL, -1); - if (p11_ctx->ocsp_deadline != -1 && p11_ctx->cert_verify_opts->soft_ocsp) { - req_timeout = p11_ctx->ocsp_deadline - time(NULL); + if (p11_ctx->cert_verify_opts->soft_ocsp) { + if (p11_ctx->ocsp_deadline != -1) { + req_timeout = p11_ctx->ocsp_deadline - time(NULL); + } else { + /* No explicit timeout was provided but soft_ocsp is enabled. + * Use a default timeout to prevent the BIO connect from + * blocking indefinitely when the OCSP host is unreachable. */ + DEBUG(SSSDBG_TRACE_INTERNAL, + "No timeout set with soft_ocsp, using default [%d]s.\n", + SOFT_OCSP_DEFAULT_TIMEOUT); + req_timeout = SOFT_OCSP_DEFAULT_TIMEOUT; + } if (req_timeout <= 0) { /* no time left for OCSP */ DEBUG(SSSDBG_TRACE_INTERNAL, @@ -594,15 +607,18 @@ errno_t init_p11_ctx(TALLOC_CTX *mem_ctx, const char *ca_db, return ENOMEM; } - if (timeout == 1) { - /* timeout of 1 sec is too short (see -1 in deadline calculation), - * increasing to 2 and hope that the ocsp operation finishes - * before p11_child is terminated. - */ - timeout = 2; + /* Use half the total timeout for OCSP to leave p11_child enough margin + * to output results before the PAM responder's kill timer fires (RHEL-5043). */ + if (timeout > 0) { + time_t ocsp_budget = timeout / 2; + if (ocsp_budget < 1) { + ocsp_budget = 1; + } + ctx->ocsp_deadline = time(NULL) + ocsp_budget; + } else { + /* timeout <= 0 means no timeout specified */ + ctx->ocsp_deadline = -1; } - /* timeout <= 0 means no timeout specified */ - ctx->ocsp_deadline = timeout > 0 ? time(NULL) + timeout - 1 : -1; /* See https://wiki.openssl.org/index.php/Library_Initialization for * details. */ diff --git a/src/tests/system/requirements.txt b/src/tests/system/requirements.txt index 788c9285d55..6814a4d9cd0 100644 --- a/src/tests/system/requirements.txt +++ b/src/tests/system/requirements.txt @@ -5,4 +5,5 @@ git+https://github.com/next-actions/pytest-mh git+https://github.com/next-actions/pytest-ticket git+https://github.com/next-actions/pytest-tier git+https://github.com/next-actions/pytest-output -git+https://github.com/SSSD/sssd-test-framework +#git+https://github.com/SSSD/sssd-test-framework +git+https://github.com/krishnavema/sssd-test-framework@smartcard-pkcs11 diff --git a/src/tests/system/tests/test_smartcard_soft_ocsp.py b/src/tests/system/tests/test_smartcard_soft_ocsp.py new file mode 100644 index 00000000000..8945aff7d29 --- /dev/null +++ b/src/tests/system/tests/test_smartcard_soft_ocsp.py @@ -0,0 +1,212 @@ +""" +SSSD smart card authentication with soft_ocsp + +Regression tests for RHEL-5043: when ``soft_ocsp`` is configured and the OCSP +responder is unreachable, smart card authentication should still succeed. + +:requirement: smartcard_authentication +""" + +from __future__ import annotations + +import time + +import pytest +from sssd_test_framework.roles.client import Client +from sssd_test_framework.roles.ipa import IPA +from sssd_test_framework.topology import KnownTopology + +#: Seconds to wait after restarting virt_cacard so that pcscd registers +#: the virtual card reader and the card is fully presented. +VIRT_CACARD_SETTLE_SECONDS = 5 + +#: Label for PKCS#11 objects stored in SoftHSM. ``p11_child``'s +#: ``parse_p11_child_response()`` rejects certificates that have an +#: empty label field, so every object must carry a non-empty one. +PKCS11_LABEL = "cert" + + +def _enroll_smartcard(client: Client, ipa: IPA, username: str) -> None: + """Request a certificate from the IPA CA and load it into a SoftHSM token. + + Follows the same ordering as + ``test_ipa__switch_user_with_smartcard_authentication`` which is the + canonical IPA smart-card test. + """ + cert, key, _ = ipa.ca.request(username) + + cert_content = ipa.fs.read(cert) + key_content = ipa.fs.read(key) + client.fs.write(f"/opt/test_ca/{username}.crt", cert_content) + client.fs.write(f"/opt/test_ca/{username}.key", key_content) + + client.smartcard.initialize_card() + client.smartcard.add_key(f"/opt/test_ca/{username}.key", label=PKCS11_LABEL) + client.smartcard.add_cert(f"/opt/test_ca/{username}.crt", label=PKCS11_LABEL) + + +def _configure_smartcard_and_start( + client: Client, + *, + certificate_verification: str | None = None, + p11_child_timeout: int = 30, +) -> None: + """Configure SSSD for smart-card authentication and present a virtual card.""" + client.authselect.select("sssd", ["with-smartcard"]) + + if certificate_verification is not None: + client.sssd.sssd["certificate_verification"] = certificate_verification + + client.sssd.pam["pam_cert_auth"] = "True" + client.sssd.pam["p11_child_timeout"] = str(p11_child_timeout) + + client.sssd.start() + client.svc.restart("virt_cacard.service") + time.sleep(VIRT_CACARD_SETTLE_SECONDS) + + +def _assert_smartcard_auth_success(client: Client, username: str) -> None: + """Run the double-``su`` pattern and verify PIN-based authentication.""" + result = client.host.conn.run( + f"su - {username} -c 'su - {username} -c whoami'", + input="123456", + ) + assert "PIN" in result.stderr, f"String 'PIN' was not found in stderr! Stderr content: {result.stderr}" + assert username in result.stdout, f"'{username}' not found in 'whoami' output! Stdout content: {result.stdout}" + + +def _redirect_ocsp_responder(client: Client, ipa: IPA, target_ip: str) -> None: + """Point the IPA OCSP responder hostname to *target_ip* via ``/etc/hosts``.""" + ipa_ca_hostname = f"ipa-ca.{ipa.domain}" + client.fs.append("/etc/hosts", f"\n{target_ip} {ipa_ca_hostname}\n") + + +@pytest.mark.ticket(jira="RHEL-5043") +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.IPA) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__soft_ocsp_with_unreachable_responder(client: Client, ipa: IPA): + """ + :title: Smart card authentication succeeds with soft_ocsp when OCSP responder is unreachable + :setup: + 1. Create an IPA user and enroll a smart card. + 2. Configure ``certificate_verification = soft_ocsp``. + 3. Point ipa-ca to 192.168.123.1 (non-routable, packets silently dropped). + 4. Start SSSD and present the virtual smart card. + :steps: + 1. Authenticate via ``su`` with the smart card PIN. + :expectedresults: + 1. PIN prompt appears and authentication succeeds despite the + unreachable OCSP responder. + :customerscenario: True + """ + username = "smartcarduser1" + + ipa.user(username).add() + _enroll_smartcard(client, ipa, username) + + # 192.168.123.1: non-routable → TCP SYN silently dropped → triggers the bug + _redirect_ocsp_responder(client, ipa, "192.168.123.1") + _configure_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth_success(client, username) + + +@pytest.mark.ticket(jira="RHEL-5043") +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.IPA) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__soft_ocsp_with_reachable_responder(client: Client, ipa: IPA): + """ + :title: Smart card authentication succeeds with soft_ocsp when OCSP responder is reachable + :setup: + 1. Create an IPA user and enroll a smart card. + 2. Configure ``certificate_verification = soft_ocsp``. + 3. Start SSSD and present the virtual smart card (OCSP responder is reachable). + :steps: + 1. Authenticate via ``su`` with the smart card PIN. + :expectedresults: + 1. PIN prompt appears and authentication succeeds; the OCSP check + completes normally. + :customerscenario: True + """ + username = "smartcarduser2" + + ipa.user(username).add() + _enroll_smartcard(client, ipa, username) + + # No /etc/hosts redirect – OCSP responder is reachable. + _configure_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth_success(client, username) + + +@pytest.mark.ticket(jira="RHEL-5043") +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.IPA) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__soft_ocsp_with_connection_refused(client: Client, ipa: IPA): + """ + :title: Smart card authentication succeeds with soft_ocsp when OCSP connection is refused + :setup: + 1. Create an IPA user and enroll a smart card. + 2. Configure ``certificate_verification = soft_ocsp``. + 3. Point ipa-ca to 127.0.0.7 (loopback, immediate TCP RST). + 4. Start SSSD and present the virtual smart card. + :steps: + 1. Authenticate via ``su`` with the smart card PIN. + :expectedresults: + 1. PIN prompt appears and authentication succeeds; the OCSP + connection is immediately refused and soft_ocsp skips the check. + :customerscenario: True + """ + username = "smartcarduser3" + + ipa.user(username).add() + _enroll_smartcard(client, ipa, username) + + # 127.0.0.7: loopback → immediate connection-refused (TCP RST) + _redirect_ocsp_responder(client, ipa, "127.0.0.7") + _configure_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth_success(client, username) + + +@pytest.mark.ticket(jira="RHEL-5043") +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.IPA) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__without_soft_ocsp_with_unreachable_responder(client: Client, ipa: IPA): + """ + :title: Smart card authentication fails without soft_ocsp when OCSP responder is unreachable + :setup: + 1. Create an IPA user and enroll a smart card. + 2. Do NOT set ``certificate_verification`` (default OCSP behaviour). + 3. Point ipa-ca to 192.168.123.1 (unreachable). + 4. Start SSSD and present the virtual smart card. + :steps: + 1. Attempt to authenticate via ``su`` with the smart card PIN. + :expectedresults: + 1. Without ``soft_ocsp``, the certificate check fails because the + OCSP responder is unreachable. The user sees a password prompt + (not a PIN prompt) or the authentication fails outright. + :customerscenario: True + """ + username = "smartcarduser4" + + ipa.user(username).add() + _enroll_smartcard(client, ipa, username) + + _redirect_ocsp_responder(client, ipa, "192.168.123.1") + # certificate_verification is left unset → default OCSP is enforced. + _configure_smartcard_and_start(client, certificate_verification=None) + + result = client.host.conn.run( + f"su - {username} -c 'su - {username} -c whoami'", + input="123456", + raise_on_error=False, + ) + + assert ( + "PIN" not in result.stderr or result.rc != 0 + ), f"Expected authentication to fail without soft_ocsp when OCSP is unreachable! rc={result.rc}"