diff --git a/src/p11_child/p11_child_common.c b/src/p11_child/p11_child_common.c index 9839b753586..cada44a0f95 100644 --- a/src/p11_child/p11_child_common.c +++ b/src/p11_child/p11_child_common.c @@ -367,6 +367,13 @@ int main(int argc, const char *argv[]) timeout = -1; } + /* When soft_ocsp is enabled and no timeout was specified, use a sensible + * default so that the OCSP connect does not block indefinitely when the + * responder is unreachable (RHEL-5043). */ + if (cert_verify_opts->soft_ocsp && timeout == -1) { + timeout = 10; + } + ret = do_work(main_ctx, mode, ca_db, cert_verify_opts, wait_for_card, cert_b64, pin, module_name, token_name, key_id, label, uri, timeout, &multi); diff --git a/src/p11_child/p11_child_openssl.c b/src/p11_child/p11_child_openssl.c index 9478d7450d8..c6f5917bcec 100644 --- a/src/p11_child/p11_child_openssl.c +++ b/src/p11_child/p11_child_openssl.c @@ -43,7 +43,7 @@ struct p11_ctx { const char *ca_db; bool wait_for_card; struct cert_verify_opts *cert_verify_opts; - time_t ocsp_deadline; + time_t timeout; }; static OCSP_RESPONSE *query_responder(BIO *cbio, const char *host, @@ -381,14 +381,12 @@ 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 (req_timeout <= 0) { - /* no time left for OCSP */ - DEBUG(SSSDBG_TRACE_INTERNAL, - "Timeout before we could run OCSP request.\n"); - req_timeout = 0; - } + if (p11_ctx->cert_verify_opts->soft_ocsp) { + /* soft_ocsp requires a finite timeout so that p11_child does not + * block indefinitely when the OCSP responder is unreachable. + * The timeout is guaranteed to be >= 0 at this point because + * main() sets it to 10 when soft_ocsp is enabled (RHEL-5043). */ + req_timeout = (int)p11_ctx->timeout; } if (req_timeout != 0) { ocsp_resp = process_responder(ocsp_req, host, path, port, use_ssl, @@ -594,16 +592,6 @@ 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; - } - /* 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. */ ret = OPENSSL_init_ssl(0, NULL); @@ -615,6 +603,7 @@ errno_t init_p11_ctx(TALLOC_CTX *mem_ctx, const char *ca_db, ctx->ca_db = ca_db; ctx->wait_for_card = wait_for_card; + ctx->timeout = timeout; talloc_set_destructor(ctx, p11_ctx_destructor); *p11_ctx = ctx; 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.py b/src/tests/system/tests/test_smartcard.py index bad58979b57..4896dc197ab 100644 --- a/src/tests/system/tests/test_smartcard.py +++ b/src/tests/system/tests/test_smartcard.py @@ -8,9 +8,56 @@ 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 +def _enroll_ipa_smartcard(client: Client, ipa: IPA, username: str) -> None: + """Request a certificate from the IPA CA and load it into a SoftHSM token.""" + 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") + client.smartcard.add_cert(f"/opt/test_ca/{username}.crt") + + +def _configure_ipa_smartcard_and_start( + client: Client, + *, + certificate_verification: str | None = None, +) -> None: + """Configure SSSD for IPA 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.start() + client.svc.restart("virt_cacard.service") + + +def _assert_smartcard_auth(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.importance("critical") @pytest.mark.topology(KnownTopology.Client) @pytest.mark.builtwith(client="virtualsmartcard") @@ -30,3 +77,130 @@ def test_smartcard__su_as_local_user(client: Client): result = client.host.conn.run("su - localuser1 -c 'su - localuser1 -c whoami'", input="123456") assert "PIN" in result.stderr, "String 'PIN' was not found in stderr!" assert "localuser1" in result.stdout, "'localuser1' not found in 'whoami' output!" + + +@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_ipa_smartcard(client, ipa, username) + + _redirect_ocsp_responder(client, ipa, "192.168.123.1") + _configure_ipa_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth(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_ipa_smartcard(client, ipa, username) + + _configure_ipa_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth(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_ipa_smartcard(client, ipa, username) + + _redirect_ocsp_responder(client, ipa, "127.0.0.7") + _configure_ipa_smartcard_and_start(client, certificate_verification="soft_ocsp") + + _assert_smartcard_auth(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_ipa_smartcard(client, ipa, username) + + _redirect_ocsp_responder(client, ipa, "192.168.123.1") + _configure_ipa_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}"